Merge branch 'sort-selector' into devel

This commit is contained in:
David Glasser
2014-03-17 02:56:12 -07:00
8 changed files with 356 additions and 127 deletions

View File

@@ -27,6 +27,14 @@
`{a: [{x: 0, y: 5}, {x: 1, y: 3}]}`, because the 3 should not be used as a
tie-breaker because it is not "next to" the tied 0s.
* minimongo: Fix sort implementation when selector and sort key share a field,
that field matches an array in the document, and only some values of the array
match the selector. eg, ensure that with sort key `{a: 1}` and selector
`{a: {$gt: 3}}`, the document `{a: [4, 6]}` sorts before `{a: [1, 5]}`,
because the 1 should not be used as a sort key because it does not match the
selector. (We only approximate the MongoDB behavior here by only supporting
relatively selectors.)
* Use `faye-websocket` (0.7.2) npm module instead of `websocket` (1.0.8) for
server-to-server DDP.

View File

@@ -356,6 +356,10 @@ EJSON.clone = function (v) {
return null; // null has typeof "object"
if (v instanceof Date)
return new Date(v.getTime());
// RegExps are not really EJSON elements (eg we don't define a serialization
// for them), but they're immutable anyway, so we can support them in clone.
if (v instanceof RegExp)
return v;
if (EJSON.isBinary(v)) {
ret = EJSON.newBinary(v.length);
for (var i = 0; i < v.length; i++) {

View File

@@ -89,18 +89,20 @@ LocalCollection.Cursor = function (collection, selector, options) {
var self = this;
if (!options) options = {};
this.collection = collection;
self.collection = collection;
self.sorter = null;
if (LocalCollection._selectorIsId(selector)) {
// stash for fast path
self._selectorId = selector;
self.matcher = new Minimongo.Matcher(selector, self);
self.sorter = undefined;
} else {
self._selectorId = undefined;
self.matcher = new Minimongo.Matcher(selector, self);
self.sorter = (self.matcher.hasGeoQuery() || options.sort) ?
new Minimongo.Sorter(options.sort || []) : null;
if (self.matcher.hasGeoQuery() || options.sort) {
self.sorter = new Minimongo.Sorter(options.sort || [],
{ matcher: self.matcher });
}
}
self.skip = options.skip;
self.limit = options.limit;

View File

@@ -92,6 +92,8 @@ Tinytest.add("minimongo - modifier affects selector", function (test) {
affected({ 'foo.bar.baz': 0 }, { $unset: { 'foo.3.bar': 1 } }, "delicate work with numeric fields in selector");
affected({ 'foo.0.bar': 0 }, { $set: { 'foo.0.0.bar': 1 } }, "delicate work with nested arrays and selectors by indecies");
affected({foo: {$elemMatch: {bar: 5}}}, {$set: {'foo.4.bar': 5}}, "$elemMatch");
});
Tinytest.add("minimongo - selector and projection combination", function (test) {

View File

@@ -1781,19 +1781,32 @@ Tinytest.add("minimongo - array sort", function (test) {
// in the document, and when sorting descending, you use the maximum value you
// can find. So [1, 4] shows up in the 1 slot when sorting ascending and the 4
// slot when sorting descending.
c.insert({up: 1, down: 1, a: {x: [1, 4]}});
c.insert({up: 2, down: 2, a: [{x: [2]}, {x: 3}]});
c.insert({up: 0, down: 4, a: {x: 0}});
c.insert({up: 3, down: 3, a: {x: 2.5}});
c.insert({up: 4, down: 0, a: {x: 5}});
//
// Similarly, "selected" is the index that the doc should have in the query
// that sorts ascending on "a.x" and selects {'a.x': {$gt: 1}}. In this case,
// the 1 in [1, 4] may not be used as a sort key.
c.insert({up: 1, down: 1, selected: 2, a: {x: [1, 4]}});
c.insert({up: 2, down: 2, selected: 0, a: [{x: [2]}, {x: 3}]});
c.insert({up: 0, down: 4, a: {x: 0}});
c.insert({up: 3, down: 3, selected: 1, a: {x: 2.5}});
c.insert({up: 4, down: 0, selected: 3, a: {x: 5}});
test.equal(
_.pluck(c.find({}, {sort: {'a.x': 1}}).fetch(), 'up'),
_.range(c.find().count()));
// Test that the the documents in "cursor" contain values with the name
// "field" running from 0 to the max value of that name in the collection.
var testCursorMatchesField = function (cursor, field) {
var fieldValues = [];
c.find().forEach(function (doc) {
if (_.has(doc, field))
fieldValues.push(doc[field]);
});
test.equal(_.pluck(cursor.fetch(), field),
_.range(_.max(fieldValues) + 1));
};
test.equal(
_.pluck(c.find({}, {sort: {'a.x': -1}}).fetch(), 'down'),
_.range(c.find().count()));
testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up');
testCursorMatchesField(c.find({}, {sort: {'a.x': -1}}), 'down');
testCursorMatchesField(c.find({'a.x': {$gt: 1}}, {sort: {'a.x': 1}}),
'selected');
});
Tinytest.add("minimongo - sort keys", function (test) {
@@ -1859,6 +1872,87 @@ Tinytest.add("minimongo - sort keys", function (test) {
{x: 2, y: [4, 5]}]});
});
Tinytest.add("minimongo - sort key filter", function (test) {
var testOrder = function (sortSpec, selector, doc1, doc2) {
var matcher = new Minimongo.Matcher(selector);
var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher});
var comparator = sorter.getComparator();
var comparison = comparator(doc1, doc2);
test.isTrue(comparison < 0);
};
testOrder({'a.x': 1}, {'a.x': {$gt: 1}},
{a: {x: 3}},
{a: {x: [1, 4]}});
testOrder({'a.x': 1}, {'a.x': {$gt: 0}},
{a: {x: [1, 4]}},
{a: {x: 3}});
var keyCompatible = function (sortSpec, selector, key, compatible) {
var matcher = new Minimongo.Matcher(selector);
var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher});
var actual = sorter._keyCompatibleWithSelector(key);
test.equal(actual, compatible);
};
keyCompatible({a: 1}, {a: 5}, [5], true);
keyCompatible({a: 1}, {a: 5}, [8], false);
keyCompatible({a: 1}, {a: {x: 5}}, [{x: 5}], true);
keyCompatible({a: 1}, {a: {x: 5}}, [{x: 5, y: 9}], false);
keyCompatible({'a.x': 1}, {a: {x: 5}}, [5], true);
// To confirm this:
// > db.x.insert({_id: "q", a: [{x:1}, {x:5}], b: 2})
// > db.x.insert({_id: "w", a: [{x:5}, {x:10}], b: 1})
// > db.x.find({}).sort({'a.x': 1, b: 1})
// { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 }
// { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 }
// > db.x.find({a: {x:5}}).sort({'a.x': 1, b: 1})
// { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 }
// { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 }
// > db.x.find({'a.x': 5}).sort({'a.x': 1, b: 1})
// { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 }
// { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 }
// ie, only the last one manages to trigger the key compatibility code,
// not the previous one. (The "b" sort is necessary because when the key
// compatibility code *does* kick in, both documents only end up with "5"
// for the first field as their only sort key, and we need to differentiate
// somehow...)
keyCompatible({'a.x': 1}, {a: {x: 5}}, [1], true);
keyCompatible({'a.x': 1}, {'a.x': 5}, [5], true);
keyCompatible({'a.x': 1}, {'a.x': 5}, [1], false);
// Regex key check.
keyCompatible({a: 1}, {a: /^foo+/}, ['foo'], true);
keyCompatible({a: 1}, {a: /^foo+/}, ['foooo'], true);
keyCompatible({a: 1}, {a: /^foo+/}, ['foooobar'], true);
keyCompatible({a: 1}, {a: /^foo+/}, ['afoooo'], false);
keyCompatible({a: 1}, {a: /^foo+/}, [''], false);
keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foo'], true);
keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooo'], true);
keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooobar'], true);
keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['afoooo'], false);
keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, [''], false);
keyCompatible({a: 1}, {a: /^foo+/i}, ['foo'], true);
// Key compatibility check appears to be turned off for regexps with flags.
keyCompatible({a: 1}, {a: /^foo+/i}, ['bar'], true);
keyCompatible({a: 1}, {a: /^foo+/m}, ['bar'], true);
keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "i"}}, ['bar'], true);
keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "m"}}, ['bar'], true);
// Multiple keys!
keyCompatible({a: 1, b: 1, c: 1},
{a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 2], true);
keyCompatible({a: 1, b: 1, c: 1},
{a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 4], false);
keyCompatible({a: 1, b: 1, c: 1},
{a: {$gt: 5}, c: {$lt: 3}}, [3, "bla", 1], false);
// No filtering is done (ie, all keys are compatible) if the first key isn't
// constrained.
keyCompatible({a: 1, b: 1, c: 1},
{c: {$lt: 3}}, [3, "bla", 4], true);
});
Tinytest.add("minimongo - binary search", function (test) {
var forwardCmp = function (a, b) {
return a - b;

View File

@@ -35,7 +35,10 @@ Minimongo.Matcher = function (selector) {
// Set to a dummy document which always matches this Matcher. Or set to null
// if such document is too hard to find.
self._matchingDocument = undefined;
// A clone of the original selector. Used by canBecomeTrueByModifier.
// A clone of the original selector. It may just be a function if the user
// passed in a function; otherwise is definitely an object (eg, IDs are
// translated into {_id: ID} first. Used by canBecomeTrueByModifier and
// Sorter._useWithMatcher.
self._selector = null;
self._docMatcher = self._compileSelector(selector);
};
@@ -201,7 +204,7 @@ var convertElementMatcherToBranchedMatcher = function (
};
// Takes a RegExp object and returns an element matcher.
var regexpElementMatcher = function (regexp) {
regexpElementMatcher = function (regexp) {
return function (value) {
if (value instanceof RegExp) {
// Comparing two regexps means seeing if the regexps are identical
@@ -217,7 +220,7 @@ var regexpElementMatcher = function (regexp) {
// Takes something that is not an operator object and returns an element matcher
// for equality with that thing.
var equalityElementMatcher = function (elementSelector) {
equalityElementMatcher = function (elementSelector) {
if (isOperatorObject(elementSelector))
throw Error("Can't create equalityValueSelector for operator object");
@@ -261,8 +264,6 @@ var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) {
VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot));
} else if (_.has(ELEMENT_OPERATORS, operator)) {
var options = ELEMENT_OPERATORS[operator];
if (typeof options === 'function')
options = {compileElementSelector: options};
operatorMatchers.push(
convertElementMatcherToBranchedMatcher(
options.compileElementSelector(
@@ -380,7 +381,7 @@ var VALUE_OPERATORS = {
},
$nin: function (operand) {
return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(
ELEMENT_OPERATORS.$in(operand)));
ELEMENT_OPERATORS.$in.compileElementSelector(operand)));
},
$exists: function (operand) {
var exists = convertElementMatcherToBranchedMatcher(function (value) {
@@ -508,47 +509,51 @@ var pointToArray = function (point) {
// Helper for $lt/$gt/$lte/$gte.
var makeInequality = function (cmpValueComparator) {
return function (operand) {
// Arrays never compare false with non-arrays for any inequality.
// XXX This was behavior we observed in pre-release MongoDB 2.5, but
// it seems to have been reverted.
// See https://jira.mongodb.org/browse/SERVER-11444
if (isArray(operand)) {
return function () {
return false;
return {
compileElementSelector: function (operand) {
// Arrays never compare false with non-arrays for any inequality.
// XXX This was behavior we observed in pre-release MongoDB 2.5, but
// it seems to have been reverted.
// See https://jira.mongodb.org/browse/SERVER-11444
if (isArray(operand)) {
return function () {
return false;
};
}
// Special case: consider undefined and null the same (so true with
// $gte/$lte).
if (operand === undefined)
operand = null;
var operandType = LocalCollection._f._type(operand);
return function (value) {
if (value === undefined)
value = null;
// Comparisons are never true among things of different type (except
// null vs undefined).
if (LocalCollection._f._type(value) !== operandType)
return false;
return cmpValueComparator(LocalCollection._f._cmp(value, operand));
};
}
// Special case: consider undefined and null the same (so true with
// $gte/$lte).
if (operand === undefined)
operand = null;
var operandType = LocalCollection._f._type(operand);
return function (value) {
if (value === undefined)
value = null;
// Comparisons are never true among things of different type (except null
// vs undefined).
if (LocalCollection._f._type(value) !== operandType)
return false;
return cmpValueComparator(LocalCollection._f._cmp(value, operand));
};
};
};
// Each element selector is a function with args:
// - operand - the "right hand side" of the operator
// - valueSelector - the "context" for the operator (so that $regex can find
// $options)
// Or is an object with an compileElementSelector field (the above) and optional
// flags dontExpandLeafArrays and dontIncludeLeafArrays which control if
// expandArraysInBranches is called and if it takes an optional argument.
//
// An element selector compiler returns a function mapping a single value to
// bool.
var ELEMENT_OPERATORS = {
// Each element selector contains:
// - compileElementSelector, a function with args:
// - operand - the "right hand side" of the operator
// - valueSelector - the "context" for the operator (so that $regex can find
// $options)
// - matcher - the Matcher this is going into (so that $elemMatch can compile
// more things)
// returning a function mapping a single value to bool.
// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from
// being called
// - dontIncludeLeafArrays, a bool which causes an argument to be passed to
// expandArraysInBranches if it is called
ELEMENT_OPERATORS = {
$lt: makeInequality(function (cmpValue) {
return cmpValue < 0;
}),
@@ -561,41 +566,45 @@ var ELEMENT_OPERATORS = {
$gte: makeInequality(function (cmpValue) {
return cmpValue >= 0;
}),
$mod: function (operand) {
if (!(isArray(operand) && operand.length === 2
&& typeof(operand[0]) === 'number'
&& typeof(operand[1]) === 'number')) {
throw Error("argument to $mod must be an array of two numbers");
$mod: {
compileElementSelector: function (operand) {
if (!(isArray(operand) && operand.length === 2
&& typeof(operand[0]) === 'number'
&& typeof(operand[1]) === 'number')) {
throw Error("argument to $mod must be an array of two numbers");
}
// XXX could require to be ints or round or something
var divisor = operand[0];
var remainder = operand[1];
return function (value) {
return typeof value === 'number' && value % divisor === remainder;
};
}
// XXX could require to be ints or round or something
var divisor = operand[0];
var remainder = operand[1];
return function (value) {
return typeof value === 'number' && value % divisor === remainder;
};
},
$in: function (operand) {
if (!isArray(operand))
throw Error("$in needs an array");
$in: {
compileElementSelector: function (operand) {
if (!isArray(operand))
throw Error("$in needs an array");
var elementMatchers = [];
_.each(operand, function (option) {
if (option instanceof RegExp)
elementMatchers.push(regexpElementMatcher(option));
else if (isOperatorObject(option))
throw Error("cannot nest $ under $in");
else
elementMatchers.push(equalityElementMatcher(option));
});
return function (value) {
// Allow {a: {$in: [null]}} to match when 'a' does not exist.
if (value === undefined)
value = null;
return _.any(elementMatchers, function (e) {
return e(value);
var elementMatchers = [];
_.each(operand, function (option) {
if (option instanceof RegExp)
elementMatchers.push(regexpElementMatcher(option));
else if (isOperatorObject(option))
throw Error("cannot nest $ under $in");
else
elementMatchers.push(equalityElementMatcher(option));
});
};
return function (value) {
// Allow {a: {$in: [null]}} to match when 'a' does not exist.
if (value === undefined)
value = null;
return _.any(elementMatchers, function (e) {
return e(value);
});
};
}
},
$size: {
// {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we
@@ -630,30 +639,32 @@ var ELEMENT_OPERATORS = {
};
}
},
$regex: function (operand, valueSelector) {
if (!(typeof operand === 'string' || operand instanceof RegExp))
throw Error("$regex has to be a string or RegExp");
$regex: {
compileElementSelector: function (operand, valueSelector) {
if (!(typeof operand === 'string' || operand instanceof RegExp))
throw Error("$regex has to be a string or RegExp");
var regexp;
if (valueSelector.$options !== undefined) {
// Options passed in $options (even the empty string) always overrides
// options in the RegExp object itself. (See also
// Meteor.Collection._rewriteSelector.)
var regexp;
if (valueSelector.$options !== undefined) {
// Options passed in $options (even the empty string) always overrides
// options in the RegExp object itself. (See also
// Meteor.Collection._rewriteSelector.)
// Be clear that we only support the JS-supported options, not extended
// ones (eg, Mongo supports x and s). Ideally we would implement x and s
// by transforming the regexp, but not today...
if (/[^gim]/.test(valueSelector.$options))
throw new Error("Only the i, m, and g regexp options are supported");
// Be clear that we only support the JS-supported options, not extended
// ones (eg, Mongo supports x and s). Ideally we would implement x and s
// by transforming the regexp, but not today...
if (/[^gim]/.test(valueSelector.$options))
throw new Error("Only the i, m, and g regexp options are supported");
var regexSource = operand instanceof RegExp ? operand.source : operand;
regexp = new RegExp(regexSource, valueSelector.$options);
} else if (operand instanceof RegExp) {
regexp = operand;
} else {
regexp = new RegExp(operand);
var regexSource = operand instanceof RegExp ? operand.source : operand;
regexp = new RegExp(regexSource, valueSelector.$options);
} else if (operand instanceof RegExp) {
regexp = operand;
} else {
regexp = new RegExp(operand);
}
return regexpElementMatcher(regexp);
}
return regexpElementMatcher(regexp);
},
$elemMatch: {
dontExpandLeafArrays: true,

View File

@@ -11,8 +11,9 @@
// first object comes first in order, 1 if the second object comes
// first, or 0 if neither object comes before the other.
Minimongo.Sorter = function (spec) {
Minimongo.Sorter = function (spec, options) {
var self = this;
options = options || {};
self._sortSpecParts = [];
@@ -59,6 +60,12 @@ Minimongo.Sorter = function (spec) {
_.map(self._sortSpecParts, function (spec, i) {
return self._keyFieldComparator(i);
}));
// If you specify a matcher for this Sorter, _keyFilter may be set to a
// function which selects whether or not a given "sort key" (tuple of values
// for the different sort spec fields) is compatible with the selector.
self._keyFilter = null;
options.matcher && self._useWithMatcher(options.matcher);
};
// In addition to these methods, sorter_project.js defines combineIntoProjection
@@ -106,23 +113,8 @@ _.extend(Minimongo.Sorter.prototype, {
var minKey = null;
self._generateKeysFromDoc(doc, function (key) {
// XXX This is actually wrong! In fact, the whole attempt to compile sort
// functions independently of selectors is wrong. In MongoDB, if you
// have documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]},
// then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5).
// But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does
// not match the selector, and 5 comes before 10).
//
// The way this works is pretty subtle! For example, if the documents
// are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and
// {_id: 'y', a: [{x: 5}, {x: 15}]}),
// then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and
// C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}})
// both follow this rule (y before x). (ie, you do have to apply this
// through $elemMatch.)
//
// So we ought to skip keys here that don't work for the selector, but
// we don't do that yet.
if (!self._keyCompatibleWithSelector(key))
return;
if (minKey === null) {
minKey = key;
@@ -133,11 +125,18 @@ _.extend(Minimongo.Sorter.prototype, {
}
});
// This could happen if our key filter somehow filters out all the keys even
// though somehow the selector matches.
if (minKey === null)
throw Error("sort selector found no keys in doc?");
return minKey;
},
_keyCompatibleWithSelector: function (key) {
var self = this;
return !self._keyFilter || self._keyFilter(key);
},
// Iterates over each possible "key" from doc (ie, over each branch), calling
// 'cb' with the key.
_generateKeysFromDoc: function (doc, cb) {
@@ -281,6 +280,114 @@ _.extend(Minimongo.Sorter.prototype, {
var key2 = self._getMinKeyFromDoc(doc2);
return self._compareKeys(key1, key2);
};
},
// In MongoDB, if you have documents
// {_id: 'x', a: [1, 10]} and
// {_id: 'y', a: [5, 15]},
// then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5).
// But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not
// match the selector, and 5 comes before 10).
//
// The way this works is pretty subtle! For example, if the documents
// are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and
// {_id: 'y', a: [{x: 5}, {x: 15}]}),
// then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and
// C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}})
// both follow this rule (y before x). (ie, you do have to apply this
// through $elemMatch.)
//
// So if you pass a matcher to this sorter's constructor, we will attempt to
// skip sort keys that don't match the selector. The logic here is pretty
// subtle and undocumented; we've gotten as close as we can figure out based
// on our understanding of Mongo's behavior.
_useWithMatcher: function (matcher) {
var self = this;
if (self._keyFilter)
throw Error("called _useWithMatcher twice?");
// If we are only sorting by distance, then we're not going to bother to
// build a key filter.
// XXX figure out how geoqueries interact with this stuff
if (_.isEmpty(self._sortSpecParts))
return;
var selector = matcher._selector;
// If the user just passed a literal function to find(), then we can't get a
// key filter from it.
if (selector instanceof Function)
return;
var constraintsByPath = {};
_.each(self._sortSpecParts, function (spec, i) {
constraintsByPath[spec.path] = [];
});
_.each(selector, function (subSelector, key) {
// XXX support $and and $or
var constraints = constraintsByPath[key];
if (!constraints)
return;
// XXX it looks like the real MongoDB implementation isn't "does the
// regexp match" but "does the value fall into a range named by the
// literal prefix of the regexp", ie "foo" in /^foo(bar|baz)+/ But
// "does the regexp match" is a good approximation.
if (subSelector instanceof RegExp) {
// As far as we can tell, using either of the options that both we and
// MongoDB support ('i' and 'm') disables use of the key filter. This
// makes sense: MongoDB mostly appears to be calculating ranges of an
// index to use, which means it only cares about regexps that match
// one range (with a literal prefix), and both 'i' and 'm' prevent the
// literal prefix of the regexp from actually meaning one range.
if (subSelector.ignoreCase || subSelector.multiline)
return;
constraints.push(regexpElementMatcher(subSelector));
return;
}
if (isOperatorObject(subSelector)) {
_.each(subSelector, function (operand, operator) {
if (_.contains(['$lt', '$lte', '$gt', '$gte'], operator)) {
// XXX this depends on us knowing that these operators don't use any
// of the arguments to compileElementSelector other than operand.
constraints.push(
ELEMENT_OPERATORS[operator].compileElementSelector(operand));
}
// See comments in the RegExp block above.
if (operator === '$regex' && !subSelector.$options) {
constraints.push(
ELEMENT_OPERATORS.$regex.compileElementSelector(
operand, subSelector));
}
// XXX support {$exists: true}, $mod, $type, $in, $elemMatch
});
return;
}
// OK, it's an equality thing.
constraints.push(equalityElementMatcher(subSelector));
});
// It appears that the first sort field is treated differently from the
// others; we shouldn't create a key filter unless the first sort field is
// restricted, though after that point we can restrict the other sort fields
// or not as we wish.
if (_.isEmpty(constraintsByPath[self._sortSpecParts[0].path]))
return;
self._keyFilter = function (key) {
return _.all(self._sortSpecParts, function (specPart, index) {
return _.all(constraintsByPath[specPart.path], function (f) {
return f(key[index]);
});
});
};
}
});

View File

@@ -1023,7 +1023,8 @@ MongoConnection.prototype._observeChanges = function (
if (!cursorDescription.options.sort)
return true;
try {
sorter = new Minimongo.Sorter(cursorDescription.options.sort);
sorter = new Minimongo.Sorter(cursorDescription.options.sort,
{ matcher: matcher });
return true;
} catch (e) {
// XXX make all compilation errors MinimongoError or something