1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('core') ; 9 sc_require('models/record'); 10 11 /** 12 @class 13 14 This class permits you to perform queries on your data store or a remote data store. Here is a 15 simple example of a *local* (see below) query, 16 17 query = SC.Query.create({ 18 conditions: "firstName = 'Johnny' AND lastName = 'Cash'" 19 }); 20 21 This query, when used with the store, will return all records which have record attributes of 22 `firstName` equal to 'Johnny' and `lastName` equal to 'Cash'. To use the query with the store 23 simple pass it to `find` like so, 24 25 records = MyApp.store.find(query); 26 27 In this example, `records` will be an `SC.RecordArray` array containing all matching records. The 28 amazing feature of record arrays backed by *local* queries is that they update automatically 29 whenever the records in the store change. This means that once we've run the query once, any time 30 data is loaded or unloaded from the store, the results of the query (i.e. `records`) will 31 update automatically. This allows for truly powerful and dynamic uses, such as having a list of 32 filtered results that continually updates instantly as data pages in or out in the background. 33 34 To limit the query to a record type of `MyApp.MyModel`, you can specify the type as a property of 35 the query like this, 36 37 query = SC.Query.create({ 38 conditions: "firstName = 'Johnny' AND lastName = 'Cash'", 39 recordType: MyApp.MyModel 40 }); 41 42 Calling `find()` like above will now return only records of type MyApp.MyModel. It is recommended 43 to limit your query to a record type, since the query will have to look for matching records in 44 the whole store if no record type is given. 45 46 You can also give an order for *local* queries, which the resulting records will use, 47 48 query = SC.Query.create({ 49 conditions: "firstName = 'Johnny' AND lastName = 'Cash'", 50 recordType: MyApp.MyModel, 51 orderBy: "lastName, year DESC" 52 }); 53 54 The default order direction is ascending. You can change it to descending by writing `'DESC'` 55 behind the property name as was done in the example above. If no order is given, or records are 56 equal in respect to a given order, records will be ordered by their storeKeys which increment 57 depending on load order. 58 59 Note, you can check if a certain record matches the query by calling `query.contains(record)`. 60 61 ## Local vs. Remote Queries 62 63 The default type for queries is 'local', but there is another type we can use called 'remote'. 64 The distinction between local and remote queries is a common tripping point for new SproutCore 65 developers, but hopefully the following description helps keep it clear. The terms local and 66 remote refer to the *location of the data where the query is performed and by whom*. A local query 67 will be run by the client against the store of data within the client, while a remote query will 68 be run on by the server against some remote store of data. This seems simple enough, but it can 69 lead to a few misconceptions. 70 71 The first misconception is that local queries don't ever result in a call to a server. This is 72 not the case; when a local query is first used with the store it will generally result in a call 73 to the server, but whether or not it does depends on your store's data source. Keep this in mind, 74 local queries *only run against the data loaded in the client's store*. If the client store is 75 empty, the results of the query will be empty even though there may be thousands of matching 76 records on a server somewhere. That's why when a query is first used, the store will look for a 77 data source that implements the `fetch(store, query)` method. Your data source can then decide 78 whether additional data should be loaded into the store first in order to better fulfill the 79 query. 80 81 This is entirely up to your client/server implementation, but a common use case is to have a 82 general query trigger the load of a large set of data and then any more specific queries will only 83 run against what was already loaded. For more details, @see SC.DataSource.prototype.fetch. So to 84 recap, local queries are passed to the data source fetch method the first time they are used so 85 that the data source has a chance to load additional data that the query may need. 86 87 Once we get past the first misconception; local queries are actually pretty easy to understand and 88 to work with. We run the queries, get the resulting record array and watch as the results almost 89 magically update as the data in the client changes. Local queries are the default type, and 90 typically we will use local queries almost exclusively in SproutCore apps. So why do we have a 91 remote type? 92 93 In a previous paragraph, we considered how a local query would be empty if the local store was 94 empty even though there may be thousands of matching records on a server. Well what if there were 95 millions of records on the server? When dealing with extremely large datasets, it's not feasible 96 to load all of the records into the client so that the client can run a query against them. This 97 is the role of the 'remote' type. Remote queries are not actually "run" by the client at all, 98 which is the next misconception; you cannot run a remote query. 99 100 This misconception is another way of saying that you can't set conditions or order on a remote 101 query. The 'remote' SC.Query type is simply a reflection of some database query that was run 102 against a data store somewhere outside of the client. For example, say we want to still find all 103 the records on the server with `firstName` equal to 'Johnny' and `lastName` equal to 'Cash', but 104 now there are millions of records. This type of query is best left to a MySQL or other database on 105 a server and thus the server will have exposed an API endpoint that will return the results of 106 such a search when passed some search terms. 107 108 Again, this is entirely up to your client/server configuration, but the way it is handled by the 109 data source is nearly identical to how local queries will be handled. In both situations, the 110 data source is passed the query the first time it is run and when the data source is done it calls 111 the same method on the store, `dataSourceDidFetchQuery`. In both situations too, any data that the 112 data source receives should be loaded into the client store using `loadRecords`. However, because 113 there may be lots of other data in the client store, the 'remote' query must also be told which 114 records pertain to it and in what order, which is done by passing the store keys of the new data 115 also to `dataSourceDidFetchQuery`. 116 117 So to recap, use 'local' queries to filter the data currently in the client store and use 'remote' 118 queries to represent results filtered by a remote server. Both may be used by a data source to 119 load data from a server. 120 121 ## SproutCore Query Language 122 123 Features of the query language: 124 125 Primitives: 126 127 - record properties 128 - `null`, `undefined` 129 - `true`, `false` 130 - numbers (integers and floats) 131 - strings (double or single quoted) 132 133 Parameters: 134 135 - `%@` (wild card) 136 - `{parameterName}` (named parameter) 137 138 Wild cards are used to identify parameters by the order in which they appear 139 in the query string. Named parameters can be used when tracking the order 140 becomes difficult. Both types of parameters can be used by giving the 141 parameters as a property to your query object: 142 143 yourQuery.parameters = yourParameters 144 145 where yourParameters should have one of the following formats: 146 147 * for wild cards: `[firstParam, secondParam, thirdParam]` 148 * for named params: `{name1: param1, mane2: parma2}` 149 150 You cannot use both types of parameters in a single query! 151 152 ### Operators: 153 154 - `=` 155 - `!=` 156 - `<` 157 - `<=` 158 - `>` 159 - `>=` 160 - `BEGINS_WITH` -- (checks if a string starts with another one) 161 - `ENDS_WITH` -- (checks if a string ends with another one) 162 - `CONTAINS` -- (checks if a string contains another one, or if an 163 object is in an array) 164 - `MATCHES` -- (checks if a string is matched by a regexp, 165 you will have to use a parameter to insert the regexp) 166 - `ANY` -- (checks if the thing on its left is contained in the array 167 on its right, you will have to use a parameter 168 to insert the array) 169 - `TYPE_IS` -- (unary operator expecting a string containing the name 170 of a Model class on its right side, only records of this 171 type will match) 172 173 ### Boolean Operators: 174 175 - `AND` 176 - `OR` 177 - `NOT` 178 179 Parenthesis for grouping: 180 181 - `(` and `)` 182 183 184 ## Adding Your Own Query Handlers 185 186 You can extend the query language with your own operators by calling: 187 188 SC.Query.registerQueryExtension('your_operator', your_operator_definition); 189 190 See details below. As well you can provide your own comparison functions 191 to control ordering of specific record properties like this: 192 193 SC.Query.registerComparison(property_name, comparison_for_this_property); 194 195 @extends SC.Object 196 @extends SC.Copyable 197 @extends SC.Freezable 198 @since SproutCore 1.0 199 */ 200 // TODO: Rename local vs. remote to avoid confusion. 201 SC.Query = SC.Object.extend(SC.Copyable, SC.Freezable, 202 /** @scope SC.Query.prototype */ { 203 204 //@if(debug) 205 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 206 207 /* @private */ 208 toString: function () { 209 var conditions = this.get('conditions'), 210 location = this.get('location'), 211 parameters = this.get('parameters'); 212 213 return "%@.%@({ conditions: '%@', parameters: %@, … })".fmt(this.constructor.toString(), location, conditions, SC.inspect(parameters)); 214 }, 215 216 /* END DEBUG ONLY PROPERTIES AND METHODS */ 217 //@endif 218 219 // .......................................................... 220 // PROPERTIES 221 // 222 223 /** 224 Walk like a duck. 225 226 @type Boolean 227 */ 228 isQuery: YES, 229 230 /** 231 Unparsed query conditions. If you are handling a query yourself, then 232 you will find the base query string here. 233 234 @type String 235 */ 236 conditions: null, 237 238 /** 239 Optional orderBy parameters. This can be a string of keys, optionally 240 ending with the strings `" DESC"` or `" ASC"` to select descending or 241 ascending order. 242 243 Alternatively, you can specify a comparison function, in which case the 244 two records will be sent to it. Your comparison function, as with any 245 other, is expected to return -1, 0, or 1. 246 247 @type String | Function 248 */ 249 orderBy: null, 250 251 /** 252 The base record type or types for the query. This must be specified to 253 filter the kinds of records this query will work on. You may either 254 set this to a single record type or to an array or set of record types. 255 256 @type SC.Record 257 */ 258 recordType: null, 259 260 /** 261 Optional array of multiple record types. If the query accepts multiple 262 record types, this is how you can check for it. 263 264 @type SC.Enumerable 265 */ 266 recordTypes: null, 267 268 /** 269 Returns the complete set of `recordType`s matched by this query. Includes 270 any named `recordType`s plus their subclasses. 271 272 @property 273 @type SC.Enumerable 274 */ 275 expandedRecordTypes: function() { 276 var ret = SC.CoreSet.create(), rt, q ; 277 278 if (rt = this.get('recordType')) this._scq_expandRecordType(rt, ret); 279 else if (rt = this.get('recordTypes')) { 280 rt.forEach(function(t) { this._scq_expandRecordType(t, ret); }, this); 281 } else this._scq_expandRecordType(SC.Record, ret); 282 283 // save in queue. if a new recordtype is defined, we will be notified. 284 q = SC.Query._scq_queriesWithExpandedRecordTypes; 285 if (!q) { 286 q = SC.Query._scq_queriesWithExpandedRecordTypes = SC.CoreSet.create(); 287 } 288 q.add(this); 289 290 return ret.freeze() ; 291 }.property('recordType', 'recordTypes').cacheable(), 292 293 /** @private 294 expands a single record type into the set. called recursively 295 */ 296 _scq_expandRecordType: function(recordType, set) { 297 if (set.contains(recordType)) return; // nothing to do 298 set.add(recordType); 299 300 if (SC.typeOf(recordType)===SC.T_STRING) { 301 recordType = SC.objectForPropertyPath(recordType); 302 } 303 304 recordType.subclasses.forEach(function(t) { 305 this._scq_expandRecordType(t, set); 306 }, this); 307 }, 308 309 /** 310 Optional hash of parameters. These parameters may be interpolated into 311 the query conditions. If you are handling the query manually, these 312 parameters will not be used. 313 314 @type Hash 315 */ 316 parameters: null, 317 318 /** 319 Indicates the location where the result set for this query is stored. 320 Currently the available options are: 321 322 - `SC.Query.LOCAL` -- indicates that the query results will be 323 automatically computed from the in-memory store. 324 - `SC.Query.REMOTE` -- indicates that the query results are kept on a 325 remote server and hence must be loaded from the `DataSource`. 326 327 The default setting for this property is `SC.Query.LOCAL`. 328 329 Note that even if a query location is `LOCAL`, your `DataSource` will 330 still have its `fetch()` method called for the query. For `LOCAL` 331 queries, you won't need to explicitly provide the query result set; you 332 can just load records into the in-memory store as needed and let the query 333 recompute automatically. 334 335 If your query location is `REMOTE`, then your `DataSource` will need to 336 provide the actual set of query results manually. Usually you will only 337 need to use a `REMOTE` query if you are retrieving a large data set and you 338 don't want to pay the cost of computing the result set client side. 339 340 @type String 341 */ 342 location: 'local', // SC.Query.LOCAL 343 344 /** 345 Another query that will optionally limit the search of records. This is 346 usually configured for you when you do `find()` from another record array. 347 348 @type SC.Query 349 */ 350 scope: null, 351 352 353 /** 354 Returns `YES` if query location is Remote. This is sometimes more 355 convenient than checking the location. 356 357 @property 358 @type Boolean 359 */ 360 isRemote: function() { 361 return this.get('location') === SC.Query.REMOTE; 362 }.property('location').cacheable(), 363 364 /** 365 Returns `YES` if query location is Local. This is sometimes more 366 convenient than checking the location. 367 368 @property 369 @type Boolean 370 */ 371 isLocal: function() { 372 return this.get('location') === SC.Query.LOCAL; 373 }.property('location').cacheable(), 374 375 /** 376 Indicates whether a record is editable or not. Defaults to `NO`. Local 377 queries should never be made editable. Remote queries may be editable or 378 not depending on the data source. 379 */ 380 isEditable: NO, 381 382 // .......................................................... 383 // PRIMITIVE METHODS 384 // 385 386 /** 387 Returns `YES` if record is matched by the query, `NO` otherwise. This is 388 used when computing a query locally. 389 390 @param {SC.Record} record the record to check 391 @param {Hash} parameters optional override parameters 392 @returns {Boolean} YES if record belongs, NO otherwise 393 */ 394 contains: function(record, parameters) { 395 396 // check the recordType if specified 397 var rtype, ret = YES ; 398 if (rtype = this.get('recordTypes')) { // plural form 399 ret = rtype.find(function(t) { return SC.kindOf(record, t); }); 400 } else if (rtype = this.get('recordType')) { // singular 401 ret = SC.kindOf(record, rtype); 402 } 403 404 if (!ret) return NO ; // if either did not pass, does not contain 405 406 // if we have a scope - check for that as well 407 var scope = this.get('scope'); 408 if (scope && !scope.contains(record)) return NO ; 409 410 // now try parsing 411 if (!this._isReady) this.parse(); // prepare the query if needed 412 if (!this._isReady) return NO ; 413 if (parameters === undefined) parameters = this.parameters || this; 414 415 // if parsing worked we check if record is contained 416 // if parsing failed no record will be contained 417 return this._tokenTree.evaluate(record, parameters); 418 }, 419 420 /** 421 Returns `YES` if the query matches one or more of the record types in the 422 passed set. 423 424 @param {SC.Set} types set of record types 425 @returns {Boolean} YES if record types match 426 */ 427 containsRecordTypes: function(types) { 428 var rtype = this.get('recordType'); 429 if (rtype) { 430 return !!types.find(function(t) { return SC.kindOf(t, rtype); }); 431 432 } else if (rtype = this.get('recordTypes')) { 433 return !!rtype.find(function(t) { 434 return !!types.find(function(t2) { return SC.kindOf(t2,t); }); 435 }); 436 437 } else return YES; // allow anything through 438 }, 439 440 /** 441 Returns the sort order of the two passed records, taking into account the 442 orderBy property set on this query. This method does not verify that the 443 two records actually belong in the query set or not; this is checked using 444 `contains()`. 445 446 @param {SC.Record} record1 the first record 447 @param {SC.Record} record2 the second record 448 @returns {Number} -1 if record1 < record2, 449 +1 if record1 > record2, 450 0 if equal 451 */ 452 compare: function(record1, record2) { 453 var result = 0, 454 propertyName, order, len, i, methodName; 455 456 // fast cases go here 457 if (record1 === record2) return 0; 458 459 // if called for the first time we have to build the order array 460 if (!this._isReady) this.parse(); 461 if (!this._isReady) { // can't parse, so use storeKey. Not proper, but consistent. 462 return SC.compare(record1.get('storeKey'),record2.get('storeKey')); 463 } 464 465 // For every property specified in orderBy until non-eql result is found. 466 // Or, if orderBy is a comparison function, simply invoke it with the 467 // records. 468 order = this._order; 469 if (SC.typeOf(order) === SC.T_FUNCTION) { 470 result = order.call(null, record1, record2); 471 } 472 else { 473 len = order ? order.length : 0; 474 for (i=0; result===0 && (i < len); i++) { 475 propertyName = order[i].propertyName; 476 methodName = /\./.test(propertyName) ? 'getPath' : 'get'; 477 // if this property has a registered comparison use that 478 if (SC.Query.comparisons[propertyName]) { 479 result = SC.Query.comparisons[propertyName]( 480 record1[methodName](propertyName), record2[methodName](propertyName)); 481 482 // if not use default SC.compare() 483 } else { 484 result = SC.compare( 485 record1[methodName](propertyName), record2[methodName](propertyName)); 486 } 487 488 if ((result!==0) && order[i].descending) result = (-1) * result; 489 } 490 } 491 492 // return result or compare by storeKey 493 if (result !== 0) return result ; 494 else return SC.compare(record1.get('storeKey'), record2.get('storeKey')); 495 }, 496 497 /** @private 498 Becomes YES once the query has been successfully parsed 499 */ 500 _isReady: NO, 501 502 /** 503 This method has to be called before the query object can be used. 504 You will normally not have to do this; it will be called automatically 505 if you try to evaluate a query. 506 You can, however, use this function for testing your queries. 507 508 @returns {Boolean} true if parsing succeeded, false otherwise 509 */ 510 parse: function() { 511 var conditions = this.get('conditions'), 512 lang = this.get('queryLanguage'), 513 tokens, tree; 514 515 tokens = this._tokenList = this.tokenizeString(conditions, lang); 516 tree = this._tokenTree = this.buildTokenTree(tokens, lang); 517 this._order = this.buildOrder(this.get('orderBy')); 518 519 this._isReady = !!tree && !tree.error; 520 if (tree && tree.error) SC.throw(tree.error); 521 return this._isReady; 522 }, 523 524 /** 525 Returns the same query but with the scope set to the passed record array. 526 This will copy the receiver. It also stores these queries in a cache to 527 reuse them if possible. 528 529 @param {SC.RecordArray} recordArray the scope 530 @returns {SC.Query} new query 531 */ 532 queryWithScope: function(recordArray) { 533 // look for a cached query on record array. 534 var key = SC.keyFor('__query__', SC.guidFor(this)), 535 ret = recordArray[key]; 536 537 if (!ret) { 538 recordArray[key] = ret = this.copy(); 539 ret.set('scope', recordArray); 540 ret.freeze(); 541 } 542 543 return ret ; 544 }, 545 546 // .......................................................... 547 // PRIVATE SUPPORT 548 // 549 550 /** @private 551 Properties that need to be copied when cloning the query. 552 */ 553 copyKeys: ['conditions', 'orderBy', 'recordType', 'recordTypes', 'parameters', 'location', 'scope'], 554 555 /** @private */ 556 concatenatedProperties: ['copyKeys'], 557 558 /** @private 559 Implement the Copyable API to clone a query object once it has been 560 created. 561 */ 562 copy: function() { 563 var opts = {}, 564 keys = this.get('copyKeys'), 565 loc = keys ? keys.length : 0, 566 key, value, ret; 567 568 while(--loc >= 0) { 569 key = keys[loc]; 570 value = this.get(key); 571 if (value !== undefined) opts[key] = value ; 572 } 573 574 ret = this.constructor.create(opts); 575 opts = null; 576 return ret ; 577 }, 578 579 // .......................................................... 580 // QUERY LANGUAGE DEFINITION 581 // 582 583 584 /** 585 This is the definition of the query language. You can extend it 586 by using `SC.Query.registerQueryExtension()`. 587 */ 588 queryLanguage: { 589 590 'UNKNOWN': { 591 firstCharacter: /[^\s'"\w\d\(\)\{\}]/, 592 notAllowed: /[\-\s'"\w\d\(\)\{\}]/ 593 }, 594 595 'PROPERTY': { 596 firstCharacter: /[a-zA-Z_]/, 597 notAllowed: /[^a-zA-Z_0-9\.]/, 598 evalType: 'PRIMITIVE', 599 600 /** @ignore */ 601 evaluate: function (r,w) { 602 var tokens = this.tokenValue.split('.'); 603 604 var len = tokens.length; 605 if (len < 2) return r.get(this.tokenValue); 606 607 var ret = r; 608 for (var i = 0; i < len; i++) { 609 if (!ret) return; 610 if (ret.get) { 611 ret = ret.get(tokens[i]); 612 } else { 613 ret = ret[tokens[i]]; 614 } 615 } 616 return ret; 617 } 618 }, 619 620 'NUMBER': { 621 firstCharacter: /[\d\-]/, 622 notAllowed: /[^\d\-\.]/, 623 format: /^-?\d+$|^-?\d+\.\d+$/, 624 evalType: 'PRIMITIVE', 625 626 /** @ignore */ 627 evaluate: function (r,w) { return parseFloat(this.tokenValue); } 628 }, 629 630 'STRING': { 631 firstCharacter: /['"]/, 632 delimited: true, 633 evalType: 'PRIMITIVE', 634 635 /** @ignore */ 636 evaluate: function (r,w) { return this.tokenValue; } 637 }, 638 639 'PARAMETER': { 640 firstCharacter: /\{/, 641 lastCharacter: '}', 642 delimited: true, 643 evalType: 'PRIMITIVE', 644 645 /** @ignore */ 646 evaluate: function (r,w) { return w[this.tokenValue]; } 647 }, 648 649 '%@': { 650 rememberCount: true, 651 reservedWord: true, 652 evalType: 'PRIMITIVE', 653 654 /** @ignore */ 655 evaluate: function (r,w) { return w[this.tokenValue]; } 656 }, 657 658 'OPEN_PAREN': { 659 firstCharacter: /\(/, 660 singleCharacter: true 661 }, 662 663 'CLOSE_PAREN': { 664 firstCharacter: /\)/, 665 singleCharacter: true 666 }, 667 668 'AND': { 669 reservedWord: true, 670 leftType: 'BOOLEAN', 671 rightType: 'BOOLEAN', 672 evalType: 'BOOLEAN', 673 674 /** @ignore */ 675 evaluate: function (r,w) { 676 var left = this.leftSide.evaluate(r,w); 677 var right = this.rightSide.evaluate(r,w); 678 return left && right; 679 } 680 }, 681 682 'OR': { 683 reservedWord: true, 684 leftType: 'BOOLEAN', 685 rightType: 'BOOLEAN', 686 evalType: 'BOOLEAN', 687 688 /** @ignore */ 689 evaluate: function (r,w) { 690 var left = this.leftSide.evaluate(r,w); 691 var right = this.rightSide.evaluate(r,w); 692 return left || right; 693 } 694 }, 695 696 'NOT': { 697 reservedWord: true, 698 rightType: 'BOOLEAN', 699 evalType: 'BOOLEAN', 700 701 /** @ignore */ 702 evaluate: function (r,w) { 703 var right = this.rightSide.evaluate(r,w); 704 return !right; 705 } 706 }, 707 708 '=': { 709 reservedWord: true, 710 leftType: 'PRIMITIVE', 711 rightType: 'PRIMITIVE', 712 evalType: 'BOOLEAN', 713 714 /** @ignore */ 715 evaluate: function (r,w) { 716 var left = this.leftSide.evaluate(r,w); 717 var right = this.rightSide.evaluate(r,w); 718 return SC.isEqual(left, right); 719 } 720 }, 721 722 '!=': { 723 reservedWord: true, 724 leftType: 'PRIMITIVE', 725 rightType: 'PRIMITIVE', 726 evalType: 'BOOLEAN', 727 728 /** @ignore */ 729 evaluate: function (r,w) { 730 var left = this.leftSide.evaluate(r,w); 731 var right = this.rightSide.evaluate(r,w); 732 return !SC.isEqual(left, right); 733 } 734 }, 735 736 '<': { 737 reservedWord: true, 738 leftType: 'PRIMITIVE', 739 rightType: 'PRIMITIVE', 740 evalType: 'BOOLEAN', 741 742 /** @ignore */ 743 evaluate: function (r,w) { 744 var left = this.leftSide.evaluate(r,w); 745 var right = this.rightSide.evaluate(r,w); 746 return SC.compare(left, right) === -1; //left < right; 747 } 748 }, 749 750 '<=': { 751 reservedWord: true, 752 leftType: 'PRIMITIVE', 753 rightType: 'PRIMITIVE', 754 evalType: 'BOOLEAN', 755 756 /** @ignore */ 757 evaluate: function (r,w) { 758 var left = this.leftSide.evaluate(r,w); 759 var right = this.rightSide.evaluate(r,w); 760 return SC.compare(left, right) !== 1; //left <= right; 761 } 762 }, 763 764 '>': { 765 reservedWord: true, 766 leftType: 'PRIMITIVE', 767 rightType: 'PRIMITIVE', 768 evalType: 'BOOLEAN', 769 770 /** @ignore */ 771 evaluate: function (r,w) { 772 var left = this.leftSide.evaluate(r,w); 773 var right = this.rightSide.evaluate(r,w); 774 return SC.compare(left, right) === 1; //left > right; 775 } 776 }, 777 778 '>=': { 779 reservedWord: true, 780 leftType: 'PRIMITIVE', 781 rightType: 'PRIMITIVE', 782 evalType: 'BOOLEAN', 783 784 /** @ignore */ 785 evaluate: function (r,w) { 786 var left = this.leftSide.evaluate(r,w); 787 var right = this.rightSide.evaluate(r,w); 788 return SC.compare(left, right) !== -1; //left >= right; 789 } 790 }, 791 792 'BEGINS_WITH': { 793 reservedWord: true, 794 leftType: 'PRIMITIVE', 795 rightType: 'PRIMITIVE', 796 evalType: 'BOOLEAN', 797 798 /** @ignore */ 799 evaluate: function (r,w) { 800 var all = this.leftSide.evaluate(r,w); 801 var start = this.rightSide.evaluate(r,w); 802 return ( all && all.indexOf(start) === 0 ); 803 } 804 }, 805 806 'ENDS_WITH': { 807 reservedWord: true, 808 leftType: 'PRIMITIVE', 809 rightType: 'PRIMITIVE', 810 evalType: 'BOOLEAN', 811 812 /** @ignore */ 813 evaluate: function (r,w) { 814 var all = this.leftSide.evaluate(r,w); 815 var end = this.rightSide.evaluate(r,w); 816 return ( all && all.length >= end.length && all.lastIndexOf(end) === (all.length - end.length)); 817 } 818 }, 819 820 'CONTAINS': { 821 reservedWord: true, 822 leftType: 'PRIMITIVE', 823 rightType: 'PRIMITIVE', 824 evalType: 'BOOLEAN', 825 826 /** @ignore */ 827 evaluate: function (r,w) { 828 var all = this.leftSide.evaluate(r,w) || []; 829 var value = this.rightSide.evaluate(r,w); 830 831 var allType = SC.typeOf(all); 832 if (allType === SC.T_STRING) { 833 return (all.indexOf(value) !== -1); 834 } else if (allType === SC.T_ARRAY || all.toArray) { 835 if (allType !== SC.T_ARRAY) all = all.toArray(); 836 var found = false; 837 var i = 0; 838 while ( found === false && i < all.length ) { 839 if ( value == all[i] ) found = true; 840 i++; 841 } 842 return found; 843 } 844 } 845 }, 846 847 'ANY': { 848 reservedWord: true, 849 leftType: 'PRIMITIVE', 850 rightType: 'PRIMITIVE', 851 evalType: 'BOOLEAN', 852 853 /** @ignore */ 854 evaluate: function (r,w) { 855 var prop = this.leftSide.evaluate(r,w); 856 var values = this.rightSide.evaluate(r,w); 857 var found = false; 858 var i = 0; 859 while ( found===false && i<values.length ) { 860 if ( prop == values[i] ) found = true; 861 i++; 862 } 863 return found; 864 } 865 }, 866 867 'MATCHES': { 868 reservedWord: true, 869 leftType: 'PRIMITIVE', 870 rightType: 'PRIMITIVE', 871 evalType: 'BOOLEAN', 872 873 /** @ignore */ 874 evaluate: function (r,w) { 875 var toMatch = this.leftSide.evaluate(r,w); 876 var matchWith = this.rightSide.evaluate(r,w); 877 return matchWith.test(toMatch); 878 } 879 }, 880 881 'TYPE_IS': { 882 reservedWord: true, 883 rightType: 'PRIMITIVE', 884 evalType: 'BOOLEAN', 885 886 /** @ignore */ 887 evaluate: function (r,w) { 888 var actualType = SC.Store.recordTypeFor(r.storeKey); 889 var right = this.rightSide.evaluate(r,w); 890 var expectType = SC.objectForPropertyPath(right); 891 return actualType == expectType; 892 } 893 }, 894 895 'null': { 896 reservedWord: true, 897 evalType: 'PRIMITIVE', 898 899 /** @ignore */ 900 evaluate: function (r,w) { return null; } 901 }, 902 903 'undefined': { 904 reservedWord: true, 905 evalType: 'PRIMITIVE', 906 907 /** @ignore */ 908 evaluate: function (r,w) { return undefined; } 909 }, 910 911 'false': { 912 reservedWord: true, 913 evalType: 'PRIMITIVE', 914 915 /** @ignore */ 916 evaluate: function (r,w) { return false; } 917 }, 918 919 'true': { 920 reservedWord: true, 921 evalType: 'PRIMITIVE', 922 923 /** @ignore */ 924 evaluate: function (r,w) { return true; } 925 }, 926 927 'YES': { 928 reservedWord: true, 929 evalType: 'PRIMITIVE', 930 931 /** @ignore */ 932 evaluate: function (r,w) { return true; } 933 }, 934 935 'NO': { 936 reservedWord: true, 937 evalType: 'PRIMITIVE', 938 939 /** @ignore */ 940 evaluate: function (r,w) { return false; } 941 } 942 943 }, 944 945 946 // .......................................................... 947 // TOKENIZER 948 // 949 950 951 /** 952 Takes a string and tokenizes it based on the grammar definition 953 provided. Called by `parse()`. 954 955 @param {String} inputString the string to tokenize 956 @param {Object} grammar the grammar definition (normally queryLanguage) 957 @returns {Array} list of tokens 958 */ 959 tokenizeString: function (inputString, grammar) { 960 961 962 var tokenList = [], 963 c = null, 964 t = null, 965 token = null, 966 currentToken = null, 967 currentTokenType = null, 968 currentTokenValue = null, 969 currentDelimiter = null, 970 endOfString = false, 971 endOfToken = false, 972 skipThisCharacter = false, 973 rememberCount = {}; 974 975 976 // helper function that adds tokens to the tokenList 977 978 function addToken (tokenType, tokenValue) { 979 t = grammar[tokenType]; 980 //tokenType = t.tokenType; 981 982 // handling of special cases 983 // check format 984 if (t.format && !t.format.test(tokenValue)) tokenType = "UNKNOWN"; 985 // delimited token (e.g. by ") 986 if (t.delimited) skipThisCharacter = true; 987 988 // reserved words 989 if ( !t.delimited ) { 990 for ( var anotherToken in grammar ) { 991 if (grammar[anotherToken].reservedWord && 992 anotherToken == tokenValue ) { 993 tokenType = anotherToken; 994 } 995 } 996 } 997 998 // reset t 999 t = grammar[tokenType]; 1000 // remembering count type 1001 if ( t && t.rememberCount ) { 1002 if (!rememberCount[tokenType]) rememberCount[tokenType] = 0; 1003 tokenValue = rememberCount[tokenType]; 1004 rememberCount[tokenType] += 1; 1005 } 1006 1007 // push token to list 1008 tokenList.push( {tokenType: tokenType, tokenValue: tokenValue} ); 1009 1010 // and clean up currentToken 1011 currentToken = null; 1012 currentTokenType = null; 1013 currentTokenValue = null; 1014 } 1015 1016 1017 // stepping through the string: 1018 1019 if (!inputString) return []; 1020 1021 var iStLength = inputString.length; 1022 1023 for (var i=0; i < iStLength; i++) { 1024 1025 // end reached? 1026 endOfString = (i===iStLength-1); 1027 1028 // current character 1029 c = inputString.charAt(i); 1030 1031 // set true after end of delimited token so that 1032 // final delimiter is not caught again 1033 skipThisCharacter = false; 1034 1035 1036 // if currently inside a token 1037 1038 if ( currentToken ) { 1039 1040 // some helpers 1041 t = grammar[currentToken]; 1042 endOfToken = t.delimited ? c===currentDelimiter : t.notAllowed.test(c); 1043 1044 // if still in token 1045 if ( !endOfToken ) currentTokenValue += c; 1046 1047 // if end of token reached 1048 if (endOfToken || endOfString) { 1049 addToken(currentToken, currentTokenValue); 1050 } 1051 1052 // if end of string don't check again 1053 if ( endOfString && !endOfToken ) skipThisCharacter = true; 1054 } 1055 1056 // if not inside a token, look for next one 1057 1058 if ( !currentToken && !skipThisCharacter ) { 1059 // look for matching tokenType 1060 for ( token in grammar ) { 1061 t = grammar[token]; 1062 if (t.firstCharacter && t.firstCharacter.test(c)) { 1063 currentToken = token; 1064 } 1065 } 1066 1067 // if tokenType found 1068 if ( currentToken ) { 1069 t = grammar[currentToken]; 1070 currentTokenValue = c; 1071 // handling of special cases 1072 if ( t.delimited ) { 1073 currentTokenValue = ""; 1074 if ( t.lastCharacter ) currentDelimiter = t.lastCharacter; 1075 else currentDelimiter = c; 1076 } 1077 1078 if ( t.singleCharacter || endOfString ) { 1079 addToken(currentToken, currentTokenValue); 1080 } 1081 } 1082 } 1083 } 1084 1085 return tokenList; 1086 }, 1087 1088 1089 1090 // .......................................................... 1091 // BUILD TOKEN TREE 1092 // 1093 1094 /** 1095 Takes an array of tokens and returns a tree, depending on the 1096 specified tree logic. The returned object will have an error property 1097 if building of the tree failed. Check it to get some information 1098 about what happend. 1099 If everything worked, the tree can be evaluated by calling 1100 1101 tree.evaluate(record, parameters) 1102 1103 If `tokenList` is empty, a single token will be returned which will 1104 evaluate to true for all records. 1105 1106 @param {Array} tokenList the list of tokens 1107 @param {Object} treeLogic the logic definition (normally queryLanguage) 1108 @returns {Object} token tree 1109 */ 1110 buildTokenTree: function (tokenList, treeLogic) { 1111 1112 var l = tokenList.slice(); 1113 var i = 0; 1114 var openParenthesisStack = []; 1115 var shouldCheckAgain = false; 1116 var error = []; 1117 1118 1119 // empty tokenList is a special case 1120 if (!tokenList || tokenList.length === 0) { 1121 return { evaluate: function(){ return true; } }; 1122 } 1123 1124 1125 // some helper functions 1126 1127 function tokenLogic (position) { 1128 var p = position; 1129 if ( p < 0 ) return false; 1130 1131 var tl = treeLogic[l[p].tokenType]; 1132 1133 if ( ! tl ) { 1134 error.push("logic for token '"+l[p].tokenType+"' is not defined"); 1135 return false; 1136 } 1137 1138 // save evaluate in token, so that we don't have 1139 // to look it up again when evaluating the tree 1140 l[p].evaluate = tl.evaluate; 1141 return tl; 1142 } 1143 1144 function expectedType (side, position) { 1145 var p = position; 1146 var tl = tokenLogic(p); 1147 if ( !tl ) return false; 1148 if (side === 'left') return tl.leftType; 1149 if (side === 'right') return tl.rightType; 1150 } 1151 1152 function evalType (position) { 1153 var p = position; 1154 var tl = tokenLogic(p); 1155 if ( !tl ) return false; 1156 else return tl.evalType; 1157 } 1158 1159 function removeToken (position) { 1160 l.splice(position, 1); 1161 if ( position <= i ) i--; 1162 } 1163 1164 function preceedingTokenExists (position) { 1165 var p = position || i; 1166 if ( p > 0 ) return true; 1167 else return false; 1168 } 1169 1170 function tokenIsMissingChilds (position) { 1171 var p = position; 1172 if ( p < 0 ) return true; 1173 return (expectedType('left',p) && !l[p].leftSide) || 1174 (expectedType('right',p) && !l[p].rightSide); 1175 } 1176 1177 function typesAreMatching (parent, child) { 1178 var side = (child < parent) ? 'left' : 'right'; 1179 if ( parent < 0 || child < 0 ) return false; 1180 if ( !expectedType(side,parent) ) return false; 1181 if ( !evalType(child) ) return false; 1182 if ( expectedType(side,parent) == evalType(child) ) return true; 1183 else return false; 1184 } 1185 1186 function preceedingTokenCanBeMadeChild (position) { 1187 var p = position; 1188 if ( !tokenIsMissingChilds(p) ) return false; 1189 if ( !preceedingTokenExists(p) ) return false; 1190 if ( typesAreMatching(p,p-1) ) return true; 1191 else return false; 1192 } 1193 1194 function preceedingTokenCanBeMadeParent (position) { 1195 var p = position; 1196 if ( tokenIsMissingChilds(p) ) return false; 1197 if ( !preceedingTokenExists(p) ) return false; 1198 if ( !tokenIsMissingChilds(p-1) ) return false; 1199 if ( typesAreMatching(p-1,p) ) return true; 1200 else return false; 1201 } 1202 1203 function makeChild (position) { 1204 var p = position; 1205 if (p<1) return false; 1206 l[p].leftSide = l[p-1]; 1207 removeToken(p-1); 1208 } 1209 1210 function makeParent (position) { 1211 var p = position; 1212 if (p<1) return false; 1213 l[p-1].rightSide = l[p]; 1214 removeToken(p); 1215 } 1216 1217 function removeParenthesesPair (position) { 1218 removeToken(position); 1219 removeToken(openParenthesisStack.pop()); 1220 } 1221 1222 // step through the tokenList 1223 1224 for (i = 0; i < l.length; i++) { 1225 shouldCheckAgain = false; 1226 1227 if ( l[i].tokenType === 'UNKNOWN' ) { 1228 error.push('found unknown token: '+l[i].tokenValue); 1229 } 1230 1231 if ( l[i].tokenType === 'OPEN_PAREN' ) openParenthesisStack.push(i); 1232 if ( l[i].tokenType === 'CLOSE_PAREN' ) removeParenthesesPair(i); 1233 1234 if ( preceedingTokenCanBeMadeChild(i) ) makeChild(i); 1235 1236 if ( preceedingTokenCanBeMadeParent(i) ){ 1237 makeParent(i); 1238 shouldCheckAgain = true; 1239 } 1240 1241 if ( shouldCheckAgain ) i--; 1242 1243 } 1244 1245 // error if tokenList l is not a single token now 1246 if (l.length === 1) l = l[0]; 1247 else error.push('string did not resolve to a single tree'); 1248 1249 // If we have errors, return an error object. 1250 if (error.length > 0) { 1251 return { 1252 error: error.join(',\n'), 1253 tree: l, 1254 // Conform to SC.Error. 1255 isError: YES, 1256 errorVal: function() { return this.error; } 1257 }; 1258 } 1259 // Otherwise the token list is now a tree and can be returned. 1260 else { 1261 return l; 1262 } 1263 1264 }, 1265 1266 1267 // .......................................................... 1268 // ORDERING 1269 // 1270 1271 /** 1272 Takes a string containing an order statement and returns an array 1273 describing this order for easier processing. 1274 Called by `parse()`. 1275 1276 @param {String | Function} orderOp the string containing the order statement, or a comparison function 1277 @returns {Array | Function} array of order statement, or a function if a function was specified 1278 */ 1279 buildOrder: function (orderOp) { 1280 if (!orderOp) { 1281 return []; 1282 } 1283 else if (SC.typeOf(orderOp) === SC.T_FUNCTION) { 1284 return orderOp; 1285 } 1286 else { 1287 // @if(debug) 1288 // debug mode syntax checks. 1289 // first check is position of ASC or DESC 1290 var ASCpos = orderOp.indexOf("ASC"); 1291 var DESCpos = orderOp.indexOf("DESC"); 1292 if (ASCpos > -1 || DESCpos > -1) { // if they exist 1293 if (ASCpos > -1 && (ASCpos + 3) !== orderOp.length) { 1294 SC.warn("Developer Warning: You have an orderBy syntax error in a Query, %@: ASC should be in the last position.".fmt(orderOp)); 1295 } 1296 if (DESCpos > -1 && (DESCpos + 4) !== orderOp.length) { 1297 SC.warn("Developer Warning: You have an orderBy syntax error in a Query, %@: DESC should be in the last position.".fmt(orderOp)); 1298 } 1299 } 1300 1301 // check for improper separation chars 1302 if (orderOp.indexOf(":") > -1 || orderOp.indexOf(":") > -1) { 1303 SC.warn("Developer Warning: You have an orderBy syntax error in a Query, %@: Colons or semicolons should not be used as a separation character.".fmt(orderOp)); 1304 } 1305 // @endif 1306 1307 var o = orderOp.split(','); 1308 for (var i=0; i < o.length; i++) { 1309 var p = o[i]; 1310 p = p.replace(/^\s+|\s+$/,''); 1311 p = p.replace(/\s+/,','); 1312 p = p.split(','); 1313 o[i] = {propertyName: p[0]}; 1314 if (p[1] && p[1] === 'DESC') o[i].descending = true; 1315 } 1316 1317 return o; 1318 } 1319 1320 } 1321 1322 }); 1323 1324 1325 // Class Methods 1326 SC.Query.mixin( /** @scope SC.Query */ { 1327 1328 /** 1329 Constant used for `SC.Query#location` 1330 1331 @type String 1332 */ 1333 LOCAL: 'local', 1334 1335 /** 1336 Constant used for `SC.Query#location` 1337 1338 @type String 1339 */ 1340 REMOTE: 'remote', 1341 1342 /** 1343 Given a query, returns the associated `storeKey`. For the inverse of this 1344 method see `SC.Store.queryFor()`. 1345 1346 @param {SC.Query} query the query 1347 @returns {Number} a storeKey. 1348 */ 1349 storeKeyFor: function(query) { 1350 return query ? query.get('storeKey') : null; 1351 }, 1352 1353 /** 1354 Will find which records match a give `SC.Query` and return an array of 1355 store keys. This will also apply the sorting for the query. 1356 1357 @param {SC.Query} query to apply 1358 @param {SC.RecordArray} records to search within 1359 @param {SC.Store} store to materialize record from 1360 @returns {Array} array instance of store keys matching the SC.Query (sorted) 1361 */ 1362 containsRecords: function(query, records, store) { 1363 var ret = []; 1364 for(var idx=0,len=records.get('length');idx<len;idx++) { 1365 var record = records.objectAt(idx); 1366 if(record && query.contains(record)) { 1367 ret.push(record.get('storeKey')); 1368 } 1369 } 1370 1371 ret = SC.Query.orderStoreKeys(ret, query, store); 1372 1373 return ret; 1374 }, 1375 1376 /** 1377 Sorts a set of store keys according to the orderBy property 1378 of the `SC.Query`. 1379 1380 @param {Array} storeKeys to sort 1381 @param {SC.Query} query to use for sorting 1382 @param {SC.Store} store to materialize records from 1383 @returns {Array} sorted store keys. may be same instance as passed value 1384 */ 1385 orderStoreKeys: function(storeKeys, query, store) { 1386 // apply the sort if there is one 1387 if (storeKeys) { 1388 storeKeys.sort(function(a, b) { 1389 return SC.Query.compareStoreKeys(query, store, a, b); 1390 }); 1391 } 1392 1393 return storeKeys; 1394 }, 1395 1396 /** 1397 Default sort method that is used when calling `containsStoreKeys()` 1398 or `containsRecords()` on this query. Simply materializes two records 1399 based on `storekey`s before passing on to `compare()`. 1400 1401 @param {Number} storeKey1 a store key 1402 @param {Number} storeKey2 a store key 1403 @returns {Number} -1 if record1 < record2, +1 if record1 > record2, 0 if equal 1404 */ 1405 compareStoreKeys: function(query, store, storeKey1, storeKey2) { 1406 var record1 = store.materializeRecord(storeKey1), 1407 record2 = store.materializeRecord(storeKey2); 1408 1409 return query.compare(record1, record2); 1410 }, 1411 1412 /** 1413 Returns a `SC.Query` instance reflecting the passed properties. Where 1414 possible this method will return cached query instances so that multiple 1415 calls to this method will return the same instance. This is not possible 1416 however, when you pass custom parameters or set ordering. All returned 1417 queries are frozen. 1418 1419 Usually you will not call this method directly. Instead use the more 1420 convenient `SC.Query.local()` and `SC.Query.remote()`. 1421 1422 Examples 1423 1424 There are a number of different ways you can call this method. 1425 1426 The following return local queries selecting all records of a particular 1427 type or types, including any subclasses: 1428 1429 var people = SC.Query.local(Ab.Person); 1430 var peopleAndCompanies = SC.Query.local([Ab.Person, Ab.Company]); 1431 1432 var people = SC.Query.local('Ab.Person'); 1433 var peopleAndCompanies = SC.Query.local('Ab.Person Ab.Company'.w()); 1434 1435 var allRecords = SC.Query.local(SC.Record); 1436 1437 The following will match a particular type of condition: 1438 1439 var married = SC.Query.local(Ab.Person, "isMarried=YES"); 1440 var married = SC.Query.local(Ab.Person, "isMarried=%@", [YES]); 1441 var married = SC.Query.local(Ab.Person, "isMarried={married}", { 1442 married: YES 1443 }); 1444 1445 You can also pass a hash of options as the second parameter. This is 1446 how you specify an order, for example: 1447 1448 var orderedPeople = SC.Query.local(Ab.Person, { orderBy: "firstName" }); 1449 1450 @param {String} location the query location. 1451 @param {SC.Record|Array} recordType the record type or types. 1452 @param {String} [conditions] The conditions string. 1453 @param {Object} [parameters] The parameters object. 1454 @returns {SC.Query} 1455 */ 1456 build: function(location, recordType, conditions, parameters) { 1457 1458 var opts = null, 1459 ret, cache, key, tmp; 1460 1461 // fast case for query objects. 1462 if (recordType && recordType.isQuery) { 1463 if (recordType.get('location') === location) return recordType; 1464 else return recordType.copy().set('location', location).freeze(); 1465 } 1466 1467 // normalize recordType 1468 if (typeof recordType === SC.T_STRING) { 1469 ret = SC.objectForPropertyPath(recordType); 1470 if (!ret) throw new Error("%@ did not resolve to a class".fmt(recordType)); 1471 recordType = ret ; 1472 } else if (recordType && recordType.isEnumerable) { 1473 ret = []; 1474 recordType.forEach(function(t) { 1475 if (typeof t === SC.T_STRING) t = SC.objectForPropertyPath(t); 1476 if (!t) throw new Error("cannot resolve record types: %@".fmt(recordType)); 1477 ret.push(t); 1478 }, this); 1479 recordType = ret ; 1480 } else if (!recordType) recordType = SC.Record; // find all records 1481 1482 if (parameters === undefined) parameters = null; 1483 if (conditions === undefined) conditions = null; 1484 1485 // normalize other parameters. if conditions is just a hash, treat as opts 1486 if (!parameters && (typeof conditions !== SC.T_STRING)) { 1487 opts = conditions; 1488 conditions = null ; 1489 } 1490 1491 // special case - easy to cache. 1492 if (!parameters && !opts) { 1493 1494 tmp = SC.Query._scq_recordTypeCache; 1495 if (!tmp) tmp = SC.Query._scq_recordTypeCache = {}; 1496 cache = tmp[location]; 1497 if (!cache) cache = tmp[location] = {}; 1498 1499 if (recordType.isEnumerable) { 1500 key = recordType.map(function(k) { return SC.guidFor(k); }); 1501 key = key.sort().join(':'); 1502 } else key = SC.guidFor(recordType); 1503 1504 if (conditions) key = [key, conditions].join('::'); 1505 1506 ret = cache[key]; 1507 if (!ret) { 1508 if (recordType.isEnumerable) { 1509 opts = { recordTypes: recordType.copy() }; 1510 } else opts = { recordType: recordType }; 1511 1512 opts.location = location ; 1513 opts.conditions = conditions ; 1514 ret = cache[key] = SC.Query.create(opts).freeze(); 1515 } 1516 // otherwise parse extra conditions and handle them 1517 } else { 1518 1519 if (!opts) opts = {}; 1520 if (!opts.location) opts.location = location ; // allow override 1521 1522 // pass one or more recordTypes. 1523 if (recordType && recordType.isEnumerable) { 1524 opts.recordTypes = recordType; 1525 } else opts.recordType = recordType; 1526 1527 // set conditions and parameters if needed 1528 if (conditions) opts.conditions = conditions; 1529 if (parameters) opts.parameters = parameters; 1530 1531 ret = SC.Query.create(opts).freeze(); 1532 } 1533 1534 return ret ; 1535 }, 1536 1537 /** 1538 Returns a `LOCAL` query with the passed properties. 1539 1540 For example, 1541 1542 // Show all the accounts with a value greater than 100. 1543 query = SC.Query.local(MyApp.Account, { 1544 conditions: 'value > {amt}', 1545 parameters: { amt: 100 }, 1546 orderBy: 'value DESC' 1547 }); 1548 1549 @param {SC.Record|Array} recordType the record type or types. 1550 @param {Object} [properties] Additional properties to be added to the query. 1551 @returns {SC.Query} 1552 */ 1553 local: function(recordType, properties, oldParameters) { 1554 //@if(debug) 1555 // We are going to remove all argument overloading in the framework. It adds 1556 // code bloat, increased complexity, edge case errors and makes 1557 // memorizing the API difficult. Rather than support a long list of 1558 // arguments that we can't safely collapse, it makes more sense to just 1559 // accept a properties object as the proper argument. 1560 if (SC.none(properties) && !SC.none(oldParameters) || SC.typeOf(properties) === SC.T_STRING) { 1561 SC.warn("Developer Warning: Passing a conditions string and parameters object to SC.Query.local has been deprecated. Please use a properties hash as per the documentation."); 1562 } 1563 //@endif 1564 return this.build(SC.Query.LOCAL, recordType, properties, oldParameters); 1565 }, 1566 1567 /** 1568 Returns a `REMOTE` query with the passed properties. 1569 1570 For example, 1571 1572 // The data source can alter its remote request using the value of 1573 // `query.beginsWith`. 1574 query = SC.Query.remote(MyApp.Person, { beginsWith: 'T' }); 1575 1576 @param {SC.Record|Array} recordType the record type or types. 1577 @param {Object} [properties] Additional properties to be added to the query. 1578 @returns {SC.Query} 1579 */ 1580 remote: function(recordType, properties, oldParameters) { 1581 // This used to have arguments: conditions and params. Because both 1582 // conditions and params are optional, the developer may be passing in null 1583 // conditions with a params object in order to use query.parameters or they 1584 // may be passing a conditions object which ended up becoming direct 1585 // properties of the query. 1586 // Long story short, argument overloading continues to suck ass! 1587 // @if(debug) 1588 if (SC.none(properties) && !SC.none(oldParameters) || SC.typeOf(properties) === SC.T_STRING) { 1589 SC.warn("Developer Warning: SC.Query.remote should not include conditions and parameters arguments. These properties are unique to local queries. To add properties to a remote query for the data source to use, please pass a properties hash as the second argument to `remote`."); 1590 } 1591 // @endif 1592 return this.build(SC.Query.REMOTE, recordType, properties, oldParameters); 1593 }, 1594 1595 /** @private 1596 called by `SC.Record.extend()`. invalidates `expandedRecordTypes` 1597 */ 1598 _scq_didDefineRecordType: function() { 1599 var q = SC.Query._scq_queriesWithExpandedRecordTypes; 1600 if (q) { 1601 q.forEach(function(query) { 1602 query.notifyPropertyChange('expandedRecordTypes'); 1603 }, this); 1604 q.clear(); 1605 } 1606 } 1607 1608 }); 1609 1610 1611 /** @private 1612 Hash of registered comparisons by property name. 1613 */ 1614 SC.Query.comparisons = {}; 1615 1616 /** 1617 Call to register a comparison for a specific property name. 1618 The function you pass should accept two values of this property 1619 and return -1 if the first is smaller than the second, 1620 0 if they are equal and 1 if the first is greater than the second. 1621 1622 @param {String} name of the record property 1623 @param {Function} custom comparison function 1624 @returns {SC.Query} receiver 1625 */ 1626 SC.Query.registerComparison = function(propertyName, comparison) { 1627 SC.Query.comparisons[propertyName] = comparison; 1628 }; 1629 1630 1631 /** 1632 Call to register an extension for the query language. 1633 You should provide a name for your extension and a definition 1634 specifying how it should be parsed and evaluated. 1635 1636 Have a look at `queryLanguage` for examples of definitions. 1637 1638 TODO add better documentation here 1639 1640 @param {String} tokenName name of the operator 1641 @param {Object} token extension definition 1642 @returns {SC.Query} receiver 1643 */ 1644 SC.Query.registerQueryExtension = function(tokenName, token) { 1645 SC.Query.prototype.queryLanguage[tokenName] = token; 1646 }; 1647 1648 // shorthand 1649 SC.Q = SC.Query.from ; 1650 1651