Kaynağa Gözat

Merge branch 'feature/mongo_2.6.5_matcher' into feature/mongo_2.6.5_matcher_FalseMatch

* feature/mongo_2.6.5_matcher: (94 commits)
  EAGLESIX-3009: Removed unnecessary memory related functions
  EAGLESIX-2653: Remove debugger statement
  EAGLESIX-3006: Added setTag() code to shallowClone().
  EAGLESIX-3007: Added setTag() code to shallowClone().
  EAGLESIX-2653: Reformat and use proper dependencies
  EAGLESIX-3006: Added return (only missing code) to shallowClone().
  EAGLESIX-2653: Add tag cloning to shallowClone
  EAGLESIX-2653: ExistsMatchExpression formatting fixes
  EAGLESIX-2653: Fix comments and make equivalent strict
  EAGLESIX-2653: Remove a debugger statement
  EAGLESIX-3004: Addressed comments concerning missing semicolons.
  EAGLESIX-3004: "object" for "Object".
  EAGLESIX-3004: e replaced with the currval.
  EAGLESIX-3004: e replaced with the currval.
  EAGLESIX-3004: Removed terse code.
  EAGLESIX-2653: Remove debugger statement
  EAGLESIX-3004: Stray character removed.
  EAGLESIX-3004: Stray GIT head block removed.
  EAGLESIX-3004: Added respective code to MatchExpressionParser.js.
  EAGLESIX-2653: fix port to match current functionality/tests
  ...
Chris Sexton 11 yıl önce
ebeveyn
işleme
4de8bed67f
53 değiştirilmiş dosya ile 2710 ekleme ve 2384 silme
  1. 18 16
      lib/pipeline/DepsTracker.js
  2. 2 2
      lib/pipeline/Document.js
  3. 20 24
      lib/pipeline/expressions/CoerceToBoolExpression.js
  4. 32 87
      lib/pipeline/expressions/CondExpression.js
  5. 4 0
      lib/pipeline/expressions/ConstantExpression.js
  6. 22 23
      lib/pipeline/expressions/DivideExpression.js
  7. 9 24
      lib/pipeline/expressions/IfNullExpression.js
  8. 25 36
      lib/pipeline/expressions/ModExpression.js
  9. 11 24
      lib/pipeline/expressions/NotExpression.js
  10. 170 165
      lib/pipeline/expressions/ObjectExpression.js
  11. 9 20
      lib/pipeline/expressions/SizeExpression.js
  12. 35 24
      lib/pipeline/expressions/SubtractExpression.js
  13. 28 56
      lib/pipeline/matcher/AllElemMatchOp.js
  14. 6 0
      lib/pipeline/matcher/AndMatchExpression.js
  15. 1 1
      lib/pipeline/matcher/ElemMatchObjectMatchExpression.js
  16. 6 9
      lib/pipeline/matcher/ExistsMatchExpression.js
  17. 58 65
      lib/pipeline/matcher/InMatchExpression.js
  18. 53 43
      lib/pipeline/matcher/ListOfMatchExpression.js
  19. 42 49
      lib/pipeline/matcher/MatchDetails.js
  20. 139 88
      lib/pipeline/matcher/MatchExpression.js
  21. 117 52
      lib/pipeline/matcher/MatchExpressionParser.js
  22. 14 279
      lib/pipeline/matcher/Matcher2.js
  23. 10 16
      lib/pipeline/matcher/ModMatchExpression.js
  24. 5 0
      lib/pipeline/matcher/NorMatchExpression.js
  25. 9 16
      lib/pipeline/matcher/NotMatchExpression.js
  26. 6 0
      lib/pipeline/matcher/OrMatchExpression.js
  27. 14 17
      lib/pipeline/matcher/SizeMatchExpression.js
  28. 112 0
      lib/pipeline/matcher/TextMatchExpression.js
  29. 25 0
      lib/pipeline/matcher/TextMatchExpressionParser.js
  30. 56 34
      test/lib/pipeline/expressions/CoerceToBoolExpression.js
  31. 0 72
      test/lib/pipeline/expressions/CondExpression.js
  32. 93 104
      test/lib/pipeline/expressions/CondExpression_test.js
  33. 47 0
      test/lib/pipeline/expressions/DivideExpression_test.js
  34. 0 58
      test/lib/pipeline/expressions/IfNullExpression.js
  35. 40 45
      test/lib/pipeline/expressions/IfNullExpression_test.js
  36. 62 35
      test/lib/pipeline/expressions/ModExpression.js
  37. 0 45
      test/lib/pipeline/expressions/NotExpression.js
  38. 47 0
      test/lib/pipeline/expressions/NotExpression_test.js
  39. 637 631
      test/lib/pipeline/expressions/ObjectExpression.js
  40. 36 30
      test/lib/pipeline/expressions/SizeExpression.js
  41. 111 27
      test/lib/pipeline/expressions/SubtractExpression.js
  42. 0 131
      test/lib/pipeline/matcher/AllElemMatchOp.js
  43. 128 0
      test/lib/pipeline/matcher/AllElemMatchOp_test.js
  44. 8 8
      test/lib/pipeline/matcher/AndMatchExpression.js
  45. 7 7
      test/lib/pipeline/matcher/ExistsMatchExpression.js
  46. 72 0
      test/lib/pipeline/matcher/ListOfMatchExpression.js
  47. 62 0
      test/lib/pipeline/matcher/MatchDetails.js
  48. 71 13
      test/lib/pipeline/matcher/MatchExpressionParser.js
  49. 78 0
      test/lib/pipeline/matcher/MatchExpression_test.js
  50. 86 0
      test/lib/pipeline/matcher/Matcher2.js
  51. 4 4
      test/lib/pipeline/matcher/NorMatchExpression.js
  52. 4 4
      test/lib/pipeline/matcher/OrMatchExpression.js
  53. 59 0
      test/lib/pipeline/matcher/TextMatchExpression_test.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]];

+ 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

+ 32 - 87
lib/pipeline/expressions/CondExpression.js

@@ -6,108 +6,53 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var CondExpression = module.exports = function CondExpression(vars) {
-		if (arguments.length !== 0) throw new Error("zero args expected");
+ */
+var CondExpression = module.exports = function CondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
     base.call(this);
-}, klass = CondExpression,
-	base = require("./FixedArityExpressionT")(klass, 3),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = CondExpression, base = require("./FixedArityExpressionT")(CondExpression, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-    Expression = require("./Expression"),
-	FixedArityExpressionT = require("./FixedArityExpressionT");
+    Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$cond";
-proto.getOpName = function getOpName() {
-    return klass.opName;
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var cond = this.operands[0].evaluateInternal(vars);
+	var idx = Value.coerceToBool(cond) ? 1 : 2;
+	return this.operands[idx].evaluateInternal(vars);
 };
 
-/**
- *
- * @param expr	- I expect this to be the RHS of $cond:{...} or $cond:[,,,]
- * @param vps
- * @returns {*}
- */
 klass.parse = function parse(expr, vps) {
-	// 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;
-	// 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);
-
-	// ...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+"'");
+    if (Value.getType(expr) !== "Object") {
+		return base.parse(expr, vps);
 	}
+	// verify(str::equals(expr.fieldName(), "$cond")); //NOTE: DEVIATION FROM MONGO: we do not have fieldName any more and not sure this is even possible anyway
 
     var ret = new CondExpression();
-
-	// 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");
+	ret.operands.length = 3;
+
+	var args = expr;
+	for (var argfieldName in args) {
+		if (!args.hasOwnProperty(argfieldName)) continue;
+		if (argfieldName === "if") {
+			ret.operands[0] = Expression.parseOperand(args.if, vps);
+		} else if (argfieldName === "then") {
+			ret.operands[1] = Expression.parseOperand(args.then, vps);
+		} else if (argfieldName === "else") {
+			ret.operands[2] = Expression.parseOperand(args.else, vps);
+		} else {
+			throw new Error("Unrecognized parameter to $cond: '" + argfieldName + "'; uasserted 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");
+    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; uassert code 17080");
+    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; uassert code 17081");
+    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; uassert code 17082");
 
     return ret;
 };
 
-/**
- * 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) {
-		var pCond1 = this.operands[0].evaluateInternal(vars);
-
-		this.idx = 0;
-		if (pCond1.coerceToBool()) {
-			this.idx = 1;
-		} else {
-			this.idx = 2;
-		}
+Expression.registerExpression("$cond", CondExpression.parse);
 
-		return this.operands[this.idx].evaluateInternal(vars);
+proto.getOpName = function getOpName() {
+	return "$cond";
 };
-
-/** Register Expression */
-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";
+};

+ 9 - 24
lib/pipeline/expressions/IfNullExpression.js

@@ -7,39 +7,24 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var IfNullExpression = module.exports = function IfNullExpression() {
-	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
 	base.call(this);
-}, klass = IfNullExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = IfNullExpression, base = require("./FixedArityExpressionT")(IfNullExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$ifNull";
-};
-
-// virtuals from ExpressionNary
-
-/**
- * Use the $ifNull operator with the following syntax: { $ifNull: [ <expression>, <replacement-if-null> ] }
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var left = this.operands[0].evaluateInternal(vars);
-	if (left !== undefined && left !== null) return left;
+	if (left !== undefined && left !== null)
+		return left;
 	var right = this.operands[1].evaluateInternal(vars);
 	return right;
 };
 
-/** Register Expression */
 Expression.registerExpression("$ifNull", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$ifNull";
+};

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

+ 11 - 24
lib/pipeline/expressions/NotExpression.js

@@ -7,36 +7,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var NotExpression = module.exports = function NotExpression() {
-		if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
 	base.call(this);
-}, klass = NotExpression,
-	base = require("./FixedArityExpressionT")(klass, 1),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = NotExpression, base = require("./FixedArityExpressionT")(NotExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$not";
-proto.getOpName = function getOpName() {
-	return klass.opName;
-};
-
-/**
- * Returns the boolean opposite value passed to it. When passed a true value, $not returns false; when passed a false value, $not returns true.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var op = this.operands[0].evaluateInternal(vars);
-	return !Value.coerceToBool(op);
+	var op = this.operands[0].evaluateInternal(vars),
+		b = Value.coerceToBool(op);
+	return !b;
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$not", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$not";
+};

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

+ 28 - 56
lib/pipeline/matcher/AllElemMatchOp.js

@@ -2,8 +2,7 @@
 
 var MatchExpression = require('./MatchExpression');
 
-
-// Autogenerated by cport.py on 2013-09-17 14:37
+// From expression_array.h
 var AllElemMatchOp = module.exports = function AllElemMatchOp(){
 	base.call(this);
 	this._matchType = 'ALL';
@@ -11,26 +10,15 @@ var AllElemMatchOp = module.exports = function AllElemMatchOp(){
 	this._list = [];
 }, klass = AllElemMatchOp, base =  MatchExpression , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
 	ElementPath = require('./ElementPath.js');
 
-// File: expression_array.h lines: 175-175
-//         ElementPath _elementPath;
-
+// ElementPath _elementPath
 proto._elementPath = undefined;
-
-
-// File: expression_array.h lines: 176-176
-//         std::vector< const ArrayMatchingMatchExpression* > _list;
-
+// std::vector< MatchExpression* > _list;
 proto._list = undefined;
-
-
-// File: expression_array.h lines: 174-174
-//         StringData _path;
-
+// StringData _path;
 proto._path = undefined;
 
 /**
@@ -40,18 +28,16 @@ proto._path = undefined;
  * @param anArray
  *
  */
-proto._allMatch = function _allMatch( anArray ){ //  const BSONObj& anArray
-	// File: expression_array.cpp lines: 208-215
-	if(this._list.length === 0) { return false; }
+proto._allMatch = function _allMatch(anArray) {
+	if (this._list.length === 0) return false;
 
 	for (var i = 0; i < this._list.length; i++) {
-		if( ! this._list[i].matchesArray( anArray, null ) ) { return false; }
+		if (!this._list[i].matchesArray(anArray, null)) return false;
 	}
 
 	return true;
 };
 
-
 /**
  *
  * This method adds a new expression to the internal array of expression
@@ -59,13 +45,11 @@ proto._allMatch = function _allMatch( anArray ){ //  const BSONObj& anArray
  * @param expr
  *
  */
-proto.add = function add( expr ){//  const ArrayMatchingMatchExpression* expr
-	// File: expression_array.cpp lines: 184-186
+proto.add = function add(expr) {
 	if (!expr) throw new Error("AllElemMatchOp:add#68 failed to verify expr");
 	this._list.push(expr);
 };
 
-
 /**
  *
  * Writes a debug string for this object
@@ -73,15 +57,13 @@ proto.add = function add( expr ){//  const ArrayMatchingMatchExpression* expr
  * @param level
  *
  */
-proto.debugString = function debugString( level ){ //   StringBuilder& debug, int level
-	// File: expression_array.cpp lines: 219-224
+proto.debugString = function debugString(level) {
 	console.debug(this._debugAddSpace(level) + this._path + " AllElemMatchOp: " + this._path + '\n');
 	for (var i = 0; i < this._list.length; i++) {
 		this._list[i].debugString(level +1);
 	}
 };
 
-
 /**
  *
  * checks if this expression is == to the other
@@ -89,29 +71,27 @@ proto.debugString = function debugString( level ){ //   StringBuilder& debug, in
  * @param other
  *
  */
-proto.equivalent = function equivalent( other ){//  const MatchExpression* other
-// File: expression_array.cpp lines: 227-242
+proto.equivalent = function equivalent(other) {
 	if (this.matchType() != other.matchType()) {
 		return false;
 	}
 
-	if( this._path != other._path ) {
+	if (this._path != other._path) {
 		return false;
 	}
 
-	if( this._list.length != other._list.length ) {
+	if (this._list.length != other._list.length) {
 		return false;
 	}
+
 	for (var i = 0; i < this._list.length; i++) {
-		if ( !this._list[i].equivalent( other._list[i] ) ) {
+		if (!this._list[i].equivalent(other._list[i])) {
 			return false;
 		}
 	}
 	return true;
 };
 
-
-
 /**
  *
  * gets the specified item from the list
@@ -119,12 +99,10 @@ proto.equivalent = function equivalent( other ){//  const MatchExpression* other
  * @param i
  *
  */
-proto.getChild = function getChild( i ){ //  size_t i
-// File: expression_array.h lines: 167-166
+proto.getChild = function getChild(i) {
 	return this._list[i];
 };
 
-
 /**
  *
  * Initialize the necessary items
@@ -132,11 +110,10 @@ proto.getChild = function getChild( i ){ //  size_t i
  * @param path
  *
  */
-proto.init = function init( path ){ //  const StringData& path
-// File: expression_array.cpp lines: 177-181
+proto.init = function init(path) {
 	this._path = path;
-	var s = this._elementPath.init( this._path );
-	this._elementPath.setTraverseLeafArray( false );
+	var s = this._elementPath.init(this._path);
+	this._elementPath.setTraverseLeafArray(false);
 	return s;
 };
 
@@ -148,17 +125,18 @@ proto.init = function init( path ){ //  const StringData& path
  * @param details
  *
  */
-proto.matches = function matches(doc, details){
-	// File: expression_array.cpp lines: 189-198
+proto.matches = function matches(doc, details) {
 	var self = this,
 		checker = function(element) {
-			if (!(element instanceof Array))
+			if (!(element instanceof Array)) {
 				return false;
+			}
 
 			//var amIRoot = (element.length === 0);
 
-			if (self._allMatch(element))
+			if (self._allMatch(element)) {
 				return true;
+			}
 
 			/*
 			if (!amIRoot && details && details.needRecord() {
@@ -177,15 +155,13 @@ proto.matches = function matches(doc, details){
  * @param e
  *
  */
-proto.matchesSingleElement = function matchesSingleElement( e ){ //  const BSONElement& e
-	// File: expression_array.cpp lines: 201-205
+proto.matchesSingleElement = function matchesSingleElement(e) {
 	if (!(e instanceof Array)) {
 		return false;
 	}
 	return this._allMatch(e);
 };
 
-
 /**
  *
  * return the length of the internal array
@@ -193,8 +169,7 @@ proto.matchesSingleElement = function matchesSingleElement( e ){ //  const BSONE
  * @param
  *
  */
-proto.numChildren = function numChildren( /*  */ ){
-// File: expression_array.h lines: 166-165
+proto.numChildren = function numChildren() {
 	return this._list.length;
 };
 
@@ -206,8 +181,7 @@ proto.numChildren = function numChildren( /*  */ ){
  * @param
  *
  */
-proto.path = function path( /*  */ ){
-// File: expression_array.h lines: 169-168
+proto.path = function path() {
 	return this._path;
 };
 
@@ -219,11 +193,9 @@ proto.path = function path( /*  */ ){
  * @param
  *
  */
-proto.shallowClone = function shallowClone( /*  */ ){
-// File: expression_array.h lines: 145-152
+proto.shallowClone = function shallowClone() {
 	var e = new AllElemMatchOp();
-	e.init( this._path );
+	e.init(this._path);
 	e._list = this._list.slice(0);
 	return e;
 };
-

+ 6 - 0
lib/pipeline/matcher/AndMatchExpression.js

@@ -78,5 +78,11 @@ proto.shallowClone = function shallowClone( /*  */ ){
 	for (var i = 0; i < this.numChildren(); i++) {
 		e.add(this.getChild(i).shallowClone());
 	}
+
+	if (this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
+
+	return e; // Return the shallow copy.
 };
 

+ 1 - 1
lib/pipeline/matcher/ElemMatchObjectMatchExpression.js

@@ -62,7 +62,7 @@ proto.matchesArray = function matchesArray(anArray, details){
 		var inner = anArray[i];
 		if (!(inner instanceof Object))
 			continue;
-		if (this._sub.matchesBSON(inner, null)) {
+		if (this._sub.matchesJSON(inner, null)) {
 			if (details && details.needRecord()) {
 				details.setElemMatchKey(i);
 			}

+ 6 - 9
lib/pipeline/matcher/ExistsMatchExpression.js

@@ -1,7 +1,7 @@
 "use strict";
 var LeafMatchExpression = require('./LeafMatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression_leaf.cpp
 var ExistsMatchExpression = module.exports = function ExistsMatchExpression(){
 	base.call(this);
 	this._matchType = 'EXISTS';
@@ -15,7 +15,6 @@ var ExistsMatchExpression = module.exports = function ExistsMatchExpression(){
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 286-294
 	return this._debugAddSpace( level ) + this.path() + " exists" + (this.getTag() ? " " + this.getTag().debugString() : "") + "\n";
 };
 
@@ -27,8 +26,7 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 297-302
-	if(this._matchType != other._matchType)	{
+	if(this._matchType !== other._matchType) {
 		return false;
 	}
 	return this.path() == other.path();
@@ -43,7 +41,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.init = function init(path) {
-	// File: expression_leaf.cpp lines: 278-279
 	return this.initPath( path );
 };
 
@@ -55,12 +52,11 @@ proto.init = function init(path) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_leaf.cpp lines: 282-283
-	if(typeof(e) == 'undefined')
+	if(typeof(e) === 'undefined')
 		return false;
 	if(e === null)
 		return true;
-	if(typeof(e) == 'object')
+	if(typeof(e) === 'object')
 		return (Object.keys(e).length > 0);
 	else
 		return true;
@@ -73,9 +69,10 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.h lines: 220-223
 	var e = new ExistsMatchExpression();
 	e.init(this.path());
+	if (this.getTag())
+		e.setTag(this.getTag().clone());
 	return e;
 };
 

+ 58 - 65
lib/pipeline/matcher/InMatchExpression.js

@@ -1,7 +1,6 @@
 "use strict";
 var LeafMatchExpression = require('./LeafMatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var InMatchExpression = module.exports = function InMatchExpression(){
 	base.call(this);
 	this._matchType = 'MATCH_IN';
@@ -13,9 +12,19 @@ var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
 	ArrayFilterEntries = require("./ArrayFilterEntries.js");
 
-// File: expression_leaf.h lines: 294-294
 proto._arrayEntries = null;
 
+/**
+ *
+ * Initialize the necessary items
+ * @method init
+ * @param path
+ *
+ */
+proto.init = function init(path) {
+	return this.initPath( path );
+};
+
 /**
  *
  * Check if the input element matches a real element
@@ -24,7 +33,6 @@ proto._arrayEntries = null;
  *
  */
 proto._matchesRealElement = function _matchesRealElement(e) {
-	// File: expression_leaf.cpp lines: 422-431
 	if(this._arrayEntries.contains(e)) { // array wrapper.... so no e "in" array
 		return true;
 	}
@@ -33,14 +41,14 @@ proto._matchesRealElement = function _matchesRealElement(e) {
 		if(e.match && e.match(this._arrayEntries.regex(i)._regex)) {
 			return true;
 		} else if (e instanceof RegExp) {
-			if(e.toString() == this._arrayEntries.regex(i)._regex.toString()) {
+			if(e.toString() === this._arrayEntries.regex(i)._regex.toString()) {
 				return true;
 			}
 		}
 	}
 
-	if(typeof(e) == 'undefined') {
-		return true; // Every Set contains the Null Set, man.
+	if(typeof(e) === 'undefined') {
+		return true; // Every Set contains the Null Set.
 	}
 
 	return false;
@@ -48,15 +56,27 @@ proto._matchesRealElement = function _matchesRealElement(e) {
 
 /**
  *
- * Copy our array to the input array
- * @method copyTo
- * @param toFillIn
+ * Check if the input element matches
+ * @method matchesSingleElement
+ * @param e
  *
  */
-proto.copyTo = function copyTo(toFillIn) {
-	// File: expression_leaf.cpp lines: 481-483
-	toFillIn.init(this.path());
-	this._arrayEntries.copyTo( toFillIn._arrayEntries );
+proto.matchesSingleElement = function matchesSingleElement(e) {
+	if( this._arrayEntries === null && typeof(e) == 'object' && Object.keys(e).length === 0) {
+		return true;
+	}
+	if (this._matchesRealElement( e )) {
+		return true;
+	}
+	/*if (e instanceof Array){
+		for (var i = 0; i < e.length; i++) {
+			if(this._matchesRealElement( e[i] )) {
+				return true;
+			}
+		}
+
+	}*/
+	return false;
 };
 
 /**
@@ -67,8 +87,7 @@ proto.copyTo = function copyTo(toFillIn) {
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 455-463
-	return this._debugAddSpace( level ) + this.path() + ";$in: TODO " + (this.getTag() ? this.getTag().debugString() : '') + "\n";
+	return this._debugAddSpace( level ) + this.path() + " $in " + this._arrayEntries + (this.getTag() ? this.getTag().debugString() : '') + "\n";
 };
 
 /**
@@ -79,83 +98,57 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 466-472
 	if ( other._matchType != 'MATCH_IN' ) {
 		return false;
 	}
-	return this.path() == other.path() && this._arrayEntries.equivalent( other._arrayEntries );
+	return this.path() === other.path() && this._arrayEntries.equivalent( other._arrayEntries );
 };
 
 /**
  *
- * Return the _arrayEntries property
- * @method getArrayFilterEntries
+ * clone this instance to a new one
+ * @method shallowClone
  *
  */
-proto.getArrayFilterEntries = function getArrayFilterEntries(){
-	// File: expression_leaf.h lines: 280-279
-	return this._arrayEntries;
+proto.shallowClone = function shallowClone(){
+	var e = new InMatchExpression();
+	this.copyTo( e );
+	if ( this.getTag() ){
+		e.setTag(this.getTag().Clone());
+	}
+	return e;
 };
 
 /**
  *
- * Return the _arrayEntries property
- * @method getData
+ * Copy our array to the input array
+ * @method copyTo
+ * @param toFillIn
  *
  */
-proto.getData = function getData(){
-	// File: expression_leaf.h lines: 290-289
-	return this._arrayEntries;
+proto.copyTo = function copyTo(toFillIn) {
+	toFillIn.init(this.path());
+	this._arrayEntries.copyTo( toFillIn._arrayEntries );
 };
 
 /**
  *
- * Initialize the necessary items
- * @method init
- * @param path
+ * Return the _arrayEntries property
+ * @method getArrayFilterEntries
  *
  */
-proto.init = function init(path) {
-	// File: expression_leaf.cpp lines: 418-419
-	return this.initPath( path );
+proto.getArrayFilterEntries = function getArrayFilterEntries(){
+	return this._arrayEntries;
 };
 
 /**
  *
- * Check if the input element matches
- * @method matchesSingleElement
- * @param e
+ * Return the _arrayEntries property
+ * @method getData
  *
  */
-proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_leaf.cpp lines: 434-452
-	if( this._arrayEntries === null && typeof(e) == 'object' && Object.keys(e).length === 0) {
-		return true;
-	}
-	if (this._matchesRealElement( e )) {
-		return true;
-	}
-	/*if (e instanceof Array){
-		for (var i = 0; i < e.length; i++) {
-			if(this._matchesRealElement( e[i] )) {
-				return true;
-			}
-		}
-
-	}*/
-	return false;
+proto.getData = function getData(){
+	return this._arrayEntries;
 };
 
-/**
- *
- * clone this instance to a new one
- * @method shallowClone
- *
- */
-proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.cpp lines: 475-478
-	var e = new InMatchExpression();
-	this.copyTo( e );
-	return e;
-};
 

+ 53 - 43
lib/pipeline/matcher/ListOfMatchExpression.js

@@ -2,29 +2,21 @@
 
 var MatchExpression = require('./MatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * Create a match expression to match a list of
+ * @class ListOfMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var ListOfMatchExpression = module.exports = function ListOfMatchExpression(matchType){
 	base.call(this);
 	this._expressions = [];
 	this._matchType = matchType;
 }, klass = ListOfMatchExpression, base =  MatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: expression_tree.h lines: 56-56
 proto._expressions = undefined;
 
-/**
- *
- * Print the debug info from each expression in the list
- * @method _debugList
- * @param level
- *
- */
-proto._debugList = function _debugList(level){
-	// File: expression_tree.cpp lines: 40-42
-	for (var i = 0; i < this._expressions.length; i++ )
-		this._expressions[i].debugString(level + 1); // debug only takes level now
-};
-
 /**
  *
  * Append a new expression to our list
@@ -33,10 +25,9 @@ proto._debugList = function _debugList(level){
  *
  */
 proto.add = function add( exp ){
-	// File: expression_tree.cpp lines: 34-36
 	// verify(expression)
 	if(!exp)
-		throw new Error(exp + " failed verify on ListOfMatchExpression:34");
+		throw new Error(exp + " failed verify on ListOfMatchExpression:add");
 	if(this._expressions) {
 		this._expressions.push(exp);
 	} else {
@@ -52,10 +43,54 @@ proto.add = function add( exp ){
  *
  */
 proto.clearAndRelease = function clearAndRelease(){
-	// File: expression_tree.h lines: 45-44
 	this._expressions = []; // empty the expressions
 };
 
+/**
+ *
+ * Get the length of the list
+ * @method numChildren
+ * @param
+ *
+ */
+proto.numChildren = function numChildren(){
+	return this._expressions.length;
+};
+
+/**
+ *
+ * Get an item from the expressions
+ * @method getChild
+ * @param i index of the child
+ *
+ */
+proto.getChild = function getChild(i){
+	return this._expressions[i];
+};
+
+/**
+ *
+ * Get the expressions
+ * @method getChildVector
+ * @param
+ *
+ */
+proto.getChildVector = function getChildVector(){
+	return this._expressions;
+};
+
+/**
+ *
+ * Print the debug info from each expression in the list
+ * @method _debugList
+ * @param level
+ *
+ */
+proto._debugList = function _debugList(debug, level){
+	for (var i = 0; i < this._expressions.length; i++ )
+		this._expressions[i].debugString(debug, level + 1);
+};
+
 /**
  *
  * Check if the input list is considered the same as this one
@@ -64,7 +99,6 @@ proto.clearAndRelease = function clearAndRelease(){
  *
  */
 proto.equivalent = function equivalent(other){
-	// File: expression_tree.cpp lines: 45-59
 	if (this._matchType != other._matchType)
 		return false;
 
@@ -80,27 +114,3 @@ proto.equivalent = function equivalent(other){
 
 	return true;
 };
-
-/**
- *
- * Get an item from the expressions
- * @method getChild
- * @param
- *
- */
-proto.getChild = function getChild(i){
-	// File: expression_tree.h lines: 48-47
-	return this._expressions[i];
-};
-
-/**
- *
- * Get the length of the list
- * @method numChildren
- * @param
- *
- */
-proto.numChildren = function numChildren(){
-	// File: expression_tree.h lines: 47-46
-	return this._expressions.length;
-};

+ 42 - 49
lib/pipeline/matcher/MatchDetails.js

@@ -1,42 +1,53 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * MatchDetails
+ * @class MatchDetails
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ **/
 var MatchDetails = module.exports = function (){
-	// File: match_details.cpp lines: 27-29
 	this._elemMatchKeyRequested = false;
 	this.resetOutput();
 }, klass = MatchDetails, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: match_details.h lines: 60-60
 proto._elemMatchKey = undefined;
 
-// File: match_details.h lines: 59-59
 proto._elemMatchKeyRequested = undefined;
 
-// File: match_details.h lines: 58-58
 proto._loadedRecord = undefined;
 
 /**
  *
- * Return the _elemMatchKey property
- * @method elemMatchKey
+ * Set _loadedRecord to false and _elemMatchKey to undefined
+ * @method resetOutput
  *
  */
-proto.elemMatchKey = function elemMatchKey(){
-	// File: match_details.cpp lines: 41-43
-	if (!this.hasElemMatchKey()) throw new Error("no elem match key MatchDetails:29");
-	return this._elemMatchKey;
+proto.resetOutput = function resetOutput(){
+	this._loadedRecord = false;
+	this._elemMatchKey = undefined;
 };
 
 /**
  *
- * Return the _elemMatchKey property so we can check if exists
- * @method hasElemMatchKey
+ * Return a string representation of ourselves
+ * @method toString
  *
  */
-proto.hasElemMatchKey = function hasElemMatchKey(){
-	// File: match_details.cpp lines: 37-38
-	return this._elemMatchKey;
+proto.toString = function toString(){
+	return "loadedRecord: " + this._loadedRecord + " " + "elemMatchKeyRequested: " + this._elemMatchKeyRequested + " " + "elemMatchKey: " + ( this._elemMatchKey ? this._elemMatchKey : "NONE" ) + " ";
+};
+
+/**
+ *
+ * Set the _loadedRecord property
+ * @method setLoadedRecord
+ * @param loadedRecord
+ *
+ */
+proto.setLoadedRecord = function setLoadedRecord(loadedRecord){
+	this._loadedRecord = loadedRecord;
 };
 
 /**
@@ -46,7 +57,6 @@ proto.hasElemMatchKey = function hasElemMatchKey(){
  *
  */
 proto.hasLoadedRecord = function hasLoadedRecord(){
-	// File: match_details.h lines: 41-40
 	return this._loadedRecord;
 };
 
@@ -57,7 +67,6 @@ proto.hasLoadedRecord = function hasLoadedRecord(){
  *
  */
 proto.needRecord = function needRecord(){
-	// File: match_details.h lines: 45-44
 	return this._elemMatchKeyRequested;
 };
 
@@ -68,20 +77,28 @@ proto.needRecord = function needRecord(){
  *
  */
 proto.requestElemMatchKey = function requestElemMatchKey(){
-	// File: match_details.h lines: 50-49
 	this._elemMatchKeyRequested = true;
 };
 
 /**
  *
- * Set _loadedRecord to false and _elemMatchKey to undefined
- * @method resetOutput
+ * Return the _elemMatchKey property so we can check if exists
+ * @method hasElemMatchKey
  *
  */
-proto.resetOutput = function resetOutput(){
-	// File: match_details.cpp lines: 32-34
-	this._loadedRecord = false;
-	this._elemMatchKey = undefined;
+proto.hasElemMatchKey = function hasElemMatchKey(){
+	return (typeof this._elemMatchKey !== 'undefined');
+};
+
+/**
+ *
+ * Return the _elemMatchKey property
+ * @method elemMatchKey
+ *
+ */
+proto.elemMatchKey = function elemMatchKey(){
+	if (!this.hasElemMatchKey()) throw new Error("no elem match key MatchDetails:29");
+	return this._elemMatchKey;
 };
 
 /**
@@ -92,31 +109,7 @@ proto.resetOutput = function resetOutput(){
  *
  */
 proto.setElemMatchKey = function setElemMatchKey(elemMatchKey){
-	// File: match_details.cpp lines: 46-49
 	if ( this._elemMatchKeyRequested ) {
 		this._elemMatchKey = elemMatchKey;
 	}
 };
-
-/**
- *
- * Set the _loadedRecord property
- * @method setLoadedRecord
- * @param loadedRecord
- *
- */
-proto.setLoadedRecord = function setLoadedRecord(loadedRecord){
-	// File: match_details.h lines: 39-38
-	this._loadedRecord = loadedRecord;
-};
-
-/**
- *
- * Return a string representation of ourselves
- * @method toString
- *
- */
-proto.toString = function toString(){
-	// File: match_details.cpp lines: 52-57
-	return "loadedRecord: " + this._loadedRecord + " " + "elemMatchKeyRequested: " + this._elemMatchKeyRequested + " " + "elemMatchKey: " + ( this._elemMatchKey ? this._elemMatchKey : "NONE" ) + " ";
-};

+ 139 - 88
lib/pipeline/matcher/MatchExpression.js

@@ -1,172 +1,223 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
-var MatchExpression = module.exports = function MatchExpression( type ){
+/**
+ * Files: matcher/expression.h/cpp
+ * Function order follows that in the header file
+ * @class MatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ * @param type {String} The type of the match expression
+ */
+var MatchExpression = module.exports = function MatchExpression(type){
 	this._matchType = type;
-}, klass = MatchExpression, base =  Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = MatchExpression, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes;
 
-	// File: expression.h lines: 172-172
-proto._matchType = undefined;
-
-// File: expression.h lines: 173-173
-proto._tagData = undefined;
+/**
+ * Return the _matchType property
+ * @method matchType
+ */
+proto.matchType = function matchType(){
+	return this._matchType;
+};
 
 /**
- *
- * Writes a debug string for this object
- * @method debugString
- * @param level
- *
+ * Return the number of children we have
+ * @method numChildren
  */
-proto._debugAddSpace = function _debugAddSpace(level){
-	// File: expression.cpp lines: 37-39
-	return new Array( level + 1).join("    ");
+proto.numChildren = function numChildren( ){
+	return 0;
 };
 
+
 /**
- *
- * Get our child elements
+ * Get the i-th child.
  * @method getChild
- *
  */
-proto.getChild = function getChild() {
-	// File: expression.h lines: 78-77
-	throw new Error('Virtual function called.');
+proto.getChild = function getChild(i) {
+	return null;
 };
 
+/**
+ * Get all the children of a node.
+ * @method getChild
+ */
+proto.getChildVector = function getChildVector(i) {
+	return null;
+};
 
 /**
+ * Get the path of the leaf.  Returns StringData() if there is
+ * no path (node is logical).
+ * @method path
+ */
+proto.path = function path( ){
+	return "";
+};
+
+/*
+ * Notes on structure:
+ * isLogical, isArray, and isLeaf define three partitions of all possible operators.
  *
- * Return the _tagData property
- * @method getTag
+ * isLogical can have children and its children can be arbitrary operators.
+ *
+ * isArray can have children and its children are predicates over one field.
  *
+ * isLeaf is a predicate over one field.
  */
-proto.getTag = function getTag(){
-	// File: expression.h lines: 159-158
-	return this._tagData;
+
+/**
+ * Is this node a logical operator?  All of these inherit from ListOfMatchExpression.
+ * AND, OR, NOT, NOR.
+ * @method isLogical
+ */
+proto.isLogical = function isLogical(){
+	switch( this._matchType ){
+		case "AND":
+		case "OR":
+		case "NOT":
+		case "NOR":
+			return true;
+		default:
+			return false;
+	}
+	return false;
 };
 
 /**
+ * Is this node an array operator?  Array operators have multiple clauses but operate on one
+ * field.
  *
- * Return if our _matchType needs an array
+ * ALL (AllElemMatchOp)
+ * ELEM_MATCH_VALUE, ELEM_MATCH_OBJECT, SIZE (ArrayMatchingMatchExpression)
  * @method isArray
- *
  */
 proto.isArray = function isArray(){
-	// File: expression.h lines: 111-113
 	switch (this._matchType){
-		case 'SIZE':
-		case 'ALL':
-		case 'ELEM_MATCH_VALUE':
-		case 'ELEM_MATCH_OBJECT':
+		case "SIZE":
+		case "ALL":
+		case "ELEM_MATCH_VALUE":
+		case "ELEM_MATCH_OBJECT":
 			return true;
 		default:
 			return false;
 	}
-
 	return false;
 };
 
 /**
+ * Not-internal nodes, predicates over one field.  Almost all of these inherit
+ * from LeafMatchExpression.
  *
- * Check if we do not need an array, and we are not a logical element (leaves are very emotional)
+ * Exceptions: WHERE, which doesn't have a field.
+ *             TYPE_OPERATOR, which inherits from MatchExpression due to unique
+ * 							array semantics.
  * @method isLeaf
- *
  */
 proto.isLeaf = function isLeaf(){
-	// File: expression.h lines: 124-125
 	return !this.isArray() && !this.isLogical();
 };
 
 /**
- *
- * Check if we are a vulcan
- * @method isLogical
- *
+ * XXX: document
+ * @method shallowClone
+ * @return {MatchExpression}
+ * @abstract
  */
-proto.isLogical = function isLogical(){
-	// File: expression.h lines: 100-101
-	switch( this._matchType ){
-		case 'AND':
-		case 'OR':
-		case 'NOT':
-		case 'NOR':
-			return true;
-		default:
-			return false;
-	}
-	return false;
+proto.shallowClone = function shallowClone() {
+	throw new Error("NOT IMPLEMENTED");
 };
 
 /**
- *
- * Return the _matchType property
- * @method matchType
- *
+ * XXX document
+ * @method equivalent
+ * @return {Boolean}
+ * @abstract
  */
-proto.matchType = function matchType(){
-	// File: expression.h lines: 67-66
-	return this._matchType;
+proto.equivalent = function equivalent() {
+	throw new Error("NOT IMPLEMENTED");
 };
 
+//
+// Determine if a document satisfies the tree-predicate.
+//
+
+/**
+ * @method matches
+ * @return {Boolean}
+ * @abstract
+ */
+proto.matches = function matches(doc, details/* = 0 */) {
+	throw new Error("NOT IMPLEMENTED");
+};
+
+
 /**
- *
  * Wrapper around matches function
- * @method matchesBSON
- * @param
- *
+ * @method matchesJSON
  */
-proto.matchesBSON = function matchesBSON(doc, details){
-	// File: expression.cpp lines: 42-44
+proto.matchesJSON = function matchesJSON(doc, details/* = 0 */){
 	return this.matches(doc, details);
 };
 
 /**
- *
- * Return the number of children we have
- * @method numChildren
- *
+ * Determines if the element satisfies the tree-predicate.
+ * Not valid for all expressions (e.g. $where); in those cases, returns false.
+ * @method matchesSingleElement
  */
-proto.numChildren = function numChildren( ){
-	// File: expression.h lines: 73-72
-	return 0;
+proto.matchesSingleElement = function matchesSingleElement(doc) {
+	throw new Error("NOT IMPLEMENTED");
 };
 
 /**
- *
- * Return our internal path
- * @method path
- *
+ * Return the _tagData property
+ * @method getTag
  */
-proto.path = function path( ){
-	// File: expression.h lines: 83-82
-	return '';
+proto.getTag = function getTag(){
+	return this._tagData;
 };
 
 /**
- *
  * Set the _tagData property
  * @method setTag
  * @param data
- *
  */
 proto.setTag = function setTag(data){
-	// File: expression.h lines: 158-157
 	this._tagData = data;
 };
 
+proto.resetTag = function resetTag() {
+	this.setTag(null);
+	for(var i=0; i<this.numChildren(); i++) {
+		this.getChild(i).resetTag();
+	}
+};
+
 /**
- *
  * Call the debugString method
  * @method toString
- *
  */
 proto.toString = function toString(){
-	// File: expression.cpp lines: 31-34
-	return this.debugString( 0 );
+	return this.debugString(0);
 };
+/**
+ * Debug information
+ * @method debugString
+ */
+proto.debugString = function debugString(level) {
+	throw new Error("NOT IMPLEMENTED");
+};
+/**
+ * @method _debugAddSpace
+ * @param level
+ */
+proto._debugAddSpace = function _debugAddSpace(level){
+	return new Array(level+1).join("    ");
+};
+
+
 

+ 117 - 52
lib/pipeline/matcher/MatchExpressionParser.js

@@ -1,6 +1,6 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression_parser.cpp
 var MatchExpressionParser = module.exports = function (){
 
 }, klass = MatchExpressionParser, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
@@ -32,6 +32,11 @@ var errors = require("../../Errors.js"),
 	AllElemMatchOp = require("./AllElemMatchOp.js"),
 	AtomicMatchExpression = require("./AtomicMatchExpression.js");
 
+proto.expressionParserTextCallback = require('./TextMatchExpressionParser').expressionParserTextCallbackReal;
+
+// The maximum allowed depth of a query tree. Just to guard against stack overflow.
+var MAXIMUM_TREE_DEPTH = 100;
+
 /**
  *
  * Check if the input element is an expression
@@ -39,8 +44,7 @@ var errors = require("../../Errors.js"),
  * @param element
  *
  */
-proto._isExpressionDocument = function _isExpressionDocument(element){
-	// File: expression_parser.cpp lines: 340-355
+proto._isExpressionDocument = function _isExpressionDocument(element, allowIncompleteDBRef){
 	if (!(element instanceof Object))
 		return false;
 
@@ -51,26 +55,50 @@ proto._isExpressionDocument = function _isExpressionDocument(element){
 	if (name[0] != '$')
 		return false;
 
-	if ("$ref" == name)
+	if (this._isDBRefDocument(element, allowIncompleteDBRef))
 		return false;
 
 	return true;
 };
 
+proto._isDBRefDocument = function _isDBRefDocument(obj, allowIncompleteDBRef) {
+	var hasRef, hasID, hasDB = false;
+
+	var i, fieldName, element = null,
+		keys = Object.keys(obj), length = keys.length;
+	for (i = 0; i < length; i++) {
+		fieldName = keys[i];
+		element = obj[fieldName];
+		if (!hasRef && fieldName === '$ref')
+			hasRef = true;
+		else if (!hasID && fieldName === '$id')
+			hasID = true;
+		else if (!hasDB && fieldName === '$db')
+			hasDB = true;
+	}
+
+	return allowIncompleteDBRef && (hasRef || hasID || hasDB) || (hasRef && hasID);
+};
+
 /**
  *
  * Parse the input object into individual elements
  * @method _parse
  * @param obj
- * @param topLevel
+ * @param level
  *
  */
-proto._parse = function _parse(obj, topLevel){
-	// File: expression_parser.cpp lines: 217-319
+proto._parse = function _parse(obj, level){
+	if (level > MAXIMUM_TREE_DEPTH)
+		return {code:ErrorCodes.BAD_VALUE, description:"exceeded maximum query tree depth of " +
+			MAXIMUM_TREE_DEPTH + " at " + JSON.stringify(obj)};
+
 	var rest, temp, status, element, eq, real;
 	var root = new AndMatchExpression();
 	var objkeys = Object.keys(obj);
 	var currname, currval;
+	var topLevel = level === 0;
+	level++;
 	for (var i = 0; i < objkeys.length; i++) {
 		currname = objkeys[i];
 		currval = obj[currname];
@@ -82,7 +110,7 @@ proto._parse = function _parse(obj, topLevel){
 				if (!(currval instanceof Array))
 					return {code:ErrorCodes.BAD_VALUE, description:"$or needs an array"};
 				temp = new OrMatchExpression();
-				status = this._parseTreeList(currval, temp);
+				status = this._parseTreeList(currval, temp, level);
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(temp);
@@ -91,7 +119,7 @@ proto._parse = function _parse(obj, topLevel){
 				if (!(currval instanceof Array))
 					return {code:ErrorCodes.BAD_VALUE, description:"and needs an array"};
 				temp = new AndMatchExpression();
-				status = this._parseTreeList(currval, temp);
+				status = this._parseTreeList(currval, temp, level);
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(temp);
@@ -100,7 +128,7 @@ proto._parse = function _parse(obj, topLevel){
 				if (!(currval instanceof Array))
 					return {code:ErrorCodes.BAD_VALUE, description:"and needs an array"};
 				temp = new NorMatchExpression();
-				status = this._parseTreeList(currval, temp);
+				status = this._parseTreeList(currval, temp, level);
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(temp);
@@ -123,10 +151,14 @@ proto._parse = function _parse(obj, topLevel){
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(status.result);*/
+			} else if ('text' === rest) {
+				if (typeof currval !== 'object') {
+					return {code: ErrorCodes.BAD_VALUE, description: '$text expects an object'};
+				}
+
+				return this.expressionTextCallback(currval);
 			}
-			else if ("comment" == rest) {
-				1+1;
-			}
+			else if ("comment" == rest) {}
 			else {
 				return {code:ErrorCodes.BAD_VALUE, description:"unknown top level operator: " + currname};
 			}
@@ -135,7 +167,7 @@ proto._parse = function _parse(obj, topLevel){
 		}
 
 		if (this._isExpressionDocument(currval)) {
-			status = this._parseSub(currname, currval, root);
+			status = this._parseSub(currname, currval, root, level);
 			if (status.code != ErrorCodes.OK)
 				return status;
 			continue;
@@ -172,8 +204,7 @@ proto._parse = function _parse(obj, topLevel){
  * @param element
  *
  */
-proto._parseAll = function _parseAll(name, element){
-	// File: expression_parser.cpp lines: 512-583
+proto._parseAll = function _parseAll(name, element, level){
 	var status, i;
 
 	if (!(element instanceof Array))
@@ -201,7 +232,7 @@ proto._parseAll = function _parseAll(name, element){
 				return {code:ErrorCodes.BAD_VALUE, description:"$all/$elemMatch has to be consistent"};
 			}
 
-			status = this._parseElemMatch("", hopefullyElemMatchElement.$elemMatch ); // TODO: wrong way to do this?
+			status = this._parseElemMatch("", hopefullyElemMatchElement.$elemMatch, level);
 			if (status.code != ErrorCodes.OK)
 				return status;
 			temp.add(status.result);
@@ -249,11 +280,14 @@ proto._parseAll = function _parseAll(name, element){
  *
  */
 proto._parseArrayFilterEntries = function _parseArrayFilterEntries(entries, theArray){
-	// File: expression_parser.cpp lines: 445-468
 	var status, e, r;
 	for (var i = 0; i < theArray.length; i++) {
 		e = theArray[i];
 
+		if (this._isExpressionDocument(e, false)) {
+			return {code:ErrorCodes.BAD_VALUE, description:"cannot nest $ under $in"};
+		}
+
 		if (e instanceof RegExp ) {
 			r = new RegexMatchExpression();
 			status = r.init("", e);
@@ -282,7 +316,6 @@ proto._parseArrayFilterEntries = function _parseArrayFilterEntries(entries, theA
  *
  */
 proto._parseComparison = function _parseComparison(name, cmp, element){
-	// File: expression_parser.cpp lines: 34-43
 	var temp = new ComparisonMatchExpression(cmp);
 
 	var status = temp.init(name, element);
@@ -300,17 +333,31 @@ proto._parseComparison = function _parseComparison(name, cmp, element){
  * @param element
  *
  */
-proto._parseElemMatch = function _parseElemMatch(name, element){
-	// File: expression_parser.cpp lines: 471-509
+proto._parseElemMatch = function _parseElemMatch(name, element, level){
 	var temp, status;
 	if (!(element instanceof Object))
 		return {code:ErrorCodes.BAD_VALUE, description:"$elemMatch needs an Object"};
 
-	if (this._isExpressionDocument(element)) {
+	// $elemMatch value case applies when the children all
+	// work on the field 'name'.
+	// This is the case when:
+	//     1) the argument is an expression document; and
+	//     2) expression is not a AND/NOR/OR logical operator. Children of
+	//        these logical operators are initialized with field names.
+	//     3) expression is not a WHERE operator. WHERE works on objects instead
+	//        of specific field.
+	var elt = element[Object.keys(element)[0]],
+		isElemMatchValue = this._isExpressionDocument(element, true) &&
+			elt !== '$and' &&
+			elt !== '$nor' &&
+			elt !== '$or' &&
+			elt !== '$where';
+
+	if (isElemMatchValue) {
 		// value case
 
 		var theAnd = new AndMatchExpression();
-		status = this._parseSub("", element, theAnd);
+		status = this._parseSub("", element, theAnd, level);
 		if (status.code != ErrorCodes.OK)
 			return status;
 
@@ -327,9 +374,13 @@ proto._parseElemMatch = function _parseElemMatch(name, element){
 		return {code:ErrorCodes.OK, result:temp};
 	}
 
+	// DBRef value case
+	// A DBRef document under a $elemMatch should be treated as an object case
+	// because it may contain non-DBRef fields in addition to $ref, $id and $db.
+
 	// object case
 
-	status = this._parse(element, false);
+	status = this._parse(element, level);
 	if (status.code != ErrorCodes.OK)
 		return status;
 
@@ -350,7 +401,6 @@ proto._parseElemMatch = function _parseElemMatch(name, element){
  *
  */
 proto._parseMOD = function _parseMOD(name, element){
-	// File: expression_parser.cpp lines: 360-387
 	var d,r;
 	if (!(element instanceof Array))
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, needs to be an array"};
@@ -358,13 +408,13 @@ proto._parseMOD = function _parseMOD(name, element){
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, not enough elements"};
 	if (element.length > 2)
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, too many elements"};
-	if ((typeof(element[0]) != 'number')) {
+	if (typeof element[0] !== 'number') {
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, divisor not a number"};
 	} else {
 		d = element[0];
 	}
-	if (( typeof(element[1]) != 'number') ) {
-		r = 0;
+	if (typeof element[1] !== 'number') {
+		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, remainder not a number"};
 	} else {
 		r = element[1];
 	}
@@ -385,8 +435,7 @@ proto._parseMOD = function _parseMOD(name, element){
  * @param element
  *
  */
-proto._parseNot = function _parseNot(name, element){
-	// File: expression_parser_tree.cpp lines: 55-91
+proto._parseNot = function _parseNot(name, element, level){
 	var status;
 	if (element instanceof RegExp) {
 		status = this._parseRegexElement(name, element);
@@ -406,7 +455,7 @@ proto._parseNot = function _parseNot(name, element){
 		return {code:ErrorCodes.BAD_VALUE, result:"$not cannot be empty"};
 
 	var theAnd = new AndMatchExpression();
-	status = this._parseSub(name, element, theAnd);
+	status = this._parseSub(name, element, theAnd, level);
 	if (status.code != ErrorCodes.OK)
 		return status;
 
@@ -434,7 +483,6 @@ proto._parseNot = function _parseNot(name, element){
  *
  */
 proto._parseRegexDocument = function _parseRegexDocument(name, doc){
-	// File: expression_parser.cpp lines: 402-442
 	var regex = '', regexOptions = '', e;
 
 	if(doc.$regex) {
@@ -450,7 +498,7 @@ proto._parseRegexDocument = function _parseRegexDocument(name, doc){
 			}
 			regex = (flagIndex? str : str.substr(1, flagIndex-1));
 			regexOptions = str.substr(flagIndex, str.length);
-		} else if (typeof(e) == 'string') {
+		} else if (typeof e  === 'string') {
 			regex = e;
 		} else {
 			return {code:ErrorCodes.BAD_VALUE, description:"$regex has to be a string"};
@@ -459,7 +507,7 @@ proto._parseRegexDocument = function _parseRegexDocument(name, doc){
 
 	if(doc.$options) {
 		e = doc.$options;
-		if(typeof(e) == 'string') {
+		if(typeof(e) === 'string') {
 			regexOptions = e;
 		} else {
 			return {code:ErrorCodes.BAD_VALUE, description:"$options has to be a string"};
@@ -484,7 +532,6 @@ proto._parseRegexDocument = function _parseRegexDocument(name, doc){
  *
  */
 proto._parseRegexElement = function _parseRegexElement(name, element){
-	// File: expression_parser.cpp lines: 390-399
 	if (!(element instanceof RegExp))
 		return {code:ErrorCodes.BAD_VALUE, description:"not a regex"};
 
@@ -516,18 +563,26 @@ proto._parseRegexElement = function _parseRegexElement(name, element){
  * @param root
  *
  */
-proto._parseSub = function _parseSub(name, sub, root){
-	// File: expression_parser.cpp lines: 322-337
+proto._parseSub = function _parseSub(name, sub, root, level){
 	var subkeys = Object.keys(sub),
 		currname, currval;
 
+	if (level > MAXIMUM_TREE_DEPTH) {
+		return {code:ErrorCodes.BAD_VALUE, description:"exceeded maximum query tree depth of " +
+			MAXIMUM_TREE_DEPTH + " at " + JSON.stringify(sub)};
+	}
+
+	level++;
+
+	// DERIVATION: We are not implementing Geo functions yet.
+
 	for (var i = 0; i < subkeys.length; i++) {
 		currname = subkeys[i];
 		currval = sub[currname];
 		var deep = {};
 		deep[currname] = currval;
 
-		var status = this._parseSubField(sub, root, name, deep);
+		var status = this._parseSubField(sub, root, name, deep, level);
 		if (status.code != ErrorCodes.OK)
 			return status;
 
@@ -548,20 +603,19 @@ proto._parseSub = function _parseSub(name, sub, root){
  * @param element
  *
  */
-proto._parseSubField = function _parseSubField(context, andSoFar, name, element){
-	// File: expression_parser.cpp lines: 46-214
+proto._parseSubField = function _parseSubField(context, andSoFar, name, element, level){
 	// TODO: these should move to getGtLtOp, or its replacement
 	var currname = Object.keys(element)[0];
 	var currval = element[currname];
 	if ("$eq" == currname)
 		return this._parseComparison(name, 'EQ', currval);
 
-	if ("$not" == currname) {
-		return this._parseNot(name, currval);
-	}
+	if ("$not" == currname)
+		return this._parseNot(name, currval, level);
 
 	var status, temp, temp2;
 	switch (currname) {
+		// TODO: -1 is apparently a value for mongo, but we handle strings so...
 	case '$lt':
 		return this._parseComparison(name, 'LT', currval);
 	case '$lte':
@@ -571,6 +625,11 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 	case '$gte':
 		return this._parseComparison(name, 'GTE', currval);
 	case '$ne':
+		// Just because $ne can be rewritten as the negation of an
+		// equality does not mean that $ne of a regex is allowed. See SERVER-1705.
+		if (currval instanceof RegExp) {
+			return {code:ErrorCodes.BAD_VALUE, description:"Can't have regex as arg to $ne."};
+		}
 		status = this._parseComparison(name, 'EQ', currval);
 		if (status.code != ErrorCodes.OK)
 			return status;
@@ -618,11 +677,19 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 			// matching old odd semantics
 			size = 0;
 		else if (typeof(currval) === 'number')
-			size = currval;
+			// SERVER-11952. Setting 'size' to -1 means that no documents
+			// should match this $size expression.
+			if (currval < 0)
+				size = -1;
+			else
+				size = currval;
 		else {
 			return {code:ErrorCodes.BAD_VALUE, description:"$size needs a number"};
 		}
 
+		// DERIVATION/Potential bug: Mongo checks to see if doube values are exactly equal to
+		// their int converted version. If not, size = -1.
+
 		temp = new SizeMatchExpression();
 		status = temp.init(name, size);
 		if (status.code != ErrorCodes.OK)
@@ -637,7 +704,7 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 		status = temp.init(name);
 		if (status.code != ErrorCodes.OK)
 			return status;
-		if (currval)
+		if (currval) // DERIVATION: This may have to check better than truthy? Need to look at TrueValue
 			return {code:ErrorCodes.OK, result:temp};
 		temp2 = new NotMatchExpression();
 		status = temp2.init(temp);
@@ -674,10 +741,10 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 		return this._parseRegexDocument(name, context);
 
 	case '$elemMatch':
-		return this._parseElemMatch(name, currval);
+		return this._parseElemMatch(name, currval, level);
 
 	case '$all':
-		return this._parseAll(name, currval);
+		return this._parseAll(name, currval, level);
 
 	case '$geoWithin':
 	case '$geoIntersects':
@@ -701,8 +768,7 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
  * @param out
  *
  */
-proto._parseTreeList = function _parseTreeList(arr, out){
-	// File: expression_parser_tree.cpp lines: 33-52
+proto._parseTreeList = function _parseTreeList(arr, out, level){
 	if (arr.length === 0)
 		return {code:ErrorCodes.BAD_VALUE, description:"$and/$or/$nor must be a nonempty array"};
 
@@ -713,7 +779,7 @@ proto._parseTreeList = function _parseTreeList(arr, out){
 		if (!(element instanceof Object))
 			return {code:ErrorCodes.BAD_VALUE, description:"$or/$and/$nor entries need to be full objects"};
 
-		status = this._parse(element, false);
+		status = this._parse(element, level);
 		if (status.code != ErrorCodes.OK)
 			return status;
 
@@ -730,6 +796,5 @@ proto._parseTreeList = function _parseTreeList(arr, out){
  *
  */
 proto.parse = function parse(obj){
-	// File: expression_parser.h lines: 40-41
-	return this._parse(obj, true);
+	return this._parse(obj, 0);
 };

+ 14 - 279
lib/pipeline/matcher/Matcher2.js

@@ -1,8 +1,13 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * Matcher2 is a simple wrapper around a JSONObj and the MatchExpression created from it.
+ * @class Matcher2
+ * @namespace mungedb-aggregate.pipeline.matcher2
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var Matcher2 = module.exports = function Matcher2(pattern, nested){
-	// File: matcher.cpp lines: 83-92
 	this._pattern = pattern;
 	this.parser = new MatchExpressionParser();
 	var result = this.parser.parse(pattern);
@@ -14,243 +19,12 @@ var Matcher2 = module.exports = function Matcher2(pattern, nested){
 // DEPENDENCIES
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
-	MatchExpression = require("./MatchExpression.js"),
-	MatchExpressionParser = require("./MatchExpressionParser.js"),
-	FalseMatchExpression = require("./FalseMatchExpression.js"),
-	ComparisonMatchExpression = require("./ComparisonMatchExpression.js"),
-	InMatchExpression = require("./InMatchExpression.js"),
-	AndMatchExpression = require("./AndMatchExpression.js"),
-	OrMatchExpression = require("./OrMatchExpression.js"),
-	IndexKeyMatchableDocument = require('./IndexKeyMatchableDocument.js'),
-	ListOfMatchExpression = require('./ListOfMatchExpression.js'),
-	LeafMatchExpression = require("./LeafMatchExpression.js");
+	MatchExpressionParser = require("./MatchExpressionParser.js");
 
-// File: matcher.h lines: 82-82
+//PROTOTYPE MEMBERS
 proto._expression = undefined;
-
-// File: matcher.h lines: 80-80
-proto._indexKey = undefined;
-
-// File: matcher.h lines: 79-79
 proto._pattern = undefined;
 
-// File: matcher.h lines: 84-84
-proto._spliceInfo = undefined;
-
-/**
- *
- * Figure out where our index is
- * @method _spliceForIndex
- * @param keys
- * @param full
- * @param spliceInfo
- *
- */
-proto._spliceForIndex = function _spliceForIndex(keys, full, spliceInfo){
-	// File: matcher.cpp lines: 236-380
-	var dup, i, obj, lme;
-	switch (full) {
-		case MatchExpression.ALWAYS_FALSE:
-			return new FalseMatchExpression();
-
-		case MatchExpression.GEO_NEAR:
-		case MatchExpression.NOT:
-		case MatchExpression.NOR:
-			// maybe?
-			return null;
-
-		case MatchExpression.OR:
-
-		case MatchExpression.AND:
-			dup = new ListOfMatchExpression();
-			for (i = 0; i < full.numChildren(); i++) {
-				var sub = this._spliceForIndex(keys, full.getChild(i), spliceInfo);
-				if (!sub)
-					continue;
-				if (!dup.get()) {
-					if (full.matchType() == MatchExpression.AND)
-						dup.reset(new AndMatchExpression());
-					else
-						dup.reset(new OrMatchExpression());
-				}
-				dup.add(sub);
-			}
-			if (dup.get()) {
-				if (full.matchType() == MatchExpression.OR &&  dup.numChildren() != full.numChildren()) {
-					// TODO: I think this should actuall get a list of all the fields
-					// and make sure that's the same
-					// with an $or, have to make sure its all or nothing
-					return null;
-				}
-				return dup.release();
-			}
-			return null;
-
-		case MatchExpression.EQ:
-			var cmp = new ComparisonMatchExpression(full);
-
-			if (cmp.getRHS().type() == Array) {
-				// need to convert array to an $in
-
-				if (!keys.count(cmp.path().toString()))
-					return null;
-
-				var newIn = new InMatchExpression();
-				newIn.init(cmp.path());
-
-				if (newIn.getArrayFilterEntries().addEquality(cmp.getRHS()).isOK())
-					return null;
-
-				if (cmp.getRHS().Obj().isEmpty())
-					newIn.getArrayFilterEntries().addEquality(undefined);
-
-				obj = cmp.getRHS().Obj();
-				for(i in obj) {
-					var s = newIn.getArrayFilterEntries().addEquality( obj[i].next() );
-					if (s.code != ErrorCodes.OK)
-						return null;
-				}
-
-				return newIn.release();
-			}
-			else if (cmp.getRHS().type() === null) {
-				//spliceInfo.hasNullEquality = true;
-				return null;
-			}
-			break;
-
-		case MatchExpression.LTE:
-		case MatchExpression.LT:
-		case MatchExpression.GT:
-		case MatchExpression.GTE:
-			cmp = new ComparisonMatchExpression(full);
-
-			if ( cmp.getRHS().type() === null) {
-				// null and indexes don't play nice
-				//spliceInfo.hasNullEquality = true;
-				return null;
-			}
-			break;
-
-		case MatchExpression.REGEX:
-		case MatchExpression.MOD:
-			lme = new LeafMatchExpression(full);
-			if (!keys.count(lme.path().toString()))
-				return null;
-			return lme.shallowClone();
-
-		case MatchExpression.MATCH_IN:
-			lme = new LeafMatchExpression(full);
-			if (!keys.count(lme.path().toString()))
-				return null;
-			var cloned = new InMatchExpression(lme.shallowClone());
-			if (cloned.getArrayFilterEntries().hasEmptyArray())
-				cloned.getArrayFilterEntries().addEquality(undefined);
-
-			// since { $in : [[1]] } matches [1], need to explode
-			for (i = cloned.getArrayFilterEntries().equalities().begin(); i != cloned.getArrayFilterEntries().equalities().end(); ++i) {
-				var x = cloned[i];
-				if (x.type() == Array) {
-					for(var j in x) {
-						cloned.getArrayFilterEntries().addEquality(x[j]);
-					}
-				}
-			}
-
-			return cloned;
-
-		case MatchExpression.ALL:
-			// TODO: convert to $in
-			return null;
-
-		case MatchExpression.ELEM_MATCH_OBJECT:
-		case MatchExpression.ELEM_MATCH_VALUE:
-			// future
-			return null;
-
-		case MatchExpression.GEO:
-		case MatchExpression.SIZE:
-		case MatchExpression.EXISTS:
-		case MatchExpression.NIN:
-		case MatchExpression.TYPE_OPERATOR:
-		case MatchExpression.ATOMIC:
-		case MatchExpression.WHERE:
-			// no go
-			return null;
-	}
-
-	return null;
-};
-
-/**
- *
- * return if our _expression property is atomic or not
- * @method atomic
- *
- */
-proto.atomic = function atomic(){
-	// File: matcher.cpp lines: 120-133
-	if (!this._expression)
-		return false;
-
-	if (this._expression.matchType() == MatchExpression.ATOMIC)
-		return true;
-
-	// we only go down one level
-	for (var i = 0; i < this._expression.numChildren(); i++) {
-		if (this._expression.getChild(i).matchType() == MatchExpression.ATOMIC)
-			return true;
-	}
-
-	return false;
-};
-
-/**
- *
- * Return the _pattern property
- * @method getQuery
- *
- */
-proto.getQuery = function getQuery(){
-	// File: matcher.h lines: 65-64
-	return this._pattern;
-};
-
-/**
- *
- * Check if we exist
- * @method hasExistsFalse
- *
- */
-proto.hasExistsFalse = function hasExistsFalse(){
-	// File: matcher.cpp lines: 172-180
-	if (this._spliceInfo.hasNullEquality) {
-		// { a : NULL } is very dangerous as it may not got indexed in some cases
-		// so we just totally ignore
-		return true;
-	}
-
-	return this._isExistsFalse(this._expression.get(), false, this._expression.matchType() == MatchExpression.AND ? -1 : 0);
-};
-
-/**
- *
- * Find if we have a matching key inside us
- * @method keyMatch
- * @param docMatcher
- *
- */
-proto.keyMatch = function keyMatch(docMatcher){
-	// File: matcher.cpp lines: 199-206
-	if (!this._expression)
-		return docMatcher._expression.get() === null;
-	if (!docMatcher._expression)
-		return false;
-	if (this._spliceInfo.hasNullEquality)
-		return false;
-	return this._expression.equivalent(docMatcher._expression.get());
-};
-
 /**
  *
  * matches checks the input doc against the internal element path to see if it is a match
@@ -260,58 +34,20 @@ proto.keyMatch = function keyMatch(docMatcher){
  *
  */
 proto.matches = function matches(doc, details){
-	// File: matcher.cpp lines: 105-116
 	if (!this._expression)
 		return true;
 
-	if (this._indexKey == {})
-		return this._expression.matchesBSON(doc, details);
-
-	if ((doc != {}) && (Object.keys(doc)[0]))
-		return this._expression.matchesBSON(doc, details);
-
-	var mydoc = new IndexKeyMatchableDocument(this._indexKey, doc);
-	return this._expression.matches(mydoc, details);
+	return this._expression.matchesJSON(doc, details);
 };
 
 /**
  *
- * Check if we are a simple match
- * @method singleSimpleCriterion
- *
- */
-proto.singleSimpleCriterion = function singleSimpleCriterion(){
-	// File: matcher.cpp lines: 184-196
-	if (!this._expression)
-		return false;
-
-	if (this._expression.matchType() == MatchExpression.EQ)
-		return true;
-
-	if (this._expression.matchType() == MatchExpression.AND && this._expression.numChildren() == 1 && this._expression.getChild(0).matchType() == MatchExpression.EQ)
-		return true;
-
-	return false;
-};
-
-/**
- *
- * Wrapper around _spliceForIndex
- * @method spliceForIndex
- * @param key
- * @param full
- * @param spliceInfo
+ * Return the _pattern property
+ * @method getQuery
  *
  */
-proto.spliceForIndex = function spliceForIndex(key, full, spliceInfo){
-	// File: matcher.cpp lines: 209-217
-	var keys = [],
-		e, i;
-	for (i in key) {
-		e = key[i];
-		keys.insert(e.fieldName());
-	}
-	return this._spliceForIndex(keys, full, spliceInfo);
+proto.getQuery = function getQuery(){
+	return this._pattern;
 };
 
 /**
@@ -321,6 +57,5 @@ proto.spliceForIndex = function spliceForIndex(key, full, spliceInfo){
  *
  */
 proto.toString = function toString(){
-	// File: matcher.h lines: 66-65
 	return this._pattern.toString();
 };

+ 10 - 16
lib/pipeline/matcher/ModMatchExpression.js

@@ -1,17 +1,16 @@
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression');
+var LeafMatchExpression = require('./LeafMatchExpression'),
+	ErrorCodes = require("../../Errors.js").ErrorCodes;
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression_leaf.h
 var ModMatchExpression = module.exports = function ModMatchExpression(){
 	base.call(this);
 	this._matchType = 'MOD';
 }, klass = ModMatchExpression, base =  LeafMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// File: expression_leaf.h lines: 210-210
 proto._divisor = undefined;
 
-// File: expression_leaf.h lines: 211-211
 proto._remainder = undefined;
 
 /**
@@ -22,7 +21,6 @@ proto._remainder = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 253-261
 	return this._debugAddSpace( level ) + this.path() + " mod " + this._divisor + " % x == " + this._remainder + (this.getTag() ? " " + this.getTag().debugString() : '') + "\n";
 };
 
@@ -34,10 +32,9 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 264-272
-	if(other._matchType != 'MOD')
+	if(other._matchType !== this._matchType)
 		return false;
-	return this.path() == other.path() && this._divisor == other._divisor && this._remainder == other._remainder;
+	return this.path() === other.path() && this._divisor === other._divisor && this._remainder === other._remainder;
 };
 
 /**
@@ -47,7 +44,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.getDivisor = function getDivisor(){
-	// File: expression_leaf.h lines: 206-205
 	return this._divisor;
 };
 
@@ -58,7 +54,6 @@ proto.getDivisor = function getDivisor(){
  *
  */
 proto.getRemainder = function getRemainder( /*  */ ){
-	// File: expression_leaf.h lines: 207-206
 	return this._remainder;
 };
 
@@ -71,9 +66,8 @@ proto.getRemainder = function getRemainder( /*  */ ){
  *
  */
 proto.init = function init(path,divisor,remainder) {
-	// File: expression_leaf.cpp lines: 239-244
 	if (divisor === 0 ){
-		return {'code':'BAD_VALUE', 'desc':'Divisor cannot be 0'};
+		return {'code':ErrorCodes.BAD_VALUE, 'desc':'Divisor cannot be 0'};
 	}
 
 	this._divisor = divisor;
@@ -89,12 +83,11 @@ proto.init = function init(path,divisor,remainder) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_leaf.cpp lines: 247-250
-	if(typeof(e) != 'number') {
+	if(typeof(e) !== 'number') {
 		return false;
 	}
 
-	return (e % this._divisor) == this._remainder;
+	return (e % this._divisor) === this._remainder;
 };
 
 /**
@@ -104,9 +97,10 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.h lines: 194-197
 	var e = new ModMatchExpression();
 	e.init(this.path(),this._divisor, this._remainder);
+	if (this.getTag())
+		e.setTag(this.getTag().clone());
 	return e;
 };
 

+ 5 - 0
lib/pipeline/matcher/NorMatchExpression.js

@@ -73,6 +73,11 @@ proto.shallowClone = function shallowClone(){
 	for (var i = 0; i < this.numChildren(); i++) {
 		e.add(this.getChild(i).shallowClone());
 	}
+
+	if (this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
+
 	return e;
 };
 

+ 9 - 16
lib/pipeline/matcher/NotMatchExpression.js

@@ -1,13 +1,10 @@
 "use strict";
 var MatchExpression = require('./MatchExpression');
-
-	// Autogenerated by cport.py on 2013-09-17 14:37
 var NotMatchExpression = module.exports = function NotMatchExpression(){
 	base.call(this);
 	this._matchType = 'NOT';
 }, klass = NotMatchExpression, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-	// File: expression_tree.h lines: 152-152
 proto._exp = undefined;
 
 /**
@@ -18,7 +15,6 @@ proto._exp = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_tree.cpp lines: 146-149
 	return this._debugAddSpace( level ) + "$not\n" + this._exp._debugString( level + 1 );
 };
 
@@ -30,19 +26,17 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_tree.cpp lines: 152-156
 	return other._matchType == 'NOT' && this._exp.equivalent(other.getChild(0));
 };
 
 /**
  *
- * Return the _exp property
- * @method getChild
+ * Return the reset child
+ * @method resetChild
  *
  */
-proto.getChild = function getChild() {
-	// File: expression_tree.h lines: 148-147
-	return this._exp;
+proto.resetChild = function resetChild(newChild) {
+	this._exp.reset(newChild);
 };
 
 /**
@@ -53,7 +47,6 @@ proto.getChild = function getChild() {
  *
  */
 proto.init = function init(exp) {
-	// File: expression_tree.h lines: 123-125
 	this._exp = exp;
 	return {'code':'OK'};
 };
@@ -66,9 +59,8 @@ proto.init = function init(exp) {
  * @param details
  *
  */
-proto.matches = function matches(doc,details) {
-	// File: expression_tree.h lines: 135-136
-	return ! this._exp.matches( doc,null );
+proto.matches = function matches(doc, details) {
+	return ! this._exp.matches(doc, null);
 };
 
 /**
@@ -79,7 +71,6 @@ proto.matches = function matches(doc,details) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_tree.h lines: 139-140
 	return ! this._exp.matchesSingleElement( e );
 };
 
@@ -91,7 +82,6 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.numChildren = function numChildren(){
-	// File: expression_tree.h lines: 147-146
 	return 1;
 };
 
@@ -105,6 +95,9 @@ proto.shallowClone = function shallowClone(){
 	// File: expression_tree.h lines: 128-132
 	var e = new NotMatchExpression();
 	e.init(this._exp.shallowClone());
+	if ( this.getTag() ) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
 

+ 6 - 0
lib/pipeline/matcher/OrMatchExpression.js

@@ -64,9 +64,15 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 proto.shallowClone = function shallowClone(){
 	// File: expression_tree.h lines: 86-91
 	var clone = new OrMatchExpression();
+
 	for (var i = 0; i < this.numChildren(); i++) {
 		clone.add(this.getChild(i).shallowClone());
 	}
+
+	if (this.getTag()) {
+		clone.setTag(this.getTag().clone());
+	}
+
 	return clone;
 };
 

+ 14 - 17
lib/pipeline/matcher/SizeMatchExpression.js

@@ -1,14 +1,10 @@
 "use strict";
 var ArrayMatchingMatchExpression = require('./ArrayMatchingMatchExpression');
 
-
-// Autogenerated by cport.py on 2013-09-17 14:37
 var SizeMatchExpression = module.exports = function SizeMatchExpression(){
-	base.call(this);
-	this._matchType = 'SIZE';
+	base.call(this, 'SIZE');
 }, klass = SizeMatchExpression, base =  ArrayMatchingMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: expression_array.h lines: 131-131
 proto._size = undefined;
 
 /**
@@ -19,8 +15,13 @@ proto._size = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_array.cpp lines: 259-261
-	return this._debugAddSpace( level ) + this.path() + " $size : " + this._size.toString() + "\n";
+	var debug = this._debugAddSpace( level ) + this.path() + " $size : " + this._size.toString() + "\n";
+	
+	var td = this.tagData();
+	if (td !== null){
+		debug += " " + td.debugString();
+	}
+	return debug;
 };
 
 /**
@@ -31,11 +32,10 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_array.cpp lines: 264-269
-	if(other._matchType != 'SIZE') {
+	if(other.matchType() !== this.matchType()) {
 		return false;
 	}
-	return this._size == other._size && this._path == other._path;
+	return this._size === other._size && this.path() === other.path();
 };
 
 /**
@@ -45,7 +45,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.getData = function getData(){
-	// File: expression_array.h lines: 128-127
 	return this._size;
 };
 
@@ -58,9 +57,6 @@ proto.getData = function getData(){
  *
  */
 proto.init = function init(path,size) {
-	// File: expression_array.cpp lines: 248-250
-	if(size === null)
-		return {code:'BAD_VALUE', 'description':'Cannot assign null to size'};
 	this._size = size;
 	return this.initPath(path);
 };
@@ -74,11 +70,10 @@ proto.init = function init(path,size) {
  *
  */
 proto.matchesArray = function matchesArray(anArray, details) {
-	// File: expression_array.cpp lines: 253-256
 	if(this._size < 0) {
 		return false;
 	}
-	return anArray.length == this._size;
+	return anArray.length === this._size;
 };
 
 /**
@@ -91,6 +86,8 @@ proto.shallowClone = function shallowClone(){
 	// File: expression_array.h lines: 116-119
 	var e = new SizeMatchExpression();
 	e.init(this.path(),this._size);
+	if ( this.getTag() ) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 112 - 0
lib/pipeline/matcher/TextMatchExpression.js

@@ -0,0 +1,112 @@
+"use strict";
+
+var LeafMatchExpression = require('./LeafMatchExpression.js');
+
+var TextMatchExpression = module.exports = function TextMatchExpression() {
+	base.call(this, 'TEXT');
+}, klass = TextMatchExpression, base = LeafMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+
+/**
+ *
+ * Initializes the class object.
+ *
+ * @param query
+ * @param language
+ * @returns {*}
+ */
+proto.init = function init(query, language) {
+	this._query = query;
+	this._language = language;
+
+	return this.initPath('_fts');
+};
+
+/**
+ * Gets the query.
+ *
+ * @returns {*}
+ */
+proto.getQuery = function getQuery() {
+	return this._query;
+};
+
+/**
+ * Gets the language.
+ *
+ * @returns {*}
+ */
+proto.getLanguage = function getLanguage() {
+	return this._language;
+};
+
+/**
+ * Check if the input element matches.
+ *
+ * @param e
+ * @returns {boolean}
+ */
+proto.matchesSingleElement = function matchesSingleElement(e) {
+	return true;
+};
+
+/**
+ * Debug a string.
+ *
+ * @param level
+ * @returns {string}
+ */
+proto.debugString = function debugString(level) {
+	var rtn = this._debugAddSpace(level);
+
+	rtn += 'TEXT : query=' + ', language=' + this._language + ', tag=';
+
+	var tagData = this.getTag();
+
+	if (tagData !== null) {
+		tagData.debugString(level);
+	} else {
+		rtn += 'NULL';
+	}
+
+	return rtn + '\n';
+};
+
+/**
+ * Verifies the equivalency of two operands.
+ *
+ * @param other
+ * @returns {boolean}
+ */
+proto.equivalent = function equivalent(other) {
+	if (this.matchType() !== other.matchType()) {
+		return false;
+	}
+
+	if (other.getQuery() !== this._query) {
+		return false;
+	}
+
+	if (other.getLanguage() !== this._language) {
+		return false;
+	}
+
+	return true;
+};
+
+/**
+ * Clone this instance into a new one.
+ *
+ * @returns {TextMatchExpression}
+ */
+proto.shallowClone = function shallowClone() {
+	var next = new TextMatchExpression();
+
+	next.init(this._query, this._language);
+
+	if (this.getTag()) {
+		next.getTag(this.getTag().clone());
+	}
+
+	return next;
+};
+

+ 25 - 0
lib/pipeline/matcher/TextMatchExpressionParser.js

@@ -0,0 +1,25 @@
+/**
+ * Expression parser's text callback function.
+ *
+ * @param queryObj
+ * @returns {*}
+ * @private
+ */
+var _expressionParserTextCallbackReal = function _expressionParserTextCallbackReal(queryObj) {
+	if (queryObj.$search._type !== 'string') {
+		return {code: ErrorCodes.BadValue, description: '$search needs a String'};
+	}
+
+	var e = new TextMatchExpression(),
+		s = e.init(query, language);
+
+	if (s.code !== 'OK') {
+		return s;
+	}
+
+	return e.release();
+};
+
+module.exports = {
+	expressionParserTextCallbackReal: _expressionParserTextCallbackReal
+};

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

+ 0 - 72
test/lib/pipeline/expressions/CondExpression.js

@@ -1,72 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"CondExpression": {
-
-		"constructor()": {
-
-			"should throw Error when constructing without args": function testConstructor(){
-				assert.throws(function(){
-					new CondExpression();
-				});
-			},
-
-			"should throw Error when constructing with 1 arg": function testConstructor1(){
-				assert.throws(function(){
-					new CondExpression({if:true === true});
-				});
-			},
-			"should throw Error when constructing with 2 args": function testConstructor2(){
-				assert.throws(function(){
-					new CondExpression(true === true,1);
-				});
-			},
-			"should now throw Error when constructing with 3 args": function testConstructor3(){
-				assert.doesNotThrow(function(){
-					//new CondExpression({$cond:[{"if":"true === true"},{"then":"1"},{"else":"0"}]});
-					new CondExpression({$cond:[ true === true, 1, 0 ]});
-				});
-			},
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $cond": function testOpName(){
-				assert.equal(new CondExpression().getOpName(), "$cond");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ true === true, 1, 0 ]}).evaluateInternal({}), 1);
-			},
-
-			"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ false === true, 1, 0 ]}).evaluateInternal({}), 0);
-			}, 
-
-			"should evaluate boolean expression as true, then return 1; [ (true === true) && true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ (true === true) && true , 1, 0 ]}).evaluateInternal({}), 1);
-			},
-
-			"should evaluate boolean expression as false, then return 0; [ (false === true) && true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ (false === true) && true, 1, 0 ]}).evaluateInternal({}), 0);
-			},
-
-			"should evaluate complex boolean expression as true, then return 1; [ ( 1 > 0 ) && (( 'a' == 'b' ) || ( 3 <= 5 )), 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ ( 1 > 0 ) && (( 'a' == 'b' ) || ( 3 <= 5 )), 1, 0 ]}).evaluate({}), 1);
-			},
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 93 - 104
test/lib/pipeline/expressions/CondExpression_test.js

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

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

+ 0 - 58
test/lib/pipeline/expressions/IfNullExpression.js

@@ -1,58 +0,0 @@
-"use strict";
-var assert = require("assert"),
-		IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"IfNullExpression": {
-
-				"constructor()": {
-
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new IfNullExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $ifNull": function testOpName() {
-								assert.equal(new IfNullExpression().getOpName(), "$ifNull");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"should return the left hand side if the left hand side is not null or undefined": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										a: 1,
-										b: 2
-								}), 1);
-						},
-						"should return the right hand side if the left hand side is null or undefined": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										a: null,
-										b: 2
-								}), 2);
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										b: 2
-								}), 2);
-						}
-				}
-
-		}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 40 - 45
test/lib/pipeline/expressions/IfNullExpression_test.js

@@ -6,59 +6,54 @@ var assert = require("assert"),
 	Variables = require("../../../../lib/pipeline/expressions/Variables"),
 	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.IfNullExpression = {
 
-	"IfNullExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function () {
+				new IfNullExpression();
+			});
+		},
 
-			"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);
-				});
-			}
+		"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");
-			}
+	"#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);
-			}
-		}
-	}
-};
+	},
+
+	"#evaluate()": {
+
+		beforeEach: function () {
+			this.vps = new VariablesParseState(new VariablesIdGenerator());
+			this.parsed = 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({a: 1, b: 2}), 1);
+		},
 
-if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should return the right hand side if the left hand side is null": function() {
+			assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
+		},
+
+		"should return the right hand side if the left hand side is undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
+		},
+
+	},
+
+};

+ 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 - 45
test/lib/pipeline/expressions/NotExpression.js

@@ -1,45 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"NotExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new NotExpression();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $not": function testOpName(){
-				assert.equal(new NotExpression().getOpName(), "$not");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			"should return false for a true input; false for true": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$not:true}).evaluateInternal({}), false);
-			},
-
-			"should return true for a false input; true for false": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$not:false}).evaluateInternal({}), true);
-			}
-
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
+	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.NotExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function(){
+				new NotExpression();
+			});
+		},
+
+		"should throw when constructing with args": function() {
+			assert.throws(function(){
+				new NotExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $not": function() {
+			assert.equal(new NotExpression().getOpName(), "$not");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should return false for a true input; false for true": function() {
+			assert.strictEqual(Expression.parseOperand({$not:true}, {}).evaluateInternal({}), false);
+		},
+
+		"should return true for a false input; true for false": function() {
+			assert.strictEqual(Expression.parseOperand({$not:false}, {}).evaluateInternal({}), true);
+		},
+
+	},
+
+};

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

+ 0 - 131
test/lib/pipeline/matcher/AllElemMatchOp.js

@@ -1,131 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	EqualityMatchExpression = require("../../../../lib/pipeline/matcher/EqualityMatchExpression.js"),
-	ElemMatchObjectMatchExpression = require("../../../../lib/pipeline/matcher/ElemMatchObjectMatchExpression.js"),
-	ElemMatchValueMatchExpression = require("../../../../lib/pipeline/matcher/ElemMatchValueMatchExpression.js"),
-	AndMatchExpression = require("../../../../lib/pipeline/matcher/AndMatchExpression.js"),
-	LTMatchExpression = require("../../../../lib/pipeline/matcher/LTMatchExpression.js"),
-	GTMatchExpression = require("../../../../lib/pipeline/matcher/GTMatchExpression.js"),
-	AllElemMatchOp = require("../../../../lib/pipeline/matcher/AllElemMatchOp.js");
-
-
-module.exports = {
-	"AllElemMatchOp": {
-		"Should match an element": function() {
-			var baseOperanda1={"a":1},
-				eqa1 = new EqualityMatchExpression();
-
-			assert.strictEqual(eqa1.init("a", baseOperanda1.a).code, 'OK');
-
-			var baseOperandb1={"b":1},
-				eqb1 = new EqualityMatchExpression(),
-				and1 = new AndMatchExpression(),
-				elemMatch1 = new ElemMatchObjectMatchExpression();
-
-			assert.strictEqual(eqb1.init("b", baseOperandb1.b).code, 'OK');
-
-			and1.add(eqa1);
-			and1.add(eqb1);
-			// and1 = { a : 1, b : 1 }
-
-			elemMatch1.init("x", and1);
-			// elemMatch1 = { x : { $elemMatch : { a : 1, b : 1 } } }
-
-			var baseOperanda2={"a":2},
-				eqa2 = new EqualityMatchExpression();
-
-			assert.strictEqual(eqa2.init("a", baseOperanda2.a).code, 'OK');
-
-			var baseOperandb2={"b":2},
-				eqb2 = new EqualityMatchExpression(),
-				and2 = new AndMatchExpression(),
-				elemMatch2 = new ElemMatchObjectMatchExpression(),
-				op = new AllElemMatchOp();
-
-			assert.strictEqual(eqb2.init("b", baseOperandb2.b).code, 'OK');
-
-			and2.add(eqa2);
-			and2.add(eqb2);
-
-			elemMatch2.init("x", and2);
-			// elemMatch2 = { x : { $elemMatch : { a : 2, b : 2 } } }
-
-			op.init("");
-			op.add(elemMatch1);
-			op.add(elemMatch2);
-
-			var nonArray={"x":4},
-				emptyArray={"x":[]},
-				nonObjArray={"x":[4]},
-				singleObjMatch={"x":[{"a":1, "b":1}]},
-				otherObjMatch={"x":[{"a":2, "b":2}]},
-				bothObjMatch={"x":[{"a":1, "b":1}, {"a":2, "b":2}]},
-				noObjMatch={"x":[{"a":1, "b":2}, {"a":2, "b":1}]};
-
-			assert.ok(!op.matchesSingleElement(nonArray.x));
-			assert.ok(!op.matchesSingleElement(emptyArray.x));
-			assert.ok(!op.matchesSingleElement(nonObjArray.x));
-			assert.ok(!op.matchesSingleElement(singleObjMatch.x));
-			assert.ok(!op.matchesSingleElement(otherObjMatch.x));
-			assert.ok(op.matchesSingleElement(bothObjMatch.x));
-			assert.ok(!op.matchesSingleElement(noObjMatch.x));
-		},
-
-		"Should match things": function() {
-			var baseOperandgt1={"$gt":1},
-				gt1 = new GTMatchExpression();
-
-			assert.strictEqual(gt1.init("", baseOperandgt1.$gt).code, 'OK');
-
-			var baseOperandlt1={"$lt":10},
-				lt1 = new LTMatchExpression(),
-				elemMatch1 = new ElemMatchValueMatchExpression();
-
-			assert.strictEqual(lt1.init("", baseOperandlt1.$lt).code, 'OK');
-
-			elemMatch1.init("x");
-			elemMatch1.add(gt1);
-			elemMatch1.add(lt1);
-
-			var baseOperandgt2={"$gt":101},
-				gt2 = new GTMatchExpression();
-
-			assert.strictEqual(gt2.init("", baseOperandgt2.$gt).code, 'OK');
-
-			var baseOperandlt2={"$lt":110},
-				lt2 = new LTMatchExpression(),
-				elemMatch2 = new ElemMatchValueMatchExpression(),
-				op = new AllElemMatchOp();
-
-			assert.strictEqual(lt2.init("", baseOperandlt2.$lt).code, 'OK');
-
-			elemMatch2.init("x");
-			elemMatch2.add(gt2);
-			elemMatch2.add(lt2);
-
-			op.init("x");
-			op.add(elemMatch1);
-			op.add(elemMatch2);
-
-
-			var nonArray={"x":4},
-				emptyArray={"x":[]},
-				nonNumberArray={"x":["q"]},
-				singleMatch={"x":[5]},
-				otherMatch={"x":[105]},
-				bothMatch={"x":[5,105]},
-				neitherMatch={"x":[0,200]};
-
-			assert.ok(!op.matches(nonArray, null));
-			assert.ok(!op.matches(emptyArray, null));
-			assert.ok(!op.matches(nonNumberArray, null));
-			assert.ok(!op.matches(singleMatch, null));
-			assert.ok(!op.matches(otherMatch, null));
-			assert.ok(op.matches(bothMatch, null));
-			assert.ok(!op.matches(neitherMatch, null));
-		},
-	}
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
-

+ 128 - 0
test/lib/pipeline/matcher/AllElemMatchOp_test.js

@@ -0,0 +1,128 @@
+"use strict";
+var assert = require("assert"),
+	EqualityMatchExpression = require("../../../../lib/pipeline/matcher/EqualityMatchExpression.js"),
+	ElemMatchObjectMatchExpression = require("../../../../lib/pipeline/matcher/ElemMatchObjectMatchExpression.js"),
+	ElemMatchValueMatchExpression = require("../../../../lib/pipeline/matcher/ElemMatchValueMatchExpression.js"),
+	AndMatchExpression = require("../../../../lib/pipeline/matcher/AndMatchExpression.js"),
+	LTMatchExpression = require("../../../../lib/pipeline/matcher/LTMatchExpression.js"),
+	GTMatchExpression = require("../../../../lib/pipeline/matcher/GTMatchExpression.js"),
+	AllElemMatchOp = require("../../../../lib/pipeline/matcher/AllElemMatchOp.js");
+
+// 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.AllElemMatchOp = {
+	"Should match an element": function() {
+		var baseOperanda1={"a":1},
+			eqa1 = new EqualityMatchExpression();
+
+		assert.strictEqual(eqa1.init("a", baseOperanda1.a).code, 'OK');
+
+		var baseOperandb1={"b":1},
+			eqb1 = new EqualityMatchExpression(),
+			and1 = new AndMatchExpression(),
+			elemMatch1 = new ElemMatchObjectMatchExpression();
+
+		assert.strictEqual(eqb1.init("b", baseOperandb1.b).code, 'OK');
+
+		and1.add(eqa1);
+		and1.add(eqb1);
+		// and1 = { a : 1, b : 1 }
+
+		elemMatch1.init("x", and1);
+		// elemMatch1 = { x : { $elemMatch : { a : 1, b : 1 } } }
+
+		var baseOperanda2={"a":2},
+			eqa2 = new EqualityMatchExpression();
+
+		assert.strictEqual(eqa2.init("a", baseOperanda2.a).code, 'OK');
+
+		var baseOperandb2={"b":2},
+			eqb2 = new EqualityMatchExpression(),
+			and2 = new AndMatchExpression(),
+			elemMatch2 = new ElemMatchObjectMatchExpression(),
+			op = new AllElemMatchOp();
+
+		assert.strictEqual(eqb2.init("b", baseOperandb2.b).code, 'OK');
+
+		and2.add(eqa2);
+		and2.add(eqb2);
+
+		elemMatch2.init("x", and2);
+		// elemMatch2 = { x : { $elemMatch : { a : 2, b : 2 } } }
+
+		op.init("");
+		op.add(elemMatch1);
+		op.add(elemMatch2);
+
+		var nonArray={"x":4},
+			emptyArray={"x":[]},
+			nonObjArray={"x":[4]},
+			singleObjMatch={"x":[{"a":1, "b":1}]},
+			otherObjMatch={"x":[{"a":2, "b":2}]},
+			bothObjMatch={"x":[{"a":1, "b":1}, {"a":2, "b":2}]},
+			noObjMatch={"x":[{"a":1, "b":2}, {"a":2, "b":1}]};
+
+		assert.ok(!op.matchesSingleElement(nonArray.x));
+		assert.ok(!op.matchesSingleElement(emptyArray.x));
+		assert.ok(!op.matchesSingleElement(nonObjArray.x));
+		assert.ok(!op.matchesSingleElement(singleObjMatch.x));
+		assert.ok(!op.matchesSingleElement(otherObjMatch.x));
+		assert.ok(op.matchesSingleElement(bothObjMatch.x));
+		assert.ok(!op.matchesSingleElement(noObjMatch.x));
+	},
+
+	"Should match things": function() {
+		var baseOperandgt1={"$gt":1},
+			gt1 = new GTMatchExpression();
+
+		assert.strictEqual(gt1.init("", baseOperandgt1.$gt).code, 'OK');
+
+		var baseOperandlt1={"$lt":10},
+			lt1 = new LTMatchExpression(),
+			elemMatch1 = new ElemMatchValueMatchExpression();
+
+		assert.strictEqual(lt1.init("", baseOperandlt1.$lt).code, 'OK');
+
+		elemMatch1.init("x");
+		elemMatch1.add(gt1);
+		elemMatch1.add(lt1);
+
+		var baseOperandgt2={"$gt":101},
+			gt2 = new GTMatchExpression();
+
+		assert.strictEqual(gt2.init("", baseOperandgt2.$gt).code, 'OK');
+
+		var baseOperandlt2={"$lt":110},
+			lt2 = new LTMatchExpression(),
+			elemMatch2 = new ElemMatchValueMatchExpression(),
+			op = new AllElemMatchOp();
+
+		assert.strictEqual(lt2.init("", baseOperandlt2.$lt).code, 'OK');
+
+		elemMatch2.init("x");
+		elemMatch2.add(gt2);
+		elemMatch2.add(lt2);
+
+		op.init("x");
+		op.add(elemMatch1);
+		op.add(elemMatch2);
+
+
+		var nonArray={"x":4},
+			emptyArray={"x":[]},
+			nonNumberArray={"x":["q"]},
+			singleMatch={"x":[5]},
+			otherMatch={"x":[105]},
+			bothMatch={"x":[5,105]},
+			neitherMatch={"x":[0,200]};
+
+		assert.ok(!op.matches(nonArray, null));
+		assert.ok(!op.matches(emptyArray, null));
+		assert.ok(!op.matches(nonNumberArray, null));
+		assert.ok(!op.matches(singleMatch, null));
+		assert.ok(!op.matches(otherMatch, null));
+		assert.ok(op.matches(bothMatch, null));
+		assert.ok(!op.matches(neitherMatch, null));
+	}
+};

+ 8 - 8
test/lib/pipeline/matcher/AndMatchExpression.js

@@ -60,11 +60,11 @@ module.exports = {
 			andOp.add(sub2);
 			andOp.add(sub3);
 
-			assert.ok(andOp.matchesBSON({"a":5, "b":6}, null));
-			assert.ok(!andOp.matchesBSON({"a":5}, null));
-			assert.ok(!andOp.matchesBSON({"b":6}, null ));
-			assert.ok(!andOp.matchesBSON({"a":1, "b":6}, null));
-			assert.ok(!andOp.matchesBSON({"a":10, "b":6}, null));
+			assert.ok(andOp.matchesJSON({"a":5, "b":6}, null));
+			assert.ok(!andOp.matchesJSON({"a":5}, null));
+			assert.ok(!andOp.matchesJSON({"b":6}, null ));
+			assert.ok(!andOp.matchesJSON({"a":1, "b":6}, null));
+			assert.ok(!andOp.matchesJSON({"a":10, "b":6}, null));
 		},
 		"Should have an elemMatchKey": function(){
 			var baseOperand1 = {"a":1},
@@ -81,11 +81,11 @@ module.exports = {
 			andOp.add(sub2);
 
 			details.requestElemMatchKey();
-			assert.ok(!andOp.matchesBSON({"a":[1]}, details));
+			assert.ok(!andOp.matchesJSON({"a":[1]}, details));
 			assert.ok(!details.hasElemMatchKey());
-			assert.ok(!andOp.matchesBSON({"b":[2]}, details));
+			assert.ok(!andOp.matchesJSON({"b":[2]}, details));
 			assert.ok(!details.hasElemMatchKey());
-			assert.ok(andOp.matchesBSON({"a":[1], "b":[1, 2]}, details));
+			assert.ok(andOp.matchesJSON({"a":[1], "b":[1, 2]}, details));
 			assert.ok(details.hasElemMatchKey());
 			// The elem match key for the second $and clause is recorded.
 			assert.strictEqual("1", details.elemMatchKey());

+ 7 - 7
test/lib/pipeline/matcher/ExistsMatchExpression.js

@@ -9,16 +9,16 @@ module.exports = {
 		"should match an element": function (){
 			var e = new ExistsMatchExpression();
 			var s = e.init('a');
-			
+
 			assert.strictEqual(s.code, 'OK');
 			assert.ok( e.matches({'a':5}) );
 			assert.ok( e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':{}}) );	
+			assert.ok( ! e.matches({'a':{}}) );
 		},
 		"should match a boolean":function() {
 			var e = new ExistsMatchExpression();
 			var s = e.init('a');
-			
+
 			assert.strictEqual( s.code, 'OK' );
 			assert.ok( e.matches({'a':5}) );
 			assert.ok( ! e.matches({}) );
@@ -27,7 +27,7 @@ module.exports = {
 		"should match a number":function() {
 			var e = new ExistsMatchExpression();
 			var s = e.init('a');
-			
+
 			assert.strictEqual( s.code, 'OK' );
 			assert.ok( e.matches({'a':1}) );
 			assert.ok( e.matches({'a':null}) );
@@ -36,9 +36,9 @@ module.exports = {
 		"should match an array":function() {
 			var e = new ExistsMatchExpression();
 			var s = e.init('a');
-			
+
 			assert.strictEqual( s.code, 'OK' );
-			assert.ok( e.matches({'a':[4,5.5]}) );	
+			assert.ok( e.matches({'a':[4,5.5]}) );
 		},
 		"should yield an elemMatchKey":function() {
 			var e = new ExistsMatchExpression();
@@ -49,7 +49,7 @@ module.exports = {
 
 			assert.ok( ! e.matches({'a':1}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
-			
+
 			assert.ok( e.matches({'a':{'b':6}}));
 			assert.ok( ! m.hasElemMatchKey() );
 

+ 72 - 0
test/lib/pipeline/matcher/ListOfMatchExpression.js

@@ -0,0 +1,72 @@
+"use strict";
+var assert = require("assert"),
+	MatchExpression = require("../../../../lib/pipeline/matcher/MatchExpression"),
+	ListOfMatchExpression = require("../../../../lib/pipeline/matcher/ListOfMatchExpression");
+
+
+module.exports = {
+	"ListOfMatchExpression": {
+
+		"Constructor": function (){
+			var e = new ListOfMatchExpression('AND');
+			assert.equal(e._matchType, "AND");
+		},
+
+		"Add": function () {
+			var e = new ListOfMatchExpression();
+			e.add(new MatchExpression("OR"));
+			assert.equal(e._expressions[0]._matchType, "OR");
+		},
+
+		"Add2": function () {
+			var e = new ListOfMatchExpression();
+			e.add(new MatchExpression("OR"));
+			e.add(new MatchExpression("NOT"));
+			assert.equal(e._expressions[0]._matchType, "OR");
+			assert.equal(e._expressions[1]._matchType, "NOT");
+		},
+
+		"ClearAndRelease": function () {
+			var e = new ListOfMatchExpression();
+			e.add(new MatchExpression("OR"));
+			e.add(new MatchExpression("NOT"));
+			e.clearAndRelease();
+			assert.equal(e._expressions.length, 0);
+		},
+
+		"NumChildren": function () {
+			var e = new ListOfMatchExpression();
+			e.add(new MatchExpression("OR"));
+			e.add(new MatchExpression("NOT"));
+			assert.equal(e.numChildren(), 2);
+		},
+
+		"GetChild": function () {
+			var e = new ListOfMatchExpression(),
+				match1 = new MatchExpression("NOT");
+			e.add(new MatchExpression("OR"));
+			e.add(match1);
+			assert.deepEqual(e.getChild(1), match1);
+		},
+
+		"GetChildVector": function () {
+			var e = new ListOfMatchExpression(),
+				match0 = new MatchExpression("NOT"),
+				match1 = new MatchExpression("OR");
+			e.add(match0);
+			e.add(match1);
+			assert.equal(e.getChildVector().length, 2);
+		},
+
+		"Equivalent": function () {
+			var e = new ListOfMatchExpression('TEXT'),
+				f = new ListOfMatchExpression("TEXT");
+			assert.equal(e.equivalent(f), true);
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+

+ 62 - 0
test/lib/pipeline/matcher/MatchDetails.js

@@ -0,0 +1,62 @@
+"use strict";
+var assert = require("assert"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails.js");
+
+module.exports = {
+	"MatchDetails": {
+		"Constructor": function() {
+			var md = new MatchDetails();
+			assert.equal(md._elemMatchKeyRequested, false);
+			assert.equal(md._loadedRecord, false);
+			assert.equal(md._elemMatchKey, undefined);
+			assert(md instanceof MatchDetails);
+		},
+
+		"ResetOutput": function() {
+			var md = new MatchDetails();
+			md.setLoadedRecord(1);
+			assert.equal(md._loadedRecord, 1);
+			md.resetOutput();
+			assert.equal(md._loadedRecord, 0);
+			assert.equal(md._elemMatchKey, undefined);
+		},
+
+		"toString": function() {
+			var md = new MatchDetails();
+			assert(typeof md.toString() === "string");
+		},
+
+		"setLoadedRecord": function() {
+			var md = new MatchDetails(),
+				rec = {"TEST":1};
+			md.setLoadedRecord(rec);
+			assert.deepEqual(md._loadedRecord, rec);
+		},
+
+		"hasLoadedRecord": function() {
+			var md = new MatchDetails(),
+				rec = true;
+			md.setLoadedRecord(rec);
+			assert.equal(md.hasLoadedRecord(), true);
+		},
+
+		"requestElemMatchKey": function() {
+			var md = new MatchDetails();
+			md.requestElemMatchKey();
+			assert(md.needRecord, true);	//should be true after request
+		},
+
+		"setElemMatchKey": function() {
+			var md = new MatchDetails(),
+				key = "TEST";
+			md.setElemMatchKey(key);
+			assert.equal(md.hasElemMatchKey(), false);	//should not be set unless requested
+			md.requestElemMatchKey();
+			md.setElemMatchKey(key);
+			assert.equal(md.hasElemMatchKey(), true);
+			assert.equal(md.elemMatchKey(), key);
+		}
+	}
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 71 - 13
test/lib/pipeline/matcher/MatchExpressionParser.js

@@ -275,17 +275,6 @@ module.exports = {
 			assert.ok( ! res.result.matches({'x':4}) );
 			assert.ok( res.result.matches({'x':8}) );
 		},
-		"Should treat a second arg to $mod that is a string as a 0": function() {
-			var parser = new MatchExpressionParser();
-			var q = {'x':{'$mod':[2,'r']}};
-
-			var res = parser.parse( q );
-			assert.strictEqual( res.code,'OK',res.description );
-			assert.ok( res.result.matches({'x':2}) );
-			assert.ok( res.result.matches({'x':4}) );
-			assert.ok( ! res.result.matches({'x':5}) );
-			assert.ok( ! res.result.matches({'x':'a'}) );
-		},
 		"Should parse and match a simple $in": function() {
 			var parser = new MatchExpressionParser();
 			var q = {'x': {'$in':[2,3]}};
@@ -518,6 +507,77 @@ module.exports = {
 			assert.ok( res.result.matches({'x':2}) );
 			assert.ok( ! res.result.matches({'x':8}) );
 		},
+		"should allow trees less than the maximum recursion depth": function() {
+			var parser = new MatchExpressionParser(),
+				depth = 60,
+				q = "",
+				i;
+
+			for (i = 0; i < depth/2; i++) {
+				q = q + '{"$and": [{"a":3}, {"$or": [{"b":2},';
+			}
+			q = q + '{"b": 4}';
+			for (i = 0; i < depth/2; i++) {
+				q = q + "]}]}";
+			}
+
+			var res = parser.parse(JSON.parse(q));
+			assert.strictEqual(res.code, 'OK', res.description);
+		},
+		"should error when depth limit is exceeded": function() {
+			var parser = new MatchExpressionParser(),
+				depth = 105,
+				q = "",
+				i;
+
+			for (i = 0; i < depth/2; i++) {
+				q = q + '{"$and": [{"a":3}, {"$or": [{"b":2},';
+			}
+			q = q + '{"b": 4}';
+			for (i = 0; i < depth/2; i++) {
+				q = q + "]}]}";
+			}
+
+			var res = parser.parse(JSON.parse(q));
+			assert.strictEqual(res.description.substr(0, 43), 'exceeded maximum query tree depth of 100 at');
+			assert.strictEqual(res.code, 'BAD_VALUE');
+		},
+		"should error when depth limit is reached through a $not": function() {
+			var parser = new MatchExpressionParser(),
+				depth = 105,
+				q = '{"a": ',
+				i;
+
+			for (i = 0; i < depth; i++) {
+				q = q + '{"$not": ';
+			}
+			q = q + '{"$eq": 5}';
+			for (i = 0; i < depth+1; i++) {
+				q = q + "}";
+			}
+
+			var res = parser.parse(JSON.parse(q));
+			assert.strictEqual(res.description.substr(0, 43), 'exceeded maximum query tree depth of 100 at');
+			assert.strictEqual(res.code, 'BAD_VALUE');
+		},
+		"should error when depth limit is reached through an $elemMatch": function() {
+			var parser = new MatchExpressionParser(),
+				depth = 105,
+				q = '',
+				i;
+
+			for (i = 0; i < depth; i++) {
+				q = q + '{"a": {"$elemMatch": ';
+			}
+			q = q + '{"b": 5}';
+			for (i = 0; i < depth; i++) {
+				q = q + "}}";
+			}
+
+			var res = parser.parse(JSON.parse(q));
+			assert.strictEqual(res.description.substr(0, 43), 'exceeded maximum query tree depth of 100 at');
+			assert.strictEqual(res.code, 'BAD_VALUE');
+		},
 		"Should parse $not $regex and match properly": function() {
 			var parser = new MatchExpressionParser();
 			var a = /abc/i;
@@ -528,8 +588,6 @@ module.exports = {
 			assert.ok( ! res.result.matches({'x':'ABC'}) );
 			assert.ok( res.result.matches({'x':'AC'}) );
 		}
-
-
 	}
 };
 

+ 78 - 0
test/lib/pipeline/matcher/MatchExpression_test.js

@@ -0,0 +1,78 @@
+"use strict";
+
+var assert = require("assert"),
+	EqualityMatchExpression = require("../../../../lib/pipeline/matcher/EqualityMatchExpression"),
+	LTEMatchExpression = require("../../../../lib/pipeline/matcher/LTEMatchExpression"),
+	LTMatchExpression = require("../../../../lib/pipeline/matcher/LTMatchExpression"),
+	GTEMatchExpression = require("../../../../lib/pipeline/matcher/GTEMatchExpression"),
+	GTMatchExpression = require("../../../../lib/pipeline/matcher/GTMatchExpression");
+
+module.exports = {
+
+	"LeafMatchExpression":{
+
+		"Equal1":function Equal1() {
+			var temp = {x:5},
+				e = new EqualityMatchExpression();
+			e.init("x", temp.x);
+			assert(e.matchesJSON({x:5}));
+			assert(e.matchesJSON({x:[5]}));
+			assert(e.matchesJSON({x:[1,5]}));
+			assert(e.matchesJSON({x:[1,5,2]}));
+			assert(e.matchesJSON({x:[5,2]}));
+
+			assert(!(e.matchesJSON({x:null})));
+			assert(!(e.matchesJSON({x:6})));
+			assert(!(e.matchesJSON({x:[4,2]})));
+			assert(!(e.matchesJSON({x:[[5]]})));
+		},
+
+		"Comp1":{
+
+			"LTEMatchExpression": function () {
+				var temp = {x:5},
+					e = new LTEMatchExpression();
+				e.init("x", temp.x);
+				assert(e.matchesJSON({x:5}));
+				assert(e.matchesJSON({x:4}));
+				assert(!(e.matchesJSON({x:6})));
+				assert(!(e.matchesJSON({x:"eliot"})));
+			},
+
+			"LTMatchExpression": function () {
+				var temp = {x:5},
+					e = new LTMatchExpression();
+				e.init("x", temp.x);
+				assert(!(e.matchesJSON({x:5})));
+				assert(e.matchesJSON({x:4}));
+				assert(!(e.matchesJSON({x:6})));
+				assert(!(e.matchesJSON({x:"eliot"})));
+			},
+
+			"GTEMatchExpression": function () {
+				var temp = {x:5},
+					e = new GTEMatchExpression();
+				e.init("x", temp.x);
+				assert(e.matchesJSON({x:5}));
+				assert(!(e.matchesJSON({x:4})));
+				assert(e.matchesJSON({x:6}));
+				assert(!(e.matchesJSON({x:"eliot"})));
+			},
+
+			"GTMatchExpression": function () {
+				var temp = {x:5},
+					e = new GTMatchExpression();
+				e.init("x", temp.x);
+				assert(!(e.matchesJSON({x:5})));
+				assert(!(e.matchesJSON({x:4})));
+				assert(e.matchesJSON({x:6}));
+				assert(!(e.matchesJSON({x:"eliot"})));
+			}
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 86 - 0
test/lib/pipeline/matcher/Matcher2.js

@@ -0,0 +1,86 @@
+"use strict";
+
+var assert = require("assert"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails.js"),
+	Matcher2 = require("../../../../lib/pipeline/matcher/Matcher2.js");
+
+module.exports = {
+	"Matcher2": {
+		"Constructor": function() {
+			var m = new Matcher2({"a":1});
+		},
+
+		"Basic": function() {
+			var query = {"a":"b"},
+				m = new Matcher2(query);
+			assert(m.matches(query));
+		},
+
+		"DoubleEqual": function() {
+			var query = {"a":5},
+				m = new Matcher2(query);
+			assert(m.matches(query));
+		},
+
+		"MixedNumericEqual": function() {	//not sure if we need this.  Same as DoubleEqual in munge
+			var query = {"a":5},
+				m = new Matcher2(query);
+			assert(m.matches(query));
+		},
+
+		"MixedNumericGt": function() {
+			var query = {"a":{"$gt":4}},
+				m = new Matcher2(query);
+			assert.ok(m.matches({"a":5}));
+		},
+
+		"MixedNumericIN": function() {
+			var query = {"a":{"$in":[4,6]}},
+				m = new Matcher2(query);
+			assert.ok(m.matches({"a":4.0}));
+			assert.ok(!m.matches({"a":5.0}));
+			assert.ok(m.matches({"a":4}));
+		},
+
+		"MixedNumericEmbedded": function() {
+			var query = {"a":{"x":1}},
+				m = new Matcher2(query);
+			assert.ok(m.matches({"a":{"x":1}}));
+			assert.ok(m.matches({"a":{"x":1.0}}));
+		},
+
+		"Size": function() {
+			var query = {"a":{"$size":4}},
+				m = new Matcher2(query);
+			assert.ok(m.matches({"a":[1,2,3,4]}));
+			assert.ok(!m.matches({"a":[1,2,3]}));
+			assert.ok(!m.matches({"a":[1,2,3,'a','b']}));
+			assert.ok(!m.matches({"a":[[1,2,3,4]]}));
+		},
+
+		"WithinBox - mongo Geo function, not porting": function() {},
+
+		"WithinPolygon - mongo Geo function, not porting": function() {},
+
+		"WithinCenter - mongo Geo function, not porting": function() {},
+
+		"ElemMatchKey": function() {
+			var query = {"a.b":1},
+				m = new Matcher2(query),
+				md = new MatchDetails();
+			md.requestElemMatchKey();
+			assert.ok(!md.hasElemMatchKey());
+			assert.ok(m.matches({"a":[{"b":1}]}, md));
+			assert.ok(md.hasElemMatchKey());
+			assert.equal("0", md.elemMatchKey());
+		},
+
+		"WhereSimple1 - mongo MapReduce function, not available ": function() {
+		},
+
+		"AllTiming - mongo benchmarking function, not available": function() {
+		}
+		}
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 4 - 4
test/lib/pipeline/matcher/NorMatchExpression.js

@@ -84,13 +84,13 @@ module.exports = {
 			orOp.add(sub2);
 
 			details.requestElemMatchKey();
-			assert.ok( orOp.matchesBSON({"a":[10], 'b':[10]}, details));
+			assert.ok( orOp.matchesJSON({"a":[10], 'b':[10]}, details));
 			assert.ok(!details.hasElemMatchKey());
 
-			assert.ok( ! orOp.matchesBSON({"a":[1], "b":[1, 2]}, details));
+			assert.ok( ! orOp.matchesJSON({"a":[1], "b":[1, 2]}, details));
 			assert.ok(!details.hasElemMatchKey());
-		
-			
+
+
 		}
 
 

+ 4 - 4
test/lib/pipeline/matcher/OrMatchExpression.js

@@ -84,13 +84,13 @@ module.exports = {
 			orOp.add(sub2);
 
 			details.requestElemMatchKey();
-			assert.ok(!orOp.matchesBSON({"a":[10], 'b':[10]}, details));
+			assert.ok(!orOp.matchesJSON({"a":[10], 'b':[10]}, details));
 			assert.ok(!details.hasElemMatchKey());
 
-			assert.ok(orOp.matchesBSON({"a":[1], "b":[1, 2]}, details));
+			assert.ok(orOp.matchesJSON({"a":[1], "b":[1, 2]}, details));
 			assert.ok(!details.hasElemMatchKey());
-		
-			
+
+
 		}
 
 

+ 59 - 0
test/lib/pipeline/matcher/TextMatchExpression_test.js

@@ -0,0 +1,59 @@
+"use strict";
+
+var assert = require('assert'),
+	TextMatchExpression = require('../../../../lib/pipeline/matcher/TextMatchExpression.js'),
+	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails.js');
+
+module.exports = {
+	'TextMatchExpression': {
+		'Should match an element, regardless of what is provided.': function() {
+			var text = new TextMatchExpression(),
+				text2 = new TextMatchExpression();
+
+			assert.strictEqual(text.init('query', 'language').code, 'OK');
+			assert.strictEqual(text2.init('query2', 'language2').code, 'OK');
+
+			assert.ok(text.matchesSingleElement(text2)); // It'll always work. Just the way it is in source.
+		},
+
+		'Should return the query provided in the init.': function() {
+			var text = new TextMatchExpression();
+
+			text.init('query', 'language');
+
+			assert.strictEqual(text.getQuery(), 'query');
+		},
+
+		'Should return the language provided in the init.': function() {
+			var text = new TextMatchExpression();
+
+			text.init('query', 'language');
+
+			assert.strictEqual(text.getLanguage(), 'language');
+		},
+
+		'Should return equivalency.': function() {
+			var text1 = new TextMatchExpression(),
+				text2 = new TextMatchExpression(),
+				text3 = new TextMatchExpression();
+
+			text1.init('query', 'language');
+			text2.init('query', 'language');
+			text3.init('query2', 'language2');
+
+			assert.ok(text1.equivalent(text1));
+			assert.ok(text1.equivalent(text2));
+			assert.ok(!text1.equivalent(text3));
+		},
+
+		'Should return a shallow copy of the original text match expression.': function() {
+			var text1 = new TextMatchExpression(),
+				status = text1.init('query', 'language'),
+				text2 = text1.shallowClone();
+
+			assert.ok(text1.equivalent(text2));
+		}
+	}
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);