Преглед изворни кода

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

Conflicts:
	lib/pipeline/expressions/Expression.js
Kyle P Davis пре 11 година
родитељ
комит
5c638c0d0d
28 измењених фајлова са 2031 додато и 1653 уклоњено
  1. 18 16
      lib/pipeline/DepsTracker.js
  2. 2 2
      lib/pipeline/Document.js
  3. 30 27
      lib/pipeline/FieldPath.js
  4. 20 24
      lib/pipeline/expressions/CoerceToBoolExpression.js
  5. 59 23
      lib/pipeline/expressions/CondExpression.js
  6. 4 0
      lib/pipeline/expressions/ConstantExpression.js
  7. 22 23
      lib/pipeline/expressions/DivideExpression.js
  8. 91 70
      lib/pipeline/expressions/Expression.js
  9. 114 161
      lib/pipeline/expressions/FieldPathExpression.js
  10. 3 2
      lib/pipeline/expressions/FixedArityExpressionT.js
  11. 25 36
      lib/pipeline/expressions/ModExpression.js
  12. 3 0
      lib/pipeline/expressions/NaryBaseExpressionT.js
  13. 5 4
      lib/pipeline/expressions/NotExpression.js
  14. 170 165
      lib/pipeline/expressions/ObjectExpression.js
  15. 9 20
      lib/pipeline/expressions/SizeExpression.js
  16. 35 24
      lib/pipeline/expressions/SubtractExpression.js
  17. 3 2
      lib/pipeline/expressions/VariadicExpressionT.js
  18. 130 143
      test/lib/pipeline/FieldPath.js
  19. 56 34
      test/lib/pipeline/expressions/CoerceToBoolExpression.js
  20. 128 0
      test/lib/pipeline/expressions/CondExpression_test.js
  21. 47 0
      test/lib/pipeline/expressions/DivideExpression_test.js
  22. 147 146
      test/lib/pipeline/expressions/FieldPathExpression.js
  23. 64 0
      test/lib/pipeline/expressions/IfNullExpression_test.js
  24. 62 35
      test/lib/pipeline/expressions/ModExpression.js
  25. 0 8
      test/lib/pipeline/expressions/NotExpression.js
  26. 637 631
      test/lib/pipeline/expressions/ObjectExpression.js
  27. 36 30
      test/lib/pipeline/expressions/SizeExpression.js
  28. 111 27
      test/lib/pipeline/expressions/SubtractExpression.js

+ 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]];

+ 30 - 27
lib/pipeline/FieldPath.js

@@ -11,62 +11,65 @@
  * @module mungedb-aggregate
  * @constructor
  * @param fieldPath the dotted field path string or non empty pre-split vector.
- **/
+ */
 var FieldPath = module.exports = function FieldPath(path) {
-	var fields = typeof path === "object" && typeof path.length === "number" ? path : path.split(".");
-	if(fields.length === 0) throw new Error("FieldPath cannot be constructed from an empty vector (String or Array).; code 16409");
-	for(var i = 0, n = fields.length; i < n; ++i){
-		var field = fields[i];
-		if(field.length === 0) throw new Error("FieldPath field names may not be empty strings; code 15998");
-		if(field[0] == "$") throw new Error("FieldPath field names may not start with '$'; code 16410");
-		if(field.indexOf("\0") != -1) throw new Error("FieldPath field names may not contain '\\0'; code 16411");
-		if(field.indexOf(".") != -1) throw new Error("FieldPath field names may not contain '.'; code 16412");
+	var fieldNames = typeof path === "object" && typeof path.length === "number" ? path : path.split(".");
+	if (fieldNames.length === 0) throw new Error("FieldPath cannot be constructed from an empty vector (String or Array).; massert code 16409");
+	this.fieldNames = [];
+	for (var i = 0, n = fieldNames.length; i < n; ++i) {
+		this._pushFieldName(fieldNames[i]);
 	}
-	this.path = path;
-	this.fields = fields;
 }, klass = FieldPath, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// STATIC MEMBERS
 klass.PREFIX = "$";
 
-// PROTOTYPE MEMBERS
 /**
  * Get the full path.
- *
  * @method getPath
  * @param fieldPrefix whether or not to include the field prefix
  * @returns the complete field path
- **/
-proto.getPath = function getPath(withPrefix) {
-	return ( !! withPrefix ? FieldPath.PREFIX : "") + this.fields.join(".");
+ */
+proto.getPath = function getPath(fieldPrefix) {
+	return (!!fieldPrefix ? FieldPath.PREFIX : "") + this.fieldNames.join(".");
 };
 
+//SKIPPED: writePath - merged into getPath
+
 /**
  * A FieldPath like this but missing the first element (useful for recursion). Precondition getPathLength() > 1.
- *
  * @method tail
- **/
+ */
 proto.tail = function tail() {
-	return new FieldPath(this.fields.slice(1));
+	return new FieldPath(this.fieldNames.slice(1));
 };
 
 /**
  * Get a particular path element from the path.
- *
  * @method getFieldName
  * @param i the zero based index of the path element.
  * @returns the path element
- **/
-proto.getFieldName = function getFieldName(i){	//TODO: eventually replace this with just using .fields[i] directly
-	return this.fields[i];
+ */
+proto.getFieldName = function getFieldName(i) {	//TODO: eventually replace this with just using .fieldNames[i] directly
+	return this.fieldNames[i];
+};
+
+klass._uassertValidFieldName = function _uassertValidFieldName(fieldName) {
+	if (fieldName.length === 0) throw new Error("FieldPath field names may not be empty strings; code 15998");
+	if (fieldName[0] === "$") throw new Error("FieldPath field names may not start with '$'; code 16410");
+	if (fieldName.indexOf("\0") !== -1) throw new Error("FieldPath field names may not contain '\\0'; code 16411");
+	if (fieldName.indexOf(".") !== -1) throw new Error("FieldPath field names may not contain '.'; code 16412");
+};
+
+proto._pushFieldName = function _pushFieldName(fieldName) {
+	klass._uassertValidFieldName(fieldName);
+	this.fieldNames.push(fieldName);
 };
 
 /**
  * Get the number of path elements in the field path.
- *
  * @method getPathLength
  * @returns the number of path elements
- **/
+ */
 proto.getPathLength = function getPathLength() {
-	return this.fields.length;
+	return this.fieldNames.length;
 };

+ 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

+ 59 - 23
lib/pipeline/expressions/CondExpression.js

@@ -8,13 +8,10 @@
  * @constructor
  **/
 var CondExpression = module.exports = function CondExpression(vars) {
-    this.pCond = this.evaluateInternal(vars);
-    this.idx = this.pCond.coerceToBool() ? 1 : 2;
-    if (arguments.length !== 3) throw new Error("three args expected");
+		if (arguments.length !== 0) throw new Error("zero args expected");
     base.call(this);
 }, klass = CondExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 3),
-	base = FixedArityExpression,
+	base = require("./FixedArityExpressionT")(klass, 3),
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -23,41 +20,80 @@ var CondExpression = module.exports = function CondExpression(vars) {
 
 // DEPENDENCIES
 var Value = require("../Value"),
-    Expression = require("./Expression");
+    Expression = require("./Expression"),
+	FixedArityExpressionT = require("./FixedArityExpressionT");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$cond";
 proto.getOpName = function getOpName() {
-    return "$cond";
+    return klass.opName;
 };
 
+/**
+ *
+ * @param expr	- I expect this to be the RHS of $cond:{...} or $cond:[,,,]
+ * @param vps
+ * @returns {*}
+ */
 klass.parse = function parse(expr, vps) {
-    this.checkArgLimit(3);
+	// There may only be one argument - an array of 3 items, or a hash containing 3 keys.
+    //this.checkArgLimit(3);
 
     // if not an object, return;
-    if (typeof(expr) !== Object)
-		return Expression.parse(expr, vps);
+	// todo I don't understand why we'd do this.  shouldn't expr be {}, [], or wrong?
+    if (typeof(expr) !== Object || )
+		return FixedArityExpressionT.parse(expr, vps);
 
-    // verify
-    if (Expression.parseOperand(expr) !== "$cond")
-		throw new Error("Invalid expression");
+	// ...or expr could be the entirety of $cond:{...} or $cond:[,,,].
+	if(!(klass.opName in expr)) {
+		throw new Error("Invalid expression. Expected to see '"+klass.opName+"'");
+	}
 
     var ret = new CondExpression();
 
-    var ex = Expression.parseObject(expr);
-    var args = Expression.parseOperand(expr, vps);
-    if (args[0] !== "if")
-		throw new Error("Missing 'if' parameter to $cond");
-    if (args[1] !== "then")
-		throw new Error("Missing 'then' parameter to $cond");
-    if (args[2] !== "else")
-		throw new Error("Missing 'else' parameter to $cond");
+	// If this is an Object and not an array, verify all the bits are specified.
+	// If this is an Object that is an array, verify there are three bits.
+	// (My issue here is that we got to this parse function when we parsed the $cond:{...} item, and we're calling
+	// parseOperand (again) without altering the input.)
+//    var args = Expression.parseOperand(expr, vps);
+
+	var args = expr[getOpName()];
+
+	if (typeof args !== 'object') throw new Error("this should not happen");
+	if (args instanceof Array) {
+		// it's the array form. Convert it to the object form.
+		if (args.length !== 3) throw new Error("$cond requires exactly three arguments");
+		args = {if: args[0], then: args[1], else: args[2]};
+	}
+
+	// One way or the other, args is now in object form.
+	Object.keys(args).forEach(function(arg) {
+		if (arg === 'if') {
+			ret.operands[0] = Expression.parseOperand(args['if'], vps);
+		}
+		else if (arg === 'then') {
+			ret.operands[1] = Expression.parseOperand(args['then'], vps);
+		}
+		else if (arg === 'else') {
+			ret.operands[2] = Expression.parseOperand(args['else'], vps);
+		}
+		else {
+			throw new Error("Unrecognized parameter to $cond: '" + arg + "'; code 17083");
+		}
+	});
 
+    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; code 17080");
+    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; code 17081");
+    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; code 17082");
 
     return ret;
 };
 
 /**
- * Use the $cond operator with the following syntax:  { $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
+ * Use the $cond operator with the following syntax:
+ * { $cond: { if: <boolean-expression>, then: <true-case>, else: <false-case-> } }
+ * -or-
+ * { $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
  * @method evaluate
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
@@ -74,4 +110,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$cond", klass.parse);
+Expression.registerExpression(klass.opName, klass.parse);

+ 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";
+};

+ 91 - 70
lib/pipeline/expressions/Expression.js

@@ -19,7 +19,8 @@ var Expression = module.exports = function Expression() {
 
 
 var Value = require("../Value"),
-	Document = require("../Document");
+	Document = require("../Document"),
+	Variables = require("./Variables");
 
 
 /**
@@ -44,7 +45,7 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 	 *      @param [opts.isInclusionOk]     {Boolean}
 	 */
 	var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/ ) {
-		if (!(opts instanceof Object && opts.constructor == Object)) throw new Error("opts is required and must be an Object containing named args");
+		if (!(opts instanceof Object && opts.constructor === Object)) throw new Error("opts is required and must be an Object containing named args");
 		for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
 			if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
 		}
@@ -64,31 +65,19 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 })();
 
 
-/**
- * Produce a field path string with the field prefix removed.
- * Throws an error if the field prefix is not present.
- *
- * @static
- * @param prefixedField the prefixed field
- * @returns the field path with the prefix removed
- **/
-klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
-	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
-	return prefixedField.substr(1);
-};
-
+//
+// Diagram of relationship between parse functions when parsing a $op:
+//
+// { someFieldOrArrayIndex: { $op: [ARGS] } }
+//                             ^ parseExpression on inner $op BSONElement
+//                          ^ parseObject on BSONObject
+//             ^ parseOperand on outer BSONElement wrapping the $op Object
+//
 
 /**
- * Parse an Object.  The object could represent a functional expression or a Document expression.
- *
- * An object expression can take any of the following forms:
- *
- *      f0: {f1: ..., f2: ..., f3: ...}
- *      f0: {$operator:[operand1, operand2, ...]}
- *
- * @static
+ * Parses a JSON Object that could represent a functional expression or a Document expression.
  * @method parseObject
+ * @static
  * @param obj   the element representing the object
  * @param ctx   a MiniCtx representing the options above
  * @param vps	Variables Parse State
@@ -96,6 +85,12 @@ klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  */
 klass.parseObject = function parseObject(obj, ctx, vps) {
 	if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
+	/*
+	  An object expression can take any of the following forms:
+
+	  f0: {f1: ..., f2: ..., f3: ...}
+	  f0: {$operator:[operand1, operand2, ...]}
+	*/
 
 	var expression, // the result
 		expressionObject, // the alt result
@@ -196,6 +191,7 @@ klass.registerExpression = function registerExpression(key, parserFunc) {
 };
 
 
+//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
 /**
  * Parses a BSONElement which has already been determined to be functional expression.
  * @static
@@ -205,13 +201,13 @@ klass.registerExpression = function registerExpression(key, parserFunc) {
  * @param vps the variable parse state
  * @returns the parsed Expression
  */
-//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
 klass.parseExpression = function parseExpression(exprElementKey, exprElementValue, vps) {
-	if (!(exprElementKey in Expression.expressionParserMap)) {
-		throw new Error("Invalid operator : " + exprElementKey + "; code 15999");
-	}
-	debugger
-	return Expression.expressionParserMap[exprElementKey](exprElementValue, vps);
+	var opName = exprElementKey,
+		op = Expression.expressionParserMap[opName];
+	if (!op) throw new Error("invalid operator : " + exprElementKey + "; uassert code 15999");
+
+	// make the expression node
+	return op(exprElementValue, vps);
 };
 
 
@@ -231,84 +227,109 @@ klass.parseExpression = function parseExpression(exprElementKey, exprElementValu
  */
 klass.parseOperand = function parseOperand(exprElement, vps) {
 	var t = typeof(exprElement);
-	if (t === "string" && exprElement[0] == "$") { //if we got here, this is a field path expression
-	    return new FieldPathExpression.parse(exprElement, vps);
+	if (t === "string" && exprElement[0] === "$") {
+		//if we got here, this is a field path expression
+	    return FieldPathExpression.parse(exprElement, vps);
 	} else if (t === "object" && exprElement && exprElement.constructor === Object) {
-		return Expression.parseObject(exprElement, new ObjectCtx({
+		var oCtx = new ObjectCtx({
 			isDocumentOk: true
-		}), vps);
+		});
+		return Expression.parseObject(exprElement, oCtx, vps);
 	} else {
 		return ConstantExpression.parse(exprElement, vps);
 	}
 };
 
 
-/**
- * Evaluate the Expression using the given document as input.
- *
- * @method evaluate
- * @returns the computed value
- */
-proto.evaluateInternal = function evaluateInternal(obj) {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
-};
-
-
-/**
- * Evaluate expression with specified inputs and return result.
- *
- * While vars is non-const, if properly constructed, subexpressions modifications to it
- * should not effect outer expressions due to unique variable Ids.
- */
-proto.evaluate = function evaluate(vars) {
-	return this.evaluateInternal(vars);
-};
-
-
 /**
  * Optimize the Expression.
  *
  * This provides an opportunity to do constant folding, or to collapse nested
- *  operators that have the same precedence, such as $add, $and, or $or.
+ * operators that have the same precedence, such as $add, $and, or $or.
  *
  * The Expression should be replaced with the return value, which may or may
- *  not be the same object.  In the case of constant folding, a computed
- *  expression may be replaced by a constant.
+ * not be the same object.  In the case of constant folding, a computed
+ * expression may be replaced by a constant.
  *
  * @method optimize
  * @returns the optimized Expression
  */
 proto.optimize = function optimize() {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+	return this;
 };
 
+
 /**
- * Add this expression's field dependencies to the set Expressions are trees, so this is often recursive.
- *
- * Top-level ExpressionObject gets pointer to empty vector.
- * If any other Expression is an ancestor, or in other cases where {a:1} inclusion objects aren't allowed, they get NULL.
+ * Add this expression's field dependencies to the set.
+ * Expressions are trees, so this is often recursive.
  *
  * @method addDependencies
- * @param deps  output parameter
- * @param path  path to self if all ancestors are ExpressionObjects.
+ * @param deps Fully qualified paths to depended-on fields are added to this set.
+ *             Empty string means need full document.
+ * @param path path to self if all ancestors are ExpressionObjects.
+ *             Top-level ExpressionObject gets pointer to empty vector.
+ *             If any other Expression is an ancestor, or in other cases
+ *             where {a:1} inclusion objects aren't allowed, they get
+ *             NULL.
  */
 proto.addDependencies = function addDependencies(deps, path) {
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
+
 /**
  * simple expressions are just inclusion exclusion as supported by ExpressionObject
- * @method getIsSimple
+ * @method isSimple
  */
-proto.getIsSimple = function getIsSimple() {
+proto.isSimple = function isSimple() {
 	return false;
 };
 
+/**
+ * Serialize the Expression tree recursively.
+ * If explain is false, returns a Value parsable by parseOperand().
+ * @method serialize
+ */
+proto.serialize = function serialize(explain) {
+	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+};
+
+/**
+ * Evaluate expression with specified inputs and return result.
+ *
+ * While vars is non-const, if properly constructed, subexpressions modifications to it
+ * should not effect outer expressions due to unique variable Ids.
+ *
+ * @method evaluate
+ * @param vars
+ */
+proto.evaluate = function evaluate(vars) {
+	if (!(vars instanceof Variables)) vars = new Variables(0, vars); /// Evaluate expression with specified inputs and return result. (only used by tests)
+	return this.evaluateInternal(vars);
+};
 
-proto.toMatcherBson = function toMatcherBson() {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!"); //verify(false && "Expression::toMatcherBson()");
+/**
+ * Produce a field path string with the field prefix removed.
+ * Throws an error if the field prefix is not present.
+ * @method removeFieldPrefix
+ * @static
+ * @param prefixedField the prefixed field
+ * @returns the field path with the prefix removed
+ */
+klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
+	if (prefixedField.indexOf("\0") !== -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
+	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
+	return prefixedField.substr(1);
 };
 
+/**
+ * Evaluate the subclass Expression using the given Variables as context and return result.
+ * @method evaluate
+ * @returns the computed value
+ */
+proto.evaluateInternal = function evaluateInternal(vars) {
+	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+};
 
 var ObjectExpression = require("./ObjectExpression"),
 	FieldPathExpression = require("./FieldPathExpression"),

+ 114 - 161
lib/pipeline/expressions/FieldPathExpression.js

@@ -1,207 +1,160 @@
 "use strict";
 
+var Expression = require("./Expression"),
+    Variables = require("./Variables"),
+    Value = require("../Value"),
+    FieldPath = require("../FieldPath");
+
 /**
- * Create a field path expression. Evaluation will extract the value associated with the given field path from the source document.
+ * Create a field path expression.
+ *
+ * Evaluation will extract the value associated with the given field
+ * path from the source document.
+ *
  * @class FieldPathExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
- * @param {String} fieldPath the field path string, without any leading document indicator
- **/
-
-var Expression = require("./Expression"),
-    Variables = require("./Variables"),
-    Value = require("../Value"),
-    FieldPath = require("../FieldPath");
-
-
-var FieldPathExpression = module.exports = function FieldPathExpression(path, variableId){
-    if (arguments.length > 2) throw new Error("args expected: path[, vps]");
-    this.path = new FieldPath(path);
-    if(arguments.length == 2) {
-        this.variable = variableId;
-    } else {
-        this.variable = Variables.ROOT_ID;
-    }
-}, klass = FieldPathExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-klass.create = function create(path) {
-    return new FieldPathExpression("CURRENT."+path, Variables.ROOT_ID);
-};
+ * @param {String} theFieldPath the field path string, without any leading document indicator
+ */
+var FieldPathExpression = module.exports = function FieldPathExpression(theFieldPath, variable) {
+    if (arguments.length != 2) throw new Error(klass.name + ": expected args: theFieldPath[, variable]");
+    this._fieldPath = new FieldPath(theFieldPath);
+    this._variable = variable;
+}, klass = FieldPathExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars){
-
-    if(this.path.fields.length === 1) {
-        return vars.getValue(this.variable);
-    }
-
-    if(this.variable === Variables.ROOT_ID) {
-        return this.evaluatePath(1, vars.getRoot());
-    }
-
-    var vari = vars.getValue(this.variable);
-    if(vari instanceof Array) {
-        return this.evaluatePathArray(1,vari);
-    } else if (vari instanceof Object) {
-        return this.evaluatePath(1, vari);
-    } else {
-        return undefined;
-    }
+/**
+ * Create a field path expression using old semantics (rooted off of CURRENT).
+ *
+ * // NOTE: this method is deprecated and only used by tests
+ * // TODO remove this method in favor of parse()
+ *
+ * Evaluation will extract the value associated with the given field
+ * path from the source document.
+ *
+ * @param fieldPath the field path string, without any leading document
+ * indicator
+ * @returns the newly created field path expression
+ **/
+klass.create = function create(fieldPath) {
+    return new FieldPathExpression("CURRENT." + fieldPath, Variables.ROOT_ID);
 };
 
-
+// this is the new version that supports every syntax
 /**
- * Parses a fieldpath using the mongo 2.5 spec with optional variables
- *
+ * Like create(), but works with the raw string from the user with the "$" prefixes.
  * @param raw raw string fieldpath
  * @param vps variablesParseState
  * @returns a new FieldPathExpression
- **/
+ */
 klass.parse = function parse(raw, vps) {
-    if(raw[0] !== "$") {
-        throw new Error("FieldPath: '" + raw + "' doesn't start with a $");
-    }
-    if(raw.length === 1) {
-        throw new Error("'$' by itself is not a valid FieldPath");
-    }
-
-    if(raw[1] === "$") {
-        var firstPeriod = raw.indexOf('.');
-        var varname = (firstPeriod === -1 ? raw.slice(2) : raw.slice(2,firstPeriod));
-        Variables.uassertValidNameForUserRead(varname);
-        return new FieldPathExpression(raw.slice(2), vps.getVariableName(varname));
+    if (raw[0] !== "$") throw new Error("FieldPath: '" + raw + "' doesn't start with a $; uassert code 16873");
+    if (raw.length < 2) throw new Error("'$' by itself is not a valid FieldPath; uassert code 16872"); // need at least "$" and either "$" or a field name
+    if (raw[1] === "$") {
+        var fieldPath = raw.substr(2), // strip off $$
+            varName = fieldPath.substr(0, fieldPath.indexOf("."));
+        Variables.uassertValidNameForUserRead(varName);
+        return new FieldPathExpression(raw.slice(2), vps.getVariableName(varName));
     } else {
-        return new FieldPathExpression("CURRENT." + raw.slice(1), vps.getVariable("CURRENT"));
+        return new FieldPathExpression("CURRENT." + raw.substr(1), vps.getVariable("CURRENT"));
     }
 };
 
-
-/**
- * Parses a fieldpath using the mongo 2.5 spec with optional variables
- *
- * @param raw raw string fieldpath
- * @param vps variablesParseState
- * @returns a new FieldPathExpression
- **/
 proto.optimize = function optimize() {
+    // nothing can be done for these
     return this;
 };
 
-
-/**
- * Internal implementation of evaluate(), used recursively.
- *
- * The internal implementation doesn't just use a loop because of the
- * possibility that we need to skip over an array.  If the path is "a.b.c",
- * and a is an array, then we fan out from there, and traverse "b.c" for each
- * element of a:[...].  This requires that a be an array of objects in order
- * to navigate more deeply.
- *
- * @param index current path field index to extract
- * @param pathLength maximum number of fields on field path
- * @param pDocument current document traversed to (not the top-level one)
- * @returns the field found; could be an array
- **/
-proto._evaluatePath = function _evaluatePath(obj, i, len){
-	var fieldName = this.path.fields[i],
-		field = obj[fieldName]; // It is possible we won't have an obj (document) and we need to not fail if that is the case
-
-	// if the field doesn't exist, quit with an undefined value
-	if (field === undefined) return undefined;
-
-	// if we've hit the end of the path, stop
-	if (++i >= len) return field;
-
-	// We're diving deeper.  If the value was null, return null
-	if(field === null) return undefined;
-
-	if (field.constructor === Object) {
-		return this._evaluatePath(field, i, len);
-	} else if (Array.isArray(field)) {
-		var results = [];
-		for (var i2 = 0, l2 = field.length; i2 < l2; i2++) {
-			var subObj = field[i2],
-				subObjType = typeof(subObj);
-			if (subObjType === "undefined" || subObj === null) {
-				results.push(subObj);
-			} else if (subObj.constructor === Object) {
-				results.push(this._evaluatePath(subObj, i, len));
-			} else {
-				throw new Error("the element '" + fieldName + "' along the dotted path '" + this.path.getPath() + "' is not an object, and cannot be navigated.; code 16014");
-			}
-		}
-		return results;
-	}
-	return undefined;
+proto.addDependencies = function addDependencies(deps) {
+    if (this._variable === Variables.ROOT_ID) {
+        if (this._fieldPath.fieldNames.length === 1) {
+            deps.needWholeDocument = true; // need full doc if just "$$ROOT"
+        } else {
+            deps.fields[this._fieldPath.tail().getPath(false)] = 1;
+        }
+    }
 };
 
-proto.evaluatePathArray = function evaluatePathArray(index, input) {
+/**
+ * Helper for evaluatePath to handle Array case
+ */
+proto._evaluatePathArray = function _evaluatePathArray(index, input) {
+    if (!(input instanceof Array)) throw new Error("must be array; dassert");
 
-    if(!(input instanceof Array)) {
-        throw new Error("evaluatePathArray called on non-array");
-    }
+    // Check for remaining path in each element of array
     var result = [];
+    for (var i = 0, l = input.length; i < l; i++) {
+        if (!(input[i] instanceof Object))
+            continue;
 
-    for(var ii = 0; ii < input.length; ii++) {
-        if(input[ii] instanceof Object) {
-            var nested = this.evaluatePath(index, input[ii]);
-            if(nested) {
-				result.push(nested);
-            }
-        }
+        var nested = this._evaluatePath(index, input[i]);
+        if (nested !== undefined)
+            result.push(nested);
     }
     return result;
 };
 
-
-proto.evaluatePath = function(index, input) {
-    if(index === this.path.fields.length -1) {
-        return input[this.path.fields[index]];
-    }
-    var val = input[this.path.fields[index]];
-    if(val instanceof Array) {
-        return this.evaluatePathArray(index+1, val);
-    } else if (val instanceof Object) {
-        return this.evaluatePath(index+1, val);
+/**
+ * Internal implementation of evaluateInternal(), used recursively.
+ *
+ * The internal implementation doesn't just use a loop because of
+ * the possibility that we need to skip over an array.  If the path
+ * is "a.b.c", and a is an array, then we fan out from there, and
+ * traverse "b.c" for each element of a:[...].  This requires that
+ * a be an array of objects in order to navigate more deeply.
+ *
+ * @param index current path field index to extract
+ * @param input current document traversed to (not the top-level one)
+ * @returns the field found; could be an array
+ */
+proto._evaluatePath = function _evaluatePath(index, input) {
+    // Note this function is very hot so it is important that is is well optimized.
+    // In particular, all return paths should support RVO.
+
+    // if we've hit the end of the path, stop
+    if (index == this._fieldPath.fieldNames.length - 1)
+        return input[this._fieldPath.fieldNames[index]];
+
+    // Try to dive deeper
+    var val = input[this._fieldPath.fieldNames[index]];
+    if (val instanceof Object && val.constructor === Object) {
+        return this._evaluatePath(index + 1, val);
+    } else if (val instanceof Array) {
+        return this._evaluatePathArray(index + 1, val);
     } else {
         return undefined;
     }
-
 };
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+    if (this._fieldPath.fieldNames.length === 1) // get the whole variable
+        return vars.getValue(this._variable);
 
+    if (this._variable === Variables.ROOT_ID) {
+        // ROOT is always a document so use optimized code path
+        return this._evaluatePath(1, vars.getRoot());
+    }
 
-proto.optimize = function(){
-        return this;
-};
-
-proto.addDependencies = function addDependencies(deps){
-	if(this.path.fields[0] === "CURRENT" || this.path.fields[0] === "ROOT") {
-		if(this.path.fields.length === 1) {
-			deps[""] = 1;
-		} else {
-			deps[this.path.tail().getPath(false)] = 1;
-		}
-	}
-};
-
-// renamed write to get because there are no streams
-proto.getFieldPath = function getFieldPath(usePrefix){
-        return this.path.getPath(usePrefix);
+    var val = vars.getValue(this._variable);
+    if (val instanceof Object && val.constructor === Object) {
+        return this._evaluatePath(1, val);
+    } else if(val instanceof Array) {
+        return this._evaluatePathArray(1,val);
+    } else {
+        return undefined;
+    }
 };
 
-proto.serialize = function toJSON(){
-    if(this.path.fields[0] === "CURRENT" && this.path.fields.length > 1) {
-        return "$" + this.path.tail().getPath(false);
+proto.serialize = function serialize(){
+    if(this._fieldPath.fieldNames[0] === "CURRENT" && this._fieldPath.fieldNames.length > 1) {
+        // use short form for "$$CURRENT.foo" but not just "$$CURRENT"
+        return "$" + this._fieldPath.tail().getPath(false);
     } else {
-        return "$$" + this.path.getPath(false);
+        return "$$" + this._fieldPath.getPath(false);
     }
 };
 
-//TODO: proto.addToBsonObj = ...?
-//TODO: proto.addToBsonArray = ...?
-
-//proto.writeFieldPath = ...?   use #getFieldPath instead
+proto.getFieldPath = function getFieldPath(){
+    return this._fieldPath;
+};

+ 3 - 2
lib/pipeline/expressions/FixedArityExpressionT.js

@@ -28,8 +28,9 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		}
 	};
 
-	klass.parse = base.parse; 	//NOTE: Need to explicitly bubble static members
-								// in our inheritance chain
+	klass.parse = base.parse; 						// NOTE: Need to explicitly
+	klass.parseArguments = base.parseArguments;		// bubble static members in
+													// our inheritance chain
 	return FixedArityExpression;
 };
 

+ 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";
+};

+ 3 - 0
lib/pipeline/expressions/NaryBaseExpressionT.js

@@ -23,5 +23,8 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		return expr;
 	};
 
+	klass.parseArguments = base.parseArguments;		// NOTE: Need to explicitly
+													// bubble static members in
+													// our inheritance chain
 	return NaryBaseExpression;
 };

+ 5 - 4
lib/pipeline/expressions/NotExpression.js

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var NotExpression = module.exports = function NotExpression() {
+		if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
 }, klass = NotExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
+	base = require("./FixedArityExpressionT")(klass, 1),
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -24,8 +24,9 @@ var Value = require("../Value"),
 	Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$not";
 proto.getOpName = function getOpName() {
-	return "$not";
+	return klass.opName;
 };
 
 /**
@@ -38,4 +39,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$not", base.parse);
+Expression.registerExpression(klass.opName, base.parse);

+ 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";
+};

+ 3 - 2
lib/pipeline/expressions/VariadicExpressionT.js

@@ -15,7 +15,8 @@ var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass
 		base.call(this);
 	}, klass = VariadicExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-	klass.parse = base.parse; 	//NOTE: Need to explicitly bubble static members
-								// in our inheritance chain
+	klass.parse = base.parse; 						// NOTE: Need to explicitly
+	klass.parseArguments = base.parseArguments;		// bubble static members in
+													// our inheritance chain
 	return VariadicExpression;
 };

+ 130 - 143
test/lib/pipeline/FieldPath.js

@@ -2,153 +2,140 @@
 var assert = require("assert"),
 	FieldPath = require("../../../lib/pipeline/FieldPath");
 
+// 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 = {
-
-	"FieldPath": {
-
-		"constructor(path)": {
-
-			"should throw Error if given an empty path String": function empty() {
-				assert.throws(function() {
-					new FieldPath("");
-				});
-			},
-
-			"should throw Error if given an empty path Array": function emptVector() {
-				assert.throws(function() {
-					new FieldPath([]);
-				});
-			},
-
-			"should accept simple paths as a String (without dots)": function simple() {
-				var path = new FieldPath("foo");
-				assert.equal(path.getPathLength(), 1);
-				assert.equal(path.getFieldName(0), "foo");
-				assert.equal(path.getPath(false), "foo");
-				assert.equal(path.getPath(true), "$foo");
-			},
-
-			"should accept simple paths as an Array of one item": function simpleVector() {
-				var path = new FieldPath(["foo"]);
-				assert.equal(path.getPathLength(), 1);
-				assert.equal(path.getFieldName(0), "foo");
-				assert.equal(path.getPath(false), "foo");
-				assert.equal(path.getPath(true), "$foo");
-			},
-
-			"should throw Error if given a '$' String": function dollarSign() {
-				assert.throws(function() {
-					new FieldPath("$");
-				});
-			},
-
-			"should throw Error if given a '$'-prefixed String": function dollarSignPrefix() {
-				assert.throws(function() {
-					new FieldPath("$a");
-				});
-			},
-
-			"should accept paths as a String with one dot": function dotted() {
-				var path = new FieldPath("foo.bar");
-				assert.equal(path.getPathLength(), 2);
-				assert.equal(path.getFieldName(0), "foo");
-				assert.equal(path.getFieldName(1), "bar");
-				assert.equal(path.getPath(false), "foo.bar");
-				assert.equal(path.getPath(true), "$foo.bar");
-			},
-
-			"should throw Error if given a path Array with items containing a dot": function vectorWithDot() {
-				assert.throws(function() {
-					new FieldPath(["fo.o"]);
-				});
-			},
-
-			"should accept paths Array of two items": function twoFieldVector() {
-				var path = new FieldPath(["foo", "bar"]);
-				assert.equal(path.getPathLength(), 2);
-				assert.equal(path.getFieldName(0), "foo");
-				assert.equal(path.getFieldName(1), "bar");
-				assert.equal(path.getPath(false), "foo.bar");
-				assert.equal(path.getPath(true), "$foo.bar");
-			},
-
-			"should throw Error if given a path String and 2nd field is a '$'-prefixed String": function dollarSignPrefixSecondField() {
-				assert.throws(function() {
-					new FieldPath("a.$b");
-				});
-			},
-
-			"should accept path String when it contains two dots": function twoDotted() {
-				var path = new FieldPath("foo.bar.baz");
-				assert.equal(path.getPathLength(), 3);
-				assert.equal(path.getFieldName(0), "foo");
-				assert.equal(path.getFieldName(1), "bar");
-				assert.equal(path.getFieldName(2), "baz");
-				assert.equal(path.getPath(false), "foo.bar.baz");
-				assert.equal(path.getPath(true), "$foo.bar.baz");
-			},
-
-			"should throw Error if given path String ends in a dot": function terminalDot() {
-				assert.throws(function() {
-					new FieldPath("foo.");
-				});
-			},
-
-			"should throw Error if given path String begins in a dot": function prefixDot() {
-				assert.throws(function() {
-					new FieldPath(".foo");
-				});
-			},
-
-			"should throw Error if given path String contains adjacent dots": function adjacentDots() {
-				assert.throws(function() {
-					new FieldPath("foo..bar");
-				});
-			},
-
-			"should accept path String containing one letter between two dots": function letterBetweenDots() {
-				var path = new FieldPath("foo.a.bar");
-				assert.equal(path.getPathLength(), 3);
-				assert.equal(path.getFieldName(0), "foo");
-				assert.equal(path.getFieldName(1), "a");
-				assert.equal(path.getFieldName(2), "bar");
-				assert.equal(path.getPath(false), "foo.a.bar");
-				assert.equal(path.getPath(true), "$foo.a.bar");
-			},
-
-			"should throw Error if given path String contains a null character": function nullCharacter() {
-				assert.throws(function() {
-					new FieldPath("foo.b\0r");
-				});
-			},
-
-			"should throw Error if given path Array contains an item with a null character": function vectorNullCharacter() {
-				assert.throws(function() {
-					new FieldPath(["foo", "b\0r"]);
-				});
-			}
-
-		},
-
-		"#tail()": {
-
-			"should be able to get all but last part of field part of path with 2 fields": function tail() {
-				var path = new FieldPath("foo.bar").tail();
-				assert.equal(path.getPathLength(), 1);
-				assert.equal(path.getPath(), "bar");
-			},
-
-			"should be able to get all but last part of field part of path with 3 fields": function tailThreeFields() {
-				var path = new FieldPath("foo.bar.baz").tail();
-				assert.equal(path.getPathLength(), 2);
-				assert.equal(path.getPath(), "bar.baz");
-			}
+exports.FieldPath = {
 
+	"constructor(path)": {
+
+		"should throw Error if given an empty path String": function testEmpty() {
+			assert.throws(function() {
+				new FieldPath("");
+			});
+		},
+
+		"should throw Error if given an empty path Array": function testEmptyVector() {
+			assert.throws(function() {
+				new FieldPath([]);
+			});
+		},
+
+		"should accept simple paths as a String (without dots)": function testSimple() {
+			var path = new FieldPath("foo");
+			assert.equal(path.getPathLength(), 1);
+			assert.equal(path.getFieldName(0), "foo");
+			assert.equal(path.getPath(false), "foo");
+			assert.equal(path.getPath(true), "$foo");
+		},
+
+		"should accept simple paths as an Array of one item": function testSimpleVector() {
+			var path = new FieldPath(["foo"]);
+			assert.equal(path.getPathLength(), 1);
+			assert.equal(path.getFieldName(0), "foo");
+			assert.equal(path.getPath(false), "foo");
+		},
+
+		"should throw Error if given a '$' String": function testDollarSign() {
+			assert.throws(function() {
+				new FieldPath("$");
+			});
+		},
+
+		"should throw Error if given a '$'-prefixed String": function testDollarSignPrefix() {
+			assert.throws(function() {
+				new FieldPath("$a");
+			});
+		},
+
+		"should accept paths as a String with one dot": function testDotted() {
+			var path = new FieldPath("foo.bar");
+			assert.equal(path.getPathLength(), 2);
+			assert.equal(path.getFieldName(0), "foo");
+			assert.equal(path.getFieldName(1), "bar");
+			assert.equal(path.getPath(false), "foo.bar");
+			assert.equal(path.getPath(true), "$foo.bar");
+		},
+
+		"should throw Error if given a path Array with items containing a dot": function testVectorWithDot() {
+			assert.throws(function() {
+				new FieldPath(["fo.o"]);
+			});
+		},
+
+		"should accept paths Array of two items": function testTwoFieldVector() {
+			var path = new FieldPath(["foo", "bar"]);
+			assert.equal(path.getPathLength(), 2);
+			assert.equal(path.getPath(false), "foo.bar");
+		},
+
+		"should throw Error if given a path String and 2nd field is a '$'-prefixed String": function testDollarSignPrefixSecondField() {
+			assert.throws(function() {
+				new FieldPath("a.$b");
+			});
+		},
+
+		"should accept path String when it contains two dots": function testTwoDotted() {
+			var path = new FieldPath("foo.bar.baz");
+			assert.equal(path.getPathLength(), 3);
+			assert.equal(path.getFieldName(0), "foo");
+			assert.equal(path.getFieldName(1), "bar");
+			assert.equal(path.getFieldName(2), "baz");
+			assert.equal(path.getPath(false), "foo.bar.baz");
+		},
+
+		"should throw Error if given path String ends in a dot": function testTerminalDot() {
+			assert.throws(function() {
+				new FieldPath("foo.");
+			});
+		},
+
+		"should throw Error if given path String begins in a dot": function testPrefixDot() {
+			assert.throws(function() {
+				new FieldPath(".foo");
+			});
+		},
+
+		"should throw Error if given path String contains adjacent dots": function testAdjacentDots() {
+			assert.throws(function() {
+				new FieldPath("foo..bar");
+			});
+		},
+
+		"should accept path String containing one letter between two dots": function testLetterBetweenDots() {
+			var path = new FieldPath("foo.a.bar");
+			assert.equal(path.getPathLength(), 3);
+			assert.equal(path.getPath(false), "foo.a.bar");
+		},
+
+		"should throw Error if given path String contains a null character": function testNullCharacter() {
+			assert.throws(function() {
+				new FieldPath("foo.b\0r");
+			});
+		},
+
+		"should throw Error if given path Array contains an item with a null character": function testVectorNullCharacter() {
+			assert.throws(function() {
+				new FieldPath(["foo", "b\0r"]);
+			});
+		}
+
+	},
+
+	"#tail()": {
+
+		"should be able to get all but last part of field part of path with 2 fields": function testTail() {
+			var path = new FieldPath("foo.bar").tail();
+			assert.equal(path.getPathLength(), 1);
+			assert.equal(path.getPath(), "bar");
+		},
+
+		"should be able to get all but last part of field part of path with 3 fields": function testTailThreeFields() {
+			var path = new FieldPath("foo.bar.baz").tail();
+			assert.equal(path.getPathLength(), 2);
+			assert.equal(path.getPath(), "bar.baz");
 		}
 
 	}
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run();

+ 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)]);
+		},
 
-	}
+	},
 
 };
 

+ 128 - 0
test/lib/pipeline/expressions/CondExpression_test.js

@@ -0,0 +1,128 @@
+"use strict";
+var assert = require("assert"),
+	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+
+module.exports = {
+
+	"CondExpression": {
+
+		"constructor()": {
+
+			"should not throw an Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
+					new CondExpression();
+				});
+			},
+
+			"should throw Error when constructing with 1 arg": function testConstructor1(){
+				assert.throws(function(){
+					new CondExpression(1);
+				});
+			}
+		},
+
+		"#getOpName()": {
+
+			"should return the correct op name; $cond": function testOpName(){
+				assert.equal(new CondExpression().getOpName(), "$cond");
+			}
+
+		},
+
+		"#evaluateInternal()": {
+			"array style": {
+
+				"should fail if there aren't enough arguments": function() {
+					assert.throws(function(){
+						Expression.parseOperand({$cond:[1,2]}, {});
+					})
+				},
+				"should fail if there are too many arguments": function() {
+					assert.throws(function(){
+						Expression.parseOperand({$cond:[1, 2, 3, 4]}, {});
+					})
+				},
+				"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function () {
+					assert.strictEqual(Expression.parseOperand({$cond: [ true, 1, 0 ]}, {}).evaluateInternal({}), 1);
+				},
+
+				"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function () {
+					assert.strictEqual(Expression.parseOperand({$cond: [ false, 1, 0 ]}, {}).evaluateInternal({}), 0);
+				},
+				"should fail when the 'if' position is empty": function(){
+					assert.throws(function(){
+						Expression.parseOperand({$cond:[undefined, 2, 3]}, {});
+					})
+				},
+				"should fail when the 'then' position is empty": function(){
+					assert.throws(function(){
+						Expression.parseOperand({$cond:[1, undefined, 3]}, {});
+					})
+				},
+				"should fail when the 'else' position is empty": function(){
+					assert.throws(function(){
+						Expression.parseOperand({$cond:[1, 2, undefined]}, {});
+					})
+				}
+			},
+
+			"object style": {
+				beforeEach: function(){
+					this.shouldFail = function(expr) {
+						assert.throws(function(){
+							Expression.parseOperand(expr, {});
+						});
+					}
+				},
+				"should fail because the $cond is missing": function(){
+					this.shouldFail({$zoot:[true, 1, 0 ]}, {});
+				},
+				"should fail because of missing if": function(){
+					this.shouldFail({$cond:{xif:1, then:2, else:3}});
+				},
+				"should fail because of missing then": function(){
+					this.shouldFail({$cond:{if:1, xthen:2, else:3}});
+				},
+				"should fail because of missing else": function(){
+					this.shouldFail({$cond:{if:1, then:2, xelse:3}});
+				},
+				"should fail because of empty if": function(){
+					this.shouldFail({$cond:{if:undefined, then:2, else:3}});
+				},
+				"should fail because of empty then": function(){
+					this.shouldFail({$cond:{if:1, then:undefined, else:3}});
+				},
+				"should fail because of empty else": function(){
+					this.shouldFail({$cond:{if:1, then:2, else:undefined}});
+				},
+				"should fail because of mystery args": function(){
+					this.shouldFail({$cond:{if:1, then:2, else:3, zoot:4}});
+				},
+				"should evaluate true": function(){
+					assert.strictEqual(
+						Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
+						1);
+				},
+				"should evaluate true even with mixed up args": function(){
+					assert.strictEqual(
+						Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, {}).evaluate({$a: 1}),
+						1);
+				},
+				"should evaluate false": function(){
+					assert.strictEqual(
+						Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, {}).evaluate({$a: 0}),
+						1);
+				},
+				"should evaluate false even with mixed up args": function() {
+					assert.strictEqual(
+						Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, {}).evaluate({$a: 0}),
+						1);
+				}
+			}
+		}
+	}
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 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);
+		}
+
+	}
+
+};

+ 147 - 146
test/lib/pipeline/expressions/FieldPathExpression.js

@@ -1,161 +1,162 @@
 "use strict";
 var assert = require("assert"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-    Variables = require("../../../../lib/pipeline/expressions/Variables");
-
-
-module.exports = {
-
-	"FieldPathExpression": {
-
-		"constructor()": {
-
-			"should throw Error if empty field path": function testInvalid(){
-				assert.throws(function() {
-					new FieldPathExpression('');
-				});
-			}
-
-		},
-
-		"#evaluate()": {
-
-			"should return undefined if field path is missing": function testMissing(){
-				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1, {})), undefined);
-			},
-
-			"should return value if field path is present": function testPresent(){
-			    var vars = new Variables(1, {a:123}),
-				fieldPath = FieldPathExpression.create('a'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.strictEqual(results, 123);
-			},
-
-			"should return undefined if field path is nested below null": function testNestedBelowNull(){
-			    var vars = new Variables(1,{a:null}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-				assert.strictEqual(results, undefined);
-			},
-
-			"should return undefined if field path is nested below undefined": function NestedBelowUndefined(){
-			    var vars = new Variables(1,{a:undefined}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-				assert.strictEqual(results, undefined);
-			},
-
-			"should return undefined if field path is nested below Number": function testNestedBelowInt(){
-			    var vars = new Variables(1,{a:2}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.strictEqual(results, undefined);
-			},
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker");
 
-			"should return value if field path is nested": function testNestedValue(){
-			    var vars = new Variables(1,{a:{b:55}}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.strictEqual(results, 55);
-			},
+// 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));
 
-			"should return undefined if field path is nested below empty Object": function testNestedBelowEmptyObject(){
-			    var vars = new Variables(1,{a:{}}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.strictEqual(results, undefined);
-			},
+exports.FieldPathExpression = {
 
-			"should return empty Array if field path is nested below empty Array": function testNestedBelowEmptyArray(){
-			    var vars = new Variables(1,{a:[]}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.deepEqual(results, []);
-			},
-			"should return empty Array if field path is nested below Array containing null": function testNestedBelowArrayWithNull(){
-			    var vars = new Variables(1,{a:[null]}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.deepEqual(results, []);
-			},
+	".constructor()": {
 
-			"should return empty Array if field path is nested below Array containing undefined": function testNestedBelowArrayWithUndefined(){
-			    var vars = new Variables(1,{a:[undefined]}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.deepEqual(results, []);
-			},
+		"should throw Error if empty field path": function testInvalid(){
+			assert.throws(function() {
+				new FieldPathExpression("");
+			});
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should return undefined if field path is missing": function testMissing(){
+			var expr = FieldPathExpression.create("a");
+			assert.strictEqual(expr.evaluate({}), undefined);
+		},
+
+		"should return value if field path is present": function testPresent(){
+			var expr = FieldPathExpression.create("a");
+			assert.strictEqual(expr.evaluateInternal(new Variables(1, {a:123})), 123);
+		},
+
+		"should return undefined if field path is nested below null": function testNestedBelowNull(){
+			var expr = FieldPathExpression.create("a.b");
+			assert.strictEqual(expr.evaluateInternal(new Variables(1,{a:null})), undefined);
+		},
+
+		"should return undefined if field path is nested below undefined": function NestedBelowUndefined(){
+			var expr = FieldPathExpression.create("a.b");
+			assert.strictEqual(expr.evaluateInternal(new Variables(1,{a:undefined})), undefined);
+		},
+
+		"should return undefined if field path is nested below missing": function testNestedBelowMissing(){
+			var expr = FieldPathExpression.create("a.b");
+			assert.strictEqual(expr.evaluateInternal(new Variables(1,{z:1})), undefined);
+		},
+
+		"should return undefined if field path is nested below Number": function testNestedBelowInt(){
+			var vars = new Variables(1,{a:2}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.strictEqual(results, undefined);
+		},
+
+		"should return value if field path is nested": function testNestedValue(){
+			var vars = new Variables(1,{a:{b:55}}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.strictEqual(results, 55);
+		},
+
+		"should return undefined if field path is nested below empty Object": function testNestedBelowEmptyObject(){
+			var vars = new Variables(1,{a:{}}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.strictEqual(results, undefined);
+		},
 
-			"should return empty Array if field path is nested below Array containing a Number": function testNestedBelowArrayWithInt(){
-			    var vars = new Variables(1,{a:[9]}),
-				fieldPath = FieldPathExpression.create('a.b'),
-				results = fieldPath.evaluateInternal(vars);
-			    assert.deepEqual(results, []);
-			},
+		"should return empty Array if field path is nested below empty Array": function testNestedBelowEmptyArray(){
+			var vars = new Variables(1,{a:[]}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.deepEqual(results, []);
+		},
+		"should return empty Array if field path is nested below Array containing null": function testNestedBelowArrayWithNull(){
+			var vars = new Variables(1,{a:[null]}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.deepEqual(results, []);
+		},
 
-			"should return Array with value if field path is in Object within Array": function testNestedWithinArray(){
-				assert.deepEqual(FieldPathExpression.create('a.b').evaluateInternal(new Variables(1,{a:[{b:9}]})), [9]);
-			},
+		"should return empty Array if field path is nested below Array containing undefined": function testNestedBelowArrayWithUndefined(){
+			var vars = new Variables(1,{a:[undefined]}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.deepEqual(results, []);
+		},
 
-			"should return Array with multiple value types if field path is within Array with multiple value types": function testMultipleArrayValues(){
-				var path = 'a.b',
-					doc = {a:[{b:9},null,undefined,{g:4},{b:20},{}]},
-					expected = [9,20];
-				assert.deepEqual(FieldPathExpression.create(path).evaluateInternal(new Variables(1,doc)), expected);
-			},
-
-			"should return Array with expanded values from nested multiple nested Arrays": function testExpandNestedArrays(){
-				var path = 'a.b.c',
-					doc = {a:[{b:[{c:1},{c:2}]},{b:{c:3}},{b:[{c:4}]},{b:[{c:[5]}]},{b:{c:[6,7]}}]},
-					expected = [[1,2],3,[4],[[5]],[6,7]];
-				assert.deepEqual(FieldPathExpression.create(path).evaluateInternal(new Variables(1,doc)), expected);
-			},
-
-			"should return null if field path points to a null value": function testPresentNull(){
-				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1,{a:null})), null);
-			},
-
-			"should return undefined if field path points to a undefined value": function testPresentUndefined(){
-				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1,{a:undefined})), undefined);
-			},
-
-			"should return Number if field path points to a Number value": function testPresentNumber(){
-				assert.strictEqual(FieldPathExpression.create('a').evaluateInternal(new Variables(1,{a:42})), 42);
-			}
-
-		},
-
-		"#optimize()": {
-
-			"should not optimize anything": function testOptimize(){
-				var expr = FieldPathExpression.create('a');
-				assert.strictEqual(expr, expr.optimize());
-			}
-
-		},
-
-		"#addDependencies()": {
-
-			"should return the field path itself as a dependency": function testDependencies(){
-				var deps = {};
-				var fpe = FieldPathExpression.create('a.b');
-				fpe.addDependencies(deps);
-				assert.strictEqual(Object.keys(deps).length, 1);
-				assert.ok(deps['a.b']);
-			}
-
-		},
-
-		"#toJSON()": {
-
-			"should output path String with a '$'-prefix": function testJson(){
-				assert.equal(FieldPathExpression.create('a.b.c').serialize(), "$a.b.c");
-			}
+		"should return empty Array if field path is nested below Array containing a Number": function testNestedBelowArrayWithInt(){
+			var vars = new Variables(1,{a:[9]}),
+				expr = FieldPathExpression.create("a.b"),
+				results = expr.evaluateInternal(vars);
+			assert.deepEqual(results, []);
+		},
 
+		"should return Array with value if field path is in Object within Array": function testNestedWithinArray(){
+			assert.deepEqual(FieldPathExpression.create("a.b").evaluateInternal(new Variables(1,{a:[{b:9}]})), [9]);
+		},
+
+		"should return Array with multiple value types if field path is within Array with multiple value types": function testMultipleArrayValues(){
+			var path = "a.b",
+				doc = {a:[{b:9},null,undefined,{g:4},{b:20},{}]},
+				expected = [9,20];
+			assert.deepEqual(FieldPathExpression.create(path).evaluateInternal(new Variables(1,doc)), expected);
+		},
+
+		"should return Array with expanded values from nested multiple nested Arrays": function testExpandNestedArrays(){
+			var path = "a.b.c",
+				doc = {a:[{b:[{c:1},{c:2}]},{b:{c:3}},{b:[{c:4}]},{b:[{c:[5]}]},{b:{c:[6,7]}}]},
+				expected = [[1,2],3,[4],[[5]],[6,7]];
+			assert.deepEqual(FieldPathExpression.create(path).evaluateInternal(new Variables(1,doc)), expected);
+		},
+
+		"should return null if field path points to a null value": function testPresentNull(){
+			assert.strictEqual(FieldPathExpression.create("a").evaluateInternal(new Variables(1,{a:null})), null);
+		},
+
+		"should return undefined if field path points to a undefined value": function testPresentUndefined(){
+			assert.strictEqual(FieldPathExpression.create("a").evaluateInternal(new Variables(1,{a:undefined})), undefined);
+		},
+
+		"should return Number if field path points to a Number value": function testPresentNumber(){
+			assert.strictEqual(FieldPathExpression.create("a").evaluateInternal(new Variables(1,{a:42})), 42);
 		}
 
-	}
+	},
 
-};
+	"#optimize()": {
+
+		"should not optimize anything": function testOptimize(){
+			var expr = FieldPathExpression.create("a");
+			// An attempt to optimize returns the Expression itself.
+			assert.strictEqual(expr, expr.optimize());
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	},
+
+	"#addDependencies()": {
+
+		"should return the field path itself as a dependency": function testDependencies(){
+			var fpe = FieldPathExpression.create("a.b"),
+				deps = new DepsTracker();
+			fpe.addDependencies(deps);
+			assert.strictEqual(Object.keys(deps.fields).length, 1);
+			assert("a.b" in deps.fields);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
+		},
+
+	},
+
+	"#serialize()": {
+
+		"should output path String with a '$'-prefix": function testJson(){
+			assert.equal(FieldPathExpression.create("a.b.c").serialize(), "$a.b.c");
+		},
+
+	},
+
+
+};

+ 64 - 0
test/lib/pipeline/expressions/IfNullExpression_test.js

@@ -0,0 +1,64 @@
+"use strict";
+var assert = require("assert"),
+	IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+
+module.exports = {
+
+	"IfNullExpression": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function() {
+				assert.doesNotThrow(function () {
+					new IfNullExpression();
+				});
+			},
+			"should throw Error when constructing with args": function () {
+				assert.throws(function () {
+					new IfNullExpression(1);
+				});
+			}
+		},
+
+		"#getOpName()": {
+
+			"should return the correct op name; $ifNull": function() {
+				assert.equal(new IfNullExpression().getOpName(), "$ifNull");
+			}
+
+		},
+
+		"#evaluateInternal()": {
+			beforeEach: function () {
+				this.vps = new VariablesParseState(new VariablesIdGenerator());
+				this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
+				this.vars = new Variables(2);
+				this.vars.setValue(0, "a");
+				this.vars.setValue(1, "b");
+				this.makeParsed = function(a, b) {
+					return Expression.parseExpression("$ifNull", [a, b], this.vps);
+				}
+			},
+
+			"should return the left hand side if the left hand side is not null or undefined": function() {
+				//assert.strictEqual(this.parsed.evaluate(this.vars), 1);
+				assert.strictEqual(this.makeParsed(1, 2).evaluate(this.vars), 1);
+			},
+			"should return the right hand side if the left hand side is null": function() {
+				//assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
+				assert.strictEqual(this.makeParsed(null, 2).evaluate(this.vars), 2);
+			},
+			"should return the right hand side if the left hand side is undefined": function() {
+				//assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
+				assert.strictEqual(this.makeParsed(undefined, 2).evaluate(this.vars), 2);
+			}
+		}
+	}
+};
+
+if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 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);

+ 0 - 8
test/lib/pipeline/expressions/NotExpression.js

@@ -26,14 +26,6 @@ module.exports = {
 
 		},
 
-		"#getFactory()": {
-
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new NotExpression().getFactory(), undefined);
-			}
-
-		},
-
 		"#evaluateInternal()": {
 
 			"should return false for a true input; false for true": function testStuff(){

+ 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);