diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 6e1731c08f7..4a4334cbf45 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -579,10 +579,271 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4, 2]}}); nomatch({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4]}}); + // $or + test.throws(function () { + match({$or: []}, {}); + }); + test.throws(function () { + match({$or: []}, {a: 1}); + }); + match({$or: [{a: 1}]}, {a: 1}); + nomatch({$or: [{b: 2}]}, {a: 1}); + match({$or: [{a: 1}, {b: 2}]}, {a: 1}); + nomatch({$or: [{c: 3}, {d: 4}]}, {a: 1}); + match({$or: [{a: 1}, {b: 2}]}, {a: [1, 2, 3]}); + nomatch({$or: [{a: 1}, {b: 2}]}, {c: [1, 2, 3]}); + nomatch({$or: [{a: 1}, {b: 2}]}, {a: [2, 3, 4]}); + match({$or: [{a: 1}, {a: 2}]}, {a: 1}); + match({$or: [{a: 1}, {a: 2}], b: 2}, {a: 1, b: 2}); + nomatch({$or: [{a: 2}, {a: 3}], b: 2}, {a: 1, b: 2}); + nomatch({$or: [{a: 1}, {a: 2}], b: 3}, {a: 1, b: 2}); + + // $or and $lt, $lte, $gt, $gte + match({$or: [{a: {$lte: 1}}, {a: 2}]}, {a: 1}); + nomatch({$or: [{a: {$lt: 1}}, {a: 2}]}, {a: 1}); + match({$or: [{a: {$gte: 1}}, {a: 2}]}, {a: 1}); + nomatch({$or: [{a: {$gt: 1}}, {a: 2}]}, {a: 1}); + match({$or: [{b: {$gt: 1}}, {b: {$lt: 3}}]}, {b: 2}); + nomatch({$or: [{b: {$lt: 1}}, {b: {$gt: 3}}]}, {b: 2}); + + // $or and $in + match({$or: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); + nomatch({$or: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); + match({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + match({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + nomatch({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + match({$or: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {b: 2}); + nomatch({$or: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {b: 2}); + + // $or and $nin + nomatch({$or: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); + match({$or: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); + nomatch({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {b: 2}); + nomatch({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 2}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); + + // $or and dot-notation + match({$or: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); + match({$or: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); + nomatch({$or: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); + + // $or and nested objects + match({$or: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + nomatch({$or: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + + // $or and regexes + match({$or: [{a: /a/}]}, {a: "cat"}); + nomatch({$or: [{a: /o/}]}, {a: "cat"}); + match({$or: [{a: /a/}, {a: /o/}]}, {a: "cat"}); + nomatch({$or: [{a: /i/}, {a: /o/}]}, {a: "cat"}); + match({$or: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); + + // $or and $ne + match({$or: [{a: {$ne: 1}}]}, {}); + nomatch({$or: [{a: {$ne: 1}}]}, {a: 1}); + match({$or: [{a: {$ne: 1}}]}, {a: 2}); + match({$or: [{a: {$ne: 1}}]}, {b: 1}); + match({$or: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 1}); + match({$or: [{a: {$ne: 1}}, {b: {$ne: 1}}]}, {a: 1}); + nomatch({$or: [{a: {$ne: 1}}, {b: {$ne: 2}}]}, {a: 1, b: 2}); + + // $or and $not + match({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {}); + nomatch({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 1}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 2}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$not: {$mod: [10, 2]}}}]}, {a: 1}); + nomatch({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 1}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 2}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 3}); + // this is possibly an open-ended task, so we stop here ... + + // $nor + test.throws(function () { + match({$nor: []}, {}); + }); + test.throws(function () { + match({$nor: []}, {a: 1}); + }); + nomatch({$nor: [{a: 1}]}, {a: 1}); + match({$nor: [{b: 2}]}, {a: 1}); + nomatch({$nor: [{a: 1}, {b: 2}]}, {a: 1}); + match({$nor: [{c: 3}, {d: 4}]}, {a: 1}); + nomatch({$nor: [{a: 1}, {b: 2}]}, {a: [1, 2, 3]}); + match({$nor: [{a: 1}, {b: 2}]}, {c: [1, 2, 3]}); + match({$nor: [{a: 1}, {b: 2}]}, {a: [2, 3, 4]}); + nomatch({$nor: [{a: 1}, {a: 2}]}, {a: 1}); + + // $nor and $lt, $lte, $gt, $gte + nomatch({$nor: [{a: {$lte: 1}}, {a: 2}]}, {a: 1}); + match({$nor: [{a: {$lt: 1}}, {a: 2}]}, {a: 1}); + nomatch({$nor: [{a: {$gte: 1}}, {a: 2}]}, {a: 1}); + match({$nor: [{a: {$gt: 1}}, {a: 2}]}, {a: 1}); + nomatch({$nor: [{b: {$gt: 1}}, {b: {$lt: 3}}]}, {b: 2}); + match({$nor: [{b: {$lt: 1}}, {b: {$gt: 3}}]}, {b: 2}); + + // $nor and $in + nomatch({$nor: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); + match({$nor: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); + nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + match({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {b: 2}); + match({$nor: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {b: 2}); + + // $nor and $nin + match({$nor: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); + nomatch({$nor: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); + match({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {b: 2}); + match({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 2}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); + + // $nor and dot-notation + nomatch({$nor: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); + nomatch({$nor: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); + match({$nor: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); + + // $nor and nested objects + nomatch({$nor: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + match({$nor: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + + // $nor and regexes + nomatch({$nor: [{a: /a/}]}, {a: "cat"}); + match({$nor: [{a: /o/}]}, {a: "cat"}); + nomatch({$nor: [{a: /a/}, {a: /o/}]}, {a: "cat"}); + match({$nor: [{a: /i/}, {a: /o/}]}, {a: "cat"}); + nomatch({$nor: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); + + // $nor and $ne + nomatch({$nor: [{a: {$ne: 1}}]}, {}); + match({$nor: [{a: {$ne: 1}}]}, {a: 1}); + nomatch({$nor: [{a: {$ne: 1}}]}, {a: 2}); + nomatch({$nor: [{a: {$ne: 1}}]}, {b: 1}); + nomatch({$nor: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 1}); + nomatch({$nor: [{a: {$ne: 1}}, {b: {$ne: 1}}]}, {a: 1}); + match({$nor: [{a: {$ne: 1}}, {b: {$ne: 2}}]}, {a: 1, b: 2}); + + // $nor and $not + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {}); + match({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 1}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 2}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$not: {$mod: [10, 2]}}}]}, {a: 1}); + match({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 1}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 2}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 3}); + + // $and + + test.throws(function () { + match({$and: []}, {}); + }); + test.throws(function () { + match({$and: []}, {a: 1}); + }); + match({$and: [{a: 1}]}, {a: 1}); + nomatch({$and: [{a: 1}, {a: 2}]}, {a: 1}); + nomatch({$and: [{a: 1}, {b: 1}]}, {a: 1}); + match({$and: [{a: 1}, {b: 2}]}, {a: 1, b: 2}); + nomatch({$and: [{a: 1}, {b: 1}]}, {a: 1, b: 2}); + match({$and: [{a: 1}, {b: 2}], c: 3}, {a: 1, b: 2, c: 3}); + nomatch({$and: [{a: 1}, {b: 2}], c: 4}, {a: 1, b: 2, c: 3}); + + // $and and regexes + match({$and: [{a: /a/}]}, {a: "cat"}); + match({$and: [{a: /a/i}]}, {a: "CAT"}); + nomatch({$and: [{a: /o/}]}, {a: "cat"}); + nomatch({$and: [{a: /a/}, {a: /o/}]}, {a: "cat"}); + match({$and: [{a: /a/}, {b: /o/}]}, {a: "cat", b: "dog"}); + nomatch({$and: [{a: /a/}, {b: /a/}]}, {a: "cat", b: "dog"}); + + // $and, dot-notation, and nested objects + match({$and: [{"a.b": 1}]}, {a: {b: 1}}); + match({$and: [{a: {b: 1}}]}, {a: {b: 1}}); + nomatch({$and: [{"a.b": 2}]}, {a: {b: 1}}); + nomatch({$and: [{"a.c": 1}]}, {a: {b: 1}}); + nomatch({$and: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); + nomatch({$and: [{"a.b": 1}, {a: {b: 2}}]}, {a: {b: 1}}); + match({$and: [{"a.b": 1}, {"c.d": 2}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{"a.b": 1}, {"c.d": 1}]}, {a: {b: 1}, c: {d: 2}}); + match({$and: [{"a.b": 1}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{"a.b": 1}, {c: {d: 1}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{"a.b": 2}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + match({$and: [{a: {b: 1}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{a: {b: 2}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + + // $and and $in + nomatch({$and: [{a: {$in: []}}]}, {}); + match({$and: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); + nomatch({$and: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$in: [1, 2, 3]}}, {a: {$in: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {a: 1, b: 4}); + match({$and: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {a: 1, b: 4}); + + + // $and and $nin + match({$and: [{a: {$nin: []}}]}, {}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); + match({$and: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {a: {$nin: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 4}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {a: 1, b: 4}); + + // $and and $lt, $lte, $gt, $gte + match({$and: [{a: {$lt: 2}}]}, {a: 1}); + nomatch({$and: [{a: {$lt: 1}}]}, {a: 1}); + match({$and: [{a: {$lte: 1}}]}, {a: 1}); + match({$and: [{a: {$gt: 0}}]}, {a: 1}); + nomatch({$and: [{a: {$gt: 1}}]}, {a: 1}); + match({$and: [{a: {$gte: 1}}]}, {a: 1}); + match({$and: [{a: {$gt: 0}}, {a: {$lt: 2}}]}, {a: 1}); + nomatch({$and: [{a: {$gt: 1}}, {a: {$lt: 2}}]}, {a: 1}); + nomatch({$and: [{a: {$gt: 0}}, {a: {$lt: 1}}]}, {a: 1}); + match({$and: [{a: {$gte: 1}}, {a: {$lte: 1}}]}, {a: 1}); + nomatch({$and: [{a: {$gte: 2}}, {a: {$lte: 0}}]}, {a: 1}); + + // $and and $ne + match({$and: [{a: {$ne: 1}}]}, {}); + nomatch({$and: [{a: {$ne: 1}}]}, {a: 1}); + match({$and: [{a: {$ne: 1}}]}, {a: 2}); + nomatch({$and: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 2}); + match({$and: [{a: {$ne: 1}}, {a: {$ne: 3}}]}, {a: 2}); + + // $and and $not + match({$and: [{a: {$not: {$gt: 2}}}]}, {a: 1}); + nomatch({$and: [{a: {$not: {$lt: 2}}}]}, {a: 1}); + match({$and: [{a: {$not: {$lt: 0}}}, {a: {$not: {$gt: 2}}}]}, {a: 1}); + nomatch({$and: [{a: {$not: {$lt: 2}}}, {a: {$not: {$gt: 0}}}]}, {a: 1}); + + // $where + match({$where: "this.a === 1"}, {a: 1}); + nomatch({$where: "this.a !== 1"}, {a: 1}); + nomatch({$where: "this.a === 1", a: 2}, {a: 1}); + match({$where: "this.a === 1", b: 2}, {a: 1, b: 2}); + match({$where: "this.a === 1 && this.b === 2"}, {a: 1, b: 2}); + match({$where: "Array.isArray(this.a)"}, {a: []}); + nomatch({$where: "Array.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"}]}); + + // $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: - // - $or, $and, $nor, $where // - $elemMatch - // - people.2.name // - non-scalar arguments to $gt, $lt, etc }); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index e85dce63ff8..2fc8f4173ed 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -1,3 +1,285 @@ +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; +} + +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; + } + 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; + }, + + "$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 { + 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.. @@ -258,6 +540,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,292 +591,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"); }; - -// XXX implement ordinal indexing: 'people.2.name' - -// 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') { - var clauses = []; - _.each(value, function (c) { - clauses.push(LocalCollection._exprForSelector(c, literals)); - }); - if (clauses.length === 0) return 'true'; - return '(' + clauses.join('||') +')'; - } - - if (op === '$and') { - var clauses = []; - _.each(value, function (c) { - clauses.push(LocalCollection._exprForSelector(c, literals)); - }); - if (clauses.length === 0) return 'true'; - return '(' + clauses.join('&&') +')'; - } - - if (op === '$nor') { - var clauses = []; - _.each(value, function (c) { - clauses.push("!(" + LocalCollection._exprForSelector(c, literals) + ")"); - }); - if (clauses.length === 0) return 'true'; - 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("Unrecogized 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; - while (keyparts.length) { - var part = keyparts.pop(); - var formal = keyparts.length ? "x" : "doc"; - if (innermost) { - ret = '(function(x){return ' + predcode + ';})(' + formal + '&&' + formal + '[' + - JSON.stringify(part) + '])'; - innermost = false; - } else { - // for all but the innermost level of a dotted expression, - // if the runtime type is an array, search it - ret = 'f._matches(' + formal + '&&' + formal + '[' + JSON.stringify(part) + - '], function(x){return ' + ret + ';})'; - } - } - - 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; -};