Created
May 15, 2009 15:17
-
-
Save thomaslang/112247 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ========================================================================== | |
| // Project: SproutCore - JavaScript Application Framework | |
| // Copyright: ©2006-2009 Sprout Systems, Inc. and contributors. | |
| // Portions ©2008-2009 Apple, Inc. All rights reserved. | |
| // License: Licened under MIT license (see license.js) | |
| // ========================================================================== | |
| require('core') ; | |
| /** | |
| @class | |
| This permits you to perform queries on your data store, | |
| written in a SQL-like language. Here is a simple example: | |
| {{{ | |
| q = SC.Query.create({ | |
| conditions: "firstName = 'Jonny' AND lastName = 'Cash'" | |
| }) | |
| }}} | |
| You can check if a certain record matches the query by calling: | |
| {{{ | |
| q.contains(record) | |
| }}} | |
| To find all records of your store, that match query q, use findAll with | |
| query q as argument: | |
| {{{ | |
| r = MyApp.store.findAll(q) | |
| }}} | |
| r will be a record array containing all matching records. | |
| To limit the query to a record type of MyApp.MyModel, | |
| you can specify the type as a property of the query like this: | |
| {{{ | |
| q = SC.Query.create({ | |
| conditions: "firstName = 'Jonny' AND lastName = 'Cash'", | |
| recordType: MyApp.MyModel | |
| }) | |
| }}} | |
| Calling findAll() like above will now return only records of type t. | |
| It is recommended to limit your query to a record type, since the query will | |
| have to look for matching records in the whole store, if no record type | |
| is given. | |
| You can give an order, which the resulting records should follow, like this: | |
| {{{ | |
| q = SC.Query.create({ | |
| conditions: "firstName = 'Jonny' AND lastName = 'Cash'", | |
| recordType: MyApp.MyModel, | |
| orderBy: "lastName, year DESC" | |
| }) | |
| }}} | |
| The default order direction is ascending. You can change it to descending | |
| by writing DESC behind the property name like in the example above. | |
| If no order is given, or records are equal in respect to a given order, | |
| records will be ordered by guid. | |
| h1. The query language | |
| h2. Primitives | |
| - record properties | |
| - null, undefined | |
| - true, false | |
| - numbers (integers and floats) | |
| - strings (double or single quoted) | |
| h2. Parameters | |
| - %@ (wild card) | |
| - {parameterName} (named parameter) | |
| Wild cards are used to identify parameters by the order in | |
| which they appear in the query string. Named parameters can be | |
| used when tracking the order becomes difficult. | |
| Both types of parameters can be used by giving the parameters | |
| as a property to your query object: | |
| yourQuery.parameters = yourParameters | |
| where yourParameters should have one of the following formats: | |
| for wild cards: [firstParam, secondParam, thirdParam] | |
| for named params: {name1: param1, mane2: parma2} | |
| You cannot use both types of parameters in a single query! | |
| h2. Operators | |
| - = | |
| - != | |
| - < | |
| - <= | |
| - > | |
| - >= | |
| - BEGINS_WITH (checks if a string starts with another one) | |
| - ENDS_WITH (checks if a string ends with another one) | |
| - MATCHES (checks if a string is matched by a regexp, | |
| you will have to use a parameter to insert the regexp) | |
| - ANY (checks if the thing on its left is contained in the array | |
| on its right, you will have to use a parameter | |
| to insert the array) | |
| - TYPE_IS (unary operator expecting a string containing the name | |
| of a Model class on its right side, only records of this type | |
| will match) | |
| h2. Boolean Operators | |
| - AND | |
| - OR | |
| - NOT | |
| h2. Parenthesis | |
| - ( and ) | |
| h2. Order of evaluation | |
| Boolean Operators are evaluated from left to right: | |
| "a AND b AND c" is equal to "(a AND b) AND c" | |
| "a AND b OR c" is equal to "(a AND b) OR c" | |
| Use parentheses to change evaluation order: | |
| "a AND (b OR c)" | |
| Some example queries: | |
| TODO add examples | |
| You can extend the query language with your own operators by calling: | |
| SC.Query.registerQueryExtension('operator_name', operator_definition) | |
| See details below. As well you can provide your own comparison functions | |
| to control ordering of specific record properties like this: | |
| SC.Query.registerComparison('property_name', comparison_for_this_property) | |
| Again see below for details. | |
| @extends SC.Object | |
| @static | |
| @since SproutCore 1.0 | |
| */ | |
| SC.Query = SC.Object.extend( /** @scope SC.Query.prototype */ { | |
| /** | |
| A string giving the conditions for the query. | |
| Example: "firstName = 'John' AND lastName = 'Baker'" | |
| @property {String} | |
| */ | |
| conditions: null, | |
| /** | |
| A string giving the order for the query. | |
| Example: "lastName, firstName" | |
| @property {String} | |
| */ | |
| orderBy: null, | |
| /** | |
| The record type this query should be limited to. | |
| If left null, the whole store will be queried. | |
| @property {SC.Record} | |
| */ | |
| recordType: null, | |
| /** | |
| Parameters for this query. Two types are supported. | |
| (1) An Array containing the parameters in the order they should be | |
| substituted for wild cards in the conditions String. | |
| (2) A Hash containing the parameters keyed by the names used | |
| by the named parameters in the conditions String. | |
| You can only use one type of parameters for each query. | |
| @property {Array, Hash} | |
| */ | |
| parameters: null, | |
| /** | |
| Returns YES if record is matched by the query, NO otherwise. | |
| @param {SC.Record} record the record to check | |
| @returns {Boolean} YES if record belongs, NO otherwise | |
| */ | |
| contains: function(record, wildCardValues) { | |
| // if called for the first time we have to parse the query | |
| if (!this.conditionsAreReady) this.parseConditions(); | |
| // if parsing worked we check if record is contained | |
| // if parsing failed no record will be contained | |
| return this.conditionsAreReady && this.tokenTree.evaluate(record, wildCardValues); | |
| }, | |
| /** | |
| Default sort method that is used when calling containsStoreKeys() | |
| or containsRecords() on this query. Simply materializes two records based | |
| on storekeys before passing on to compare() . | |
| @param {Number} storeKey1 a store key | |
| @param {Number} storeKey2 a store key | |
| @returns {Number} -1 if record1 < record2, +1 if record1 > record2, 0 if equal | |
| */ | |
| compareStoreKeys: function(storeKey1, storeKey2) { | |
| var store = SC.Query._TMP_STORE; | |
| var queryKey = SC.Query._TMP_QUERY_KEY; | |
| var record1 = store.materializeRecord(storeKey1); | |
| var record2 = store.materializeRecord(storeKey2); | |
| return queryKey.compare.call(queryKey, record1, record2); | |
| }, | |
| /** | |
| This will tell you which of the two passed records is greater | |
| than the other, in respect to the orderBy property of your SC.Query object. | |
| @param {SC.Record} record1 the first record | |
| @param {SC.Record} record2 the second record | |
| @returns {Number} -1 if record1 < record2, | |
| +1 if record1 > record2, | |
| 0 if equal | |
| */ | |
| compare: function(record1, record2) { | |
| var result; | |
| var propertyName; | |
| // if called for the first time we have to build the order array | |
| if (!this.orderIsReady) this.parseOrder(); | |
| // if parsing failed we say everything is equal | |
| if (!this.orderIsReady) return 0; | |
| // for every property specified in orderBy | |
| for (var i=0, orderLength=this.order.length ; i < orderLength; i++) { | |
| propertyName = this.order[i].propertyName; | |
| // if this property has a registered comparison use that | |
| // if not use default SC.compare() | |
| if (SC.Query.comparisons[propertyName]) { | |
| result = SC.Query.comparisons[propertyName]( | |
| record1.get(propertyName),record2.get(propertyName)); | |
| } | |
| else { | |
| result = SC.compare( | |
| record1.get(propertyName),record2.get(propertyName)); | |
| } | |
| if (result != 0) { | |
| // if order is descending we invert the sign of the result | |
| if (this.order[i].descending) result = (-1) * result; | |
| return result; | |
| } | |
| }; | |
| // all properties are equal now | |
| // get order by guid | |
| return SC.compare(record1.get('guid'),record2.get('guid')); | |
| }, | |
| /** | |
| @private | |
| Some internal properties | |
| */ | |
| conditionsAreReady: false, | |
| orderIsReady: false, | |
| tokenList: null, | |
| usedProperties: null, | |
| needsRecord: false, | |
| tokenTree: null, | |
| order: [], | |
| /** | |
| This method has to be called before the query object can be used. | |
| You will normaly not have to do this, it will be called automatically | |
| if you try to evaluate a query. | |
| You can however use this function for testing your queries. | |
| @returns {Boolean} true if parsing succeeded, false otherwise | |
| */ | |
| parseConditions: function() { | |
| this.tokenList = this.tokenizeString(this.conditions, this.queryLanguage); | |
| this.tokenTree = this.buildTokenTree(this.tokenList, this.queryLanguage); | |
| if ( !this.tokenTree || this.tokenTree.error ) { | |
| return false; | |
| } | |
| else { | |
| this.conditionsAreReady = true; | |
| return true; | |
| } | |
| }, | |
| /** | |
| This method has to be called before ordering of the query object can be used. | |
| You will normaly not have to do this, it will be called automatically | |
| if you try to evaluate a query. | |
| You can however use this function for testing your queries. | |
| @returns {Boolean} true if parsing succeeded, false otherwise | |
| */ | |
| parseOrder: function() { | |
| this.order = this.buildOrder(this.orderBy); | |
| if (this.order) { | |
| this.orderIsReady = true; | |
| return true; | |
| } | |
| else return false; | |
| }, | |
| // .......................................................... | |
| // QUERY LANGUAGE DEFINITION | |
| // | |
| /** | |
| @private | |
| This is the definition of the query language. You can extend it | |
| by using SC.Query.registerQueryExtension(). | |
| */ | |
| queryLanguage: { | |
| 'UNKNOWN': { | |
| firstCharacter: /[^\s'"\w\d\(\)\{\}]/, | |
| notAllowed: /[\s'"\w\d\(\)\{\}]/ | |
| }, | |
| 'PROPERTY': { | |
| firstCharacter: /[a-zA-Z_]/, | |
| notAllowed: /[^a-zA-Z_0-9]/, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return r.get(this.tokenValue); } | |
| }, | |
| 'NUMBER': { | |
| firstCharacter: /\d/, | |
| notAllowed: /[^\d\.]/, | |
| format: /^\d+$|^\d+\.\d+$/, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return parseFloat(this.tokenValue); } | |
| }, | |
| 'STRING': { | |
| firstCharacter: /['"]/, | |
| delimeted: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return this.tokenValue; } | |
| }, | |
| 'PARAMETER': { | |
| firstCharacter: /\{/, | |
| lastCharacter: '}', | |
| delimeted: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return w[this.tokenValue]; } | |
| }, | |
| '%@': { | |
| rememberCount: true, | |
| reservedWord: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return w[this.tokenValue]; } | |
| }, | |
| 'OPEN_PAREN': { | |
| firstCharacter: /\(/, | |
| singleCharacter: true | |
| }, | |
| 'CLOSE_PAREN': { | |
| firstCharacter: /\)/, | |
| singleCharacter: true | |
| }, | |
| 'AND': { | |
| reservedWord: true, | |
| leftType: 'BOOLEAN', | |
| rightType: 'BOOLEAN', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left && right; | |
| } | |
| }, | |
| 'OR': { | |
| reservedWord: true, | |
| leftType: 'BOOLEAN', | |
| rightType: 'BOOLEAN', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left || right; | |
| } | |
| }, | |
| 'NOT': { | |
| reservedWord: true, | |
| rightType: 'BOOLEAN', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var right = this.rightSide.evaluate(r,w); | |
| return !right; | |
| } | |
| }, | |
| '=': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left == right; | |
| } | |
| }, | |
| '!=': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left != right; | |
| } | |
| }, | |
| '<': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left < right; | |
| } | |
| }, | |
| '<=': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left <= right; | |
| } | |
| }, | |
| '>': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left > right; | |
| } | |
| }, | |
| '>=': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var left = this.leftSide.evaluate(r,w); | |
| var right = this.rightSide.evaluate(r,w); | |
| return left >= right; | |
| } | |
| }, | |
| 'BEGINS_WITH': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var all = this.leftSide.evaluate(r,w); | |
| var start = this.rightSide.evaluate(r,w); | |
| return ( all.substr(0,start.length) == start ); | |
| } | |
| }, | |
| 'ENDS_WITH': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var all = this.leftSide.evaluate(r,w); | |
| var end = this.rightSide.evaluate(r,w); | |
| var suf = all.substring(all.length-end.length,all.length); | |
| return suf == end; | |
| } | |
| }, | |
| 'ANY': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var prop = this.leftSide.evaluate(r,w); | |
| var values = this.rightSide.evaluate(r,w); | |
| var found = false; | |
| var i = 0; | |
| while ( found==false && i<values.length ) { | |
| if ( prop == values[i] ) found = true; | |
| i++; | |
| }; | |
| return found; | |
| } | |
| }, | |
| 'MATCHES': { | |
| reservedWord: true, | |
| leftType: 'PRIMITIVE', | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var toMatch = this.leftSide.evaluate(r,w); | |
| var matchWith = this.rightSide.evaluate(r,w); | |
| return matchWith.test(toMatch); | |
| } | |
| }, | |
| 'TYPE_IS': { | |
| reservedWord: true, | |
| rightType: 'PRIMITIVE', | |
| evalType: 'BOOLEAN', | |
| evaluate: function (r,w) { | |
| var actualType = SC.Store.recordTypeFor(r.storeKey); | |
| var right = this.rightSide.evaluate(r,w); | |
| var expectType = SC.objectForPropertyPath(right); | |
| return actualType == expectType; | |
| } | |
| }, | |
| 'null': { | |
| reservedWord: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return null; } | |
| }, | |
| 'undefined': { | |
| reservedWord: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return undefined; } | |
| }, | |
| 'false': { | |
| reservedWord: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return false; } | |
| }, | |
| 'true': { | |
| reservedWord: true, | |
| evalType: 'PRIMITIVE', | |
| evaluate: function (r,w) { return true; } | |
| } | |
| }, | |
| // .......................................................... | |
| // TOKENIZER | |
| // | |
| /** | |
| @private | |
| Takes a string and tokenizes it based on the grammar definition | |
| provided. Called by parseQuery(). | |
| @param {String} inputString the string to tokenize | |
| @param {Object} grammar the grammar definition (normally queryLanguage) | |
| @returns {Array} list of tokens | |
| */ | |
| tokenizeString: function (inputString, grammar) { | |
| var tokenList = []; | |
| var c = null; | |
| var t = null; | |
| var token = null; | |
| var tokenType = null; | |
| var currentToken = null; | |
| var currentTokenType = null; | |
| var currentTokenValue = null; | |
| var currentDelimeter = null; | |
| var endOfString = false; | |
| var endOfToken = false; | |
| var belongsToToken = false; | |
| var skipThisCharacter = false; | |
| var rememberCount = {}; | |
| // helper function that adds tokens to the tokenList | |
| function addToken (tokenType, tokenValue) { | |
| t = grammar[tokenType]; | |
| //tokenType = t.tokenType; | |
| // handling of special cases | |
| // check format | |
| if ( t.format && !t.format.test(tokenValue) ) | |
| tokenType = "UNKNOWN"; | |
| // delimeted token (e.g. by ") | |
| if ( t.delimeted ) | |
| skipThisCharacter = true; | |
| // reserved words | |
| if ( !t.delimeted ) { | |
| for ( var anotherToken in grammar ) { | |
| if ( grammar[anotherToken].reservedWord | |
| && anotherToken == tokenValue ) { | |
| tokenType = anotherToken; | |
| } | |
| } | |
| }; | |
| // reset t | |
| t = grammar[tokenType]; | |
| // remembering count type | |
| if ( t && t.rememberCount ) { | |
| if (!rememberCount[tokenType]) rememberCount[tokenType] = 0; | |
| tokenValue = rememberCount[tokenType]; | |
| rememberCount[tokenType] += 1; | |
| }; | |
| // push token to list | |
| if (t.reservedWord && !t.rememberCount) { | |
| tokenList.push( {tokenType: tokenType} ); | |
| } | |
| else { | |
| tokenList.push( {tokenType: tokenType, tokenValue: tokenValue} ); | |
| } | |
| // and clean up currentToken | |
| currentToken = null; | |
| currentTokenType = null; | |
| currentTokenValue = null; | |
| }; | |
| // stepping through the string: | |
| if (!inputString) return []; | |
| for (var i=0; i < inputString.length; i++) { | |
| // end reached? | |
| endOfString = (i==inputString.length-1); | |
| // current character | |
| c = inputString[i]; | |
| // set true after end of delimeted token so that | |
| // final delimeter is not catched again | |
| skipThisCharacter = false; | |
| // if currently inside a token | |
| if ( currentToken ) { | |
| // some helpers | |
| t = grammar[currentToken]; | |
| endOfToken = (t.delimeted) ? c==currentDelimeter : t.notAllowed.test(c); | |
| // if still in token | |
| if ( !endOfToken ) | |
| currentTokenValue += c; | |
| // if end of token reached | |
| if ( endOfToken || endOfString ) | |
| addToken(currentToken, currentTokenValue); | |
| // if end of string don't check again | |
| if ( endOfString && !endOfToken ) | |
| skipThisCharacter = true; | |
| }; | |
| // if not inside a token, look for next one | |
| if ( !currentToken && !skipThisCharacter ) { | |
| // look for matching tokenType | |
| for ( token in grammar ) { | |
| t = grammar[token]; | |
| if ( t.firstCharacter && t.firstCharacter.test(c) ) | |
| currentToken = token; | |
| }; | |
| // if tokenType found | |
| if ( currentToken ) { | |
| t = grammar[currentToken]; | |
| currentTokenValue = c; | |
| // handling of special cases | |
| if ( t.delimeted ) { | |
| currentTokenValue = ""; | |
| if ( t.lastCharacter ) | |
| currentDelimeter = t.lastCharacter; | |
| else | |
| currentDelimeter = c; | |
| }; | |
| if ( t.singleCharacter || endOfString ) | |
| addToken(currentToken, currentTokenValue); | |
| }; | |
| }; | |
| }; | |
| return tokenList; | |
| }, | |
| // .......................................................... | |
| // BUILD TOKEN TREE | |
| // | |
| /** | |
| @private | |
| Takes an array of tokens and returns a tree, depending on the | |
| specified tree logic. Called by parseQuery(). | |
| The returned object will have an error property if building of the | |
| tree failed. Check it to get some information about what happend. | |
| If everything worked the tree can be evaluated by calling: | |
| tree.evaluate(record, parameters) | |
| If an empty token list is passed, a single token will be returned | |
| which will evaluate to true for all records. | |
| @param {Array} tokenList the list of tokens | |
| @param {Object} treeLogic the logic definition (normally queryLanguage) | |
| @returns {Object} token tree | |
| */ | |
| buildTokenTree: function (tokenList, treeLogic) { | |
| var l = tokenList.slice(); | |
| var i = 0; | |
| var openParenthesisStack = []; | |
| var shouldCheckAgain = false; | |
| var error = []; | |
| // empty tokenList is a special case | |
| if (!tokenList || tokenList.length == 0){ | |
| return {evaluate: function(){return true;}}; | |
| } | |
| // some helper functions | |
| function tokenLogic (position) { | |
| var p = position; | |
| if ( p < 0 ) return false; | |
| tl = treeLogic[l[p].tokenType]; | |
| if ( ! tl ) { | |
| error.push("logic for token '"+l[p].tokenType+"' is not defined"); | |
| return false; | |
| }; | |
| // save evaluate in token, so that we don't have | |
| // to look it up again when evaluating the tree | |
| l[p].evaluate = tl.evaluate; | |
| return tl; | |
| }; | |
| function expectedType (side, position) { | |
| var p = position; | |
| var tl = tokenLogic(p); | |
| if ( !tl ) return false; | |
| if (side == 'left') return tl.leftType; | |
| if (side == 'right') return tl.rightType; | |
| }; | |
| function evalType (position) { | |
| var p = position; | |
| var tl = tokenLogic(p); | |
| if ( !tl ) return false; | |
| else return tl.evalType; | |
| }; | |
| function removeToken (position) { | |
| l.splice(position, 1); | |
| if ( position <= i ) i--; | |
| }; | |
| function preceedingTokenExists (position) { | |
| var p = position || i; | |
| if ( p > 0 ) return true; | |
| else return false; | |
| }; | |
| function tokenIsMissingChilds (position) { | |
| var p = position; | |
| if ( p < 0 ) return true; | |
| if (( expectedType('left',p) && !l[p].leftSide ) | |
| || ( expectedType('right',p) && !l[p].rightSide )) | |
| return true; | |
| else return false; | |
| }; | |
| function typesAreMatching (parent, child) { | |
| var side = (child < parent) ? 'left' : 'right'; | |
| if ( parent < 0 || child < 0 ) return false; | |
| if ( !expectedType(side,parent) ) return false; | |
| if ( !evalType(child) ) return false; | |
| if ( expectedType(side,parent) == evalType(child) ) return true; | |
| else return false; | |
| }; | |
| function preceedingTokenCanBeMadeChild (position) { | |
| var p = position; | |
| if ( !tokenIsMissingChilds(p) ) return false; | |
| if ( !preceedingTokenExists(p) ) return false; | |
| if ( typesAreMatching(p,p-1) ) return true; | |
| else return false; | |
| }; | |
| function preceedingTokenCanBeMadeParent (position) { | |
| var p = position; | |
| if ( tokenIsMissingChilds(p) ) return false; | |
| if ( !preceedingTokenExists(p) ) return false; | |
| if ( !tokenIsMissingChilds(p-1) ) return false; | |
| if ( typesAreMatching(p-1,p) ) return true; | |
| else return false; | |
| }; | |
| function makeChild (position) { | |
| var p = position; | |
| if (p<1) return false; | |
| l[p].leftSide = l[p-1]; | |
| removeToken(p-1); | |
| }; | |
| function makeParent (position) { | |
| var p = position; | |
| if (p<1) return false; | |
| l[p-1].rightSide = l[p]; | |
| removeToken(p); | |
| }; | |
| function removeParenthesesPair (position) { | |
| removeToken(position); | |
| removeToken(openParenthesisStack.pop()); | |
| }; | |
| // step through the tokenList | |
| for (i=0; i < l.length; i++) { | |
| shouldCheckAgain = false; | |
| if ( l[i].tokenType == 'UNKNOWN' ) | |
| error.push('found unknown token: '+l[i].tokenValue); | |
| if ( l[i].tokenType == 'OPEN_PAREN' ) | |
| openParenthesisStack.push(i); | |
| if ( l[i].tokenType == 'CLOSE_PAREN' ) | |
| removeParenthesesPair(i); | |
| if ( preceedingTokenCanBeMadeChild(i) ) | |
| makeChild(i); | |
| if ( preceedingTokenCanBeMadeParent(i) ){ | |
| makeParent(i); | |
| shouldCheckAgain = true; | |
| } | |
| if ( shouldCheckAgain ) i--; | |
| }; | |
| // error if tokenList l is not a single token now | |
| if (l.length == 1) l = l[0]; | |
| else error.push('string did not resolve to a single tree'); | |
| // error? | |
| if (error.length > 0) return {error: error.join(',\n'), tree: l}; | |
| // everything fine - token list is now a tree and can be returned | |
| else return l; | |
| }, | |
| // .......................................................... | |
| // ORDERING | |
| // | |
| /** | |
| @private | |
| Takes a string containing an order statement and returns an array | |
| describing this order for easier processing. | |
| Called by parseQuery(). | |
| @param {String} orderString the string containing the order statement | |
| @returns {Array} array of order statement | |
| */ | |
| buildOrder: function (orderString) { | |
| if (!orderString) { | |
| return []; | |
| } | |
| else { | |
| var o = orderString.split(','); | |
| for (var i=0; i < o.length; i++) { | |
| var p = o[i]; | |
| p = p.replace(/^\s+|\s+$/,''); | |
| p = p.replace(/\s+/,','); | |
| p = p.split(','); | |
| o[i] = {propertyName: p[0]}; | |
| if (p[1] && p[1] == 'DESC') o[i].descending = true; | |
| }; | |
| return o; | |
| } | |
| }, | |
| // .......................................................... | |
| // OTHER HELPERS | |
| // not used right now | |
| // propertiesUsedInQuery: function (tokenList) { | |
| // var propertyList = []; | |
| // for (var i=0; i < tokenList.length; i++) { | |
| // if (tokenList[i].tokenType == 'PROPERTY') propertyList.push(tokenList[i].tokenValue); | |
| // }; | |
| // return propertyList; | |
| // } | |
| // .......................................................... | |
| // INTERNAL SUPPORT | |
| // | |
| /** @private | |
| Invoked whenever the conditions sting changes. Observes changes. | |
| */ | |
| _conditionsDidChange: function() { | |
| this.conditionsAreReady = false; | |
| }.observes('conditions'), | |
| /** @private | |
| Invoked whenever the orderBy sting changes. Observes changes. | |
| */ | |
| _orderByDidChange: function() { | |
| this.orderIsReady = false; | |
| }.observes('orderBy') | |
| }); | |
| // Class Methods | |
| SC.Query.mixin( /** @scope SC.Query */ { | |
| /** | |
| Will find which records match a given SC.Query and return the storeKeys. | |
| This will also apply the sorting for the query | |
| @param {SC.Query} query query to apply | |
| @param {Array} storeKeys storeKeys to search within | |
| @param {SC.Store} store store to materialize record from during sort | |
| @returns {Array} array instance of store keys matching the SC.Query (sorted) | |
| */ | |
| containsStoreKeys: function(query, storeKeys, store) { | |
| var ret = []; | |
| var recType = query.get('recordType'); | |
| // if storeKeys is not set, just get all storeKeys for this record type, | |
| // or all storeKeys in store if no record type is given | |
| if(!storeKeys) { | |
| if(recType) { | |
| storeKeys = store.storeKeysFor(recType); | |
| } | |
| else { | |
| storeKeys = store.storeKeys(); | |
| } | |
| } | |
| for(var idx=0,len=storeKeys.length;idx<len;idx++) { | |
| var record = store.materializeRecord(storeKeys[idx]); | |
| if(record && query.contains(record)) ret.push(storeKeys[idx]); | |
| } | |
| SC.Query.orderStoreKeys(ret, query, store); | |
| return ret; | |
| }, | |
| /** | |
| Will find which records match a given SC.Query and return an array of | |
| store keys. This will also apply the sorting for the query. | |
| @param {SC.Query} query query to apply | |
| @param {SC.RecordArray} records records to search within | |
| @param {SC.Store} store store to materialize record from | |
| @returns {Array} array instance of store keys matching the SC.Query (sorted) | |
| */ | |
| containsRecords: function(query, records, store) { | |
| var ret = []; | |
| for(var idx=0,len=records.get('length');idx<len;idx++) { | |
| var record = records.objectAt(idx); | |
| if(record && query.contains(record)) { | |
| ret.push(record.get('storeKey')); | |
| } | |
| } | |
| SC.Query.orderStoreKeys(ret, query, store); | |
| return ret; | |
| }, | |
| /** | |
| Sorts a set of store keys according to the orderBy property | |
| of the SC.Query. | |
| @param {Array} storeKeys to sort | |
| @param {SC.Query} query to use for sorting | |
| @param {SC.Store} store to materialize records from | |
| */ | |
| orderStoreKeys: function(storeKeys, query, store) { | |
| // apply the sort if there is one | |
| if(query.get('orderBy') && storeKeys) { | |
| // TODO: hack for now to get around the fact that we cannot pass | |
| // additional parameters to .sort() | |
| SC.Query._TMP_STORE = store; | |
| SC.Query._TMP_QUERY_KEY = query; | |
| storeKeys.sort(query.compareStoreKeys); | |
| delete SC.Query._TMP_STORE; | |
| delete SC.Query._TMP_QUERY_KEY; | |
| } | |
| } | |
| }); | |
| /** @private | |
| Hash of registered comparisons by propery name. | |
| */ | |
| SC.Query.comparisons = {}; | |
| /** | |
| Call to register a comparison for a specific property name. | |
| The function you pass should accept two values of this property | |
| and return -1 if the first is smaller than the second, | |
| 0 if they are equal and 1 if the first is greater than the second. | |
| @param {String} propertyName name of the record property | |
| @param {Function} comparison custom comparison function | |
| */ | |
| SC.Query.registerComparison = function(propertyName, comparison) { | |
| SC.Query.comparisons[propertyName] = comparison; | |
| }; | |
| /** | |
| Call to register an extension for the query language. | |
| You shoud provide a name for your extension and a definition | |
| specifying how it should be parsed and evaluated. | |
| Have a look at SC.Query.queryLanguage for examples of definitions. | |
| TODO add better documentation here | |
| @param {String} tokenName name of the operator | |
| @param {Object} token extension definition | |
| */ | |
| SC.Query.registerQueryExtension = function(tokenName, token) { | |
| SC.Query.prototype.queryLanguage[tokenName] = token; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment