From 8faeb1e303a4e6a912eed2571d7239b293e79a43 Mon Sep 17 00:00:00 2001 From: Kevin Miller Date: Mon, 28 Jan 2013 19:00:09 -0800 Subject: [PATCH 1/6] Fixed link to stack overflow in docs. --- docs/client/introduction.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/client/introduction.html b/docs/client/introduction.html index 33dd87b5b5..0c130eaacf 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -114,8 +114,8 @@ with the project!
Stack Overflow
-
The best place to ask (and answer!) technical questions is on [Stack - Overflow](http://stackoverflow.com/questions/tagged/meteor). Be sure to add +
The best place to ask (and answer!) technical questions is on Stack + Overflow. Be sure to add the meteor tag to your question.
From 6b8ca56a417761bca527dcdb189ba6f492598df6 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Sat, 2 Feb 2013 15:57:28 -0800 Subject: [PATCH 2/6] Use Twitter API v1.1 to verify_credentials --- packages/accounts-twitter/twitter_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 0a19516159..2ae037b777 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,7 +1,7 @@ (function () { Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { - var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json').data; + var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json').data; return { serviceData: { From 012e1947be8ce177ab145e43262ff23b83419ed5 Mon Sep 17 00:00:00 2001 From: Christian Schramm Date: Wed, 14 Nov 2012 18:21:30 +0100 Subject: [PATCH 3/6] Replaced _compileSelector logic with non-eval'd code. Implement $elemMatch. (Commits squashed together by glasser, and some now-dead code removed.) --- packages/minimongo/minimongo_tests.js | 10 + packages/minimongo/selector.js | 664 ++++++++++++-------------- 2 files changed, 322 insertions(+), 352 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index d1629e40f8..f6cf2608f9 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -795,11 +795,21 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({$where: "_.isArray(this.a)"}, {a: []}); nomatch({$where: "_.isArray(this.a)"}, {a: 1}); + // reaching into array match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); match({"dogs.1.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); match({"room.1b": "bla"}, {room: {"1b": "bla"}}); + // $elemMatch + match({dogs: {$elemMatch: {name: /e/}}}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + nomatch({dogs: {$elemMatch: {name: /a/}}}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({dogs: {$elemMatch: {age: {$gt: 4}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + // XXX still needs tests: // - $elemMatch // - non-scalar arguments to $gt, $lt, etc diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 5cc6e3ac4c..a4c0dcfe42 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -1,53 +1,289 @@ -// helpers used by compiled selector code -LocalCollection._f = { - // XXX for _all and _in, consider building 'inquery' at compile time.. +LocalCollection._contains = function (list, obj) { + var objStr = JSON.stringify(obj); + for (var i = 0, len_i = list.length; i < len_i; i++) { + if (objStr === JSON.stringify(list[i])) + return true; + } + return false; +} - _all: function (x, qval) { - // $all is only meaningful on arrays - if (!(x instanceof Array)) { +LocalCollection._containsSome = function (list, otherList) { + for (var i = 0, len_i = list.length; i < len_i; i++) { + var listObjStr = JSON.stringify(list[i]); + for (var j = 0, len_j = otherList.length; j < len_j; j++) { + if (listObjStr === JSON.stringify(otherList[j])) + return true; + } + } + return false; +} + +LocalCollection._containsAll = function (list, otherList) { + for (var i = 0, len_i = list.length; i < len_i; i++) { + var listObjStr = JSON.stringify(list[i]); + var matches = false; + for (var j = 0, len_j = otherList.length; j < len_j; j++) { + if (listObjStr === JSON.stringify(otherList[j])) { + matches = true; + break; + } + } + if (!matches) return false; - } - // XXX should use a canonicalizing representation, so that we - // don't get screwed by key order - var parts = {}; - var remaining = 0; - _.each(qval, function (q) { - var hash = JSON.stringify(q); - if (!(hash in parts)) { - parts[hash] = true; - remaining++; - } - }); + } + return true; +} - for (var i = 0; i < x.length; i++) { - var hash = JSON.stringify(x[i]); - if (parts[hash]) { - delete parts[hash]; - remaining--; - if (0 === remaining) - return true; +LocalCollection._gt = function (otherVal, val) { + if ((val === null) || (otherVal === null)) { + return true; + } else if (_.isObject(otherVal) && _.isObject(val)) { + // XXX: find material about actual semantics + var minOtherVal = _.min(_.flatten(_.values(otherVal))); + var minVal = _.min(_.flatten(_.values(val))); + return minOtherVal > minVal; + } else if (_.isArray(otherVal)) { + return _.max(otherVal) > val; + } else { + return otherVal > val; + } + +} + +LocalCollection._lt = function (otherVal, val) { + if ((val === null) || (otherVal === null)) { + return true; + } else if (_.isObject(otherVal) && _.isObject(val)) { + var minOtherVal = _.min(_.flatten(_.values(otherVal))); + var minVal = _.min(_.flatten(_.values(val))); + return minOtherVal < minVal; + } else if (_.isArray(otherVal)) { + return _.min(otherVal) < val; + } else { + return otherVal < val; + } + +} + +LocalCollection._checkType = function(type, value) { + switch (type) { + case 1: + return _.isNumber(value); + case 2: + return _.isString(value); + case 3: + return value instanceof Object; + case 4: + return _.isArray(value); + case 8: + return _.isBoolean(value) + case 10: + return value === null; + case 11: + return _.isRegExp(value); + case 13: + return _.isFunction(value); + default: + return false; + // XXX support some/all of these: + // 5, binary data + // 7, object id + // 9, date + // 14, symbol + // 15, javascript code with scope + // 16, 18: 32-bit/64-bit integer + // 17, timestamp + // 255, minkey + // 127, maxkey + } +} + +LocalCollection._hasOperators = function(selectorValue) { + for (var selKey in selectorValue) { + if (selKey.charCodeAt(0) === 36) // $ + return true; + } + return false; +} + +LocalCollection._evaluateSelectorValue = function(selectorValue, docValue) { + if (!_.isObject(selectorValue)) { + // Most common case: Primitive comparison or containment (e.g. `_id: `) + return selectorValue === docValue || _.contains(docValue, selectorValue) || + selectorValue === null && docValue === undefined; + } else { + if (_.isArray(selectorValue)) { + // Deep comparison or containment check. + return JSON.stringify(selectorValue) === JSON.stringify(docValue) || LocalCollection._contains(docValue, selectorValue); + } else if (_.isRegExp(selectorValue)) { + return selectorValue.test(docValue); + } else { + // It's an object, but not an array or regexp. + if (LocalCollection._hasOperators(selectorValue)) { + // This one has operators in it, let's evaluate them. + for (var selKey in selectorValue) { + if (!LocalCollection._comparisonOperators[selKey](selectorValue[selKey], docValue, selectorValue)) + return false; + } + return true; + } else { + // There are no operators, so compare it to the document value + // (via JSON.stringify, b/c that preserves key order). + return JSON.stringify(selectorValue) === JSON.stringify(docValue) || + LocalCollection._contains(docValue, selectorValue); } } + } +} +LocalCollection._logicalOperators = { + "$and": function(selectorValue, docBranch) { + if (selectorValue.length === 0) + throw Error("$and/$or/$nor must be nonempty array"); + for (var i = 0, len_i = selectorValue.length; i < len_i; i++) { + if (!(LocalCollection._evaluateSelector(selectorValue[i], docBranch))) + return false; + } + return true; + }, + + "$or": function(selectorValue, docBranch) { + if (selectorValue.length === 0) + throw Error("$and/$or/$nor must be nonempty array"); + for (var i = 0, len_i = selectorValue.length; i < len_i; i++) { + if (LocalCollection._evaluateSelector(selectorValue[i], docBranch)) + return true; + } return false; }, - _in: function (x, qval) { - if (typeof x !== "object") { - // optimization: use scalar equality (fast) - for (var i = 0; i < qval.length; i++) - if (x === qval[i]) + "$nor": function(selectorValue, docBranch) { + if (selectorValue.length === 0) + throw Error("$and/$or/$nor must be nonempty array"); + for (var i = 0, len_i = selectorValue.length; i < len_i; i++) { + if (LocalCollection._evaluateSelector(selectorValue[i], docBranch)) + return false; + } + return true; + }, + + "$where": function(selectorValue, docBranch) { + return Function("return " + selectorValue).call(docBranch); + }, +} + +LocalCollection._comparisonOperators = { + "$in": function(selectorValue, docValue) { + return LocalCollection._contains(selectorValue, docValue) || + LocalCollection._containsSome(selectorValue, docValue); + }, + + "$all": function(selectorValue, docValue) { + return _.isArray(selectorValue) && LocalCollection._containsAll(selectorValue, docValue) || + LocalCollection._contains(selectorValue, docValue); + }, + + "$lt": function(selectorValue, docValue) { + return LocalCollection._lt(docValue, selectorValue); + }, + + "$lte": function(selectorValue, docValue) { + return _.isEqual(selectorValue, docValue) || LocalCollection._lt(docValue, selectorValue); + }, + + "$gt": function(selectorValue, docValue) { + return LocalCollection._gt(docValue, selectorValue); + }, + + "$gte": function(selectorValue, docValue) { + return _.isEqual(selectorValue, docValue) || LocalCollection._gt(docValue, selectorValue); + }, + + "$ne": function(selectorValue, docValue) { + return !(selectorValue === docValue || + JSON.stringify(selectorValue) === JSON.stringify(docValue) || + _.contains(docValue, selectorValue) && + LocalCollection._contains(docValue, selectorValue)); + }, + + "$nin": function(selectorValue, docValue) { + return docValue === undefined || + !(LocalCollection._contains(selectorValue, docValue)) && + !LocalCollection._containsSome(selectorValue, docValue); + }, + + "$exists": function(selectorValue, docValue) { + return selectorValue === (docValue !== undefined) + }, + + "$mod": function(selectorValue, docValue) { + var divisor = selectorValue[0], + remainder = selectorValue[1] + if (_.isArray(docValue)) { + for (var i = 0, len_i = docValue.length; i < len_i; i++) { + if (docValue[i] % divisor === remainder) return true; + } return false; } else { - // nope, have to use deep equality - for (var i = 0; i < qval.length; i++) - if (LocalCollection._f._equal(x, qval[i])) - return true; - return false; + return docValue % divisor === remainder; } }, + "$size": function(selectorValue, docValue) { + return _.isArray(docValue) && selectorValue === docValue.length; + }, + + "$type": function(selectorValue, docValue) { + if (_.isArray(docValue)) { + for (var i = 0, len_i = docValue.length; i < len_i; i++) { + if (LocalCollection._checkType(selectorValue, docValue[i])) + return true; + } + return false; + } else { + return LocalCollection._checkType(selectorValue, docValue); + } + }, + + "$regex": function(selectorValue, docValue, selectorBranch) { + var options = selectorBranch["$options"]; + if (selectorValue instanceof RegExp) { + if (options === undefined) { + return selectorValue.test(docValue); + } else { + // If there are options given with $options, we use them instead + // and construct the rexeg anew from its .source. + return new RegExp(selectorValue.source, options).test(docValue); + } + } else { + return new RegExp(selectorValue, options).test(docValue); + } + }, + + "$options": function(selectorValue, docValue) { + // evaluation happens at the $regex function above + return true; + }, + + "$elemMatch": function(selectorValue, docValue) { + for (var i = 0, len_i = docValue.length; i < len_i; i++) { + if (LocalCollection._evaluateSelector(selectorValue, docValue[i])) + return true; + } + return false; + }, + + "$not": function(selectorValue, docValue) { + return !(LocalCollection._evaluateSelectorValue(selectorValue, docValue)); + }, + +} + +// helpers used by compiled selector code +LocalCollection._f = { + // XXX for _all and _in, consider building 'inquery' at compile time.. + _type: function (v) { if (typeof v === "number") return 1; @@ -141,36 +377,6 @@ LocalCollection._f = { return match(x, qval); }, - // if x is not an array, true iff f(x) is true. if x is an array, - // true iff f(y) is true for any y in x. - // - // this is the way most mongo operators (like $gt, $mod, $type..) - // treat their arguments. - _matches: function (x, f) { - if (x instanceof Array) { - for (var i = 0; i < x.length; i++) - if (f(x[i])) - return true; - return false; - } - return f(x); - }, - - // like _matches, but if x is an array, it's true not only if f(y) - // is true for some y in x, but also if f(x) is true. - // - // this is the way mongo value comparisons usually work, like {x: - // 4}, {x: [4]}, or {x: {$in: [1,2,3]}}. - _matches_plus: function (x, f) { - if (x instanceof Array) { - for (var i = 0; i < x.length; i++) - if (f(x[i])) - return true; - // fall through! - } - return f(x); - }, - // maps a type code to a value that can be used to sort values of // different types _typeorder: function (t) { @@ -258,6 +464,45 @@ LocalCollection._matches = function (selector, doc) { return (LocalCollection._compileSelector(selector))(doc); }; +// The main evaluation function for a given selector. +LocalCollection._evaluateSelector = function(selectorBranch, docBranch) { + try { + for (var innerKey in selectorBranch) { + var selectorValue = selectorBranch[innerKey]; + if (innerKey.charCodeAt(0) === 36) { // $ + // Outer operators are either logigal operators (they recurse back into + // this function), or $where. + if (!LocalCollection._logicalOperators[innerKey](selectorValue, docBranch)) + return false; + } else { + if (innerKey.indexOf(".") >= 0) { + // If the innerKey uses dot-notation, we move up to the last layer. + // Somehow, this magically works with reaching into arrays as well. + var keyParts = innerKey.split("."); + var docValue = docBranch; + for (var i = 0, len_i = keyParts.length; i < len_i; i++) { + docValue = docValue[keyParts[i]]; + } + } else { + docValue = docBranch[innerKey]; + } + // Here could be logical operators, containment, or comparisons. + if (!LocalCollection._evaluateSelectorValue(selectorValue, docValue)) + return false; + } + } + } catch (e) { + // If type errors occur (like checking in a non-existing array), + // we simply return false. Every other error is re-thrown. + if (!(e instanceof TypeError)) + throw e + return false; + } + // We should have returned whenever something evaluated to false, + // so it must be true. + return true; +}; + // Given a selector, return a function that takes one argument, a // document, and returns true if the document matches the selector, // else false. @@ -270,302 +515,17 @@ LocalCollection._compileSelector = function (selector) { // shorthand -- scalars match _id if (LocalCollection._selectorIsId(selector)) selector = {_id: selector}; - + // protect against dangerous selectors. falsey and {_id: falsey} // are both likely programmer error, and not what you want, // particularly for destructive operations. if (!selector || (('_id' in selector) && !selector._id)) return function (doc) {return false;}; - // eval() does not return a value in IE8, nor does the spec say it - // should. Assign to a local to get the value, instead. - var _func; - eval("_func = (function(f,literals){return function(doc){return " + - LocalCollection._exprForSelector(selector, literals) + - ";};})"); - return _func(LocalCollection._f, literals); + return function(doc) {return LocalCollection._evaluateSelector(selector, doc);}; }; // Is this selector just shorthand for lookup by _id? LocalCollection._selectorIsId = function (selector) { return (typeof selector === "string") || (typeof selector === "number"); }; - -// Given an arbitrary Mongo-style query selector, return an expression -// that evaluates to true if the document in 'doc' matches the -// selector, else false. -LocalCollection._exprForSelector = function (selector, literals) { - var clauses = []; - for (var key in selector) { - var value = selector[key]; - - if (key.substr(0, 1) === '$') { // no indexing into strings on IE7 - // whole-document predicate like {$or: [{x: 12}, {y: 12}]} - clauses.push(LocalCollection._exprForDocumentPredicate(key, value, literals)); - } else { - // else, it's a constraint on a particular key (or dotted keypath) - clauses.push(LocalCollection._exprForKeypathPredicate(key, value, literals)); - } - }; - - if (clauses.length === 0) return 'true'; // selector === {} - return '(' + clauses.join('&&') +')'; -}; - -// 'op' is a top-level, whole-document predicate from a mongo -// selector, like '$or' in {$or: [{x: 12}, {y: 12}]}. 'value' is its -// value in the selector. Return an expression that evaluates to true -// if 'doc' matches this predicate, else false. -LocalCollection._exprForDocumentPredicate = function (op, value, literals) { - if (op === '$or' || op === '$and' || op === '$nor') { - if (_.isEmpty(value) || !_.isArray(value)) - throw Error("$and/$or/$nor must be a nonempty array"); - } - - var clauses; - if (op === '$or') { - clauses = _.map(value, function (c) { - return LocalCollection._exprForSelector(c, literals); - }); - return '(' + clauses.join('||') +')'; - } - - if (op === '$and') { - clauses = _.map(value, function (c) { - return LocalCollection._exprForSelector(c, literals); - }); - return '(' + clauses.join('&&') +')'; - } - - if (op === '$nor') { - clauses = _.map(value, function (c) { - return "!(" + LocalCollection._exprForSelector(c, literals) + ")"; - }); - return '(' + clauses.join('&&') +')'; - } - - if (op === '$where') { - if (value instanceof Function) { - literals.push(value); - return 'literals[' + (literals.length - 1) + '].call(doc)'; - } - return "(function(){return " + value + ";}).call(doc)"; - } - - throw Error("Unrecognized key in selector: ", op); -} - -// Given a single 'dotted.key.path: value' constraint from a Mongo -// query selector, return an expression that evaluates to true if the -// document in 'doc' matches the constraint, else false. -LocalCollection._exprForKeypathPredicate = function (keypath, value, literals) { - var keyparts = keypath.split('.'); - - // get the inner predicate expression - var predcode = ''; - if (value instanceof RegExp) { - predcode = LocalCollection._exprForOperatorTest(value, literals); - } else if ( !(typeof value === 'object') - || value === null - || value instanceof Array) { - // it's something like {x.y: 12} or {x.y: [12]} - predcode = LocalCollection._exprForValueTest(value, literals); - } else { - // is it a literal document or a bunch of $-expressions? - var is_literal = true; - for (var k in value) { - if (k.substr(0, 1) === '$') { // no indexing into strings on IE7 - is_literal = false; - break; - } - } - - if (is_literal) { - // it's a literal document, like {x.y: {a: 12}} - predcode = LocalCollection._exprForValueTest(value, literals); - } else { - predcode = LocalCollection._exprForOperatorTest(value, literals); - } - } - - // now, deal with the orthogonal concern of dotted.key.paths and the - // (potentially multi-level) array searching they require. - // while at it, make sure to not throw an exception if we hit undefined while - // drilling down through the dotted parts - var ret = ''; - var innermost = true; - var lastPartWasNumber = false; - while (keyparts.length) { - var part = keyparts.pop(); - var thisPartIsNumber = false; - if (/^\d+$/.test(part)) { - part = +part; - thisPartIsNumber = true; - } - var formal = keyparts.length ? "x" : "doc"; - if (innermost) { - ret = '(function(x){return ' + predcode + ';})(' + formal + '&&' + formal + '[' + - JSON.stringify(part) + '])'; - innermost = false; - } else if (lastPartWasNumber) { - // The last part was an array index, so if we find an array here we - // shouldn't search it! - ret = '(function(x){return ' + ret + ';})(' + formal + '&&' + formal + '[' + - JSON.stringify(part) + '])'; - } else { - // If the runtime type is an array, search it, unless we're already at the - // innermost bit, or if the next part is a number (ie, an array index). - ret = 'f._matches(' + formal + '&&' + formal + '[' + JSON.stringify(part) + - '], function(x){return ' + ret + ';})'; - } - lastPartWasNumber = thisPartIsNumber; - } - - return ret; -}; - -// Given a value, return an expression that evaluates to true if the -// value in 'x' matches the value, or else false. This includes -// searching 'x' if it is an array. This doesn't include regular -// expressions (that's because mongo's $not operator works with -// regular expressions but not other kinds of scalar tests.) -LocalCollection._exprForValueTest = function (value, literals) { - var expr; - - if (value === null) { - // null has special semantics - // http://www.mongodb.org/display/DOCS/Querying+and+nulls - expr = 'x===null||x===undefined'; - } else if (typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean') { - // literal scalar value - // XXX object ids, dates, timestamps? - expr = 'x===' + JSON.stringify(value); - } else if (typeof value === 'function') { - // note that typeof(/a/) === 'function' in javascript - // XXX improve error - throw Error("Bad value type in query"); - } else { - // array or literal document - expr = 'f._equal(x,' + JSON.stringify(value) + ')'; - } - - return 'f._matches_plus(x,function(x){return ' + expr + ';})'; -}; - -// In a selector like {x: {$gt: 4, $lt: 8}}, we're calling the {$gt: -// 4, $lt: 8} part an "operator." Given an operator, return an -// expression that evaluates to true if the value in 'x' matches the -// operator, or else false. This includes searching 'x' if necessary -// if it's an array. In {x: /a/}, we consider /a/ to be an operator. -LocalCollection._exprForOperatorTest = function (op, literals) { - if (op instanceof RegExp) { - return LocalCollection._exprForOperatorTest({$regex: op}, literals); - } else { - var clauses = []; - for (var type in op) - clauses.push(LocalCollection._exprForConstraint(type, op[type], - op, literals)); - if (clauses.length === 0) - return 'true'; - return '(' + clauses.join('&&') + ')'; - } -}; - -// In an operator like {$gt: 4, $lt: 8}, we call each key/value pair, -// such as $gt: 4, a constraint. Given a constraint and its arguments, -// return an expression that evaluates to true if the value in 'x' -// matches the constraint, or else false. This includes searching 'x' -// if it's an array (and it's appropriate to the constraint.) -LocalCollection._exprForConstraint = function (type, arg, others, - literals) { - var expr; - var search = '_matches'; - var negate = false; - - if (type === '$gt') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')>0'; - } else if (type === '$lt') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')<0'; - } else if (type === '$gte') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')>=0'; - } else if (type === '$lte') { - expr = 'f._cmp(x,' + JSON.stringify(arg) + ')<=0'; - } else if (type === '$all') { - expr = 'f._all(x,' + JSON.stringify(arg) + ')'; - search = null; - } else if (type === '$exists') { - if (arg) - expr = 'x!==undefined'; - else - expr = 'x===undefined'; - search = null; - } else if (type === '$mod') { - expr = 'x%' + JSON.stringify(arg[0]) + '===' + - JSON.stringify(arg[1]); - } else if (type === '$ne') { - if (typeof arg !== "object") - expr = 'x===' + JSON.stringify(arg); - else - expr = 'f._equal(x,' + JSON.stringify(arg) + ')'; - search = '_matches_plus'; - negate = true; // tricky - } else if (type === '$in') { - expr = 'f._in(x,' + JSON.stringify(arg) + ')'; - search = '_matches_plus'; - } else if (type === '$nin') { - expr = 'f._in(x,' + JSON.stringify(arg) + ')'; - search = '_matches_plus'; - negate = true; - } else if (type === '$size') { - expr = '(x instanceof Array)&&x.length===' + arg; - search = null; - } else if (type === '$type') { - // $type: 1 is true for an array if any element in the array is of - // type 1. but an array doesn't have type array unless it contains - // an array.. - expr = 'f._type(x)===' + JSON.stringify(arg); - } else if (type === '$regex') { - // XXX mongo uses PCRE and supports some additional flags: 'x' and - // 's'. javascript doesn't support them. so this is a divergence - // between our behavior and mongo's behavior. ideally we would - // implement x and s by transforming the regexp, but not today.. - if ('$options' in others && /[^gim]/.test(others['$options'])) - throw Error("Only the i, m, and g regexp options are supported"); - expr = 'literals[' + literals.length + '].test(x)'; - if (arg instanceof RegExp) { - if ('$options' in others) { - literals.push(new RegExp(arg.source, others['$options'])); - } else { - literals.push(arg); - } - } else { - literals.push(new RegExp(arg, others['$options'])); - } - } else if (type === '$options') { - expr = 'true'; - search = null; - } else if (type === '$elemMatch') { - // XXX implement - throw Error("$elemMatch unimplemented"); - } else if (type === '$not') { - // mongo doesn't support $regex inside a $not for some reason. we - // do, because there's no reason not to that I can see.. but maybe - // we should follow mongo's behavior? - expr = '!' + LocalCollection._exprForOperatorTest(arg, literals); - search = null; - } else { - throw Error("Unrecognized key in selector: " + type); - } - - if (search) { - expr = 'f.' + search + '(x,function(x){return ' + - expr + ';})'; - } - - if (negate) - expr = '!' + expr; - - return expr; -}; From 269219254756dcada1ae9cd3567b4fe85d5db2ab Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 10:52:24 -0800 Subject: [PATCH 4/6] Some modifications to Ed-von-Schleck's selector compiler patch: - Improve regex match against array. - Improve matching of explicit deep nulls. - Instead of doing a high-level "catch TypeError", do more explicit type checks. - Clone the selector (since we're going to save a reference in our closure). --- packages/minimongo/minimongo_tests.js | 28 +- packages/minimongo/selector.js | 357 ++++++++++++++------------ 2 files changed, 210 insertions(+), 175 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index f6cf2608f9..392dee15a4 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -476,6 +476,10 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: /a/}, {a: 'cut'}); nomatch({a: /a/}, {a: 'CAT'}); match({a: /a/i}, {a: 'CAT'}); + match({a: /a/}, {a: ['foo', 'bar']}); // search within array... + nomatch({a: /,/}, {a: ['foo', 'bar']}); // but not by stringifying + match({a: {$regex: 'a'}}, {a: ['foo', 'bar']}); + nomatch({a: {$regex: ','}}, {a: ['foo', 'bar']}); match({a: {$regex: /a/}}, {a: 'cat'}); nomatch({a: {$regex: /a/}}, {a: 'cut'}); nomatch({a: {$regex: /a/}}, {a: 'CAT'}); @@ -523,6 +527,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({"a.b": [1,2,3]}, {a: {b: [4]}}); match({"a.b": /a/}, {a: {b: "cat"}}); nomatch({"a.b": /a/}, {a: {b: "dog"}}); + match({"a.b.c": null}, {}); + match({"a.b.c": null}, {a: 1}); + match({"a.b.c": null}, {a: {b: 4}}); // trying to access a dotted field that is undefined at some point // down the chain @@ -802,13 +809,20 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({"room.1b": "bla"}, {room: {"1b": "bla"}}); // $elemMatch - match({dogs: {$elemMatch: {name: /e/}}}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - nomatch({dogs: {$elemMatch: {name: /a/}}}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({dogs: {$elemMatch: {age: {$gt: 4}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: /e/}}}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}); + nomatch({dogs: {$elemMatch: {name: /a/}}}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({dogs: {$elemMatch: {age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); // XXX still needs tests: // - $elemMatch diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index a4c0dcfe42..63f850b75b 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -1,13 +1,21 @@ +// does list contain obj? LocalCollection._contains = function (list, obj) { + if (!_.isArray(list)) + return false; var objStr = JSON.stringify(obj); for (var i = 0, len_i = list.length; i < len_i; i++) { if (objStr === JSON.stringify(list[i])) return true; } return false; -} +}; +// do list and otherList have nonempty intersection? LocalCollection._containsSome = function (list, otherList) { + if (!_.isArray(list)) + return false; + if (!_.isArray(otherList)) + return false; for (var i = 0, len_i = list.length; i < len_i; i++) { var listObjStr = JSON.stringify(list[i]); for (var j = 0, len_j = otherList.length; j < len_j; j++) { @@ -16,14 +24,19 @@ LocalCollection._containsSome = function (list, otherList) { } } return false; -} +}; +// does list contain all elements of otherList? LocalCollection._containsAll = function (list, otherList) { - for (var i = 0, len_i = list.length; i < len_i; i++) { - var listObjStr = JSON.stringify(list[i]); + if (!_.isArray(list)) + return false; + if (!_.isArray(otherList)) + return false; + for (var i = 0, len_i = otherList.length; i < len_i; i++) { + var otherListObjStr = JSON.stringify(otherList[i]); var matches = false; - for (var j = 0, len_j = otherList.length; j < len_j; j++) { - if (listObjStr === JSON.stringify(otherList[j])) { + for (var j = 0, len_j = list.length; j < len_j; j++) { + if (otherListObjStr === JSON.stringify(list[j])) { matches = true; break; } @@ -32,7 +45,13 @@ LocalCollection._containsAll = function (list, otherList) { return false; } return true; -} +}; + +LocalCollection._anyIfArray = function (x, f) { + if (_.isArray(x)) + return _.any(x, f); + return f(x); +}; LocalCollection._gt = function (otherVal, val) { if ((val === null) || (otherVal === null)) { @@ -47,8 +66,7 @@ LocalCollection._gt = function (otherVal, val) { } else { return otherVal > val; } - -} +}; LocalCollection._lt = function (otherVal, val) { if ((val === null) || (otherVal === null)) { @@ -62,41 +80,7 @@ LocalCollection._lt = function (otherVal, val) { } else { return otherVal < val; } - -} - -LocalCollection._checkType = function(type, value) { - switch (type) { - case 1: - return _.isNumber(value); - case 2: - return _.isString(value); - case 3: - return value instanceof Object; - case 4: - return _.isArray(value); - case 8: - return _.isBoolean(value) - case 10: - return value === null; - case 11: - return _.isRegExp(value); - case 13: - return _.isFunction(value); - default: - return false; - // XXX support some/all of these: - // 5, binary data - // 7, object id - // 9, date - // 14, symbol - // 15, javascript code with scope - // 16, 18: 32-bit/64-bit integer - // 17, timestamp - // 255, minkey - // 127, maxkey - } -} +}; LocalCollection._hasOperators = function(selectorValue) { for (var selKey in selectorValue) { @@ -107,80 +91,92 @@ LocalCollection._hasOperators = function(selectorValue) { } LocalCollection._evaluateSelectorValue = function(selectorValue, docValue) { + // Normalize undefined to null in the selector. + if (selectorValue === undefined) + selectorValue = null; + if (!_.isObject(selectorValue)) { - // Most common case: Primitive comparison or containment (e.g. `_id: `) - return selectorValue === docValue || _.contains(docValue, selectorValue) || - selectorValue === null && docValue === undefined; + // Most common case: Primitive comparison or containment (e.g. `_id: + // `). This includes null selectorValue, but not array + // selectorValue. + if (selectorValue === docValue) + return true; + if (selectorValue === null && docValue === undefined) + return true; + if (_.isArray(docValue) && _.contains(docValue, selectorValue)) + return true; + return false; + } + + if (_.isArray(selectorValue)) { + // Deep comparison or containment check. + return JSON.stringify(selectorValue) === JSON.stringify(docValue) || + LocalCollection._contains(docValue, selectorValue); } else { - if (_.isArray(selectorValue)) { - // Deep comparison or containment check. - return JSON.stringify(selectorValue) === JSON.stringify(docValue) || LocalCollection._contains(docValue, selectorValue); - } else if (_.isRegExp(selectorValue)) { - return selectorValue.test(docValue); + // It's an object, but not an array or regexp. + if (LocalCollection._hasOperators(selectorValue)) { + // This one has operators in it, let's evaluate them. + return _.all(selectorValue, function (operand, operator) { + if (!_.has(LocalCollection._comparisonOperators, operator)) + throw new Error("Unrecognized operator: " + operator); + return LocalCollection._comparisonOperators[operator]( + operand, docValue, selectorValue); + }); } else { - // It's an object, but not an array or regexp. - if (LocalCollection._hasOperators(selectorValue)) { - // This one has operators in it, let's evaluate them. - for (var selKey in selectorValue) { - if (!LocalCollection._comparisonOperators[selKey](selectorValue[selKey], docValue, selectorValue)) - return false; - } - return true; - } else { - // There are no operators, so compare it to the document value - // (via JSON.stringify, b/c that preserves key order). - return JSON.stringify(selectorValue) === JSON.stringify(docValue) || - LocalCollection._contains(docValue, selectorValue); - } + // There are no operators, so compare it to the document value + // (via JSON.stringify, b/c that preserves key order). + return JSON.stringify(selectorValue) === JSON.stringify(docValue) || + LocalCollection._contains(docValue, selectorValue); } } -} +}; LocalCollection._logicalOperators = { "$and": function(selectorValue, docBranch) { - if (selectorValue.length === 0) + if (!_.isArray(selectorValue) || _.isEmpty(selectorValue)) throw Error("$and/$or/$nor must be nonempty array"); - for (var i = 0, len_i = selectorValue.length; i < len_i; i++) { - if (!(LocalCollection._evaluateSelector(selectorValue[i], docBranch))) - return false; - } - return true; + return _.all(selectorValue, function (term) { + return LocalCollection._evaluateSelector(term, docBranch); + }); }, "$or": function(selectorValue, docBranch) { - if (selectorValue.length === 0) + if (!_.isArray(selectorValue) || _.isEmpty(selectorValue)) throw Error("$and/$or/$nor must be nonempty array"); - for (var i = 0, len_i = selectorValue.length; i < len_i; i++) { - if (LocalCollection._evaluateSelector(selectorValue[i], docBranch)) - return true; - } - return false; + return _.any(selectorValue, function (term) { + return LocalCollection._evaluateSelector(term, docBranch); + }); }, "$nor": function(selectorValue, docBranch) { - if (selectorValue.length === 0) + if (!_.isArray(selectorValue) || _.isEmpty(selectorValue)) throw Error("$and/$or/$nor must be nonempty array"); - for (var i = 0, len_i = selectorValue.length; i < len_i; i++) { - if (LocalCollection._evaluateSelector(selectorValue[i], docBranch)) - return false; - } - return true; + return _.all(selectorValue, function (term) { + return !LocalCollection._evaluateSelector(term, docBranch); + }); }, "$where": function(selectorValue, docBranch) { - return Function("return " + selectorValue).call(docBranch); - }, -} + if (selectorValue instanceof Function) { + return selectorValue.call(docBranch); + } else { + return Function("return " + selectorValue).call(docBranch); + } + } +}; LocalCollection._comparisonOperators = { "$in": function(selectorValue, docValue) { + if (!_.isArray(selectorValue)) + throw new Error("Argument to $in must be array"); return LocalCollection._contains(selectorValue, docValue) || LocalCollection._containsSome(selectorValue, docValue); }, "$all": function(selectorValue, docValue) { - return _.isArray(selectorValue) && LocalCollection._containsAll(selectorValue, docValue) || - LocalCollection._contains(selectorValue, docValue); + if (!_.isArray(selectorValue)) + throw new Error("Argument to $all must be array"); + return LocalCollection._containsAll(docValue, selectorValue); }, "$lt": function(selectorValue, docValue) { @@ -201,33 +197,33 @@ LocalCollection._comparisonOperators = { "$ne": function(selectorValue, docValue) { return !(selectorValue === docValue || - JSON.stringify(selectorValue) === JSON.stringify(docValue) || - _.contains(docValue, selectorValue) && - LocalCollection._contains(docValue, selectorValue)); + JSON.stringify(selectorValue) === JSON.stringify(docValue) || + LocalCollection._contains(docValue, selectorValue)); }, "$nin": function(selectorValue, docValue) { - return docValue === undefined || - !(LocalCollection._contains(selectorValue, docValue)) && - !LocalCollection._containsSome(selectorValue, docValue); + if (!_.isArray(selectorValue)) + throw new Error("Argument to $nin must be array"); + if (docValue === undefined) + return true; + if (LocalCollection._contains(selectorValue, docValue)) + return false; + if (_.isArray(docValue) && + LocalCollection._containsSome(selectorValue, docValue)) + return false; + return true; }, "$exists": function(selectorValue, docValue) { - return selectorValue === (docValue !== undefined) + return selectorValue === (docValue !== undefined); }, "$mod": function(selectorValue, docValue) { var divisor = selectorValue[0], - remainder = selectorValue[1] - if (_.isArray(docValue)) { - for (var i = 0, len_i = docValue.length; i < len_i; i++) { - if (docValue[i] % divisor === remainder) - return true; - } - return false; - } else { - return docValue % divisor === remainder; - } + remainder = selectorValue[1]; + return LocalCollection._anyIfArray(docValue, function (n) { + return n % divisor === remainder; + }); }, "$size": function(selectorValue, docValue) { @@ -235,30 +231,25 @@ LocalCollection._comparisonOperators = { }, "$type": function(selectorValue, docValue) { - if (_.isArray(docValue)) { - for (var i = 0, len_i = docValue.length; i < len_i; i++) { - if (LocalCollection._checkType(selectorValue, docValue[i])) - return true; - } - return false; - } else { - return LocalCollection._checkType(selectorValue, docValue); - } + return LocalCollection._anyIfArray(docValue, function (x) { + return LocalCollection._f._type(x) === selectorValue; + }); }, "$regex": function(selectorValue, docValue, selectorBranch) { - var options = selectorBranch["$options"]; - if (selectorValue instanceof RegExp) { - if (options === undefined) { - return selectorValue.test(docValue); - } else { - // If there are options given with $options, we use them instead - // and construct the rexeg anew from its .source. - return new RegExp(selectorValue.source, options).test(docValue); - } - } else { - return new RegExp(selectorValue, options).test(docValue); - } + // If the user passed in {$regex: /foo/i}, _cloneSelector changed this to + // {$regex: {$regex: 'foo', $options: 'i'}}. Pull out the inner piece. + var regexSource = _.has(selectorValue, '$regex') ? + selectorValue.$regex : selectorValue; + // Pull out of embedded {$regex: {$regex: 'foo', $options: 'i'}}, but not if + // $options exists on the outside. (The logic here is different from + // regexSource, because the outer $regex always exists.) + var regexOptions = _.has(selectorBranch, '$options') ? + regexOptions = selectorBranch.$options : selectorValue.$options; + var re = new RegExp(regexSource, regexOptions); + return LocalCollection._anyIfArray(docValue, function (x) { + return re.test(x); + }); }, "$options": function(selectorValue, docValue) { @@ -267,18 +258,17 @@ LocalCollection._comparisonOperators = { }, "$elemMatch": function(selectorValue, docValue) { - for (var i = 0, len_i = docValue.length; i < len_i; i++) { - if (LocalCollection._evaluateSelector(selectorValue, docValue[i])) - return true; - } - return false; - }, - - "$not": function(selectorValue, docValue) { - return !(LocalCollection._evaluateSelectorValue(selectorValue, docValue)); + if (!_.isArray(docValue)) + return false; + return _.any(docValue, function (x) { + return LocalCollection._evaluateSelector(selectorValue, x); + }); }, -} + "$not": function(selectorValue, docValue) { + return !(LocalCollection._evaluateSelectorValue(selectorValue, docValue)); + } +}; // helpers used by compiled selector code LocalCollection._f = { @@ -466,48 +456,75 @@ LocalCollection._matches = function (selector, doc) { // The main evaluation function for a given selector. LocalCollection._evaluateSelector = function(selectorBranch, docBranch) { - try { - for (var innerKey in selectorBranch) { - var selectorValue = selectorBranch[innerKey]; - if (innerKey.charCodeAt(0) === 36) { // $ - // Outer operators are either logigal operators (they recurse back into - // this function), or $where. - if (!LocalCollection._logicalOperators[innerKey](selectorValue, docBranch)) - return false; - } else { - if (innerKey.indexOf(".") >= 0) { - // If the innerKey uses dot-notation, we move up to the last layer. - // Somehow, this magically works with reaching into arrays as well. - var keyParts = innerKey.split("."); - var docValue = docBranch; - for (var i = 0, len_i = keyParts.length; i < len_i; i++) { - docValue = docValue[keyParts[i]]; - } - } else { - docValue = docBranch[innerKey]; + for (var innerKey in selectorBranch) { + var selectorValue = selectorBranch[innerKey]; + if (innerKey.substr(0, 1) === '$') { + // Outer operators are either logical operators (they recurse back into + // this function), or $where. + if (!_.has(LocalCollection._logicalOperators, innerKey)) + throw new Error("Unrecognized logical operator: " + innerKey); + if (!LocalCollection._logicalOperators[innerKey](selectorValue, + docBranch)) + return false; + } else { + if (innerKey.indexOf(".") >= 0) { + // If the innerKey uses dot-notation, we move up to the last layer. If + // we ever hit null/undefined, stop digging (but still evaluate the + // query against what we found). This works for arrays as well as + // objects. + var keyParts = innerKey.split("."); + var docValue = docBranch; + for (var i = 0, len_i = keyParts.length; + i < len_i && docValue != null; // not null or undefined + i++) { + docValue = docValue[keyParts[i]]; } - // Here could be logical operators, containment, or comparisons. - if (!LocalCollection._evaluateSelectorValue(selectorValue, docValue)) - return false; + } else { + docValue = docBranch[innerKey]; } + // Here could be logical operators, containment, or comparisons. + if (!LocalCollection._evaluateSelectorValue(selectorValue, docValue)) + return false; } - } catch (e) { - // If type errors occur (like checking in a non-existing array), - // we simply return false. Every other error is re-thrown. - if (!(e instanceof TypeError)) - throw e - return false; } + // We should have returned whenever something evaluated to false, // so it must be true. return true; }; +// Clones a selector, and converts RegExp objects to $regex. +LocalCollection._cloneSelector = function (v) { + if (typeof v !== "object") + return v; + if (v === null) + return null; // null has typeof "object" + if (v instanceof RegExp) { + var regexAsSelector = {$regex: v.source}; + var regexOptions = ''; + // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options + // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. + if (v.ignoreCase) + regexOptions += 'i'; + if (v.multiline) + regexOptions += 'm'; + if (regexOptions) + regexAsSelector.$options = regexOptions; + return regexAsSelector; + } + if (_.isArray(v)) + return _.map(v, LocalCollection._cloneSelector); + + var ret = {}; + for (var key in v) + ret[key] = LocalCollection._cloneSelector(v[key]); + return ret; +}; + // Given a selector, return a function that takes one argument, a // document, and returns true if the document matches the selector, // else false. LocalCollection._compileSelector = function (selector) { - var literals = []; // you can pass a literal function instead of a selector if (selector instanceof Function) return function (doc) {return selector.call(doc);}; @@ -515,13 +532,17 @@ LocalCollection._compileSelector = function (selector) { // shorthand -- scalars match _id if (LocalCollection._selectorIsId(selector)) selector = {_id: selector}; - + // protect against dangerous selectors. falsey and {_id: falsey} // are both likely programmer error, and not what you want, // particularly for destructive operations. if (!selector || (('_id' in selector) && !selector._id)) return function (doc) {return false;}; + // Clone selector, since we're going to be holding a reference to it. + // This also gets rid of RegExp objects. + selector = LocalCollection._cloneSelector(selector); + return function(doc) {return LocalCollection._evaluateSelector(selector, doc);}; }; From 0567a730b1bf09ae8076f0e6422c1854668e8b9d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 20:07:00 -0800 Subject: [PATCH 5/6] Rewrite selector compiler against with a "compile to specific function" approach. --- packages/minimongo/selector.js | 574 ++++++++++++++++----------------- 1 file changed, 284 insertions(+), 290 deletions(-) diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 63f850b75b..b2def5735c 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -1,272 +1,289 @@ -// does list contain obj? -LocalCollection._contains = function (list, obj) { - if (!_.isArray(list)) - return false; - var objStr = JSON.stringify(obj); - for (var i = 0, len_i = list.length; i < len_i; i++) { - if (objStr === JSON.stringify(list[i])) - return true; - } - return false; -}; +(function(){ -// do list and otherList have nonempty intersection? -LocalCollection._containsSome = function (list, otherList) { - if (!_.isArray(list)) - return false; - if (!_.isArray(otherList)) - return false; - for (var i = 0, len_i = list.length; i < len_i; i++) { - var listObjStr = JSON.stringify(list[i]); - for (var j = 0, len_j = otherList.length; j < len_j; j++) { - if (listObjStr === JSON.stringify(otherList[j])) - return true; - } - } - return false; -}; - -// does list contain all elements of otherList? -LocalCollection._containsAll = function (list, otherList) { - if (!_.isArray(list)) - return false; - if (!_.isArray(otherList)) - return false; - for (var i = 0, len_i = otherList.length; i < len_i; i++) { - var otherListObjStr = JSON.stringify(otherList[i]); - var matches = false; - for (var j = 0, len_j = list.length; j < len_j; j++) { - if (otherListObjStr === JSON.stringify(list[j])) { - matches = true; - break; - } - } - if (!matches) - return false; - } - return true; -}; - -LocalCollection._anyIfArray = function (x, f) { +var _anyIfArray = function (x, f) { if (_.isArray(x)) return _.any(x, f); return f(x); }; -LocalCollection._gt = function (otherVal, val) { - if ((val === null) || (otherVal === null)) { +var _anyIfArrayPlus = function (x, f) { + if (f(x)) return true; - } else if (_.isObject(otherVal) && _.isObject(val)) { - // XXX: find material about actual semantics - var minOtherVal = _.min(_.flatten(_.values(otherVal))); - var minVal = _.min(_.flatten(_.values(val))); - return minOtherVal > minVal; - } else if (_.isArray(otherVal)) { - return _.max(otherVal) > val; - } else { - return otherVal > val; - } + return _.isArray(x) && _.any(x, f); }; -LocalCollection._lt = function (otherVal, val) { - if ((val === null) || (otherVal === null)) { - return true; - } else if (_.isObject(otherVal) && _.isObject(val)) { - var minOtherVal = _.min(_.flatten(_.values(otherVal))); - var minVal = _.min(_.flatten(_.values(val))); - return minOtherVal < minVal; - } else if (_.isArray(otherVal)) { - return _.min(otherVal) < val; - } else { - return otherVal < val; +var hasOperators = function(valueSelector) { + var theseAreOperators = undefined; + for (var selKey in valueSelector) { + var thisIsOperator = selKey.substr(0, 1) === '$'; + if (theseAreOperators === undefined) { + theseAreOperators = thisIsOperator; + } else if (theseAreOperators !== thisIsOperator) { + throw new Error("Inconsistent selector: " + valueSelector); + } } + return !!theseAreOperators; // {} has no operators }; -LocalCollection._hasOperators = function(selectorValue) { - for (var selKey in selectorValue) { - if (selKey.charCodeAt(0) === 36) // $ - return true; - } - return false; -} - -LocalCollection._evaluateSelectorValue = function(selectorValue, docValue) { - // Normalize undefined to null in the selector. - if (selectorValue === undefined) - selectorValue = null; - - if (!_.isObject(selectorValue)) { - // Most common case: Primitive comparison or containment (e.g. `_id: - // `). This includes null selectorValue, but not array - // selectorValue. - if (selectorValue === docValue) - return true; - if (selectorValue === null && docValue === undefined) - return true; - if (_.isArray(docValue) && _.contains(docValue, selectorValue)) - return true; - return false; - } - - if (_.isArray(selectorValue)) { - // Deep comparison or containment check. - return JSON.stringify(selectorValue) === JSON.stringify(docValue) || - LocalCollection._contains(docValue, selectorValue); - } else { - // It's an object, but not an array or regexp. - if (LocalCollection._hasOperators(selectorValue)) { - // This one has operators in it, let's evaluate them. - return _.all(selectorValue, function (operand, operator) { - if (!_.has(LocalCollection._comparisonOperators, operator)) - throw new Error("Unrecognized operator: " + operator); - return LocalCollection._comparisonOperators[operator]( - operand, docValue, selectorValue); +var compileValueSelector = function (valueSelector) { + if (valueSelector == null) { // undefined or null + return function (value) { + return _anyIfArray(value, function (x) { + return x == null; // undefined or null }); - } else { - // There are no operators, so compare it to the document value - // (via JSON.stringify, b/c that preserves key order). - return JSON.stringify(selectorValue) === JSON.stringify(docValue) || - LocalCollection._contains(docValue, selectorValue); + }; + } + + // Selector is a non-null primitive (and not an array or RegExp either). + if (!_.isObject(valueSelector)) { + return function (value) { + return _anyIfArray(value, function (x) { + return x === valueSelector; + }); + }; + } + + if (valueSelector instanceof RegExp) { + return function (value) { + return _anyIfArray(value, function (x) { + return valueSelector.test(x); + }); + }; + } + + // Arrays match either identical arrays or arrays that contain it as a value. + if (_.isArray(valueSelector)) { + return function (value) { + if (!_.isArray(value)) + return false; + return _anyIfArrayPlus(value, function (x) { + return LocalCollection._f._equal(valueSelector, x); + }); + }; + } + + // It's an object, but not an array or regexp. + if (hasOperators(valueSelector)) { + var operatorFunctions = []; + _.each(valueSelector, function (operand, operator) { + if (!_.has(VALUE_OPERATORS, operator)) + throw new Error("Unrecognized operator: " + operator); + operatorFunctions.push(VALUE_OPERATORS[operator]( + operand, valueSelector.$options)); + }); + return function (value) { + return _.all(operatorFunctions, function (f) { + return f(value); + }); + }; + } + + // It's a literal; compare value (or element of value array) directly to the + // selector. + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._equal(valueSelector, x); + }); + }; +}; + +// XXX can factor out common logic below +var LOGICAL_OPERATORS = { + "$and": function(subSelector) { + if (!_.isArray(subSelector) || _.isEmpty(subSelector)) + throw Error("$and/$or/$nor must be nonempty array"); + var subSelectorFunctions = _.map( + subSelector, compileDocumentSelector); + return function (doc) { + return _.all(subSelectorFunctions, function (f) { + return f(doc); + }); + }; + }, + + "$or": function(subSelector) { + if (!_.isArray(subSelector) || _.isEmpty(subSelector)) + throw Error("$and/$or/$nor must be nonempty array"); + var subSelectorFunctions = _.map( + subSelector, compileDocumentSelector); + return function (doc) { + return _.any(subSelectorFunctions, function (f) { + return f(doc); + }); + }; + }, + + "$nor": function(subSelector) { + if (!_.isArray(subSelector) || _.isEmpty(subSelector)) + throw Error("$and/$or/$nor must be nonempty array"); + var subSelectorFunctions = _.map( + subSelector, compileDocumentSelector); + return function (doc) { + return _.all(subSelectorFunctions, function (f) { + return !f(doc); + }); + }; + }, + + "$where": function(selectorValue) { + if (!(selectorValue instanceof Function)) { + selectorValue = Function("return " + selectorValue); } + return function (doc) { + return selectorValue.call(doc); + }; } }; -LocalCollection._logicalOperators = { - "$and": function(selectorValue, docBranch) { - if (!_.isArray(selectorValue) || _.isEmpty(selectorValue)) - throw Error("$and/$or/$nor must be nonempty array"); - return _.all(selectorValue, function (term) { - return LocalCollection._evaluateSelector(term, docBranch); - }); - }, - - "$or": function(selectorValue, docBranch) { - if (!_.isArray(selectorValue) || _.isEmpty(selectorValue)) - throw Error("$and/$or/$nor must be nonempty array"); - return _.any(selectorValue, function (term) { - return LocalCollection._evaluateSelector(term, docBranch); - }); - }, - - "$nor": function(selectorValue, docBranch) { - if (!_.isArray(selectorValue) || _.isEmpty(selectorValue)) - throw Error("$and/$or/$nor must be nonempty array"); - return _.all(selectorValue, function (term) { - return !LocalCollection._evaluateSelector(term, docBranch); - }); - }, - - "$where": function(selectorValue, docBranch) { - if (selectorValue instanceof Function) { - return selectorValue.call(docBranch); - } else { - return Function("return " + selectorValue).call(docBranch); - } - } -}; - -LocalCollection._comparisonOperators = { - "$in": function(selectorValue, docValue) { - if (!_.isArray(selectorValue)) +var VALUE_OPERATORS = { + "$in": function (operand) { + if (!_.isArray(operand)) throw new Error("Argument to $in must be array"); - return LocalCollection._contains(selectorValue, docValue) || - LocalCollection._containsSome(selectorValue, docValue); + return function (value) { + return _anyIfArrayPlus(value, function (x) { + return _.any(operand, function (operandElt) { + return LocalCollection._f._equal(operandElt, x); + }); + }); + }; }, - "$all": function(selectorValue, docValue) { - if (!_.isArray(selectorValue)) + "$all": function (operand) { + if (!_.isArray(operand)) throw new Error("Argument to $all must be array"); - return LocalCollection._containsAll(docValue, selectorValue); + return function (value) { + if (!_.isArray(value)) + return false; + return _.all(operand, function (operandElt) { + return _.any(value, function (valueElt) { + return LocalCollection._f._equal(operandElt, valueElt); + }); + }); + }; }, - "$lt": function(selectorValue, docValue) { - return LocalCollection._lt(docValue, selectorValue); + "$lt": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) < 0; + }); + }; }, - "$lte": function(selectorValue, docValue) { - return _.isEqual(selectorValue, docValue) || LocalCollection._lt(docValue, selectorValue); + "$lte": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) <= 0; + }); + }; }, - "$gt": function(selectorValue, docValue) { - return LocalCollection._gt(docValue, selectorValue); - }, - - "$gte": function(selectorValue, docValue) { - return _.isEqual(selectorValue, docValue) || LocalCollection._gt(docValue, selectorValue); + "$gt": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) > 0; + }); + }; }, - "$ne": function(selectorValue, docValue) { - return !(selectorValue === docValue || - JSON.stringify(selectorValue) === JSON.stringify(docValue) || - LocalCollection._contains(docValue, selectorValue)); + "$gte": function (operand) { + return function (value) { + return _anyIfArray(value, function (x) { + return LocalCollection._f._cmp(x, operand) >= 0; + }); + }; }, - "$nin": function(selectorValue, docValue) { - if (!_.isArray(selectorValue)) + "$ne": function (operand) { + return function (value) { + return ! _anyIfArrayPlus(value, function (x) { + return LocalCollection._f._equal(x, operand); + }); + }; + }, + + "$nin": function (operand) { + if (!_.isArray(operand)) throw new Error("Argument to $nin must be array"); - if (docValue === undefined) - return true; - if (LocalCollection._contains(selectorValue, docValue)) - return false; - if (_.isArray(docValue) && - LocalCollection._containsSome(selectorValue, docValue)) - return false; - return true; + var inFunction = VALUE_OPERATORS.$in(operand); + return function (value) { + // Field doesn't exist, so it's not-in operand + if (value === undefined) + return true; + return !inFunction(value); + }; }, - "$exists": function(selectorValue, docValue) { - return selectorValue === (docValue !== undefined); + "$exists": function (operand) { + return function (value) { + return operand === (value !== undefined); + }; }, - "$mod": function(selectorValue, docValue) { - var divisor = selectorValue[0], - remainder = selectorValue[1]; - return LocalCollection._anyIfArray(docValue, function (n) { - return n % divisor === remainder; - }); + "$mod": function (operand) { + var divisor = operand[0], + remainder = operand[1]; + return function (value) { + return _anyIfArray(value, function (x) { + return x % divisor === remainder; + }); + }; }, - "$size": function(selectorValue, docValue) { - return _.isArray(docValue) && selectorValue === docValue.length; + "$size": function (operand) { + return function (value) { + return _.isArray(value) && operand === value.length; + }; }, - "$type": function(selectorValue, docValue) { - return LocalCollection._anyIfArray(docValue, function (x) { - return LocalCollection._f._type(x) === selectorValue; - }); + "$type": function (operand) { + return function (value) { + // Definitely not _anyIfArrayPlus: $type: 4 only matches arrays that have + // arrays as elements according to the Mongo docs. + return _anyIfArray(value, function (x) { + return LocalCollection._f._type(x) === operand; + }); + }; }, - "$regex": function(selectorValue, docValue, selectorBranch) { - // If the user passed in {$regex: /foo/i}, _cloneSelector changed this to - // {$regex: {$regex: 'foo', $options: 'i'}}. Pull out the inner piece. - var regexSource = _.has(selectorValue, '$regex') ? - selectorValue.$regex : selectorValue; - // Pull out of embedded {$regex: {$regex: 'foo', $options: 'i'}}, but not if - // $options exists on the outside. (The logic here is different from - // regexSource, because the outer $regex always exists.) - var regexOptions = _.has(selectorBranch, '$options') ? - regexOptions = selectorBranch.$options : selectorValue.$options; - var re = new RegExp(regexSource, regexOptions); - return LocalCollection._anyIfArray(docValue, function (x) { - return re.test(x); - }); + "$regex": function (operand, options) { + if (options !== undefined) { + // Options passed in $options (even the empty string) always overrides + // options in the RegExp object itself. + var regexSource = operand instanceof RegExp ? operand.source : operand; + operand = new RegExp(regexSource, options); + } else if (!(operand instanceof RegExp)) { + operand = new RegExp(operand); + } + + return function (value) { + return _anyIfArray(value, function (x) { + return operand.test(x); + }); + }; }, - "$options": function(selectorValue, docValue) { + "$options": function (operand) { // evaluation happens at the $regex function above - return true; + return function (value) { return true; }; }, - "$elemMatch": function(selectorValue, docValue) { - if (!_.isArray(docValue)) - return false; - return _.any(docValue, function (x) { - return LocalCollection._evaluateSelector(selectorValue, x); - }); + "$elemMatch": function (operand) { + var matcher = compileDocumentSelector(operand); + return function (value) { + if (!_.isArray(value)) + return false; + return _.any(value, function (x) { + return matcher(x); + }); + }; }, - "$not": function(selectorValue, docValue) { - return !(LocalCollection._evaluateSelectorValue(selectorValue, docValue)); + "$not": function (operand) { + var matcher = compileValueSelector(operand); + return function (value) { + return !matcher(value); + }; } }; @@ -454,71 +471,46 @@ LocalCollection._matches = function (selector, doc) { return (LocalCollection._compileSelector(selector))(doc); }; -// The main evaluation function for a given selector. -LocalCollection._evaluateSelector = function(selectorBranch, docBranch) { - for (var innerKey in selectorBranch) { - var selectorValue = selectorBranch[innerKey]; - if (innerKey.substr(0, 1) === '$') { - // Outer operators are either logical operators (they recurse back into - // this function), or $where. - if (!_.has(LocalCollection._logicalOperators, innerKey)) - throw new Error("Unrecognized logical operator: " + innerKey); - if (!LocalCollection._logicalOperators[innerKey](selectorValue, - docBranch)) - return false; - } else { - if (innerKey.indexOf(".") >= 0) { - // If the innerKey uses dot-notation, we move up to the last layer. If - // we ever hit null/undefined, stop digging (but still evaluate the - // query against what we found). This works for arrays as well as - // objects. - var keyParts = innerKey.split("."); - var docValue = docBranch; - for (var i = 0, len_i = keyParts.length; - i < len_i && docValue != null; // not null or undefined - i++) { - docValue = docValue[keyParts[i]]; - } - } else { - docValue = docBranch[innerKey]; - } - // Here could be logical operators, containment, or comparisons. - if (!LocalCollection._evaluateSelectorValue(selectorValue, docValue)) - return false; - } - } - - // We should have returned whenever something evaluated to false, - // so it must be true. - return true; +var makeLookupFunction = function (key) { + var dotLocation = key.indexOf('.'); + var first = dotLocation === -1 ? key : key.substr(0, dotLocation); + var lookupRest = dotLocation !== -1 && + makeLookupFunction(key.substr(dotLocation + 1)); + return function (doc) { + if (doc == null) // null or undefined + return undefined; + var firstLevel = doc[first]; + if (lookupRest) + return lookupRest(firstLevel); + return firstLevel; + }; }; -// Clones a selector, and converts RegExp objects to $regex. -LocalCollection._cloneSelector = function (v) { - if (typeof v !== "object") - return v; - if (v === null) - return null; // null has typeof "object" - if (v instanceof RegExp) { - var regexAsSelector = {$regex: v.source}; - var regexOptions = ''; - // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options - // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. - if (v.ignoreCase) - regexOptions += 'i'; - if (v.multiline) - regexOptions += 'm'; - if (regexOptions) - regexAsSelector.$options = regexOptions; - return regexAsSelector; - } - if (_.isArray(v)) - return _.map(v, LocalCollection._cloneSelector); +// The main compilation function for a given selector. +var compileDocumentSelector = function (docSelector) { + var perKeySelectors = []; + _.each(docSelector, function (subSelector, key) { + if (key.substr(0, 1) === '$') { + // Outer operators are either logical operators (they recurse back into + // this function), or $where. + if (!_.has(LOGICAL_OPERATORS, key)) + throw new Error("Unrecognized logical operator: " + key); + perKeySelectors.push(LOGICAL_OPERATORS[key](subSelector)); + } else { + var lookUpByIndex = makeLookupFunction(key); + var valueSelectorFunc = compileValueSelector(subSelector); + perKeySelectors.push(function (doc) { + return valueSelectorFunc(lookUpByIndex(doc)); + }); + } + }); - var ret = {}; - for (var key in v) - ret[key] = LocalCollection._cloneSelector(v[key]); - return ret; + + return function (doc) { + return _.all(perKeySelectors, function (f) { + return f(doc); + }); + }; }; // Given a selector, return a function that takes one argument, a @@ -531,22 +523,24 @@ LocalCollection._compileSelector = function (selector) { // shorthand -- scalars match _id if (LocalCollection._selectorIsId(selector)) - selector = {_id: selector}; + return function (doc) { return doc._id === selector;}; - // protect against dangerous selectors. falsey and {_id: falsey} - // are both likely programmer error, and not what you want, - // particularly for destructive operations. + // protect against dangerous selectors. falsey and {_id: falsey} are both + // likely programmer error, and not what you want, particularly for + // destructive operations. if (!selector || (('_id' in selector) && !selector._id)) return function (doc) {return false;}; - // Clone selector, since we're going to be holding a reference to it. - // This also gets rid of RegExp objects. - selector = LocalCollection._cloneSelector(selector); + // Top level can't be an array or true. + if (typeof(selector) === 'boolean' || _.isArray(selector)) + throw new Error("Invalid selector: " + selector); - return function(doc) {return LocalCollection._evaluateSelector(selector, doc);}; + return compileDocumentSelector(selector); }; // Is this selector just shorthand for lookup by _id? LocalCollection._selectorIsId = function (selector) { return (typeof selector === "string") || (typeof selector === "number"); }; + +})(); From 3bdf41add159bf4b71b79c9fa7f29642731ba09f Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 4 Feb 2013 16:42:18 -0800 Subject: [PATCH 6/6] Rewrite sort compiler to avoid eval, too! --- packages/minimongo/package.js | 1 - packages/minimongo/selector.js | 58 ++++++++++++++++++++++++++++++ packages/minimongo/sort.js | 65 ---------------------------------- 3 files changed, 58 insertions(+), 66 deletions(-) delete mode 100644 packages/minimongo/sort.js diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 3852607ee0..1b76f2c6b3 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -13,7 +13,6 @@ Package.on_use(function (api, where) { api.add_files([ 'minimongo.js', 'selector.js', - 'sort.js', 'uuid.js', 'modify.js', 'diff.js' diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index b2def5735c..b92db74f9f 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -543,4 +543,62 @@ LocalCollection._selectorIsId = function (selector) { return (typeof selector === "string") || (typeof selector === "number"); }; +// Give a sort spec, which can be in any of these forms: +// {"key1": 1, "key2": -1} +// [["key1", "asc"], ["key2", "desc"]] +// ["key1", ["key2", "desc"]] +// +// (.. with the first form being dependent on the key enumeration +// behavior of your javascript VM, which usually does what you mean in +// this case if the key names don't look like integers ..) +// +// return a function that takes two objects, and returns -1 if the +// first object comes first in order, 1 if the second object comes +// first, or 0 if neither object comes before the other. + +LocalCollection._compileSort = function (spec) { + var sortSpecParts = []; + + if (spec instanceof Array) { + for (var i = 0; i < spec.length; i++) { + if (typeof spec[i] === "string") { + sortSpecParts.push({ + lookup: makeLookupFunction(spec[i]), + ascending: true + }); + } else { + sortSpecParts.push({ + lookup: makeLookupFunction(spec[i][0]), + ascending: spec[i][1] !== "desc" + }); + } + } + } else if (typeof spec === "object") { + for (var key in spec) { + sortSpecParts.push({ + lookup: makeLookupFunction(key), + ascending: spec[key] >= 0 + }); + } + } else { + throw Error("Bad sort specification: ", JSON.stringify(spec)); + } + + if (sortSpecParts.length === 0) + return function () {return 0;}; + + return function (a, b) { + for (var i = 0; i < sortSpecParts.length; ++i) { + var specPart = sortSpecParts[i]; + var aValue = specPart.lookup(a); + var bValue = specPart.lookup(b); + var compare = LocalCollection._f._cmp(aValue, bValue); + if (compare !== 0) + return specPart.ascending ? compare : -compare; + }; + return 0; + }; +}; + + })(); diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js deleted file mode 100644 index 94420cf237..0000000000 --- a/packages/minimongo/sort.js +++ /dev/null @@ -1,65 +0,0 @@ -// Give a sort spec, which can be in any of these forms: -// {"key1": 1, "key2": -1} -// [["key1", "asc"], ["key2", "desc"]] -// ["key1", ["key2", "desc"]] -// -// (.. with the first form being dependent on the key enumeration -// behavior of your javascript VM, which usually does what you mean in -// this case if the key names don't look like integers ..) -// -// return a function that takes two objects, and returns -1 if the -// first object comes first in order, 1 if the second object comes -// first, or 0 if neither object comes before the other. - -LocalCollection._compileSort = function (spec) { - var keys = []; - var asc = []; - - if (spec instanceof Array) { - for (var i = 0; i < spec.length; i++) { - if (typeof spec[i] === "string") { - keys.push(spec[i]); - asc.push(true); - } else { - keys.push(spec[i][0]); - asc.push(spec[i][1] !== "desc"); - } - } - } else if (typeof spec === "object") { - for (key in spec) { - keys.push(key); - asc.push(!(spec[key] < 0)); - } - } else { - throw Error("Bad sort specification: ", JSON.stringify(spec)); - } - - if (keys.length === 0) - return function () {return 0;}; - - // eval() does not return a value in IE8, nor does the spec say it - // should. Assign to a local to get the value, instead. - var _func; - var code = "_func = (function(c){return function(a,b){var x;"; - for (var i = 0; i < keys.length; i++) { - // handle dotted subpaths. Make sure to avoid dereferencing - // undefined if a subkey doesn't exist. - var splittedKeys = keys[i].split("."); - var keyString = ""; - var aCode = "a"; - var bCode = "b"; - for(var o = 0; o < splittedKeys.length; o++) { - keyString = keyString + "[" + JSON.stringify(splittedKeys[o]) + "]"; - aCode += '&&a' + keyString; - bCode += '&&b' + keyString; - } - if (i !== 0) { - code += "if(x!==0)return x;"; - } - code += "x=" + (asc[i] ? "" : "-") + - "c(" + aCode + "," + bCode + ");"; - } - code += "return x;};})"; - eval(code); - return _func(LocalCollection._f._cmp); -};