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;