Browse Source

Merge pull request #120 from RiveraGroup/feature/mongo_2.6.5_documentSource_Group

Feature/mongo 2.6.5 document source group
Phil Murray 11 năm trước cách đây
mục cha
commit
81ef2f3c4b

+ 171 - 84
lib/pipeline/documentSources/GroupDocumentSource.js

@@ -21,22 +21,29 @@ var DocumentSource = require("./DocumentSource"),
  **/
 var GroupDocumentSource = module.exports = function GroupDocumentSource(expCtx) {
 	if (arguments.length > 1) throw new Error("up to one arg expected");
+	expCtx = !expCtx ? {} : expCtx;
 	base.call(this, expCtx);
 
 	this.populated = false;
-	this.idExpression = null;
+	this.doingMerge = false;
+	this.spilled = false;
+	this.extSortAllowed = expCtx.extSortAllowed && !expCtx.inRouter;
+
+	this.accumulatorFactories = [];
+	this.currentAccumulators = [];
 	this.groups = {}; // GroupsType Value -> Accumulators[]
 	this.groupsKeys = []; // This is to faciliate easier look up of groups
-	this.originalGroupsKeys = []; // This stores the original group key un-hashed/stringified/whatever
-	this._variables = null;
+	this.originalGroupsKeys = [];
+	this.variables = null;
 	this.fieldNames = [];
-	this.accumulatorFactories = [];
+	this.idFieldNames = [];
 	this.expressions = [];
-	this.currentDocument = null;
+	this.idExpressions = [];
 	this.currentGroupsKeysIndex = 0;
 
 }, klass = GroupDocumentSource, base = DocumentSource, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
+// TODO: Do we need this?
 klass.groupOps = {
 	"$addToSet": Accumulators.AddToSet,
 	"$avg": Accumulators.Avg,
@@ -72,12 +79,16 @@ proto.getSourceName = function getSourceName() {
 };
 
 /**
- * Gets the next document or DocumentSource.EOF if none
+ * Gets the next document or null if none
  *
  * @method getNext
  * @return {Object}
  **/
 proto.getNext = function getNext(callback) {
+	if (!callback) throw new Error(this.getSourceName() + ' #getNext() requires callback.');
+	if (this.expCtx.checkForInterrupt && this.expCtx.checkForInterrupt() === false)
+		return callback(new Error("Interrupted"));
+
 	var self = this;
 	async.series([
 		function(next) {
@@ -89,25 +100,25 @@ proto.getNext = function getNext(callback) {
 				return next();
 		},
 		function(next) {
-			if(Object.keys(self.groups).length === 0) {
-				return next(null, DocumentSource.EOF);
-			}
-
-			//Note: Skipped the spilled logic
+			// NOTE: Skipped the spilled functionality
+			if (self.spilled) {
+				throw new Error("Spilled is not implemented.");
+			} else {
+				if(self.currentGroupsKeysIndex === self.groupsKeys.length) {
+					return next(null, null);
+				}
 
-			if(self.currentGroupsKeysIndex === self.groupsKeys.length) {
-				return next(null, DocumentSource.EOF);
-			}
+				var id = self.originalGroupsKeys[self.currentGroupsKeysIndex],
+					stringifiedId = self.groupsKeys[self.currentGroupsKeysIndex],
+					accumulators = self.groups[stringifiedId],
+					out = self.makeDocument(id, accumulators, self.expCtx.inShard);
 
-			var id = self.groupsKeys[self.currentGroupsKeysIndex],
-				accumulators = self.groups[id],
-				out = self.makeDocument(id, accumulators /*,mergeableOutput*/);
+				if(++self.currentGroupsKeysIndex === self.groupsKeys.length) {
+					self.dispose();
+				}
 
-			if(++self.currentGroupsKeysIndex === self.groupsKeys.length) {
-				self.dispose();
+				return next(null, out);
 			}
-
-			return next(null, out);
 		}
 	], function(err, results) {
 		callback(err, results[1]);
@@ -134,8 +145,14 @@ proto.dispose = function dispose() {
  * @method optimize
  **/
 proto.optimize = function optimize() {
+	// TODO if all _idExpressions are ExpressionConstants after optimization, then we know there
+	// will only be one group. We should take advantage of that to avoid going through the hash
+	// table.
 	var self = this;
-	self.idExpression = self.idExpression.optimize();
+	self.idExpressions.forEach(function(expression, i) {
+		self.idExpressions[i] = expression.optimize();
+	});
+
 	self.expressions.forEach(function(expression, i) {
 		self.expressions[i] = expression.optimize();
 	});
@@ -149,25 +166,38 @@ proto.optimize = function optimize() {
  * @param explain {Boolean} Create explain output
  **/
 proto.serialize = function serialize(explain) {
-	var insides = {};
+	var self = this,
+		insides = {};
 
 	// add the _id
-	insides._id = this.idExpression.serialize(explain);
+	if (self.idFieldNames.length === 0) {
+		if (self.idExpressions.length !== 1) throw new Error("Should only have one _id field");
+		insides._id = self.idExpressions[0].serialize(explain);
+	} else {
+		if (self.idExpressions.length !== self.idFieldNames.length)
+			throw new Error("Should have the same number of idExpressions and idFieldNames.");
+
+		var md = {};
+		self.idExpressions.forEach(function(expression, i) {
+			md[self.idFieldNames[i]] = expression.serialize(explain);
+		});
+		insides._id = md;
+	}
 
 	//add the remaining fields
-	var aFacs = this.accumulatorFactories,
+	var aFacs = self.accumulatorFactories,
 		aFacLen = aFacs.length;
 
 	for(var i=0; i < aFacLen; i++) {
-		var aFac = aFacs[i](),
-			serialExpression = this.expressions[i].serialize(explain), //Get the accumulator's expression
+		var aFac = new aFacs[i](),
+			serialExpression = self.expressions[i].serialize(explain), //Get the accumulator's expression
 			serialAccumulator = {}; //Where we'll put the expression
 		serialAccumulator[aFac.getOpName()] = serialExpression;
-		insides[this.fieldNames[i]] = serialAccumulator;
+		insides[self.fieldNames[i]] = serialAccumulator;
 	}
 
 	var serialSource = {};
-	serialSource[this.getSourceName()] = insides;
+	serialSource[self.getSourceName()] = insides;
 	return serialSource;
 };
 
@@ -192,31 +222,13 @@ klass.createFromJson = function createFromJson(elem, expCtx) {
 			var groupField = groupObj[groupFieldName];
 
 			if (groupFieldName === "_id") {
-
 				if(idSet) throw new Error("15948 a group's _id may only be specified once");
 
-				if (groupField instanceof Object && groupField.constructor === Object) {
-					/*
-						Use the projection-like set of field paths to create the
-						group-by key.
-					*/
-					var objCtx = new Expression.ObjectCtx({isDocumentOk:true});
-					group.setIdExpression(Expression.parseObject(groupField, objCtx, vps));
-					idSet = true;
-
-				} else if (typeof groupField === "string") {
-					if (groupField[0] === "$") {
-						group.setIdExpression(FieldPathExpression.parse(groupField, vps));
-						idSet = true;
-					}
-				}
-
-				if (!idSet) {
-					// constant id - single group
-					group.setIdExpression(ConstantExpression.create(groupField));
-					idSet = true;
-				}
+				group.parseIdExpression(groupField, vps);
+				idSet = true;
 
+			} else if (groupFieldName === '$doingMerge' && groupField) {
+				throw new Error("17030 $doingMerge should be true if present");
 			} else {
 				/*
 					Treat as a projection field with the additional ability to
@@ -255,7 +267,7 @@ klass.createFromJson = function createFromJson(elem, expCtx) {
 
 	if (!idSet) throw new Error("15955 a group specification must include an _id");
 
-	group._variables = new Variables(idGenerator.getIdCount());
+	group.variables = new Variables(idGenerator.getIdCount());
 
 	return group;
 };
@@ -269,6 +281,7 @@ klass.createFromJson = function createFromJson(elem, expCtx) {
  **/
 proto.populate = function populate(callback) {
 	var numAccumulators = this.accumulatorFactories.length;
+	// NOTE: this is not in mongo, does it belong here?
 	if(numAccumulators !== this.expressions.length) {
 		callback(new Error("Must have equal number of accumulators and expressions"));
 	}
@@ -277,34 +290,35 @@ proto.populate = function populate(callback) {
 		self = this;
 	async.whilst(
 		function() {
-			return input !== DocumentSource.EOF;
+			return input !== null;
 		},
 		function(cb) {
 			self.source.getNext(function(err, doc) {
 				if(err) return cb(err);
-				if(doc === DocumentSource.EOF) {
+				if(doc === null) {
 					input = doc;
 					return cb(); //Need to stop now, no new input
 				}
 
 				input = doc;
-				self._variables.setRoot(input);
+				self.variables.setRoot(input);
 
 				/* get the _id value */
-				var id = self.idExpression.evaluate(self._variables);
+				var id = self.computeId(self.variables);
 
 				if(undefined === id) id = null;
 
 				var groupKey = JSON.stringify(id),
-					group = self.groups[JSON.stringify(id)];
+					group = self.groups[groupKey];
 
 				if(!group) {
+					self.originalGroupsKeys.push(id);
 					self.groupsKeys.push(groupKey);
 					group = [];
 					self.groups[groupKey] = group;
 					// Add the accumulators
 					for(var afi = 0; afi<self.accumulatorFactories.length; afi++) {
-						group.push(self.accumulatorFactories[afi]());
+						group.push(new self.accumulatorFactories[afi]());
 					}
 				}
 				//NOTE: Skipped memory usage stuff for case when group already existed
@@ -315,11 +329,11 @@ proto.populate = function populate(callback) {
 
 				//NOTE: passing the input to each accumulator
 				for(var gi=0; gi<group.length; gi++) {
-					group[gi].process(self.expressions[gi].evaluate(self._variables /*, doingMerge*/));
+					group[gi].process(self.expressions[gi].evaluate(self.variables, self.doingMerge));
 				}
 
 				// We are done with the ROOT document so release it.
-				self._variables.clearRoot();
+				self.variables.clearRoot();
 
 				//NOTE: Skipped the part about sorted files
 
@@ -336,20 +350,6 @@ proto.populate = function populate(callback) {
 	);
 };
 
-/**
- * Get the type of something. Handles objects specially to return their true type; i.e. their constructor
- *
- * @method populate
- * @param obj {Object} The object to get the type of
- * @return {String} The type of the object as a string
- * @async
- **/
-proto._getTypeStr = function _getTypeStr(obj) {
-	var typeofStr = typeof obj,
-		typeStr = (typeofStr == "object" && obj !== null) ? obj.constructor.name : typeofStr;
-	return typeStr;
-};
-
 /**
  * Get the dependencies of the group
  *
@@ -361,7 +361,9 @@ proto._getTypeStr = function _getTypeStr(obj) {
 proto.getDependencies = function getDependencies(deps) {
 	var self = this;
 	// add _id
-	this.idExpression.addDependencies(deps);
+	this.idExpressions.forEach(function(expression, i) {
+		expression.addDependencies(deps);
+	});
 	// add the rest
 	this.fieldNames.forEach(function (field, i) {
 		self.expressions[i].addDependencies(deps);
@@ -392,16 +394,16 @@ proto.addAccumulator = function addAccumulator(fieldName, accumulatorFactory, ex
  * @param accums {Array} An array of accumulators
  * @param epxression {Expression} The expression to be evaluated on incoming documents before they are accumulated
  **/
-proto.makeDocument = function makeDocument(id, accums /*,mergeableOutput*/) {
+proto.makeDocument = function makeDocument(id, accums, mergeableOutput) {
 	var out = {};
 
 	/* add the _id field */
-	out._id = id;
+	out._id = this.expandId(id);
 
 	/* add the rest of the fields */
 	this.fieldNames.forEach(function(fieldName, i) {
-		var val = accums[i].getValue(/*mergeableOutput*/);
-		if(!val) {
+		var val = accums[i].getValue(mergeableOutput);
+		if (!val) {
 			out[fieldName] = null;
 		} else {
 			out[fieldName] = val;
@@ -412,11 +414,96 @@ proto.makeDocument = function makeDocument(id, accums /*,mergeableOutput*/) {
 };
 
 /**
- * Sets the id expression for the group
+ * Computes the internal representation of the group key.
  *
- * @method setIdExpression
- * @param epxression {Expression} The expression to set
+ * @method computeId
+ * @param vars a VariablesParseState
+ * @return vals
+ */
+proto.computeId = function computeId(vars) {
+	var self = this;
+	// If only one expression return result directly
+	if (self.idExpressions.length === 1)
+		return self.idExpressions[0].evaluate(vars); // NOTE: self will probably need to be async soon
+
+	// Multiple expressions get results wrapped in an array
+	var vals = [];
+	self.idExpressions.forEach(function(expression, i) {
+		vals.push(expression.evaluate(vars));
+	});
+
+	return vals;
+};
+
+/**
+ * Converts the internal representation of the group key to the _id shape specified by the
+ * user.
+ *
+ * @method expandId
+ * @param val
+ * @return document representing an id
+ */
+proto.expandId = function expandId(val) {
+	var self = this;
+	// _id doesn't get wrapped in a document
+	if (self.idFieldNames.length === 0)
+		return val;
+
+	var doc = {};
+
+	// _id is a single-field document containing val
+	if (self.idFieldNames.length === 1) {
+		doc[self.idFieldNames[0]] = val;
+		return doc;
+	}
+
+	// _id is a multi-field document containing the elements of val
+	val.forEach(function(v, i) {
+		doc[self.idFieldNames[i]] = v;
+	});
+
+	return doc;
+};
+
+/**
+ * Parses the raw id expression into _idExpressions and possibly _idFieldNames.
+ *
+ * @method parseIdExpression
+ * @param groupField {Object} The object with the spec
+ */
+proto.parseIdExpression = function parseIdExpression(groupField, vps) {
+	var self = this;
+	if (self._getTypeStr(groupField) === 'Object' && Object.keys(groupField).length !== 0) {
+		// {_id: {}} is treated as grouping on a constant, not an expression
+
+		var idKeyObj = groupField;
+		if (Object.keys(idKeyObj)[0][0] == '$') {
+			var objCtx = new Expression.ObjectCtx({});
+			self.idExpressions.push(Expression.parseObject(idKeyObj, objCtx, vps));
+		} else {
+			Object.keys(idKeyObj).forEach(function(key, i) {
+				var field = {}; //idKeyObj[key];
+				field[key] = idKeyObj[key];
+				self.idFieldNames.push(key);
+				self.idExpressions.push(Expression.parseOperand(field[key], vps));
+			});
+		}
+	} else if (self._getTypeStr(groupField) === 'string' && groupField[0] === '$') {
+		self.idExpressions.push(FieldPathExpression.parse(groupField, vps));
+	} else {
+		self.idExpressions.push(ConstantExpression.create(groupField));
+	}
+};
+
+/**
+ * Get the type of something. Handles objects specially to return their true type; i.e. their constructor
+ *
+ * @method _getTypeStr
+ * @param obj {Object} The object to get the type of
+ * @return {String} The type of the object as a string
  **/
-proto.setIdExpression = function setIdExpression(expression) {
-	this.idExpression = expression;
+proto._getTypeStr = function _getTypeStr(obj) {
+	var typeofStr = typeof obj,
+		typeStr = (typeofStr == "object" && obj !== null) ? obj.constructor.name : typeofStr;
+	return typeStr;
 };

+ 1 - 0
lib/pipeline/expressions/index.js

@@ -4,6 +4,7 @@ module.exports = {
 	AndExpression: require("./AndExpression.js"),
 	CoerceToBoolExpression: require("./CoerceToBoolExpression.js"),
 	CompareExpression: require("./CompareExpression.js"),
+	ConcatExpression: require("./ConcatExpression.js"),
 	CondExpression: require("./CondExpression.js"),
 	ConstantExpression: require("./ConstantExpression.js"),
 	DayOfMonthExpression: require("./DayOfMonthExpression.js"),

+ 13 - 42
test/lib/pipeline/documentSources/GroupDocumentSource.js

@@ -2,21 +2,13 @@
 var assert = require("assert"),
 	DocumentSource = require("../../../../lib/pipeline/documentSources/DocumentSource"),
 	CursorDocumentSource = require("../../../../lib/pipeline/documentSources/CursorDocumentSource"),
-	Cursor = require("../../../../lib/Cursor"),
 	GroupDocumentSource = require("../../../../lib/pipeline/documentSources/GroupDocumentSource"),
-	async = require('async');
+	ArrayRunner = require("../../../../lib/query/ArrayRunner"),
+	async = require('async'),
+	utils = require("../expressions/utils"),
+	expressions = require("../../../../lib/pipeline/expressions");
 
 
-/**
- * Tests if the given spec is the same as what the DocumentSource resolves to as JSON.
- * MUST CALL WITH A DocumentSource AS THIS (e.g. checkJsonRepresentation.call(this, spec) where this is a DocumentSource and spec is the JSON used to create the source).
- **/
-var checkJsonRepresentation = function checkJsonRepresentation(self, spec) {
-	var rep = {};
-	self.serialize(rep, true);
-	assert.deepEqual(rep, {$group: spec});
-};
-
 /// An assertion for `ObjectExpression` instances based on Mongo's `ExpectedResultBase` class
 function assertExpectedResult(args) {
 	{// check for required args
@@ -29,21 +21,20 @@ function assertExpectedResult(args) {
 	// run implementation
 	if(args.expected && args.docs){
 		var gds = GroupDocumentSource.createFromJson(args.spec),
-			cwc = new CursorDocumentSource.CursorWithContext();
-		cwc._cursor = new Cursor( args.docs );
-		var next,
+			next,
 			results = [],
-			cds = new CursorDocumentSource(cwc);
+			cds = new CursorDocumentSource(null, new ArrayRunner(args.docs), null);
+			debugger;
 		gds.setSource(cds);
 		async.whilst(
 			function() {
-				next !== DocumentSource.EOF;
+				return next !== null;
 			},
 			function(done) {
 				gds.getNext(function(err, doc) {
 					if(err) return done(err);
 					next = doc;
-					if(next === DocumentSource.EOF) {
+					if(next === null) {
 						return done();
 					} else {
 						results.push(next);
@@ -52,8 +43,7 @@ function assertExpectedResult(args) {
 				});
 			},
 			function(err) {
-				assert.deepEqual(results, args.expected);
-				checkJsonRepresentation(gds, args.spec);
+				assert.equal(JSON.stringify(results), JSON.stringify(args.expected));
 				if(args.done) {
 					return args.done();
 				}
@@ -67,7 +57,6 @@ function assertExpectedResult(args) {
 		} else {
 			assert.doesNotThrow(function(){
 				var gds = GroupDocumentSource.createFromJson(args.spec);
-				checkJsonRepresentation(gds, args.spec);
 			});
 		}
 	}
@@ -97,7 +86,6 @@ module.exports = {
 
 			// $group _id is an empty object
 			"should not throw when _id is an empty object": function advanceTest(){
-				//NOTE: This is broken until expressions get #serialize methods
 				assertExpectedResult({spec:{_id:{}}, "throw":false});
 			},
 
@@ -116,13 +104,11 @@ module.exports = {
 
 			// $group _id is the empty string
 			"should not throw when _id is an empty string": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:""}, "throw":false});
 			},
 
 			// $group _id is a string constant
 			"should not throw when _id is a string constant": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:"abc"}, "throw":false});
 			},
 
@@ -133,55 +119,46 @@ module.exports = {
 
 			// $group _id is a numeric constant
 			"should not throw when _id is a numeric constant": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:2}, "throw":false});
 			},
 
 			// $group _id is an array constant
 			"should not throw when _id is an array constant": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:[1,2]}, "throw":false});
 			},
 
 			// $group _id is a regular expression (not supported)
-			"should throw when _id is a regex": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
-				assertExpectedResult({spec:{_id:/a/}});
+			"should not throw when _id is a regex": function advanceTest(){
+				assertExpectedResult({spec:{_id:/a/}, "throw":false});
 			},
 
 			// The name of an aggregate field is specified with a $ prefix
 			"should throw when aggregate field spec is specified with $ prefix": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:1, $foo:{$sum:1}}});
 			},
 
 			// An aggregate field spec that is not an object
 			"should throw when aggregate field spec is not an object": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:1, a:1}});
 			},
 
 			// An aggregate field spec that is not an object
 			"should throw when aggregate field spec is an empty object": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:1, a:{}}});
 			},
 
 			// An aggregate field spec with an invalid accumulator operator
 			"should throw when aggregate field spec is an invalid accumulator": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:1, a:{$bad:1}}});
 			},
 
 			// An aggregate field spec with an array argument
 			"should throw when aggregate field spec with an array as an argument": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:1, a:{$sum:[]}}});
 			},
 
 			// Multiple accumulator operators for a field
 			"should throw when aggregate field spec with multiple accumulators": function advanceTest(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({spec:{_id:1, a:{$sum:1, $push:1}}});
 			}
 
@@ -202,7 +179,6 @@ module.exports = {
 
 			// $group _id is computed from an object expression
 			"should compute _id from an object expression": function testAdvance_ObjectExpression(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({
 					docs: [{a:6}],
 					spec: {_id:{z:"$a"}},
@@ -212,7 +188,6 @@ module.exports = {
 
 			// $group _id is a field path expression
 			"should compute _id from a field path expression": function testAdvance_FieldPathExpression(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({
 					docs: [{a:5}],
 					spec: {_id:"$a"},
@@ -222,7 +197,6 @@ module.exports = {
 
 			// $group _id is a field path expression
 			"should compute _id from a Date": function testAdvance_Date(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				var d = new Date();
 				assertExpectedResult({
 					docs: [{a:d}],
@@ -233,7 +207,6 @@ module.exports = {
 
 			// Aggregate the value of an object expression
 			"should aggregate the value of an object expression": function testAdvance_ObjectExpression(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({
 					docs: [{a:6}],
 					spec: {_id:0, z:{$first:{x:"$a"}}},
@@ -243,7 +216,6 @@ module.exports = {
 
 			// Aggregate the value of an operator expression
 			"should aggregate the value of an operator expression": function testAdvance_OperatorExpression(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({
 					docs: [{a:6}],
 					spec: {_id:0, z:{$first:"$a"}},
@@ -253,7 +225,6 @@ module.exports = {
 
 			// Aggregate the value of an operator expression
 			"should aggregate the value of an operator expression with a null id": function testAdvance_Null(){
-				//NOTE: This is broken until expressions get ported to 2.5; specifically, until they get a #create method
 				assertExpectedResult({
 					docs: [{a:6}],
 					spec: {_id:null, z:{$first:"$a"}},
@@ -274,7 +245,7 @@ module.exports = {
 			"should make one group with two values": function TwoValuesSingleKey() {
 				assertExpectedResult({
 					docs: [{a:1}, {a:2}],
-					spec: {_id:"$_id", a:{$push:"$a"}},
+					spec: {_id:0, a:{$push:"$a"}},
 					expected: [{_id:0, a:[1,2]}]
 				});
 			},