diff --git a/History.md b/History.md
index 626c2ba91d9..6aafd15dacb 100644
--- a/History.md
+++ b/History.md
@@ -1,6 +1,15 @@
## vNEXT
+## v0.5.6
+
+* Fix 0.5.5 regression: Minimongo selectors matching subdocuments under arrays
+ did not work correctly.
+
+* Some Bootstrap icons should have appeared white.
+
+Patches contributed by GitHub user benjaminchelli.
+
## v0.5.5
* Deprecate `Meteor.autosubscribe`. `Meteor.subscribe` now works within
diff --git a/admin/debian/changelog b/admin/debian/changelog
index ef65b0aec6f..e60dea03530 100644
--- a/admin/debian/changelog
+++ b/admin/debian/changelog
@@ -1,4 +1,4 @@
-meteor (0.5.5-1) unstable; urgency=low
+meteor (0.5.6-1) unstable; urgency=low
* Automated debian build.
diff --git a/admin/install-s3.sh b/admin/install-s3.sh
index 867b9f48f7b..09206f8fd01 100755
--- a/admin/install-s3.sh
+++ b/admin/install-s3.sh
@@ -5,7 +5,7 @@
## example.
URLBASE="https://d3sqy0vbqsdhku.cloudfront.net"
-VERSION="0.5.5"
+VERSION="0.5.6"
PKGVERSION="${VERSION}-1"
UNAME=`uname`
diff --git a/admin/manifest.json b/admin/manifest.json
index ad1d1b3f86d..687a6085138 100644
--- a/admin/manifest.json
+++ b/admin/manifest.json
@@ -1,6 +1,6 @@
{
- "version": "0.5.5",
- "deb_version": "0.5.5-1",
- "rpm_version": "0.5.5-1",
+ "version": "0.5.6",
+ "deb_version": "0.5.6-1",
+ "rpm_version": "0.5.6-1",
"urlbase": "https://d3sqy0vbqsdhku.cloudfront.net"
}
diff --git a/admin/meteor.spec b/admin/meteor.spec
index 66e173c63d9..1e3123aba35 100644
--- a/admin/meteor.spec
+++ b/admin/meteor.spec
@@ -5,7 +5,7 @@
Summary: Meteor platform and JavaScript application server
Vendor: Meteor
Name: meteor
-Version: 0.5.5
+Version: 0.5.6
Release: 1
License: MIT
Group: Networking/WWW
diff --git a/app/lib/updater.js b/app/lib/updater.js
index 0b9d8e71f7d..f0a90dee1d4 100644
--- a/app/lib/updater.js
+++ b/app/lib/updater.js
@@ -2,7 +2,7 @@
// true. This will make it act as if it is at version 0.1.0 and use test URLs
// for update checks.
var testingUpdater = false;
-exports.CURRENT_VERSION = testingUpdater ? "0.1.0" : "0.5.5";
+exports.CURRENT_VERSION = testingUpdater ? "0.1.0" : "0.5.6";
var fs = require("fs");
var http = require("http");
diff --git a/app/meteor/post-upgrade.js b/app/meteor/post-upgrade.js
index 67c096209cb..e73ba341eff 100644
--- a/app/meteor/post-upgrade.js
+++ b/app/meteor/post-upgrade.js
@@ -2,7 +2,7 @@ try {
// XXX can't get this from updater.js because in 0.3.7 and before the
// updater didn't have the right NODE_PATH set. At some point we can
// remove this and just use updater.CURRENT_VERSION.
- var VERSION = "0.5.5";
+ var VERSION = "0.5.6";
var fs = require('fs');
var path = require('path');
diff --git a/docs/client/docs.html b/docs/client/docs.html
index b616d4b11ad..4d5db6529cd 100644
--- a/docs/client/docs.html
+++ b/docs/client/docs.html
@@ -11,7 +11,7 @@
-
Meteor 0.5.5
+
Meteor 0.5.6
{{> introduction }}
{{> concepts }}
{{> api }}
diff --git a/docs/client/docs.js b/docs/client/docs.js
index eff5e9690c1..3bca498c6e4 100644
--- a/docs/client/docs.js
+++ b/docs/client/docs.js
@@ -1,4 +1,4 @@
-METEOR_VERSION = "0.5.5";
+METEOR_VERSION = "0.5.6";
Meteor.startup(function () {
// XXX this is broken by the new multi-page layout. Also, it was
diff --git a/packages/bootstrap/css/bootstrap-override.css b/packages/bootstrap/css/bootstrap-override.css
index 686584e6eaf..169165e46a1 100644
--- a/packages/bootstrap/css/bootstrap-override.css
+++ b/packages/bootstrap/css/bootstrap-override.css
@@ -1,4 +1,4 @@
-/*
+/*
* XXX Hack to make bootstrap work when bundled. This needs to be included
* _after_ the standard bootstrap css files.
*
@@ -10,7 +10,28 @@
[class*=" icon-"] {
background-image: url("/packages/bootstrap/img/glyphicons-halflings.png");
}
-
-.icon-white {
+/*
+ * Selectors borrowed from bootstrap.css. For all releases of bootstrap, when
+ * we upgrade, update this file to borrow the selectors from where bootstrap.css
+ * references any .png. When we update to using .less instead, use the less
+ * directives in a less file instead.
+ */
+.icon-white,
+.nav-pills > .active > a > [class^="icon-"],
+.nav-pills > .active > a > [class*=" icon-"],
+.nav-list > .active > a > [class^="icon-"],
+.nav-list > .active > a > [class*=" icon-"],
+.navbar-inverse .nav > .active > a > [class^="icon-"],
+.navbar-inverse .nav > .active > a > [class*=" icon-"],
+.dropdown-menu > li > a:hover > [class^="icon-"],
+.dropdown-menu > li > a:focus > [class^="icon-"],
+.dropdown-menu > li > a:hover > [class*=" icon-"],
+.dropdown-menu > li > a:focus > [class*=" icon-"],
+.dropdown-menu > .active > a > [class^="icon-"],
+.dropdown-menu > .active > a > [class*=" icon-"],
+.dropdown-submenu:hover > a > [class^="icon-"],
+.dropdown-submenu:focus > a > [class^="icon-"],
+.dropdown-submenu:hover > a > [class*=" icon-"],
+.dropdown-submenu:focus > a > [class*=" icon-"] {
background-image: url("/packages/bootstrap/img/glyphicons-halflings-white.png");
}
diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js
index 3ef075bd0fc..51596bb4f46 100644
--- a/packages/minimongo/minimongo_tests.js
+++ b/packages/minimongo/minimongo_tests.js
@@ -210,6 +210,26 @@ Tinytest.add("minimongo - misc", function (test) {
test.equal(b.x.a, 14); // just to document current behavior
});
+Tinytest.add("minimongo - lookup", function (test) {
+ var lookupA = LocalCollection._makeLookupFunction('a');
+ test.equal(lookupA({}), [undefined]);
+ test.equal(lookupA({a: 1}), [1]);
+ test.equal(lookupA({a: [1]}), [[1]]);
+
+ var lookupAX = LocalCollection._makeLookupFunction('a.x');
+ test.equal(lookupAX({a: {x: 1}}), [1]);
+ test.equal(lookupAX({a: {x: [1]}}), [[1]]);
+ test.equal(lookupAX({a: 5}), [undefined]);
+ test.equal(lookupAX({a: [{x: 1}, {x: [2]}, {y: 3}]}),
+ [1, [2], undefined]);
+
+ var lookupA0X = LocalCollection._makeLookupFunction('a.0.x');
+ test.equal(lookupA0X({a: [{x: 1}]}), [1]);
+ test.equal(lookupA0X({a: [{x: [1]}]}), [[1]]);
+ test.equal(lookupA0X({a: 5}), [undefined]);
+ test.equal(lookupA0X({a: [{x: 1}, {x: [2]}, {y: 3}]}), [1]);
+});
+
Tinytest.add("minimongo - selector_compiler", function (test) {
var matches = function (should_match, selector, doc) {
var does_match = LocalCollection._matches(selector, doc);
@@ -808,6 +828,21 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]});
match({"room.1b": "bla"}, {room: {"1b": "bla"}});
+ match({"dogs.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]});
+ match({"dogs.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]});
+ match({"animals.dogs.name": "Fido"},
+ {animals: [{dogs: [{name: "Rover"}]},
+ {},
+ {dogs: [{name: "Fido"}, {name: "Rex"}]}]});
+ match({"animals.dogs.name": "Fido"},
+ {animals: [{dogs: {name: "Rex"}},
+ {dogs: {name: "Fido"}}]});
+ match({"animals.dogs.name": "Fido"},
+ {animals: [{dogs: [{name: "Rover"}]},
+ {},
+ {dogs: [{name: ["Fido"]}, {name: "Rex"}]}]});
+ nomatch({"dogs.name": "Fido"}, {dogs: []});
+
// $elemMatch
match({dogs: {$elemMatch: {name: /e/}}},
{dogs: [{name: "Fido"}, {name: "Rex"}]});
@@ -847,10 +882,19 @@ Tinytest.add("minimongo - ordering", function (test) {
});
};
+ // note: [] doesn't sort with "arrays", it sorts as "undefined". the position
+ // of arrays in _typeorder only matters for things like $lt. (This behavior
+ // verified with MongoDB 2.2.1.) We don't define the relative order of {a: []}
+ // and {c: 1} is undefined (MongoDB does seem to care but it's not clear how
+ // or why).
+ verify([{"a" : 1}, ["a"], [["a", "asc"]]],
+ [{a: []}, {a: 1}, {a: {}}, {a: true}]);
verify([{"a" : 1}, ["a"], [["a", "asc"]]],
- [{c: 1}, {a: 1}, {a: {}}, {a: []}, {a: true}]);
+ [{c: 1}, {a: 1}, {a: {}}, {a: true}]);
verify([{"a" : -1}, [["a", "desc"]]],
- [{a: true}, {a: []}, {a: {}}, {a: 1}, {c: 1}]);
+ [{a: true}, {a: {}}, {a: 1}, {c: 1}]);
+ verify([{"a" : -1}, [["a", "desc"]]],
+ [{a: true}, {a: {}}, {a: 1}, {a: []}]);
verify([{"a" : 1, "b": -1}, ["a", ["b", "desc"]],
[["a", "asc"], ["b", "desc"]]],
@@ -935,6 +979,30 @@ Tinytest.add("minimongo - subkey sort", function (test) {
test.equal(c.find({}, {sort: {'a.nope.c': -1}}).count(), 6);
});
+Tinytest.add("minimongo - array sort", function (test) {
+ var c = new LocalCollection();
+
+ // "up" and "down" are the indices that the docs should have when sorted
+ // ascending and descending by "a.x" respectively. They are not reverses of
+ // each other: when sorting ascending, you use the minimum value you can find
+ // 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}});
+
+ test.equal(
+ _.pluck(c.find({}, {sort: {'a.x': 1}}).fetch(), 'up'),
+ _.range(c.find().count()));
+
+ test.equal(
+ _.pluck(c.find({}, {sort: {'a.x': -1}}).fetch(), 'down'),
+ _.range(c.find().count()));
+});
+
Tinytest.add("minimongo - modify", function (test) {
var modify = function (doc, mod, result) {
diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js
index a9f229bd288..7396f24d864 100644
--- a/packages/minimongo/selector.js
+++ b/packages/minimongo/selector.js
@@ -478,18 +478,61 @@ LocalCollection._matches = function (selector, doc) {
return (LocalCollection._compileSelector(selector))(doc);
};
-var makeLookupFunction = function (key) {
+// _makeLookupFunction(key) returns a lookup function.
+//
+// A lookup function takes in a document and returns an array of matching
+// values. This array has more than one element if any segment of the key other
+// than the last one is an array. ie, any arrays found when doing non-final
+// lookups result in this function "branching"; each element in the returned
+// array represents the value found at this branch. If any branch doesn't have a
+// final value for the full key, its element in the returned list will be
+// undefined. It always returns a non-empty array.
+//
+// _makeLookupFunction('a.x')({a: {x: 1}}) returns [1]
+// _makeLookupFunction('a.x')({a: {x: [1]}}) returns [[1]]
+// _makeLookupFunction('a.x')({a: 5}) returns [undefined]
+// _makeLookupFunction('a.x')({a: [{x: 1},
+// {x: [2]},
+// {y: 3}]})
+// returns [1, [2], undefined]
+LocalCollection._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));
+ var first, lookupRest, nextIsNumeric;
+ if (dotLocation === -1) {
+ first = key;
+ } else {
+ first = key.substr(0, dotLocation);
+ var rest = key.substr(dotLocation + 1);
+ lookupRest = LocalCollection._makeLookupFunction(rest);
+ // Is the next (perhaps final) piece numeric (ie, an array lookup?)
+ nextIsNumeric = /^\d+(\.|$)/.test(rest);
+ }
+
return function (doc) {
if (doc == null) // null or undefined
- return undefined;
+ return [undefined];
var firstLevel = doc[first];
- if (lookupRest)
- return lookupRest(firstLevel);
- return firstLevel;
+
+ // We don't "branch" at the final level.
+ if (!lookupRest)
+ return [firstLevel];
+
+ // It's an empty array, and we're not done: we won't find anything.
+ if (_.isArray(firstLevel) && firstLevel.length === 0)
+ return [undefined];
+
+ // For each result at this level, finish the lookup on the rest of the key,
+ // and return everything we find. Also, if the next result is a number,
+ // don't branch here.
+ //
+ // Technically, in MongoDB, we should be able to handle the case where
+ // objects have numeric keys, but Mongo doesn't actually handle this
+ // consistently yet itself, see eg
+ // https://jira.mongodb.org/browse/SERVER-2898
+ // https://github.com/mongodb/mongo/blob/master/jstests/array_match2.js
+ if (!_.isArray(firstLevel) || nextIsNumeric)
+ firstLevel = [firstLevel];
+ return Array.prototype.concat.apply([], _.map(firstLevel, lookupRest));
};
};
@@ -504,10 +547,14 @@ var compileDocumentSelector = function (docSelector) {
throw new Error("Unrecognized logical operator: " + key);
perKeySelectors.push(LOGICAL_OPERATORS[key](subSelector));
} else {
- var lookUpByIndex = makeLookupFunction(key);
+ var lookUpByIndex = LocalCollection._makeLookupFunction(key);
var valueSelectorFunc = compileValueSelector(subSelector);
perKeySelectors.push(function (doc) {
- return valueSelectorFunc(lookUpByIndex(doc));
+ var branchValues = lookUpByIndex(doc);
+ // We apply the selector to each "branched" value and return true if any
+ // match. This isn't 100% consistent with MongoDB; eg, see:
+ // https://jira.mongodb.org/browse/SERVER-8585
+ return _.any(branchValues, valueSelectorFunc);
});
}
});
@@ -570,12 +617,12 @@ LocalCollection._compileSort = function (spec) {
for (var i = 0; i < spec.length; i++) {
if (typeof spec[i] === "string") {
sortSpecParts.push({
- lookup: makeLookupFunction(spec[i]),
+ lookup: LocalCollection._makeLookupFunction(spec[i]),
ascending: true
});
} else {
sortSpecParts.push({
- lookup: makeLookupFunction(spec[i][0]),
+ lookup: LocalCollection._makeLookupFunction(spec[i][0]),
ascending: spec[i][1] !== "desc"
});
}
@@ -583,7 +630,7 @@ LocalCollection._compileSort = function (spec) {
} else if (typeof spec === "object") {
for (var key in spec) {
sortSpecParts.push({
- lookup: makeLookupFunction(key),
+ lookup: LocalCollection._makeLookupFunction(key),
ascending: spec[key] >= 0
});
}
@@ -594,11 +641,49 @@ LocalCollection._compileSort = function (spec) {
if (sortSpecParts.length === 0)
return function () {return 0;};
+ // reduceValue takes in all the possible values for the sort key along various
+ // branches, and returns the min or max value (according to the bool
+ // findMin). Each value can itself be an array, and we look at its values
+ // too. (ie, we do a single level of flattening on branchValues, then find the
+ // min/max.)
+ var reduceValue = function (branchValues, findMin) {
+ var reduced;
+ var first = true;
+ // Iterate over all the values found in all the branches, and if a value is
+ // an array itself, iterate over the values in the array separately.
+ _.each(branchValues, function (branchValue) {
+ // Value not an array? Pretend it is.
+ if (!_.isArray(branchValue))
+ branchValue = [branchValue];
+ // Value is an empty array? Pretend it was missing, since that's where it
+ // should be sorted.
+ if (_.isArray(branchValue) && branchValue.length === 0)
+ branchValue = [undefined];
+ _.each(branchValue, function (value) {
+ // We should get here at least once: lookup functions return non-empty
+ // arrays, so the outer loop runs at least once, and we prevented
+ // branchValue from being an empty array.
+ if (first) {
+ reduced = value;
+ first = false;
+ } else {
+ // Compare the value we found to the value we found so far, saving it
+ // if it's less (for an ascending sort) or more (for a descending
+ // sort).
+ var cmp = LocalCollection._f._cmp(reduced, value);
+ if ((findMin && cmp > 0) || (!findMin && cmp < 0))
+ reduced = value;
+ }
+ });
+ });
+ return reduced;
+ };
+
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 aValue = reduceValue(specPart.lookup(a), specPart.ascending);
+ var bValue = reduceValue(specPart.lookup(b), specPart.ascending);
var compare = LocalCollection._f._cmp(aValue, bValue);
if (compare !== 0)
return specPart.ascending ? compare : -compare;