Merge branch 'devel' into ddp-pre1-selector-compiler-rewrite

Note: some tests fail. Will be resolved before merging to ddp-pre1.

Conflicts:
	packages/minimongo/selector.js
This commit is contained in:
David Glasser
2013-02-04 17:44:43 -08:00
6 changed files with 416 additions and 427 deletions

View File

@@ -114,8 +114,8 @@ with the project!
<dl class="involved">
<dt><span>Stack Overflow</span></dt>
<dd>The best place to ask (and answer!) technical questions is on [Stack
Overflow](http://stackoverflow.com/questions/tagged/meteor). Be sure to add
<dd>The best place to ask (and answer!) technical questions is on <a href="http://stackoverflow.com/questions/tagged/meteor">Stack
Overflow</a>. Be sure to add
the <code>meteor</code> tag to your question.
</dd>

View File

@@ -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: {

View File

@@ -477,6 +477,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'});
@@ -524,6 +528,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
@@ -796,11 +803,28 @@ 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

View File

@@ -12,7 +12,6 @@ Package.on_use(function (api, where) {
api.add_files([
'minimongo.js',
'selector.js',
'sort.js',
'uuid.js',
'modify.js',
'diff.js',

View File

@@ -1,53 +1,296 @@
(function(){
var _anyIfArray = function (x, f) {
if (_.isArray(x))
return _.any(x, f);
return f(x);
};
var _anyIfArrayPlus = function (x, f) {
if (f(x))
return true;
return _.isArray(x) && _.any(x, f);
};
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
};
var compileValueSelector = function (valueSelector) {
if (valueSelector == null) { // undefined or null
return function (value) {
return _anyIfArray(value, function (x) {
return x == null; // undefined or null
});
};
}
// 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);
};
}
};
var VALUE_OPERATORS = {
"$in": function (operand) {
if (!_.isArray(operand))
throw new Error("Argument to $in must be array");
return function (value) {
return _anyIfArrayPlus(value, function (x) {
return _.any(operand, function (operandElt) {
return LocalCollection._f._equal(operandElt, x);
});
});
};
},
"$all": function (operand) {
if (!_.isArray(operand))
throw new Error("Argument to $all must be array");
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 (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) < 0;
});
};
},
"$lte": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) <= 0;
});
};
},
"$gt": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) > 0;
});
};
},
"$gte": function (operand) {
return function (value) {
return _anyIfArray(value, function (x) {
return LocalCollection._f._cmp(x, operand) >= 0;
});
};
},
"$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");
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 (operand) {
return function (value) {
return operand === (value !== undefined);
};
},
"$mod": function (operand) {
var divisor = operand[0],
remainder = operand[1];
return function (value) {
return _anyIfArray(value, function (x) {
return x % divisor === remainder;
});
};
},
"$size": function (operand) {
return function (value) {
return _.isArray(value) && operand === value.length;
};
},
"$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 (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 (operand) {
// evaluation happens at the $regex function above
return function (value) { return true; };
},
"$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 (operand) {
var matcher = compileValueSelector(operand);
return function (value) {
return !matcher(value);
};
}
};
// helpers used by compiled selector code
LocalCollection._f = {
// XXX for _all and _in, consider building 'inquery' at compile time..
_all: function (x, qval) {
// $all is only meaningful on arrays
if (!(x instanceof Array)) {
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++;
}
});
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;
}
}
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])
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;
}
},
_type: function (v) {
if (typeof v === "number")
return 1;
@@ -83,36 +326,6 @@ LocalCollection._f = {
return EJSON.equals(a, b, {keyOrderSensitive: true});
},
// 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) {
@@ -200,311 +413,129 @@ LocalCollection._matches = function (selector, doc) {
return (LocalCollection._compileSelector(selector))(doc);
};
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;
};
};
// 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));
});
}
});
return function (doc) {
return _.all(perKeySelectors, function (f) {
return f(doc);
});
};
};
// 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);};
// 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;};
// 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);
// Top level can't be an array or true.
if (typeof(selector) === 'boolean' || _.isArray(selector))
throw new Error("Invalid selector: " + selector);
return compileDocumentSelector(selector);
};
// 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];
// 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.
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));
}
};
LocalCollection._compileSort = function (spec) {
var sortSpecParts = [];
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 if (value.serializeForEval) {
expr = 'f._equal(x,' + value.serializeForEval() + ')';
} 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']));
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 {
literals.push(arg);
sortSpecParts.push({
lookup: makeLookupFunction(spec[i][0]),
ascending: spec[i][1] !== "desc"
});
}
} 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 if (typeof spec === "object") {
for (var key in spec) {
sortSpecParts.push({
lookup: makeLookupFunction(key),
ascending: spec[key] >= 0
});
}
} else {
throw Error("Unrecognized key in selector: " + type);
throw Error("Bad sort specification: ", JSON.stringify(spec));
}
if (search) {
expr = 'f.' + search + '(x,function(x){return ' +
expr + ';})';
}
if (sortSpecParts.length === 0)
return function () {return 0;};
if (negate)
expr = '!' + expr;
return expr;
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;
};
};
})();

View File

@@ -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);
};