/WM/WMQualifier.j

http://github.com/quile/Womble · Unknown · 582 lines · 484 code · 98 blank · 0 comment · 0 complexity · 811103e528f801961f934f256756f1d8 MD5 · raw file

  1. /* --------------------------------------------------------------------
  2. * WM - Web Framework and ORM heavily influenced by WebObjects & EOF
  3. * The MIT License
  4. *
  5. * Copyright (c) 2010 kd
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. /*=====================================
  26. Qualifier
  27. Abstracts SQL qualifiers
  28. into a tree-based system of
  29. related qualifiers
  30. */
  31. @import "WMObject.j"
  32. @import "WMLog.j"
  33. @import "WMDB.j"
  34. @import "WMModel.j"
  35. @import "WMUtility.j"
  36. @import "Relationship/WMRelationshipDerived.j"
  37. @import "Relationship/WMRelationshipModelled.j"
  38. var QUALWMIER_TYPES = {
  39. "AND": 1,
  40. "OR" : 2,
  41. "KEY": 3,
  42. "SQL": 4,
  43. "MATCH": 5,
  44. };
  45. var QUALWMIER_OPERATORS = [
  46. "=",
  47. '>=',
  48. '<=',
  49. '<>',
  50. '>',
  51. '<',
  52. '!=',
  53. 'LIKE',
  54. 'REGEXP',
  55. 'IN',
  56. 'NOT IN',
  57. 'IS',
  58. 'IS NOT',
  59. ];
  60. var QUALWMIER_REGEX = "(" + QUALWMIER_OPERATORS.join("|") + ")";
  61. @implementation WMQualifier : WMObject
  62. {
  63. CPString type @accessors;
  64. bool isNegated @accessors;
  65. id entity @accessors;
  66. }
  67. - initWithType:(id)qualifierType {
  68. [self init];
  69. type = qualifierType;
  70. return self;
  71. }
  72. // helper constructors:
  73. // these should clean up the consumers a tad
  74. + and:(WMArray)qs {
  75. return [WMAndQualifier newWithQualifiers:qs];
  76. }
  77. + or:(WMArray)qs {
  78. return [WMOrQualifier newWithQualifiers:qs];
  79. }
  80. + sql:(id)condition {
  81. return [WMSQLQualifier newWithCondition:condition];
  82. }
  83. // form is
  84. // WM::Qualifier->match("attribute, attribute, ...", "terms")
  85. // where, if the attribute list is empty, we'll use all text attributes
  86. // in the entity. "terms" must be present and can use boolean
  87. // terms (in the expected MySQL format).
  88. + match:(id)attributes terms:(id)terms {
  89. return [WMMatchQualifier newWithAttributes:attributes andTerms:terms];
  90. }
  91. //--- instance methods ----
  92. - (WMSQLStatement)sqlWithBindValuesForExpression:(id)sqlExpression andModel:(id)model {
  93. return [self sqlWithBindValuesForExpression:sqlExpression andModel:model andClause:"WHERE"];
  94. }
  95. - (WMSQLStatement)sqlWithBindValuesForExpression:(id)sqlExpression andModel:(id)model andClause:(id)clause {
  96. [self subclassResponsibility];
  97. }
  98. // This is a bit of a mess because it actually assumes that the
  99. // derived source is >first< right now, as in
  100. // "DerivedSource.foo = id" whereas it should allow the
  101. // derived source to be anywhere in the key path.
  102. - translateDerivedRelationshipQualifier:(id)relationship intoSQLExpression:(id)sqlExpression forModel:(id)model {
  103. // argh parsing
  104. var re = new RegExp("^\\\s*([\\\w\\\._-]+)\\\s*" + QUALWMIER_REGEX + "\\\s*(ANY|ALL|SOME)?(.*)$", "i");
  105. var yn = condition.match(re);
  106. if (!yn) { [WMLog error:"Failed to parse " + condition + " as legitimate derived qualifier"]; return }
  107. var sourceKeyPath = yn[1];
  108. var operator = yn[2];
  109. var subqueryOperator = yn[3];
  110. var targetKeyPath = yn[4];
  111. var recd = [model entityClassDescriptionForEntityNamed:entity];
  112. var sourceGoo = [recd parseKeyPath:sourceKeyPath withSQLExpression:sqlExpression andModel:model];
  113. var rhs = targetKeyPath;
  114. if ([WMUtility expressionIsKeyPath:targetKeyPath]) {
  115. var targetGoo = [recd parseKeyPath:targetKeyPath withSQLExpression:sqlExpression andModel:model];
  116. var rtecd;
  117. if (rtecd = targetGoo.TARGET_ENTITY_CLASS_DESCRIPTION) {
  118. // create SQL for the qualifier on >that< entity
  119. var tableName = [rtecd _table];
  120. var columnName = [rtecd columnNameForAttributeName:targetGoo.TARGET_ATTRIBUTE];
  121. if ([sqlExpression hasSummaryAttribute:targetGoo.TARGET_ATTRIBUTE forTable:tableName]) {
  122. columnName = [sqlExpression aliasForSummaryAttribute:targetGoo.TARGET_ATTRIBUTE onTable:tableName];
  123. }
  124. var tableAlias = [sqlExpression aliasForTable:tableName];
  125. rhs = tableAlias + "." + columnName;
  126. }
  127. }
  128. var secd = sourceGoo.TARGET_ENTITY_CLASS_DESCRIPTION;
  129. var tableAlias = [sqlExpression aliasForTable:[relationship name]];
  130. var itn = [[[relationship fetchSpecification] entityClassDescription] _table];
  131. var columnName = [[[relationship fetchSpecification] entityClassDescription] columnNameForAttributeName:sourceGoo.TARGET_ATTRIBUTE];
  132. columnName = [[[relationship fetchSpecification] sqlExpression] aliasForColumn:columnName onTable:itn];
  133. if (!columnName) {
  134. if ([[[relationship fetchSpecification] sqlExpression] hasSummaryAttribute:sourceGoo.TARGET_ATTRIBUTE forTable: [[[relationship fetchSpecification] entityClassDescription] _table]]) {
  135. columnName = [[[relationship fetchSpecification] sqlExpression] aliasForSummaryAttribute:sourceGoo.TARGET_ATTRIBUTE onTable: [[[relationship fetchSpecification] entityClassDescription] _table]];
  136. } else {
  137. [WMLog debug:"Couldn't find alias for column " + sourceGoo.TARGET_ATTRIBUTE];
  138. }
  139. }
  140. var lhs = tableAlias + "." + columnName;
  141. return [WMSQLStatement newWithSQL:[lhs, operator, subqueryOperator, rhs].join(" ") andBindValues:bindValues];
  142. }
  143. - hasSubQuery {
  144. return ([self subQuery] ? true : false);
  145. }
  146. - subQuery {
  147. var bven = [bindValues objectEnumerator], bv;
  148. while (bv = [bven nextObject]) {
  149. if (bv.isa && [bv isKindOfClass:WMFetchSpecification]) {
  150. return bv;
  151. }
  152. }
  153. return nil;
  154. }
  155. @end
  156. // qualifier subclasses
  157. @implementation WMBooleanQualifier : WMQualifier
  158. {
  159. id subqualifiers @accessors;
  160. }
  161. + newWithType:(id)type qualifiers:(id)quals {
  162. var q = [[self alloc] initWithType:type];
  163. var qs = [WMArray arrayFromObject:quals];
  164. // don't allow these qualifiers without subqualifiers
  165. if (!qs || [qs count] == 0) { return nil };
  166. if ([qs count] == 1) { return [qs objectAtIndex:0]; }
  167. var validQualifiers = [WMArray new];
  168. for (var i=0; i < qs.length ; i++) {
  169. var cq = qs[i];
  170. if (!cq) { continue }
  171. [validQualifiers addObject:cq];
  172. }
  173. [q setSubqualifiers:validQualifiers];
  174. return q;
  175. }
  176. - (id) description {
  177. var d = "<WMQualifier [ " + entity + " ] - " + type + " " + [self subqualifiers] + ">";
  178. return d;
  179. }
  180. - setEntity:(id)e {
  181. var sqe = [subqualifiers objectEnumerator], sq;
  182. while (sq = [sqe nextObject]) {
  183. [sq setEntity:e];
  184. }
  185. entity = e;
  186. }
  187. - (WMSQLStatement)sqlWithBindValuesForExpression:(id)sqlExpression andModel:(id)model andClause:(id)clause {
  188. var subqualifierSQL = [WMArray new];
  189. var subqualifierBindValues = [WMArray new];
  190. var sqe = [subqualifiers objectEnumerator], sq;
  191. while (sq = [sqe nextObject]) {
  192. var subqualifierSQLStatement = [sq sqlWithBindValuesForExpression:sqlExpression andModel:model andClause:clause];
  193. [subqualifierSQL addObject:[subqualifierSQLStatement sql]];
  194. var bvs = [subqualifierSQLStatement bindValues];
  195. if (bvs && [bvs count] > 0) {
  196. [subqualifierBindValues addObjectsFromArray:bvs];
  197. }
  198. }
  199. if ([self isNegated]) {
  200. qualifierAsSQL = " NOT (" + qualifierAsSQL + ") ";
  201. }
  202. var qualifierAsSQL = "(" + [subqualifierSQL componentsJoinedByString:" " + type + " "] + ")";
  203. return [WMSQLStatement newWithSQL:qualifierAsSQL andBindValues:subqualifierBindValues];
  204. }
  205. @end
  206. //-------------------------------------------------------------
  207. @implementation WMAndQualifier : WMBooleanQualifier
  208. + (WMQualifier) newWithQualifiers:(id)qualifiers {
  209. return [self newWithType:"AND" qualifiers:qualifiers];
  210. }
  211. @end
  212. //-------------------------------------------------------------
  213. //-------------------------------------------------------------
  214. @implementation WMOrQualifier : WMBooleanQualifier
  215. + newWithQualifiers:(id)qualifiers {
  216. return [self newWithType:"AND" qualifiers:qualifiers];
  217. }
  218. @end
  219. //-------------------------------------------------------------
  220. //-------------------------------------------------------------
  221. @implementation WMSQLQualifier : WMQualifier
  222. {
  223. WMArray bindValues @accessors;
  224. CPString condition @accessors;
  225. }
  226. + newWithCondition:(id)condition {
  227. var q = [[self alloc] initWithType:"SQL"];
  228. [q setCondition:condition];
  229. return q;
  230. }
  231. - (WMSQLStatement)sqlWithBindValuesForExpression:(id)sqlExpression andModel:(id)model andClause:(id)clause {
  232. // short-circuit qualifiers that don't need to be translated.
  233. // TODO : rework the SQL in these to use the table aliases
  234. return [WMSQLStatement newWithSQL:condition andBindValues:bindValues];
  235. }
  236. - (id) description {
  237. var d = "<WMSQLQualifier [ " + entity + " ] - " + [self condition] + " >";
  238. return d;
  239. }
  240. @end
  241. //-------------------------------------------------------------
  242. //-------------------------------------------------------------
  243. @implementation WMKeyValueQualifier : WMQualifier
  244. {
  245. WMArray bindValues @accessors;
  246. CPString condition @accessors;
  247. bool requiresRepeatedJoin;
  248. }
  249. + key:(id)condition bindValues:(id)bvs {
  250. var q = [[self alloc] initWithType:"KEY"];
  251. [q setCondition:condition];
  252. [q setBindValues:bvs];
  253. return q;
  254. }
  255. + key:(id)condition, ... {
  256. var args = [WMArray new];
  257. for (var i=3; arguments[i] != nil; i++) {
  258. [args addObject:arguments[i]];
  259. }
  260. return [self key:condition bindValues:args];
  261. }
  262. - init {
  263. [super init]
  264. bindValues = [],
  265. requiresRepeatedJoin = false;
  266. condition = null;
  267. return self;
  268. }
  269. - (id) description {
  270. var d = "<WMKeyValueQualifier [ " + entity + " ] - " + [self condition] + " ( " + [self bindValues] + " ) >";
  271. return d;
  272. }
  273. - requiresRepeatedJoin {
  274. requiresRepeatedJoin = true;
  275. return self;
  276. }
  277. - (WMSQLStatement)sqlWithBindValuesForExpression:(id)sqlExpression andModel:(id)model andClause:(id)clause {
  278. // short-circuit qualifiers that don't need to be translated.
  279. // TODO : rework the SQL in these to use the table aliases
  280. //
  281. // There are three parts to a key-qualifier:
  282. // 1. key path
  283. // 2. operator
  284. // 3. values
  285. //
  286. var re = new RegExp("^\\\s*([\\\w\\\._-]+)\\\s*" + QUALWMIER_REGEX + "\\\s*(ANY|ALL|SOME)?(.*)$", "i");
  287. var yn = condition.match(re);
  288. if (!yn) {
  289. [CPException raise:"CPException" reason:"Qualifier condition is not well-formed: " + condition];
  290. return nil;
  291. }
  292. var keyPath = yn[1];
  293. var operator = yn[2];
  294. var subqueryOperator = yn[3];
  295. var value = yn[4];
  296. //[WMLog dump:[ keyPath, operator, subqueryOperator, value ]];
  297. var ecd = [model entityClassDescriptionForEntityNamed:entity];
  298. if (![WMLog assert:ecd message:"Entity class description exists for self.entity for self.condition"]) { return nil; }
  299. var oecd = ecd; // original ecd
  300. var cecd = ecd; // current ecd
  301. // Figure out the target ecd for the qualifier by looping through the keys in the path
  302. var bits = [keyPath componentsSeparatedByString:/\./];
  303. if ([bits count] == 0) { bits = [keyPath] }
  304. var qualifierKey;
  305. var deferredJoins = [WMArray new];
  306. for (var i=0; i < [bits count]; i++) {
  307. qualifierKey = [bits objectAtIndex:i];
  308. // if it's the last key in the path, bail now
  309. if (i >= ([bits count] - 1)) { break }
  310. // otherwise, look up the relationship
  311. var relationship = [cecd relationshipWithName:qualifierKey];
  312. // if there's no such relationship, it might be a derived data source
  313. // so check for that
  314. //
  315. if (!relationship) {
  316. relationship = [sqlExpression derivedDataSourceWithName:qualifierKey];
  317. // short circuit the rest of the loop if it's a derived
  318. // relationship because we don't need to add any
  319. // relationship traversal info to the sqlExpression
  320. //
  321. if (relationship) {
  322. return [self translateDerivedRelationshipQualifier:relationship intoSQLExpression:sqlExpression forModel:model];
  323. }
  324. }
  325. if (!relationship) {
  326. relationship = [sqlExpression dynamicRelationshipWithName:qualifierKey];
  327. //WM::Log::debug("Using dynamic relationship");
  328. }
  329. if (![WMLog assert:relationship message:"Relationship " + qualifierKey + " exists on entity " + [cecd name]]) {
  330. return [WMSQLStatement newWithSQL:"" andBindValues:[]];
  331. }
  332. var tecd = [relationship targetEntityClassDescription:model];
  333. if (![WMLog assert:tecd message:"Target entity class " + [relationship targetEntity] + " exists"]) {
  334. return [WMSQLStatement newWithSQL:"" andBindValues:[]];
  335. }
  336. //
  337. // ([tecd isAggregateEntity]) {
  338. // // We just bail on it if it's aggregate
  339. // // TODO see if there's a way to insert an aggregate qualifier into the key path
  340. // //
  341. // return [self _translateQualifierWithGoo:
  342. // bits[i+1],
  343. // relationship,
  344. // tecd,
  345. // model,
  346. // sqlExpression,
  347. // operator,
  348. // value
  349. // ];
  350. //}
  351. //
  352. // add traversed relationships to the SQL expression
  353. if (requiresRepeatedJoin) {
  354. [deferredJoins addObject:{ ecd: cecd, key: qualifierKey }];
  355. } else {
  356. [sqlExpression addTraversedRelationship:qualifierKey onEntity:cecd];
  357. }
  358. // follow it
  359. cecd = tecd;
  360. }
  361. // create SQL for the qualifier on >that< entity
  362. var tableName = [cecd _table];
  363. var columnName = [cecd columnNameForAttributeName:qualifierKey];
  364. if ([sqlExpression hasSummaryAttribute:qualifierKey forTable:tableName]) {
  365. columnName = [sqlExpression aliasForSummaryAttribute:qualifierKey onTable:tableName];
  366. }
  367. // allow a column name to be specified directly:
  368. if (!columnName && [cecd hasColumnNamed:qualifierKey]) {
  369. columnName = qualifierKey;
  370. }
  371. var tn = tableName;
  372. // XXX! Kludge! XXX!
  373. if (requiresRepeatedJoin) {
  374. tn = [sqlExpression addRepeatedTable:tn];
  375. }
  376. var tableAlias = [sqlExpression aliasForTable:tn];
  377. [WMLog assert:tableAlias message:"Alias for table tn is tableAlias"];
  378. var conditionInSQL;
  379. var bvs;
  380. if ([self hasSubQuery]) {
  381. var sq = value;
  382. var sqlWithBindValues = [[self subQuery] toSQLFromExpression];
  383. var sqre = new RegExp("\%\@");
  384. sq.replace(sqre, "(" + [sqlWithBindValues sql] + ")");
  385. conditionInSQL = tableAlias + "." + columnName + " " + operator + " " + subqueryOperator + " " + subquery;
  386. bvs = [sqlWithBindValues bindValues];
  387. } else {
  388. //
  389. //var aggregateColumns = {
  390. // uc([oecd aggregateKeyName]): 1,
  391. // uc([oecd aggregateValueName]): 1,
  392. // "creationDate": 1,
  393. // "modificationDate": 1,
  394. //};
  395. //if ([oecd isAggregateEntity]
  396. // && !aggregateColumns[uc(columnName)]
  397. // && ![oecd _primaryKey]->hasKeyField(uc(columnName))) {
  398. // conditionInSQL = "tableAlias + "[.oecd aggregateKeyName].
  399. // " = %@ AND tableAlias + "[.oecd aggregateValueName].
  400. // " operator value";
  401. // bindValues = [columnName, @{self._bindValues}];
  402. //} else {
  403. //
  404. //WM::Log::debug("MEOW $value");
  405. // TODO... I am pretty sure this code is redundant now;
  406. // the code above takes care of resolving the key paths now.
  407. //
  408. if ([WMUtility expressionIsKeyPath:value]) {
  409. //[WMLog debug:"key path"];
  410. var targetGoo = [ecd parseKeyPath:value withSQLExpression:sqlExpression andModel:model];
  411. var tecd = targetGoo.TARGET_ENTITY_CLASS_DESCRIPTION;
  412. var ta = targetGoo.TARGET_ATTRIBUTE;
  413. if (tecd) {
  414. var tn = [ecd _table];
  415. // XXX! Kludge! XXX!
  416. if (requiresRepeatedJoin) {
  417. // add that to the fetch representation
  418. tn = [sqlExpression addRepeatedTable:tn];
  419. }
  420. var targetTableAlias = [sqlExpression aliasForTable:tn];
  421. var targetColumnName = [sqlExpression aliasForColumn:ta onTable:[ecd _table]];
  422. value = targetTableAlias + "." + targetColumnName;
  423. }
  424. }
  425. conditionInSQL = tableAlias + "." + columnName + " " + operator + " " + value;
  426. bvs = bindValues;
  427. //}
  428. conditionInSQL = conditionInSQL.split("%@").join('?');
  429. }
  430. // hack to add a join to a repeated qualifier
  431. var dje = [deferredJoins objectEnumerator], dj;
  432. while (dj = [dje nextObject]) {
  433. [WMLog debug:"Adding repeated join on " + [dj.ecd name] + " with key " + dj.key];
  434. [sqlExpression addRepeatedTraversedRelationship:dj.key onEntity:dj.ecd];
  435. }
  436. return [WMSQLStatement newWithSQL:conditionInSQL andBindValues:bindValues];
  437. }
  438. @end
  439. @implementation WMMatchQualifier : WMQualifier
  440. {
  441. WMArray matchAttributes @accessors;
  442. CPString matchTerms @accessors;
  443. }
  444. + newWithAttributes:(id)attributes andTerms:(id)terms {
  445. var q = [[self alloc] initWithType:"MATCH"];
  446. var re = new RegExp(",\s*");
  447. [q setMatchAttributes:[attributes componentsSeparatedByString:re]];
  448. [q setMatchTerms:terms];
  449. return q;
  450. }
  451. - (WMSQLStatement)sqlWithBindValuesForExpression:(id)sqlExpression andModel:(id)model andClause:(id)clause {
  452. var ecd = [model entityClassDescriptionForEntityNamed:entity];
  453. if (![WMLog assert:ecd message:"Entity class description exists for self.entity"]) { return {}; }
  454. var oecd = ecd; // original ecd
  455. var cecd = ecd; // current ecd
  456. // figure out the attributes
  457. var attributes = matchAttributes ? [matchAttributes copy] : [WMArray new];
  458. if ([attributes count] == 0) {
  459. var aten = [[oecd allAttributes] objectEnumerator], attribute;
  460. while (attribute = [aten nextObject]) {
  461. if (!attribute.TYPE.match(/(CHAR|TEXT|BLOB)/i)) { continue; }
  462. [attributes addObject:attribute];
  463. }
  464. }
  465. var mappedAttributes = [WMArray new];
  466. // calculate attributes by walking the key paths... is this even valid?
  467. var aten = [attributes objectEnumerator], attributeName;
  468. while (attributeName = [aten nextObject]) {
  469. var targetGoo = [oecd parseKeyPath:attributeName withSQLExpression:sqlExpression andModel:model];
  470. var tecd = targetGoo.TARGET_ENTITY_CLASS_DESCRIPTION;
  471. if (tecd) {
  472. var tableName = [tecd _table];
  473. var columnName = [tecd columnNameForAttributeName:targetGoo.TARGET_ATTRIBUTE];
  474. var tableAlias = [sqlExpression aliasForTable:tableName];
  475. [mappedAttributes addObject:tableAlias + "." + columnName];
  476. }
  477. }
  478. [WMLog dump:"Matching on " + [mappedAttributes componentsJoinedByString:", "]];
  479. // TODO escape terms here.
  480. var terms = matchTerms.split(/\s+/);
  481. return [WMSQLStatement newWithSQL:"MATCH(" + [mappedAttributes componentsJoinedByString:", "] + ") AGAINST (? IN BOOLEAN MODE)"
  482. andBindValues:[terms componentsJoinedByString:" "]];
  483. }
  484. @end