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