فهرست منبع

Merge branch 'feature/mongo_2.6.5_expressions' into feature/mongo_2.6.5_expressions_ifNull_Cond_Not

Kyle P Davis 11 سال پیش
والد
کامیت
075968545a

+ 18 - 16
lib/pipeline/DepsTracker.js

@@ -15,7 +15,8 @@ var DepsTracker = module.exports = function DepsTracker() {
 	this.needTextScore = false;
 }, klass = DepsTracker, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-var ParsedDeps = require("./ParsedDeps");
+var ParsedDeps = require("./ParsedDeps"),
+	Document = require("./Document");
 
 /**
  * Returns a projection object covering the dependencies tracked by this class.
@@ -26,7 +27,7 @@ proto.toProjection = function toProjection() {
 	var proj = {};
 
 	// if(this.needTextScore) {
-		// bb.append(Document::metaFieldTextScore, BSON("$meta" << "textScore"));
+	// 	bb.append(Document::metaFieldTextScore, BSON("$meta" << "textScore"));
 	// }
 
 	if (this.needWholeDocument) {
@@ -40,17 +41,16 @@ proto.toProjection = function toProjection() {
 		return proj;
 	}
 
-	var last = "",
-		needId = false;
-
-	Object.keys(this.fields).sort().forEach(function (it) {
-		if (it.slice(0,3) == "_id" && (it.length == 3 || it.charAt(3) == ".")) {
+	var needId = false,
+		last = "";
+	Object.keys(this.fields).sort().forEach(function(it) {
+		if (it.indexOf("_id") === 0 && (it.length === 3 || it[3] === ".")) {
 			// _id and subfields are handled specially due in part to SERVER-7502
 			needId = true;
 			return;
 		}
 
-		if (last !== "" && it.slice(0, last.length) === last) {
+		if (last !== "" && it.indexOf(last) === 0) {
 			// we are including a parent of *it so we don't need to include this
 			// field explicitly. In fact, due to SERVER-6527 if we included this
 			// field, the parent wouldn't be fully included. This logic relies
@@ -63,7 +63,7 @@ proto.toProjection = function toProjection() {
 		proj[it] = 1;
 	});
 
-	if (needId)
+	if (needId) // we are explicit either way
 		proj._id = 1;
 	else
 		proj._id = 0;
@@ -71,23 +71,26 @@ proto.toProjection = function toProjection() {
 	return proj;
 };
 
+// ParsedDeps::_fields is a simple recursive look-up table. For each field:
+//      If the value has type==Bool, the whole field is needed
+//      If the value has type==Object, the fields in the subobject are needed
+//      All other fields should be missing which means not needed
 /**
  * Takes a depsTracker and builds a simple recursive lookup table out of it.
  * @method toParsedDeps
  * @return {ParsedDeps}
  */
 proto.toParsedDeps = function toParsedDeps() {
-	var doc = {};
+	var obj = {};
 
 	if (this.needWholeDocument || this.needTextScore) {
 		// can't use ParsedDeps in this case
-		// TODO: not sure what appropriate equivalent to boost::none is
-		return;
+		return undefined; // TODO: is this equivalent to boost::none ?
 	}
 
 	var last = "";
 	Object.keys(this.fields).sort().forEach(function (it) {
-		if (last !== "" && it.slice(0, last.length) === last) {
+		if (last !== "" && it.indexOf(last) === 0) {
 			// we are including a parent of *it so we don't need to include this
 			// field explicitly. In fact, due to SERVER-6527 if we included this
 			// field, the parent wouldn't be fully included. This logic relies
@@ -97,9 +100,8 @@ proto.toParsedDeps = function toParsedDeps() {
 		}
 
 		last = it + ".";
-		// TODO: set nested field to true; i.e. a.b.c = true, not a = true
-		doc[it] = true;
+		Document.setNestedField(obj, it, true);
 	});
 
-	return new ParsedDeps(doc);
+	return new ParsedDeps(obj);
 };

+ 2 - 2
lib/pipeline/Document.js

@@ -39,7 +39,7 @@ klass.toJson = function toJson(doc) {
 //SKIPPED: most of MutableDocument except for getNestedField and setNestedField, squashed into Document here (because that's how they use it)
 function getNestedFieldHelper(obj, path) {
 	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
-	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fieldNames : path.split(".")),
 		lastKey = keys[keys.length - 1];
 	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
 		var next = cur[keys[i]];
@@ -51,7 +51,7 @@ function getNestedFieldHelper(obj, path) {
 klass.getNestedField = getNestedFieldHelper;  // NOTE: ours is static so these are the same
 klass.setNestedField = function setNestedField(obj, path, val) {
 	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
-	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fieldNames : path.split(".")),
 		lastKey = keys[keys.length - 1];
 	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
 		var next = cur[keys[i]];

+ 20 - 24
lib/pipeline/expressions/CoerceToBoolExpression.js

@@ -6,27 +6,32 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var CoerceToBoolExpression = module.exports = function CoerceToBoolExpression(expression){
-	if (arguments.length !== 1) throw new Error("args expected: expression");
-	this.expression = expression;
+ */
+var CoerceToBoolExpression = module.exports = function CoerceToBoolExpression(theExpression){
+	if (arguments.length !== 1) throw new Error(klass.name + ": expected args: expr");
+	this.expression = theExpression;
 	base.call(this);
 }, klass = CoerceToBoolExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	AndExpression = require("./AndExpression"),
 	OrExpression = require("./OrExpression"),
 	NotExpression = require("./NotExpression"),
 	Expression = require("./Expression");
 
+klass.create = function create(expression) {
+	var newExpr = new CoerceToBoolExpression(expression);
+	return newExpr;
+};
+
 proto.optimize = function optimize() {
-	this.expression = this.expression.optimize();   // optimize the operand
+	// optimize the operand
+	this.expression = this.expression.optimize();
 
 	// if the operand already produces a boolean, then we don't need this
 	// LATER - Expression to support a "typeof" query?
 	var expr = this.expression;
-	if(expr instanceof AndExpression ||
+	if (expr instanceof AndExpression ||
 			expr instanceof OrExpression ||
 			expr instanceof NotExpression ||
 			expr instanceof CoerceToBoolExpression)
@@ -35,28 +40,19 @@ proto.optimize = function optimize() {
 };
 
 proto.addDependencies = function addDependencies(deps, path) {
-	return this.expression.addDependencies(deps);
+	this.expression.addDependencies(deps);
 };
 
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars){
+proto.evaluateInternal = function evaluateInternal(vars) {
 	var result = this.expression.evaluateInternal(vars);
 	return Value.coerceToBool(result);
 };
 
 proto.serialize = function serialize(explain) {
-	if ( explain ) {
-		return {$coerceToBool:[this.expression.toJSON()]};
-	}
-	else {
-		return {$and:[this.expression.toJSON()]};
-	}
+	// When not explaining, serialize to an $and expression. When parsed, the $and expression
+	// will be optimized back into a ExpressionCoerceToBool.
+	var name = explain ? "$coerceToBool" : "$and",
+		obj = {};
+	obj[name] = [this.expression.serialize(explain)];
+	return obj;
 };
-
-proto.toJSON = function toJSON() {
-	// Serializing as an $and expression which will become a CoerceToBool
-	return {$and:[this.expression.toJSON()]};
-};
-
-//TODO: proto.addToBsonObj   --- may be required for $project to work
-//TODO: proto.addToBsonArray

+ 4 - 0
lib/pipeline/expressions/ConstantExpression.js

@@ -58,3 +58,7 @@ Expression.registerExpression("$literal", klass.parse); // alias
 proto.getOpName = function getOpName() {
 	return "$const";
 };
+
+proto.getValue = function getValue() {
+    return this.value;
+};

+ 22 - 23
lib/pipeline/expressions/DivideExpression.js

@@ -4,43 +4,42 @@
  * A $divide pipeline expression.
  * @see evaluateInternal
  * @class DivideExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
  **/
 var DivideExpression = module.exports = function DivideExpression(){
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
     base.call(this);
-}, klass = DivideExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DivideExpression, base = require("./FixedArityExpressionT")(DivideExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){ //TODO: try to move this to a static and/or instance field instead of a getter function
-	return "$divide";
-};
-
 /**
  * Takes an array that contains a pair of numbers and returns the value of the first number divided by the second number.
  * @method evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (!(left instanceof Date) && (!right instanceof Date)) throw new Error("$divide does not support dates; code 16373");
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-	left = Value.coerceToDouble(left);
-	return left / right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+        var numer = lhs,
+            denom = rhs;
+        if (denom === 0) throw new Error("can't $divide by zero; uassert code 16608");
+
+        return numer / denom;
+    } else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+        return null;
+    } else{
+        throw new Error("User assertion: 16609: $divide only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+    }
 };
 
-/** Register Expression */
-Expression.registerExpression("$divide",base.parse);
+Expression.registerExpression("$divide", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$divide";
+};

+ 25 - 36
lib/pipeline/expressions/ModExpression.js

@@ -4,51 +4,40 @@
  * An $mod pipeline expression.
  * @see evaluate
  * @class ModExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ModExpression = module.exports = function ModExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = ModExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
-
-// DEPENDENCIES
+}, klass = ModExpression, base = require("./FixedArityExpressionT")(ModExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$mod";
-};
-
-/**
- * Takes an array that contains a pair of numbers and returns the remainder of the first number divided by the second number.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	this.checkArgCount(2);
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$mod does not support dates; code 16374");
-
-	// pass along jstNULLs and Undefineds
-	if (left === undefined || left === null) return left;
-	if (right === undefined || right === null) return right;
-
-	// ensure we aren't modding by 0
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-
-	left = Value.coerceToDouble(left);
-	return left % right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	var leftType = Value.getType(lhs),
+		rightType = Value.getType(rhs);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		// ensure we aren't modding by 0
+		if(rhs === 0) throw new Error("can't $mod by 0; uassert code 16610");
+
+		return lhs % rhs;
+	} else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	} else {
+		throw new Error("$mod only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$mod", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$mod";
+};

+ 170 - 165
lib/pipeline/expressions/ObjectExpression.js

@@ -7,129 +7,114 @@
  * @module mungedb-aggregate
  * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
- **/
-var ObjectExpression = module.exports = function ObjectExpression(atRoot){
-	if (arguments.length !== 1) throw new Error("one arg expected");
-	this.excludeId = false;	/// <Boolean> for if _id is to be excluded
-	this.atRoot = atRoot;
-	this._expressions = {};	/// <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
-	this._order = []; /// <Array<String>> this is used to maintain order for generated fields not in the source document
+ */
+var ObjectExpression = module.exports = function ObjectExpression(atRoot) {
+	if (arguments.length !== 1) throw new Error(klass.name + ": expected args: atRoot");
+	this.excludeId = false;
+	this._atRoot = atRoot;
+	this._expressions = {};
+	this._order = [];
 }, klass = ObjectExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-klass.create = function create() {
-	return new ObjectExpression(false);
-};
-
-klass.createRoot = function createRoot() {
-	return new ObjectExpression(true);
-};
 
-// DEPENDENCIES
 var Document = require("../Document"),
-	FieldPath = require("../FieldPath");
+	Value = require("../Value"),
+	FieldPath = require("../FieldPath"),
+	ConstantExpression = require("./ConstantExpression");
 
-// INSTANCE VARIABLES
-/**
- * <Boolean> for if _id is to be excluded
- * @property excludeId
- **/
-proto.excludeId = undefined;
-
-/**
- * <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
- **/
-proto._expressions = undefined;
 
-//TODO: might be able to completely ditch _order everywhere in here since `Object`s are mostly ordered anyhow but need to come back and revisit that later
 /**
- * <Array<String>> this is used to maintain order for generated fields not in the source document
- **/
-proto._order = [];
-
+ * Create an empty expression.
+ * Until fields are added, this will evaluate to an empty document.
+ * @method create
+ * @static
+ */
+klass.create = function create() {
+	return new ObjectExpression(false);
+};
 
-// PROTOTYPE MEMBERS
 
 /**
- * evaluateInternal(), but return a Document instead of a Value-wrapped Document.
- * @method evaluateDocument
- * @param currentDoc the input Document
- * @returns the result document
- **/
-proto.evaluateDocument = function evaluateDocument(vars) {
-	// create and populate the result
-	var pResult = {_id:0};
-	this.addToDocument(pResult, pResult, vars); // No inclusion field matching.
-	return pResult;
+ * Like create but uses special handling of _id for root object of $project.
+ * @method createRoot
+ * @static
+ */
+klass.createRoot = function createRoot() {
+	return new ObjectExpression(true);
 };
 
-proto.evaluateInternal = function evaluateInternal(vars) { //TODO: collapse with #evaluateDocument()?
-	return this.evaluateDocument(vars);
-};
 
-proto.optimize = function optimize(){
+proto.optimize = function optimize() {
 	for (var key in this._expressions) {
+		if (!this._expressions.hasOwnProperty(key)) continue;
 		var expr = this._expressions[key];
-		if (expr !== undefined && expr !== null) this._expressions[key] = expr.optimize();
+		if (expr)
+			expr.optimize();
 	}
 	return this;
 };
 
-proto.isSimple = function isSimple(){
+
+proto.isSimple = function isSimple() {
 	for (var key in this._expressions) {
+		if (!this._expressions.hasOwnProperty(key)) continue;
 		var expr = this._expressions[key];
-		if (expr !== undefined && expr !== null && !expr.isSimple()) return false;
+		if (expr && !expr.isSimple())
+			return false;
 	}
 	return true;
 };
 
-proto.addDependencies = function addDependencies(deps, path){
+
+proto.addDependencies = function addDependencies(deps, path) {
 	var pathStr = "";
-	if (path instanceof Array) {
+	if (path) {
 		if (path.length === 0) {
 			// we are in the top level of a projection so _id is implicit
-			if (!this.excludeId) {
-							deps[Document.ID_PROPERTY_NAME] = 1;
-						}
+			if (!this.excludeId)
+				deps.fields[Document.ID_PROPERTY_NAME] = 1;
 		} else {
-			pathStr = new FieldPath(path).getPath() + ".";
+			var f = new FieldPath(path);
+			pathStr = f.getPath(false);
+			pathStr += ".";
 		}
 	} else {
-		if (this.excludeId) throw new Error("excludeId is true!");
+		if (this.excludeId) throw new Error("Assertion error");
 	}
 	for (var key in this._expressions) {
 		var expr = this._expressions[key];
-		if (expr !== undefined && expr !== null) {
-			if (path instanceof Array) path.push(key);
+		if (expr instanceof Expression) {
+			if (path) path.push(key);
 			expr.addDependencies(deps, path);
-			if (path instanceof Array) path.pop();
+			if (path) path.pop();
 		} else { // inclusion
-			if (path === undefined || path === null) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
-			deps[pathStr + key] = 1;
+			if (!path) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
+			deps.fields[pathStr + key] = 1;
 		}
 	}
 
 	return deps;	// NOTE: added to munge as a convenience
 };
 
-/**
- * evaluateInternal(), but add the evaluated fields to a given document instead of creating a new one.
- * @method addToDocument
- * @param pResult the Document to add the evaluated expressions to
- * @param currentDoc the input Document for this level
- * @param vars the root of the whole input document
- **/
-proto.addToDocument = function addToDocument(out, currentDoc, vars){
 
 
+/**
+* evaluateInternal(), but add the evaluated fields to a given document instead of creating a new one.
+* @method addToDocument
+* @param pResult the Document to add the evaluated expressions to
+* @param currentDoc the input Document for this level
+* @param vars the root of the whole input document
+*/
+proto.addToDocument = function addToDocument(out, currentDoc, vars) {
 	var doneFields = {};	// This is used to mark fields we've done so that we can add the ones we haven't
 
-	for(var fieldName in currentDoc){
+	for (var fieldName in currentDoc) {
 		if (!currentDoc.hasOwnProperty(fieldName)) continue;
 		var fieldValue = currentDoc[fieldName];
 
 		// This field is not supposed to be in the output (unless it is _id)
 		if (!this._expressions.hasOwnProperty(fieldName)) {
-			if (!this.excludeId && this.atRoot && fieldName == Document.ID_PROPERTY_NAME) {
+			if (!this.excludeId && this._atRoot && fieldName === Document.ID_PROPERTY_NAME) {
 				// _id from the root doc is always included (until exclusion is supported)
 				// not updating doneFields since "_id" isn't in _expressions
 				out[fieldName] = fieldValue;
@@ -140,63 +125,97 @@ proto.addToDocument = function addToDocument(out, currentDoc, vars){
 		// make sure we don't add this field again
 		doneFields[fieldName] = true;
 
-		// This means pull the matching field from the input document
 		var expr = this._expressions[fieldName];
-		if (!(expr instanceof Expression)) {
+		if (!(expr instanceof Expression)) expr = undefined;
+		if (!expr) {
+			// This means pull the matching field from the input document
 			out[fieldName] = fieldValue;
 			continue;
 		}
 
-		// Check if this expression replaces the whole field
-		if (!(fieldValue instanceof Object) || (fieldValue.constructor !== Object && fieldValue.constructor !== Array) || !(expr instanceof ObjectExpression)) {
+		var objExpr = expr instanceof ObjectExpression ? expr : undefined,
+			valueType = Value.getType(fieldValue);
+		if ((valueType !== "Object" && valueType !== "Array") || !objExpr) {
+			// This expression replace the whole field
 			var pValue = expr.evaluateInternal(vars);
 
 			// don't add field if nothing was found in the subobject
-			if (expr instanceof ObjectExpression && pValue instanceof Object && Object.getOwnPropertyNames(pValue).length === 0) continue;
+			if (objExpr && Object.getOwnPropertyNames(pValue).length === 0)
+				continue;
+
+			/*
+			 * Don't add non-existent values (note:  different from NULL or Undefined);
+			 * this is consistent with existing selection syntax which doesn't
+			 * force the appearance of non-existent fields.
+			 */
+			// if (pValue !== undefined)
+				out[fieldName] = pValue; //NOTE: DEVIATION FROM MONGO: we want to keep these in JS
 
-			// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
-			// TODO make missing distinct from Undefined
-			if (pValue !== undefined) out[fieldName] = pValue;
 			continue;
 		}
 
-		// Check on the type of the input value.  If it's an object, just walk down into that recursively, and add it to the result.
-		if (fieldValue instanceof Object && fieldValue.constructor === Object) {
-			out[fieldName] = expr.addToDocument({}, fieldValue, vars);	//TODO: pretty sure this is broken;
-		} else if (fieldValue instanceof Object && fieldValue.constructor === Array) {
-			// If it's an array, we have to do the same thing, but to each array element.  Then, add the array of results to the current document.
-			var result = [];
-			for(var fvi = 0, fvl = fieldValue.length; fvi < fvl; fvi++){
-				var subValue = fieldValue[fvi];
-				if (subValue.constructor !== Object) continue;	// can't look for a subfield in a non-object value.
-				result.push(expr.addToDocument({}, subValue, vars));
+		/*
+		 * Check on the type of the input value.  If it's an
+		 * object, just walk down into that recursively, and
+		 * add it to the result.
+		 */
+		if (valueType === "Object") {
+			var sub = {};
+			objExpr.addToDocument(sub, fieldValue, vars);
+			out[fieldName] = sub;
+		} else if (valueType === "Array") {
+			/*
+			 * If it's an array, we have to do the same thing,
+			 * but to each array element.  Then, add the array
+			 * of results to the current document.
+			 */
+			var result = [],
+				input = fieldValue;
+			for (var fvi = 0, fvl = input.length; fvi < fvl; fvi++) {
+				// can't look for a subfield in a non-object value.
+				if (Value.getType(input[fvi]) !== "Object")
+					continue;
+
+				var doc = {};
+				objExpr.addToDocument(doc, input[fvi], vars);
+				result.push(doc);
 			}
+
 			out[fieldName] = result;
 		} else {
-			throw new Error("should never happen");	//verify( false );
+			throw new Error("Assertion failure");
 		}
 	}
 
-	if (Object.getOwnPropertyNames(doneFields).length == Object.getOwnPropertyNames(this._expressions).length) return out;	//NOTE: munge returns result as a convenience
+	if (Object.getOwnPropertyNames(doneFields).length === Object.getOwnPropertyNames(this._expressions).length)
+		return out;	//NOTE: munge returns result as a convenience
 
 	// add any remaining fields we haven't already taken care of
-	for(var i = 0, l = this._order.length; i < l; i++){
-		var fieldName2 = this._order[i];
-		var expr2 = this._expressions[fieldName2];
+	for (var i = 0, l = this._order.length; i < l; i++) {
+		var fieldName2 = this._order[i],
+			expr2 = this._expressions[fieldName2];
 
 		// if we've already dealt with this field, above, do nothing
-		if (doneFields.hasOwnProperty(fieldName2)) continue;
+		if (doneFields.hasOwnProperty(fieldName2))
+			continue;
 
 		// this is a missing inclusion field
-		if (!expr2) continue;
+		if (expr2 === null || expr2 === undefined)
+			continue;
 
 		var value = expr2.evaluateInternal(vars);
 
-		// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
-		if (value === undefined || (typeof(value) == 'object' && value !== null && Object.keys(value).length === 0)) continue;
+		/*
+		 * Don't add non-existent values (note:  different from NULL or Undefined);
+		 * this is consistent with existing selection syntax which doesn't
+		 * force the appearnance of non-existent fields.
+		 */
+		if (value === undefined && !(expr2 instanceof ConstantExpression)) //NOTE: DEVIATION FROM MONGO: only if not {$const:undefined}
+			continue;
 
 		// don't add field if nothing was found in the subobject
-		if (expr2 instanceof ObjectExpression && value && value instanceof Object && Object.getOwnPropertyNames(value) == {} ) continue;
+		if (expr2 instanceof ObjectExpression && Object.getOwnPropertyNames(value).length === 0)
+			continue;
 
 		out[fieldName2] = value;
 	}
@@ -204,22 +223,31 @@ proto.addToDocument = function addToDocument(out, currentDoc, vars){
 	return out;	//NOTE: munge returns result as a convenience
 };
 
+
 /**
- * estimated number of fields that will be output
- * @method getSizeHint
- **/
-proto.getSizeHint = function getSizeHint(){
+* estimated number of fields that will be output
+* @method getSizeHint
+*/
+proto.getSizeHint = function getSizeHint() {
 	// Note: this can overestimate, but that is better than underestimating
 	return Object.getOwnPropertyNames(this._expressions).length + (this.excludeId ? 0 : 1);
 };
 
 
+/**
+* evaluateInternal(), but return a Document instead of a Value-wrapped Document.
+* @method evaluateDocument
+* @param currentDoc the input Document
+* @returns the result document
+*/
 proto.evaluateDocument = function evaluateDocument(vars) {
+	// create and populate the result
 	var out = {};
-	this.addToDocument(out, {}, vars);
+	this.addToDocument(out, {}, vars);	// No inclusion field matching.
 	return out;
 };
 
+
 proto.evaluateInternal = function evaluateInternal(vars) {
 	return this.evaluateDocument(vars);
 };
@@ -230,45 +258,52 @@ proto.evaluateInternal = function evaluateInternal(vars) {
  * @method addField
  * @param fieldPath the path the evaluated expression will have in the result Document
  * @param pExpression the expression to evaluateInternal obtain this field's Value in the result Document
- **/
-proto.addField = function addField(fieldPath, pExpression){
-	if(!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
+ */
+proto.addField = function addField(fieldPath, pExpression) {
+	if (!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
 	var fieldPart = fieldPath.getFieldName(0),
 		haveExpr = this._expressions.hasOwnProperty(fieldPart),
-		subObj = this._expressions[fieldPart];	// inserts if !haveExpr //NOTE: not in munge & JS it doesn't, handled manually below
+		expr = this._expressions[fieldPart],
+		subObj = expr instanceof ObjectExpression ? expr : undefined;	// inserts if !haveExpr
 
 	if (!haveExpr) {
 		this._order.push(fieldPart);
 	} else { // we already have an expression or inclusion for this field
-		if (fieldPath.getPathLength() == 1) { // This expression is for right here
-			if (!(subObj instanceof ObjectExpression && typeof pExpression == "object" && pExpression instanceof ObjectExpression)){
-				throw new Error("can't add an expression for field `" + fieldPart + "` because there is already an expression for that field or one of its sub-fields; uassert code 16400"); // we can merge them
-			}
+		if (fieldPath.getPathLength() === 1) {
+			// This expression is for right here
+
+			var newSubObj = pExpression instanceof ObjectExpression ? pExpression : undefined;
+			if (!(subObj && newSubObj))
+				throw new Error("can't add an expression for field " + fieldPart + " because there is already an expression for that field or one of its sub-fields; uassert code 16400"); // we can merge them
 
 			// Copy everything from the newSubObj to the existing subObj
 			// This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
-			for (var key in pExpression._expressions) {
-				if (pExpression._expressions.hasOwnProperty(key)) {
-					subObj.addField(key, pExpression._expressions[key]); // asserts if any fields are dupes
-				}
+			for (var i = 0, l = newSubObj._order.length; i < l; ++i) {
+				var key = newSubObj._order[i];
+				// asserts if any fields are dupes
+				subObj.addField(key, newSubObj._expressions[key]);
 			}
 			return;
-		} else { // This expression is for a subfield
-			if(!subObj) throw new Error("can't add an expression for a subfield of `" + fieldPart + "` because there is already an expression that applies to the whole field; uassert code 16401");
+		} else {
+			// This expression is for a subfield
+			if(!subObj)
+				throw new Error("can't add an expression for a subfield of " + fieldPart + " because there is already an expression that applies to the whole field; uassert code 16401");
 		}
 	}
 
-	if (fieldPath.getPathLength() == 1) {
-		if(haveExpr) throw new Error("Internal error."); // haveExpr case handled above.
+	if (fieldPath.getPathLength() === 1) {
+		if (haveExpr) throw new Error("Assertion error."); // haveExpr case handled above.
 		this._expressions[fieldPart] = pExpression;
 		return;
 	}
 
-	if (!haveExpr) subObj = this._expressions[fieldPart] = new ObjectExpression(false);
+	if (!haveExpr)
+		this._expressions[fieldPart] = subObj = ObjectExpression.create();
 
 	subObj.addField(fieldPath.tail(), pExpression);
 };
 
+
 /**
  * Add a field path to the set of those to be included.
  *
@@ -276,68 +311,38 @@ proto.addField = function addField(fieldPath, pExpression){
  *
  * @method includePath
  * @param fieldPath the name of the field to be included
- **/
-proto.includePath = function includePath(path){
-	this.addField(path, null);
+ */
+proto.includePath = function includePath(theFieldPath) {
+	this.addField(theFieldPath, null);
 };
 
 
 proto.serialize = function serialize(explain) {
 	var valBuilder = {};
 
-	if(this._excludeId) {
-		valBuilder._id = false;
-	}
+	if (this.excludeId)
+		valBuilder[Document.ID_PROPERTY_NAME] = false;
 
-	for(var ii = 0; ii < this._order.length; ii ++) {
-		var fieldName = this._order[ii],
-			expr = this._expressions[fieldName];
+	for (var i = 0, l = this._order.length; i < l; ++i) {
+		var fieldName = this._order[i];
+		if (!this._expressions.hasOwnProperty(fieldName)) throw new Error("Assertion failure");
+		var expr = this._expressions[fieldName];
 
-		if(expr === undefined || expr === null) {
-			valBuilder[fieldName] = {$const:expr};
+		if (!expr) {
+			valBuilder[fieldName] = true;
 		} else {
 			valBuilder[fieldName] = expr.serialize(explain);
 		}
-
 	}
 	return valBuilder;
 };
 
 
-
 /**
  * Get a count of the added fields.
  * @method getFieldCount
  * @returns how many fields have been added
- **/
-proto.getFieldCount = function getFieldCount(){
+ */
+proto.getFieldCount = function getFieldCount() {
 	return Object.getOwnPropertyNames(this._expressions).length;
 };
-
-///**
-//* Specialized BSON conversion that allows for writing out a $project specification.
-//* This creates a standalone object, which must be added to a containing object with a name
-//*
-//* @param pBuilder where to write the object to
-//* @param requireExpression see Expression::addToBsonObj
-//**/
-//TODO:	proto.documentToBson = ...?
-//TODO:	proto.addToBsonObj = ...?
-//TODO: proto.addToBsonArray = ...?
-
-//NOTE: in `munge` we're not passing the `Object`s in and allowing `toJSON` (was `documentToBson`) to modify it directly and are instead building and returning a new `Object` since that's the way it's actually used
-proto.toJSON = function toJSON(requireExpression){
-	var o = {};
-	if (this.excludeId) o[Document.ID_PROPERTY_NAME] = false;
-	for (var i = 0, l = this._order.length; i < l; i++) {
-		var fieldName = this._order[i];
-		if (!this._expressions.hasOwnProperty(fieldName)) throw new Error("internal error: fieldName from _ordered list not found in _expressions");
-		var fieldValue = this._expressions[fieldName];
-		if (fieldValue === undefined) {
-			o[fieldName] = true; // this is inclusion, not an expression
-		} else {
-			o[fieldName] = fieldValue.toJSON(requireExpression);
-		}
-	}
-	return o;
-};

+ 9 - 20
lib/pipeline/expressions/SizeExpression.js

@@ -6,36 +6,25 @@
  * @class SizeExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
+ * @extends mungedb-aggregate.pipeline.FixedArityExpressionT
  * @constructor
- **/
+ */
 var SizeExpression = module.exports = function SizeExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
-}, klass = SizeExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SizeExpression, base = require("./FixedArityExpressionT")(SizeExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$size";
-};
-
-/**
- * Takes an array and return the size.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var array = this.operands[0].evaluateInternal(vars);
-	if (array instanceof Date) throw new Error("$size does not support dates; code 16376");
+	if (!(array instanceof Array)) throw new Error("The argument to $size must be an Array but was of type" + Value.getType(array) + "; uassert code 16376");
 	return array.length;
 };
 
-/** Register Expression */
 Expression.registerExpression("$size", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$size";
+};

+ 35 - 24
lib/pipeline/expressions/SubtractExpression.js

@@ -4,39 +4,50 @@
  * A $subtract pipeline expression.
  * @see evaluateInternal
  * @class SubtractExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var SubtractExpression = module.exports = function SubtractExpression(){
+ */
+var SubtractExpression = module.exports = function SubtractExpression() {
 	base.call(this);
-}, klass = SubtractExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubtractExpression, base = require("./FixedArityExpressionT")(SubtractExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$subtract";
-};
-
-/**
-* Takes an array that contains a pair of numbers and subtracts the second from the first, returning their difference.
-**/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$subtract does not support dates; code 16376");
-	return left - right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		return lhs - rhs;
+	} else if (lhs === null || lhs === undefined || rhs === null || rhs === undefined) {
+		return null;
+	} else if (lhs instanceof Date) {
+		if (rhs instanceof Date) {
+			var timeDelta = lhs - rhs;
+			return timeDelta;
+		} else if (typeof rhs === "number") {
+			var millisSinceEpoch = lhs - Value.coerceToLong(rhs);
+			return millisSinceEpoch;
+		} else {
+			throw new Error("can't $subtract a " +
+				Value.getType(rhs) +
+				" from a Date" +
+				"; uassert code 16613");
+		}
+	} else {
+		throw new Error("can't $subtract a " +
+			Value.getType(rhs) +
+			" from a " +
+			Value.getType(lhs) +
+			"; uassert code 16556");
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$subtract", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$subtract";
+};

+ 56 - 34
test/lib/pipeline/expressions/CoerceToBoolExpression.js

@@ -1,59 +1,81 @@
 "use strict";
 var assert = require("assert"),
 	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker");
 
+exports.CoerceToBoolExpression = {
 
-module.exports = {
+	"constructor()": {
 
-	"CoerceToBoolExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function construct(){
-				assert.throws(function(){
-					new CoerceToBoolExpression();
-				});
-			}
+		"should create instance": function() {
+			var nested = ConstantExpression.create(5);
+			assert(new CoerceToBoolExpression(nested) instanceof Expression);
+		},
 
+		"should throw Error unless one arg": function() {
+			assert.throws(function() {
+				new CoerceToBoolExpression();
+			});
+			assert.throws(function() {
+				new CoerceToBoolExpression("foo", "bar");
+			});
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return true if nested expression is coerced to true; {$const:5}": function testEvaluateTrue(){
-				var expr = new CoerceToBoolExpression(new ConstantExpression(5));
-				assert.equal(expr.evaluateInternal({}), true);
-			},
+	"#evaluate()": {
 
-			"should return false if nested expression is coerced to false; {$const:0}": function testEvaluateFalse(){
-				var expr = new CoerceToBoolExpression(new ConstantExpression(0));
-				assert.equal(expr.evaluateInternal({}), false);
-			}
+		"should return true if nested expression is coerced to true; {$const:5}": function testEvaluateTrue() {
+			/** Nested expression coerced to true. */
+			var nested = ConstantExpression.create(5),
+				expr = CoerceToBoolExpression.create(nested);
+			assert.strictEqual(expr.evaluate({}), true);
+		},
 
+		"should return false if nested expression is coerced to false; {$const:0}": function testEvaluateFalse() {
+			/** Nested expression coerced to false. */
+			var expr = CoerceToBoolExpression.create(ConstantExpression.create(0));
+			assert.strictEqual(expr.evaluate({}), false);
 		},
 
-		"#addDependencies()": {
+	},
 
-			"should forward dependencies of nested expression": function testDependencies(){
-				var expr = new CoerceToBoolExpression(new FieldPathExpression('a.b')),
-					deps = expr.addDependencies({});
-				assert.equal(Object.keys(deps).length, 1);
-				assert.ok(deps['a.b']);
-			}
+	"#addDependencies()": {
 
+		"should forward dependencies of nested expression": function testDependencies() {
+			/** Dependencies forwarded from nested expression. */
+			var nested = FieldPathExpression.create("a.b"),
+				expr = CoerceToBoolExpression.create(nested),
+				deps = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.strictEqual( Object.keys(deps.fields).length, 1 );
+			assert.strictEqual("a.b" in deps.fields, true);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
 		},
 
-		"#toJSON()": {
+	},
 
-			"should serialize as $and which will coerceToBool; '$foo'": function(){
-				var expr = new CoerceToBoolExpression(new FieldPathExpression('foo'));
-				assert.deepEqual(expr.toJSON(), {$and:['$foo']});
-			}
+	"#serialize": {
 
-		}
+		"should be able to output in to JSON Object": function testAddToBsonObj() {
+			/** Output to BSONObj. */
+			var expr = CoerceToBoolExpression.create(FieldPathExpression.create("foo"));
+            // serialized as $and because CoerceToBool isn't an ExpressionNary
+			assert.deepEqual({field:{$and:["$foo"]}}, {field:expr.serialize(false)});
+		},
+
+		"should be able to output in to JSON Array": function testAddToBsonArray() {
+			/** Output to BSONArray. */
+			var expr = CoerceToBoolExpression.create(FieldPathExpression.create("foo"));
+			// serialized as $and because CoerceToBool isn't an ExpressionNary
+			assert.deepEqual([{$and:["$foo"]}], [expr.serialize(false)]);
+		},
 
-	}
+	},
 
 };
 

+ 47 - 0
test/lib/pipeline/expressions/DivideExpression_test.js

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	DivideExpression = require("../../../../lib/pipeline/expressions/DivideExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.DivideExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new DivideExpression() instanceof DivideExpression);
+			assert(new DivideExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new DivideExpression("bad stuff");
+			});
+		}
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $size": function() {
+			assert.equal(new DivideExpression().getOpName(), "$divide");
+		}
+
+	},
+
+	"#evaluate()": {
+
+		"should divide two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$divide: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 3);
+		}
+
+	}
+
+};

+ 62 - 35
test/lib/pipeline/expressions/ModExpression.js

@@ -1,53 +1,80 @@
 "use strict";
 var assert = require("assert"),
 	ModExpression = require("../../../../lib/pipeline/expressions/ModExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/Expression");
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.ModExpression = {
 
-	"ModExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should construct instance": function() {
+			assert(new ModExpression() instanceof ModExpression);
+			assert(new ModExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ModExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $mod": function() {
+			assert.equal(new ModExpression().getOpName(), "$mod");
+		},
+
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new ModExpression();
-				});
-			}
+	"#evaluate()": {
 
+		"should return modulus of two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 0);
 		},
 
-		"#getOpName()": {
-			"should return the correct op name; $mod": function testOpName(){
-				assert.equal(new ModExpression().getOpName(), "$mod");
-			}
+		"should return null if first is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: null, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
+		"should return null if first is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: undefined, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
 		},
 
-		"#evaluateInternal()": {
-			"should return rhs if rhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}, new VariablesParseState()).evaluate({lhs:20.453, rhs:null}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453}), undefined);
-			},
-			"should return lhs if lhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:null, rhs:20.453}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({rhs:20.453}), undefined);
-			},
-			"should return undefined if rhs is 0": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453, rhs:0}), undefined);
-			},
-			"should return proper mod of rhs and lhs if both are numbers": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:234.4234, rhs:45}), 234.4234 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:0, rhs:45}), 0 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:-6, rhs:-0.5}), -6 % -0.5);
-			}
+		"should return null if second is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 11, b: null};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-		}
+		"should return null if second is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 42, b: undefined};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 637 - 631
test/lib/pipeline/expressions/ObjectExpression.js

@@ -1,20 +1,35 @@
 "use strict";
 var assert = require("assert"),
 	ObjectExpression = require("../../../../lib/pipeline/expressions/ObjectExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
-	Variables = require("../../../../lib/pipeline/expressions/Variables");
-
-
-function assertEqualJson(actual, expected, message){
-	if(actual.sort) {
-		actual.sort();
-		if(expected.sort) {
-			expected.sort();
-		}
-	}
-	assert.strictEqual(message + ":  " + JSON.stringify(actual), message + ":  " + JSON.stringify(expected));
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	utils = require("./utils");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var constify = utils.constify;
+//SKIPPED: assertBinaryEqual
+//SKIPPED: toJson
+function expressionToJson(expr) {
+	return expr.serialize(false);
+}
+//SKIPPED: fromJson
+//SKIPEPD: valueFromBson
+
+function assertDependencies(expectedDependencies, expression, includePath) {
+	if (includePath === undefined) includePath = true;
+	var path = [],
+		dependencies = new DepsTracker();
+	expression.addDependencies(dependencies, includePath ? path : undefined);
+	var bab = Object.keys(dependencies.fields);
+	assert.deepEqual(bab.sort(), expectedDependencies.sort());
+	assert.strictEqual(dependencies.needWholeDocument, false);
+	assert.strictEqual(dependencies.needTextScore, false);
 }
 
 /// An assertion for `ObjectExpression` instances based on Mongo's `ExpectedResultBase` class
@@ -24,637 +39,628 @@ function assertExpectedResult(args) {
 		if (!("expected" in args)) throw new Error("missing arg: `args.expected` is required");
 		if (!("expectedDependencies" in args)) throw new Error("missing arg: `args.expectedDependencies` is required");
 		if (!("expectedJsonRepresentation" in args)) throw new Error("missing arg: `args.expectedJsonRepresentation` is required");
-	}// check for required args
+	}
 	{// base args if none provided
 		if (args.source === undefined) args.source = {_id:0, a:1, b:2};
-		if (args.expectedIsSimple === undefined) args.expectedIsSimple = false;
+		if (args.expectedIsSimple === undefined) args.expectedIsSimple = true;
 		if (args.expression === undefined) args.expression = ObjectExpression.createRoot(); //NOTE: replaces prepareExpression + _expression assignment
-	}// base args if none provided
+	}
 	// run implementation
-	var result = {},
-		variable = new Variables(1, args.source);
-
-	args.expression.addToDocument(result, args.source, variable);
+	var doc = args.source,
+		result = {},
+		vars = new Variables(0, doc);
+	args.expression.addToDocument(result, doc, vars);
 	assert.deepEqual(result, args.expected);
-	var dependencies = {};
-	args.expression.addDependencies(dependencies, [/*FAKING: includePath=true*/]);
-	//dependencies.sort(), args.expectedDependencies.sort();	// NOTE: this is a minor hack added for munge because I'm pretty sure order doesn't matter for this anyhow
-	assert.deepEqual(Object.keys(dependencies).sort(), Object.keys(args.expectedDependencies).sort());
-	assert.deepEqual(args.expression.serialize(true), args.expectedJsonRepresentation);
-	assert.deepEqual(args.expression.getIsSimple(), args.expectedIsSimple);
+	assertDependencies(args.expectedDependencies, args.expression);
+	assert.deepEqual(expressionToJson(args.expression), args.expectedJsonRepresentation);
+	assert.deepEqual(args.expression.isSimple(), args.expectedIsSimple);
 }
 
+exports.ObjectExpression = {
 
-module.exports = {
-
-	"ObjectExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					ObjectExpression.create();
-				});
-			}
-
-		},
-
-		"#addDependencies":{
-
-			"should be able to get dependencies for non-inclusion expressions": function testNonInclusionDependencies(){
-				/** Dependencies for non inclusion expressions. */
-				var expr = ObjectExpression.create();
-				expr.addField("a", new ConstantExpression(5));
-				assertEqualJson(expr.addDependencies({}, [/*FAKING: includePath=true*/]), {"_id":1});
-				expr.excludeId = true;
-				assertEqualJson(expr.addDependencies({}, []), {});
-				expr.addField("b", FieldPathExpression.create("c.d"));
-				var deps = {};
-				expr.addDependencies(deps, []);
-				assert.deepEqual(deps, {"c.d":1});
-				expr.excludeId = false;
-				deps = {};
-				expr.addDependencies(deps, []);
-				assert.deepEqual(deps, {"_id":1, "c.d":1});
-			},
-
-			"should be able to get dependencies for inclusion expressions": function testInclusionDependencies(){
-				/** Dependencies for inclusion expressions. */
-				var expr = ObjectExpression.create();
-				expr.includePath( "a" );
-				assertEqualJson(expr.addDependencies({}, [/*FAKING: includePath=true*/]), {"_id":1, "a":1});
-				assert.throws(function(){
-					expr.addDependencies({});
-				}, Error);
-			}
-
-		},
-
-		"#toJSON": {
-
-			"should be able to convert to JSON representation and have constants represented by expressions": function testJson(){
-				/** Serialize to a BSONObj, with constants represented by expressions. */
-				var expr = ObjectExpression.create(true);
-				expr.addField("foo.a", new ConstantExpression(5));
-				assertEqualJson({foo:{a:{$const:5}}}, expr.serialize(true));
-			}
-
-		},
-
-		"#optimize": {
-
-			"should be able to optimize expression and sub-expressions": function testOptimize(){
-				/** Optimizing an object expression optimizes its sub expressions. */
-				var expr = ObjectExpression.createRoot();
-				// Add inclusion.
-				expr.includePath( "a" );
-				// Add non inclusion.
-				expr.addField( "b", new AndExpression());
-				expr.optimize();
-				// Optimizing 'expression' optimizes its non inclusion sub expressions, while inclusion sub expressions are passed through.
-				assertEqualJson({a:{$const:null}, b:{$const:true}}, expr.serialize(true));
-			}
-
-		},
-
-		"#evaluate()": {
-
-			"should be able to provide an empty object": function testEmpty(){
-				/** Empty object spec. */
-				var expr = ObjectExpression.createRoot();
-				assertExpectedResult({
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {}
-
-				});
-			},
-
-			"should be able to include 'a' field only": function testInclude(){
-				/** Include 'a' field only. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a" );
-				assertExpectedResult({
-					expression: expr,
-					expected: {"_id":0, "a":1},
-					expectedDependencies: {"_id":1, "a":1},
-					expectedJsonRepresentation: {"a":{$const:null}}
-				});
-			},
-
-			"should NOT be able to include missing 'a' field": function testMissingInclude(){
-				/** Cannot include missing 'a' field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a" );
-				assertExpectedResult({
-					source: {"_id":0, "b":2},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1, "a":1},
-					expectedJsonRepresentation: {"a":{$const:null}}
-				});
-			},
-
-			"should be able to include '_id' field only": function testIncludeId(){
-				/** Include '_id' field only. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "_id" );
-				assertExpectedResult({
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"_id":{$const:null}}
-				});
-			},
-
-			"should be able to exclude '_id' field": function testExcludeId(){
-				/** Exclude '_id' field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "b" );
-				expr.excludeId = true;
-				assertExpectedResult({
-					expression: expr,
-					expected: {"b":2},
-					expectedDependencies: {"b":1},
-					expectedJsonRepresentation: {"b":{$const:null}}
-				});
-			},
-
-			"should be able to include fields in source document order regardless of inclusion order": function testSourceOrder(){
-				/** Result order based on source document field order, not inclusion spec field order. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "b" );
-				expr.includePath( "a" );
-				assertExpectedResult({
-					expression: expr,
-					get expected() { return this.source; },
-					expectedDependencies: {"_id":1, "a":1, "b":1},
-					expectedJsonRepresentation: {"b":{$const:null}, "a":{$const:null}}
-				});
-			},
-
-			"should be able to include a nested field": function testIncludeNested(){
-				/** Include a nested field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "b":5, "c":6}, "z":2 },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":5} },
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should be able to include two nested fields": function testIncludeTwoNested(){
-				/** Include two nested fields. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				expr.includePath( "a.c" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "b":5, "c":6}, "z":2 },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":5, "c":6} },
-					expectedDependencies: {"_id":1, "a.b":1, "a.c":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}, "c":{$const:null}} }
-				});
-			},
-
-			"should be able to include two fields nested within different parents": function testIncludeTwoParentNested(){
-				/** Include two fields nested within different parents. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				expr.includePath( "c.d" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "b":5 }, "c":{"d":6} },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":5}, "c":{"d":6} },
-					expectedDependencies: {"_id":1, "a.b":1, "c.d":1},
-					expectedJsonRepresentation: {"a":{"b":{$const:null}}, "c":{"d":{$const:null}} }
-				});
-			},
-
-			"should be able to attempt to include a missing nested field": function testIncludeMissingNested(){
-				/** Attempt to include a missing nested field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "c":6}, "z":2 },
-					expression: expr,
-					expected: {"_id":0, "a":{} },
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should be able to attempt to include a nested field within a non object": function testIncludeNestedWithinNonObject(){
-				/** Attempt to include a nested field within a non object. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":2, "z":2},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should be able to include a nested field within an array": function testIncludeArrayNested(){
-				/** Include a nested field within an array. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {_id:0,a:[{b:5,c:6},{b:2,c:9},{c:7},[],2],z:1},
-					expression: expr,
-					expected: {_id:0,a:[{b:5},{b:2},{}]},
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should NOT include non-root '_id' field implicitly": function testExcludeNonRootId(){
-				/** Don't include not root '_id' field implicitly. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "_id":1, "b":1} },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":1} },
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}}}
-				});
-			},
-
-			"should be able to project a computed expression": function testComputed(){
-				/** Project a computed expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":5},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{ "$const":5} },
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a computed expression replacing an existing field": function testComputedReplacement(){
-				/** Project a computed expression replacing an existing field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0, "a":99},
-					expression: expr,
-					expected: {"_id": 0, "a": 5},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a": {"$const": 5}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should NOT be able to project an undefined value": function testComputedUndefined(){
-				/** An undefined value is not projected.. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(undefined));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{$const:undefined}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a computed expression replacing an existing field with Undefined": function testComputedUndefinedReplacement(){
-				/** Project a computed expression replacing an existing field with Undefined. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0, "a":99},
-					expression: expr,
-					expected: {"_id":0, "a":5},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{"$const":5}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a null value": function testComputedNull(){
-				/** A null value is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(null));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":null},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{"$const":null}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a nested value": function testComputedNested(){
-				/** A nested value is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{"b":5}},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{"b":{"$const":5}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a field path": function testComputedFieldPath(){
-				/** A field path is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", FieldPathExpression.create("x"));
-				assertExpectedResult({
-					source: {"_id":0, "x":4},
-					expression: expr,
-					expected: {"_id":0, "a":4},
-					expectedDependencies: {"_id":1, "x":1},
-					expectedJsonRepresentation: {"a":"$x"},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a nested field path": function testComputedNestedFieldPath(){
-				/** A nested field path is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", FieldPathExpression.create("x.y"));
-				assertExpectedResult({
-					source: {"_id":0, "x":{"y":4}},
-					expression: expr,
-					expected: {"_id":0, "a":{"b":4}},
-					expectedDependencies: {"_id":1, "x.y":1},
-					expectedJsonRepresentation: {"a":{"b":"$x.y"}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should NOT project an empty subobject expression for a missing field": function testEmptyNewSubobject(){
-				/** An empty subobject expression for a missing field is not projected. */
-				var expr = ObjectExpression.createRoot();
-				// Create a sub expression returning an empty object.
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", FieldPathExpression.create("a.b"));
-				expr.addField( "a", subExpr );
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1, 'a.b':1},
-					expectedJsonRepresentation: {a:{b:"$a.b"}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a non-empty new subobject": function testNonEmptyNewSubobject(){
-				/** A non empty subobject expression for a missing field is projected. */
-				var expr = ObjectExpression.createRoot();
-				// Create a sub expression returning an empty object.
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(6));
-				expr.addField( "a", subExpr );
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent": function testAdjacentDottedComputedFields(){
-				/** Two computed fields within a common parent. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				expr.addField("a.c", new ConstantExpression(7));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent (w/ one case dotted)": function testAdjacentDottedAndNestedComputedFields(){
-				/** Two computed fields within a common parent, in one case dotted. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("c", new ConstantExpression( 7 ) );
-				expr.addField("a", subExpr);
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent (in another case dotted)": function testAdjacentNestedAndDottedComputedFields(){
-				/** Two computed fields within a common parent, in another case dotted. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(6));
-				expr.addField("a", subExpr );
-				expr.addField("a.c", new ConstantExpression(7));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent (nested rather than dotted)": function testAdjacentNestedComputedFields(){
-				/** Two computed fields within a common parent, nested rather than dotted. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr1 = ObjectExpression.create();
-				subExpr1.addField("b", new ConstantExpression(6));
-				expr.addField("a", subExpr1);
-				var subExpr2 = ObjectExpression.create();
-				subExpr2.addField("c", new ConstantExpression(7));
-				expr.addField("a", subExpr2);
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project multiple nested fields out of order without affecting output order": function testAdjacentNestedOrdering(){
-				/** Field ordering is preserved when nested fields are merged. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				var subExpr = ObjectExpression.create();
-				// Add field 'd' then 'c'.  Expect the same field ordering in the result doc.
-				subExpr.addField("d", new ConstantExpression(7));
-				subExpr.addField("c", new ConstantExpression(8));
-				expr.addField("a", subExpr);
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "d":7, "c":8} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},d:{$const:7},c:{$const:8}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project adjacent fields two levels deep": function testMultipleNestedFields(){
-				/** Adjacent fields two levels deep. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b.c", new ConstantExpression(6));
-				var bSubExpression = ObjectExpression.create();
-				bSubExpression.addField("d", new ConstantExpression(7));
-				var aSubExpression = ObjectExpression.create();
-				aSubExpression.addField("b", bSubExpression);
-				expr.addField("a", aSubExpression);
-				assertExpectedResult({
-					source:{_id:0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":{ "c":6, "d":7}}},
-					expectedDependencies:{_id:1},
-					expectedJsonRepresentation:{"a":{"b":{"c":{$const:6},"d":{$const:7}}}},
-					expectedIsSimple:false
-				});
-				var res = expr.evaluateDocument(new Variables(1, {_id:1}));
-			},
-
-			"should throw an Error if two expressions generate the same field": function testConflictingExpressionFields(){
-				/** Two expressions cannot generate the same field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(6)); // Duplicate field.
-				}, Error);
-			},
-
-			"should throw an Error if an expression field conflicts with an inclusion field": function testConflictingInclusionExpressionFields(){
-				/** An expression field conflicts with an inclusion field. */
-				var expr = ObjectExpression.createRoot();
+	"constructor()": {
+
+		"should return instance if given arg": function() {
+			assert(new ObjectExpression(false) instanceof Expression);
+			assert(new ObjectExpression(true) instanceof Expression);
+		},
+
+		"should throw Error when constructing without args": function() {
+			assert.throws(function() {
+				new ObjectExpression();
+			});
+		},
+
+	},
+
+	"#addDependencies": {
+
+		"should be able to get dependencies for non-inclusion expressions": function testNonInclusionDependencies() {
+			/** Dependencies for non inclusion expressions. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assertDependencies(["_id"], expr, true);
+			assertDependencies([], expr, false);
+			expr.addField("b", FieldPathExpression.create("c.d"));
+			assertDependencies(["_id", "c.d"], expr, true);
+			assertDependencies(["c.d"], expr, false);
+		},
+
+		"should be able to get dependencies for inclusion expressions": function testInclusionDependencies() {
+			/** Dependencies for inclusion expressions. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assertDependencies(["_id", "a"], expr, true);
+			var unused = new DepsTracker();
+			assert.throws(function() {
+				expr.addDependencies(unused);
+			}, Error);
+		},
+
+	},
+
+	"#serialize": {
+
+		"should be able to convert to JSON representation and have constants represented by expressions": function testJson() {
+			/** Serialize to a BSONObj, with constants represented by expressions. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("foo.a", ConstantExpression.create(5));
+			assert.deepEqual({foo:{a:{$const:5}}}, expr.serialize());
+		},
+
+	},
+
+	"#optimize": {
+
+		"should be able to optimize expression and sub-expressions": function testOptimize() {
+			/** Optimizing an object expression optimizes its sub expressions. */
+			var expr = ObjectExpression.createRoot();
+			// Add inclusion.
+			expr.includePath("a");
+			// Add non inclusion.
+			var andExpr = new AndExpression();
+			expr.addField("b", andExpr);
+			expr.optimize();
+			// Optimizing 'expression' optimizes its non inclusion sub expressions, while
+			// inclusion sub expressions are passed through.
+			assert.deepEqual({a:true, b:{$const:true}}, expressionToJson(expr));
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should be able to provide an empty object": function testEmpty() {
+			/** Empty object spec. */
+			var expr = ObjectExpression.createRoot();
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {}
+			});
+		},
+
+		"should be able to include 'a' field only": function testInclude() {
+			/** Include 'a' field only. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:1},
+				expectedDependencies: ["_id", "a"],
+				expectedJsonRepresentation: {a:true}
+			});
+		},
+
+		"should NOT be able to include missing 'a' field": function testMissingInclude() {
+			/** Cannot include missing 'a' field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assertExpectedResult({
+				source: {_id:0, b:2},
+				expression: expr,
+				expected: {_id:0},
+				expectedDependencies: ["_id", "a"],
+				expectedJsonRepresentation: {a:true}
+			});
+		},
+
+		"should be able to include '_id' field only": function testIncludeId() {
+			/** Include '_id' field only. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("_id");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {_id:true}
+			});
+		},
+
+		"should be able to exclude '_id' field": function testExcludeId() {
+			/** Exclude '_id' field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("b");
+			expr.excludeId = true;
+			assertExpectedResult({
+				expression: expr,
+				expected: {b:2},
+				expectedDependencies: ["b"],
+				expectedJsonRepresentation: {_id:false, b:true}
+			});
+		},
+
+		"should be able to include fields in source document order regardless of inclusion order": function testSourceOrder() {
+			/** Result order based on source document field order, not inclusion spec field order. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("b");
+			expr.includePath("a");
+			assertExpectedResult({
+				expression: expr,
+				get expected() { return this.source; },
+				expectedDependencies: ["_id", "a", "b"],
+				expectedJsonRepresentation: {b:true, a:true}
+			});
+		},
+
+		"should be able to include a nested field": function testIncludeNested() {
+			/** Include a nested field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{b:5}},
+				source: {_id:0, a:{b:5, c:6}, z:2},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to include two nested fields": function testIncludeTwoNested() {
+			/** Include two nested fields. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			expr.includePath("a.c");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{b:5, c:6}},
+				source: {_id:0, a:{b:5,c:6}, z:2},
+				expectedDependencies: ["_id", "a.b", "a.c"],
+				expectedJsonRepresentation: {a:{b:true, c:true}}
+			});
+		},
+
+		"should be able to include two fields nested within different parents": function testIncludeTwoParentNested() {
+			/** Include two fields nested within different parents. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			expr.includePath("c.d");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{b:5}, c:{d:6}},
+				source: {_id:0, a:{b:5}, c:{d:6}, z:2},
+				expectedDependencies: ["_id", "a.b", "c.d"],
+				expectedJsonRepresentation: {a:{b:true}, c:{d:true}}
+			});
+		},
+
+		"should be able to attempt to include a missing nested field": function testIncludeMissingNested() {
+			/** Attempt to include a missing nested field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{}},
+				source: {_id:0, a:{c:6}, z:2},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to attempt to include a nested field within a non object": function testIncludeNestedWithinNonObject() {
+			/** Attempt to include a nested field within a non object. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0},
+				source: {_id:0, a:2, z:2},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to include a nested field within an array": function testIncludeArrayNested() {
+			/** Include a nested field within an array. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0,a:[{b:5},{b:2},{}]},
+				source: {_id:0,a:[{b:5,c:6},{b:2,c:9},{c:7},[],2],z:1},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should NOT include non-root '_id' field implicitly": function testExcludeNonRootId() {
+			/** Don't include not root '_id' field implicitly. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, a:{_id:1, b:1}},
+				expected: {_id:0, a:{b:1}},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to project a computed expression": function testComputed() {
+			/** Project a computed expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:5},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:5}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a computed expression replacing an existing field": function testComputedReplacement() {
+			/** Project a computed expression replacing an existing field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, a:99},
+				expected: {_id:0, a:5},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:5}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should NOT be able to project an undefined value": function testComputedUndefined() {
+			/** An undefined value is passed through */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(undefined));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:undefined},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:undefined}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a computed expression replacing an existing field with Undefined": function testComputedUndefinedReplacement() {
+			/** Project a computed expression replacing an existing field with Undefined. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(undefined));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, a:99},
+				expected: {_id:0, a:undefined},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:undefined}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a null value": function testComputedNull() {
+			/** A null value is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(null));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:null},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:null}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a nested value": function testComputedNested() {
+			/** A nested value is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(5));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:5}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:5}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a field path": function testComputedFieldPath() {
+			/** A field path is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", FieldPathExpression.create("x"));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, x:4},
+				expected: {_id:0, a:4},
+				expectedDependencies: ["_id", "x"],
+				expectedJsonRepresentation: {a:"$x"},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a nested field path": function testComputedNestedFieldPath() {
+			/** A nested field path is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", FieldPathExpression.create("x.y"));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, x:{y:4}},
+				expected: {_id:0, a:{b:4}},
+				expectedDependencies: ["_id", "x.y"],
+				expectedJsonRepresentation: {a:{b:"$x.y"}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should NOT project an empty subobject expression for a missing field": function testEmptyNewSubobject() {
+			/** An empty subobject expression for a missing field is not projected. */
+			var expr = ObjectExpression.createRoot();
+			// Create a sub expression returning an empty object.
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", FieldPathExpression.create("a.b"));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:"$a.b"}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a non-empty new subobject": function testNonEmptyNewSubobject() {
+			/** A non empty subobject expression for a missing field is projected. */
+			var expr = ObjectExpression.createRoot();
+			// Create a sub expression returning an empty object.
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(6));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent": function testAdjacentDottedComputedFields() {
+			/** Two computed fields within a common parent. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			expr.addField("a.c", ConstantExpression.create(7));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent (w/ one case dotted)": function testAdjacentDottedAndNestedComputedFields() {
+			/** Two computed fields within a common parent, in one case dotted. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("c", ConstantExpression.create(7));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent (in another case dotted)": function testAdjacentNestedAndDottedComputedFields() {
+			/** Two computed fields within a common parent, in another case dotted. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(6));
+			expr.addField("a", subExpr);
+			expr.addField("a.c", ConstantExpression.create(7));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent (nested rather than dotted)": function testAdjacentNestedComputedFields() {
+			/** Two computed fields within a common parent, nested rather than dotted. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr1 = ObjectExpression.create();
+			subExpr1.addField("b", ConstantExpression.create(6));
+			expr.addField("a", subExpr1);
+			var subExpr2 = ObjectExpression.create();
+			subExpr2.addField("c", ConstantExpression.create(7));
+			expr.addField("a", subExpr2);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project multiple nested fields out of order without affecting output order": function testAdjacentNestedOrdering() {
+			/** Field ordering is preserved when nested fields are merged. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			var subExpr = ObjectExpression.create();
+			// Add field 'd' then 'c'.  Expect the same field ordering in the result doc.
+			subExpr.addField("d", ConstantExpression.create(7));
+			subExpr.addField("c", ConstantExpression.create(8));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, d:7, c:8}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},d:{$const:7},c:{$const:8}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project adjacent fields two levels deep": function testMultipleNestedFields() {
+			/** Adjacent fields two levels deep. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b.c", ConstantExpression.create(6));
+			var bSubExpression = ObjectExpression.create();
+			bSubExpression.addField("d", ConstantExpression.create(7));
+			var aSubExpression = ObjectExpression.create();
+			aSubExpression.addField("b", bSubExpression);
+			expr.addField("a", aSubExpression);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:{c:6, d:7}}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{c:{$const:6},d:{$const:7}}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should throw an Error if two expressions generate the same field": function testConflictingExpressionFields() {
+			/** Two expressions cannot generate the same field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(6)); // Duplicate field.
+			}, Error);
+		},
+
+		"should throw an Error if an expression field conflicts with an inclusion field": function testConflictingInclusionExpressionFields() {
+			/** An expression field conflicts with an inclusion field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(6));
+			}, Error);
+		},
+
+		"should throw an Error if an inclusion field conflicts with an expression field": function testConflictingExpressionInclusionFields() {
+			/** An inclusion field conflicts with an expression field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assert.throws(function() {
 				expr.includePath("a");
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(6));
-				}, Error);
-			},
-
-			"should throw an Error if an inclusion field conflicts with an expression field": function testConflictingExpressionInclusionFields(){
-				/** An inclusion field conflicts with an expression field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.includePath("a");
-				}, Error);
-			},
-
-			"should throw an Error if an object expression conflicts with a constant expression": function testConflictingObjectConstantExpressionFields(){
-				/** An object expression conflicts with a constant expression. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr = ObjectExpression.create();
-				subExpr.includePath("b");
+			}, Error);
+		},
+
+		"should throw an Error if an object expression conflicts with a constant expression": function testConflictingObjectConstantExpressionFields() {
+			/** An object expression conflicts with a constant expression. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr = ObjectExpression.create();
+			subExpr.includePath("b");
+			expr.addField("a", subExpr);
+			assert.throws(function() {
+				expr.addField("a.b", ConstantExpression.create(6));
+			}, Error);
+		},
+
+		"should throw an Error if a constant expression conflicts with an object expression": function testConflictingConstantObjectExpressionFields() {
+			/** A constant expression conflicts with an object expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			var subExpr = ObjectExpression.create();
+			subExpr.includePath("b");
+			assert.throws(function() {
 				expr.addField("a", subExpr);
-				assert.throws(function(){
-					expr.addField("a.b", new ConstantExpression(6));
-				}, Error);
-			},
-
-			"should throw an Error if a constant expression conflicts with an object expression": function testConflictingConstantObjectExpressionFields(){
-				/** A constant expression conflicts with an object expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				var subExpr = ObjectExpression.create();
-				subExpr.includePath("b");
-				assert.throws(function(){
-					expr.addField("a", subExpr);
-				}, Error);
-			},
-
-			"should throw an Error if two nested expressions cannot generate the same field": function testConflictingNestedFields(){
-				/** Two nested expressions cannot generate the same field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a.b", new ConstantExpression(6));	// Duplicate field.
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a subfield of another expression": function testConflictingFieldAndSubfield(){
-				/** An expression cannot be created for a subfield of another expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a.b", new ConstantExpression(5));
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a nested field of another expression.": function testConflictingFieldAndNestedField(){
-				/** An expression cannot be created for a nested field of another expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a", subExpr);
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a parent field of another expression": function testConflictingSubfieldAndField(){
-				/** An expression cannot be created for a parent field of another expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(5));
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a parent of a nested field": function testConflictingNestedFieldAndField(){
-				/** An expression cannot be created for a parent of a nested field. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(5));
+			}, Error);
+		},
+
+		"should throw an Error if two nested expressions cannot generate the same field": function testConflictingNestedFields() {
+			/** Two nested expressions cannot generate the same field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a.b", ConstantExpression.create(6));	// Duplicate field.
+			}, Error);
+		},
+
+		"should throw an Error if an expression is created for a subfield of another expression": function testConflictingFieldAndSubfield() {
+			/** An expression cannot be created for a subfield of another expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a.b", ConstantExpression.create(5));
+			}, Error);
+		},
+
+		"should throw an Error if an expression is created for a nested field of another expression.": function testConflictingFieldAndNestedField() {
+			/** An expression cannot be created for a nested field of another expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(5));
+			assert.throws(function() {
 				expr.addField("a", subExpr);
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(5));
-				}, Error);
-			},
-
-			"should be able to evaluate expressions in general": function testEvaluate(){
-				/**
-				 * evaluate() does not supply an inclusion document.
-				 * Inclusion spec'd fields are not included.
-				 * (Inclusion specs are not generally expected/allowed in cases where evaluate is called instead of addToDocument.)
-				 */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath("a");
-				expr.addField("b", new ConstantExpression(5));
-				expr.addField("c", FieldPathExpression.create("a"));
-				var res = expr.evaluateInternal(new Variables(1, {_id:0, a:1}));
-				assert.deepEqual({"b":5, "c":1}, res);
-			}
-		}
+			}, Error);
+		},
 
-	}
+		"should throw an Error if an expression is created for a parent field of another expression": function testConflictingSubfieldAndField() {
+			/** An expression cannot be created for a parent field of another expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(5));
+			}, Error);
+		},
 
-};
+		"should throw an Error if an expression is created for a parent of a nested field": function testConflictingNestedFieldAndField() {
+			/** An expression cannot be created for a parent of a nested field. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(5));
+			expr.addField("a", subExpr);
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(5));
+			}, Error);
+		},
+
+		"should be able to evaluate expressions in general": function testEvaluate() {
+			/**
+			 * evaluate() does not supply an inclusion document.
+			 * Inclusion spec'd fields are not included.
+			 * (Inclusion specs are not generally expected/allowed in cases where evaluate is called instead of addToDocument.)
+			 */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			expr.addField("b", ConstantExpression.create(5));
+			expr.addField("c", FieldPathExpression.create("a"));
+			var res = expr.evaluateInternal(new Variables(1, {_id:0, a:1}));
+			assert.deepEqual({"b":5, "c":1}, res);
+		},
+
+	},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 36 - 30
test/lib/pipeline/expressions/SizeExpression.js

@@ -1,46 +1,52 @@
 "use strict";
-var assert = require("assert"),
-		SizeExpression = require("../../../../lib/pipeline/expressions/SizeExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+var assert = require("assert"),
+	SizeExpression = require("../../../../lib/pipeline/expressions/SizeExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-module.exports = {
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-		"SizeExpression": {
+exports.SizeExpression = {
 
-				"constructor()": {
+	"constructor()": {
 
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SizeExpression();
-								});
-						}
+		"should construct instance": function testConstructor() {
+			assert(new SizeExpression() instanceof SizeExpression);
+			assert(new SizeExpression() instanceof Expression);
+		},
 
-				},
+		"should error if given args": function testConstructor() {
+			assert.throws(function() {
+				new SizeExpression("bad stuff");
+			});
+		}
 
-				"#getOpName()": {
+	},
 
-						"should return the correct op name; $size": function testOpName() {
-								assert.equal(new SizeExpression("test").getOpName(), "$size");
-						}
+	"#evaluate()": {
 
-				},
+		"should return the size": function testSize() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$size: ["$a"]}, vps),
+				input = {
+					a: [{a:1},{b:2}],
+					b: [{c:3}]
+				};
+			assert.strictEqual(expr.evaluate(input), 2);
+		}
 
-				"#evaluateInternal()": {
+	},
 
-						// New test not working
-						"should return the size": function testSize() {
-								assert.strictEqual(Expression.parseOperand({
-										$size: ["$a"]
-								}).evaluateInternal({
-										a: [{a:1},{b:2}],
-										b: [{c:3}]
-								}), 4);
-						}
-				}
+	"#getOpName()": {
 
+		"should return the correct op name; $size": function testOpName() {
+			assert.equal(new SizeExpression().getOpName(), "$size");
 		}
 
-};
+	}
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 111 - 27
test/lib/pipeline/expressions/SubtractExpression.js

@@ -1,45 +1,129 @@
 "use strict";
 var assert = require("assert"),
 		SubtractExpression = require("../../../../lib/pipeline/expressions/SubtractExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+		Expression = require("../../../../lib/pipeline/expressions/Expression"),
+		VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+		VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.SubtractExpression = {
 
-		"SubtractExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SubtractExpression();
+			});
+		},
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new SubtractExpression();
-								});
-						}
+	},
 
-				},
+	"#getOpName()": {
 
-				"#getOpName()": {
+		"should return the correct op name; $subtract": function() {
+			assert.equal(new SubtractExpression().getOpName(), "$subtract");
+		},
 
-						"should return the correct op name; $subtract": function testOpName() {
-								assert.equal(new SubtractExpression().getOpName(), "$subtract");
-						}
+	},
 
-				},
+	"#evaluateInternal()": {
 
-				"#evaluateInternal()": {
+		"should return the result of subtraction between two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:1}),
+				expected = 1;
+			assert.strictEqual(result, expected);
+		},
 
-						"should return the result of subtraction between two numbers": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$subtract: ["$a", "$b"]
-								}).evaluateInternal({
-										a: 35636364,
-										b: -0.5656
-								}), 35636364 - (-0.5656));
-						}
-				}
+		"should return null if left is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:null, b:1}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
 
-		}
+		"should return null if left is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:undefined, b:1}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should return null if right is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:null}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should return null if right is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:undefined}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should subtract 2 dates": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date("Jan 3 1990"),
+				date1 = new Date("Jan 1 1990"),
+				result = expr.evaluate({a:date2, b:date1}),
+				expected = date2 - date1;
+			assert.strictEqual(result, expected);
+		},
+
+		"should subtract a number of millis from a date": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date("Jan 3 1990"),
+				millis = 24 * 60 * 60 * 1000,
+				result = expr.evaluate({a:date2, b:millis}),
+				expected = date2 - millis;
+			assert.strictEqual(
+				JSON.stringify(result),
+				JSON.stringify(expected)
+			);
+		},
+
+		"should throw if left is not a date or number": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = {},
+				date1 = new Date();
+			assert.throws(function() {
+				expr.evaluate({a:date2, b:date1});
+			});
+		},
+
+		"should throw if right is not a date or number": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date(),
+				date1 = {};
+			assert.throws(function() {
+				expr.evaluate({a:date2, b:date1});
+			});
+		},
+
+	},
 
 };
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);