Просмотр исходного кода

Merge branch 'feature/mongo_2.6.5_documentSource_SortDocumentSource' of https://github.com/RiveraGroup/mungedb-aggregate into feature/mongo_2.6.5_documentSource_SortDocumentSource

Scott Munday 11 лет назад
Родитель
Сommit
5a471f5895
71 измененных файлов с 1784 добавлено и 1306 удалено
  1. 38 23
      lib/pipeline/documentSources/SortDocumentSource.js
  2. 40 23
      lib/pipeline/expressions/AddExpression.js
  3. 60 44
      lib/pipeline/expressions/AndExpression.js
  4. 3 4
      lib/pipeline/expressions/ConstantExpression.js
  5. 1 1
      lib/pipeline/expressions/Expression.js
  6. 0 207
      lib/pipeline/expressions/FieldRangeExpression.js
  7. 6 7
      lib/pipeline/expressions/FixedArityExpressionT.js
  8. 75 68
      lib/pipeline/expressions/LetExpression.js
  9. 3 3
      lib/pipeline/expressions/MultiplyExpression.js
  10. 5 4
      lib/pipeline/expressions/NaryBaseExpressionT.js
  11. 2 2
      lib/pipeline/expressions/NaryExpression.js
  12. 1 1
      lib/pipeline/expressions/ObjectExpression.js
  13. 53 36
      lib/pipeline/expressions/OrExpression.js
  14. 6 5
      lib/pipeline/expressions/VariadicExpressionT.js
  15. 0 1
      lib/pipeline/expressions/index.js
  16. 20 21
      lib/pipeline/matcher/ComparisonMatchExpression.js
  17. 15 13
      lib/pipeline/matcher/GTEMatchExpression.js
  18. 14 13
      lib/pipeline/matcher/GTMatchExpression.js
  19. 14 12
      lib/pipeline/matcher/LTEMatchExpression.js
  20. 14 12
      lib/pipeline/matcher/LTMatchExpression.js
  21. 2 1
      package.json
  22. 0 0
      test/lib/pipeline/accumulators/AddToSetAccumulator_test.js
  23. 0 0
      test/lib/pipeline/accumulators/AvgAccumulator_test.js
  24. 0 0
      test/lib/pipeline/accumulators/FirstAccumulator_test.js
  25. 0 0
      test/lib/pipeline/accumulators/LastAccumulator_test.js
  26. 0 0
      test/lib/pipeline/accumulators/MinMaxAccumulator_test.js
  27. 0 0
      test/lib/pipeline/accumulators/PushAccumulator_test.js
  28. 0 0
      test/lib/pipeline/accumulators/SumAccumulator_test.js
  29. 47 46
      test/lib/pipeline/documentSources/SortDocumentSource.js
  30. 206 90
      test/lib/pipeline/expressions/AddExpression_test.js
  31. 0 0
      test/lib/pipeline/expressions/AllElementsTrueExpression_test.js
  32. 245 172
      test/lib/pipeline/expressions/AndExpression_test.js
  33. 0 0
      test/lib/pipeline/expressions/AnyElementTrueExpression_test.js
  34. 0 0
      test/lib/pipeline/expressions/CoerceToBoolExpression_test.js
  35. 0 0
      test/lib/pipeline/expressions/CompareExpression_test.js
  36. 1 1
      test/lib/pipeline/expressions/ConcatExpression_test.js
  37. 0 0
      test/lib/pipeline/expressions/DayOfMonthExpression_test.js
  38. 0 0
      test/lib/pipeline/expressions/DayOfWeekExpression_test.js
  39. 0 0
      test/lib/pipeline/expressions/DayOfYearExpression_test.js
  40. 0 0
      test/lib/pipeline/expressions/FieldPathExpression_test.js
  41. 0 139
      test/lib/pipeline/expressions/FieldRangeExpression.js
  42. 0 0
      test/lib/pipeline/expressions/HourExpression_test.js
  43. 197 0
      test/lib/pipeline/expressions/LetExpression_test.js
  44. 9 9
      test/lib/pipeline/expressions/MapExpression_test.js
  45. 0 0
      test/lib/pipeline/expressions/MillisecondExpression_test.js
  46. 0 0
      test/lib/pipeline/expressions/MinuteExpression_test.js
  47. 0 0
      test/lib/pipeline/expressions/ModExpression_test.js
  48. 0 0
      test/lib/pipeline/expressions/MonthExpression_test.js
  49. 37 33
      test/lib/pipeline/expressions/NaryExpression_test.js
  50. 0 0
      test/lib/pipeline/expressions/ObjectExpression_test.js
  51. 245 101
      test/lib/pipeline/expressions/OrExpression_test.js
  52. 0 0
      test/lib/pipeline/expressions/SecondExpression_test.js
  53. 0 0
      test/lib/pipeline/expressions/SetDifferenceExpression_test.js
  54. 0 0
      test/lib/pipeline/expressions/SetEqualsExpression_test.js
  55. 0 0
      test/lib/pipeline/expressions/SetIntersectionExpression_test.js
  56. 0 0
      test/lib/pipeline/expressions/SetIsSubsetExpression_test.js
  57. 0 0
      test/lib/pipeline/expressions/SetUnionExpression_test.js
  58. 0 0
      test/lib/pipeline/expressions/SizeExpression_test.js
  59. 0 0
      test/lib/pipeline/expressions/StrcasecmpExpression_test.js
  60. 0 0
      test/lib/pipeline/expressions/SubtractExpression_test.js
  61. 0 0
      test/lib/pipeline/expressions/VariablesIdGenerator_test.js
  62. 0 0
      test/lib/pipeline/expressions/VariablesParseState_test.js
  63. 0 0
      test/lib/pipeline/expressions/Variables_test.js
  64. 0 0
      test/lib/pipeline/expressions/WeekExpression_test.js
  65. 0 0
      test/lib/pipeline/expressions/YearExpression_test.js
  66. 0 50
      test/lib/pipeline/matcher/ComparisonMatchExpression.js
  67. 110 0
      test/lib/pipeline/matcher/ComparisonMatchExpression_test.js
  68. 67 32
      test/lib/pipeline/matcher/GTEMatchExpression.js
  69. 70 38
      test/lib/pipeline/matcher/GTMatchExpression.js
  70. 85 42
      test/lib/pipeline/matcher/LTEMatchExpression.js
  71. 93 52
      test/lib/pipeline/matcher/LTMatchExpression.js

+ 38 - 23
lib/pipeline/documentSources/SortDocumentSource.js

@@ -327,11 +327,11 @@ proto.extractKey = function extractKey(d){
 /**
  * Compare two documents according to the specified sort key.
  *
- * @param {Object} pL the left side doc
- * @param {Object} pR the right side doc
+ * @param {Object} lhs the left side doc
+ * @param {Object} rhs the right side doc
  * @returns {Number} a number less than, equal to, or greater than zero, indicating pL < pR, pL == pR, or pL > pR, respectively
 **/
-proto.compare = function compare(pL,pR) {
+proto.compare = function compare(lhs,rhs) {
 	/*
 	  populate() already checked that there is a non-empty sort key,
 	  so we shouldn't have to worry about that here.
@@ -341,18 +341,17 @@ proto.compare = function compare(pL,pR) {
 	var n = this.vSortKey.length;
 	if ( n == 1) { // simple fast case
 		if ( this.vAscending[0] ) {
-			return Value.compare(pL, pR);
+			return Value.compare(lhs, rhs);
 		} else {
-			return -Value.compare(pL, pR);
+			return -Value.compare(lhs, rhs);
 		}
 	}
 
 	// compound sort
 	for(var i = 0; i < n; ++i) {
 		/* evaluate the sort keys */
-		var fieldPathVar;
-		var pathExpr = new FieldPathExpression(this.vSortKey[i].getFieldPath(false), fieldPathVar);
-		var left = pathExpr.evaluate(pL), right = pathExpr.evaluate(pR);
+		var pathExpr = FieldPathExpression.create(this.vSortKey[i].getFieldPath(false));
+		var left = pathExpr.evaluate(lhs), right = pathExpr.evaluate(rhs);
 
 		/*
 		Compare the two values; if they differ, return.  If they are
@@ -397,22 +396,36 @@ proto.serializeSortKey = function serializeSortKey(explain) {
 	return keyObj;
 };
 
+/**
+ * Creates a new SortDocumentSource from Json
+ *
+ * @param {Object} elem
+ * @param {Object} expCtx
+ *
+**/
+klass.createFromJson = function createFromJson(elem, expCtx) {
+	if (typeof elem !== "object") throw new Error("code 15973; the " + klass.sortName + " key specification must be an object");
+
+	
+	return this.create(expCtx, elem);
+};
+
 /**
  * Creates a new SortDocumentSource
  *
- * @param {Object} jsonElement
- * @ctx {Object} context
+ * @param {Object} expCtx
+ * @param {object} sortorder 
+ * @param {int} limit
  *
 **/
-klass.createFromJson = function createFromJson(jsonElement, ctx) {
-	if (typeof jsonElement !== "object") throw new Error("code 15973; the " + klass.sortName + " key specification must be an object");
+klass.create = function create(expCtx, sortOrder, limit) {
 
 	var Sort = proto.getFactory(),
-		nextSort = new Sort(ctx);
+		nextSort = new Sort(expCtx);
 
 	/* check for then iterate over the sort object */
 	var sortKeys = 0;
-	for(var keyField in jsonElement) {
+	for(var keyField in sortOrder) {
 		var fieldName = keyField.fieldName;
 
 		if ( fieldName === "$mergePresorted" ){
@@ -429,22 +442,24 @@ klass.createFromJson = function createFromJson(jsonElement, ctx) {
 			continue;
 		}
 
-		if (typeof jsonElement[keyField] !== "number") throw new Error("code 15974; " + klass.sortName + "$sort key ordering must be specified using a number or {$meta: 'text'}");
+		if (typeof sortOrder[keyField] !== "number") throw new Error("code 15974; " + klass.sortName + "$sort key ordering must be specified using a number or {$meta: 'text'}");
 
-		var sortOrder = 0;
-		sortOrder = jsonElement[keyField];
-		if ((sortOrder != 1) && (sortOrder !== -1)) throw new Error("code 15975; " + klass.sortName + " $sort key ordering must be 1 (for ascending) or -1 (for descending)");
+		// RedBeard0531 can the thanked.
+		var sortDirection = 0;
+		sortDirection = sortOrder[keyField];
+		if ((sortDirection != 1) && (sortDirection !== -1)) throw new Error("code 15975; " + klass.sortName + " $sort key ordering must be 1 (for ascending) or -1 (for descending)");
 
-		nextSort.addKey(keyField, (sortOrder > 0));
+		nextSort.addKey(keyField, (sortDirection > 0));
 		++sortKeys;
 	}
 
 	if (sortKeys <= 0) throw new Error("code 15976; " + klass.sortName + " must have at least one sort key");
+
 	
-	// if ( limit > 0) {
-	// 	var coalesced = nextSort.coalesce( create(ctx, limit));
-	// 	// should always coalesce
-	// }
+	if ( limit > 0) {
+		var coalesced = nextSort.coalesce( create(expCtx, limit));
+		// should always coalesce
+	}
 
 	return nextSort;
 };

+ 40 - 23
lib/pipeline/expressions/AddExpression.js

@@ -3,41 +3,58 @@
 /**
  * Create an expression that finds the sum of n operands.
  * @class AddExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var AddExpression = module.exports = function AddExpression(){
-//	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
 }, klass = AddExpression, base = require("./VariadicExpressionT")(AddExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$add";
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
-
-/**
- * Takes an array of one or more numbers and adds them together, returning the sum.
- * @method @evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var total = 0;
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var value = this.operands[i].evaluateInternal(vars);
-		if (value instanceof Date) throw new Error("$add does not support dates; code 16415");
-		if (typeof value == "string") throw new Error("$add does not support strings; code 16416");
-		total += Value.coerceToDouble(value);
+	var total = 0, //NOTE: DEVIATION FROM MONGO: no need to track narrowest so just use one var
+		haveDate = false;
+
+	var n = this.operands.length;
+	for (var i = 0; i < n; ++i) {
+		var val = this.operands[i].evaluateInternal(vars);
+		if (typeof val === "number") {
+			total += val;
+		} else if (val instanceof Date) {
+			if (haveDate)
+				throw new Error("only one Date allowed in an $add expression; uassert code 16612");
+			haveDate = true;
+
+			total += val.getTime();
+		} else if (val === undefined || val === null) {
+			return null;
+		} else {
+			throw new Error("$add only supports numeric or date types, not " +
+				Value.getType(val) + "; uasserted code 16554");
+		}
+	}
+
+	if (haveDate) {
+		return new Date(total);
+	} else if (typeof total === "number") {
+		return total;
+	} else {
+		throw new Error("$add resulted in a non-numeric type; massert code 16417");
 	}
-	if (typeof total != "number") throw new Error("$add resulted in a non-numeric type; code 16417");
-	return total;
 };
 
 
-/** Register Expression */
-Expression.registerExpression(klass.opName,base.parse);
+Expression.registerExpression("$add", base.parse);
+
+proto.getOpName = function getOpName(){
+	return "$add";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 60 - 44
lib/pipeline/expressions/AndExpression.js

@@ -7,71 +7,87 @@
  * returns false on the first operand that evaluates to false.
  *
  * @class AndExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var AndExpression = module.exports = function AndExpression() {
-	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = AndExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+}, klass = AndExpression, base = require("./VariadicExpressionT")(AndExpression), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	ConstantExpression = require("./ConstantExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$and";
-proto.getOpName = function getOpName() {
-	return klass.opName;
-};
-
-/**
- * Takes an array one or more values and returns true if all of the values in the array are true. Otherwise $and returns false.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var value = this.operands[i].evaluateInternal(vars);
-		if (!Value.coerceToBool(value)) return false;
-	}
-	return true;
-};
-
-proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() { return true; };
-
 proto.optimize = function optimize() {
-	var expr = base.prototype.optimize.call(this); //optimize the conjunction as much as possible
+	// optimize the conjunction as much as possible
+	var expr = base.prototype.optimize.call(this);
 
 	// if the result isn't a conjunction, we can't do anything
-	if (!(expr instanceof AndExpression)) return expr;
-	var andExpr = expr;
+	var andExpr = expr instanceof AndExpression ? expr : undefined;
+	if (!andExpr)
+		return expr;
 
-	// Check the last argument on the result; if it's not constant (as promised by ExpressionNary::optimize(),) then there's nothing we can do.
+	/*
+	 * Check the last argument on the result; if it's not constant (as
+	 * promised by ExpressionNary::optimize(),) then there's nothing
+	 * we can do.
+	 */
 	var n = andExpr.operands.length;
 	// ExpressionNary::optimize() generates an ExpressionConstant for {$and:[]}.
-	if (!n) throw new Error("requires operands!");
-	var lastExpr = andExpr.operands[n - 1];
-	if (!(lastExpr instanceof ConstantExpression)) return expr;
+	if (n <= 0) throw new Error("Assertion failure");
+	var lastExpr = andExpr.operands[n - 1],
+		constExpr = lastExpr instanceof ConstantExpression ? lastExpr : undefined;
+	if (!constExpr)
+		return expr;
 
-	// Evaluate and coerce the last argument to a boolean.  If it's false, then we can replace this entire expression.
-	var last = Value.coerceToBool(lastExpr.evaluateInternal());
-	if (!last) return new ConstantExpression(false);
+	/*
+	 * Evaluate and coerce the last argument to a boolean.  If it's false,
+	 * then we can replace this entire expression.
+	 */
+	var last = Value.coerceToBool(constExpr.getValue());
+	if (!last)
+		return ConstantExpression.create(false);
 
-	// If we got here, the final operand was true, so we don't need it anymore.
-	// If there was only one other operand, we don't need the conjunction either.
-	// Note we still need to keep the promise that the result will be a boolean.
-	if (n == 2) return new CoerceToBoolExpression(andExpr.operands[0]);
+	/*
+	 * If we got here, the final operand was true, so we don't need it
+	 * anymore.  If there was only one other operand, we don't need the
+	 * conjunction either.  Note we still need to keep the promise that
+	 * the result will be a boolean.
+	 */
+	if (n === 2)
+		return CoerceToBoolExpression.create(andExpr.operands[0]);
 
-	//Remove the final "true" value, and return the new expression.
-	//CW TODO: Note that because of any implicit conversions, we may need to apply an implicit boolean conversion.
-	andExpr.operands.length = n - 1; //truncate the array
+	/*
+	 * Remove the final "true" value, and return the new expression.
+	 *
+	 * CW TODO:
+	 * Note that because of any implicit conversions, we may need to
+	 * apply an implicit boolean conversion.
+	 */
+	andExpr.operands.length = n - 1;
 	return expr;
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var n = this.operands.length;
+	for (var i = 0; i < n; ++i) {
+		var value = this.operands[i].evaluateInternal(vars);
+		if (!Value.coerceToBool(value))
+			return false;
+	}
+	return true;
+};
+
+Expression.registerExpression("$and", base.parse);
 
-//TODO: proto.toMatcherBson
+proto.getOpName = function getOpName() {
+	return "$and";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

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

@@ -1,8 +1,5 @@
 "use strict";
 
-var Value = require("../Value"),
-    Expression = require("./Expression");
-
 /**
  * Internal expression for constant values
  * @class ConstantExpression
@@ -14,7 +11,9 @@ var ConstantExpression = module.exports = function ConstantExpression(value){
     if (arguments.length !== 1) throw new Error(klass.name + ": args expected: value");
     this.value = value;
     base.call(this);
-}, klass = ConstantExpression, base = require("./FixedArityExpressionT")(klass,1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = ConstantExpression, base = require("./FixedArityExpressionT")(ConstantExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+var Expression = require("./Expression");
 
 klass.parse = function parse(exprElement, vps) {
 	return new ConstantExpression(exprElement);

+ 1 - 1
lib/pipeline/expressions/Expression.js

@@ -15,7 +15,7 @@
  */
 var Expression = module.exports = function Expression() {
 	if (arguments.length !== 0) throw new Error("zero args expected");
-}, klass = Expression, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = Expression, proto = klass.prototype;
 
 
 var Value = require("../Value"),

+ 0 - 207
lib/pipeline/expressions/FieldRangeExpression.js

@@ -1,207 +0,0 @@
-"use strict";
-
-/**
- * Create a field range expression.
- *
- * Field ranges are meant to match up with classic Matcher semantics, and therefore are conjunctions.
- *
- * For example, these appear in mongo shell predicates in one of these forms:
- *      { a : C } -> (a == C) // degenerate "point" range
- *      { a : { $lt : C } } -> (a < C) // open range
- *      { a : { $gt : C1, $lte : C2 } } -> ((a > C1) && (a <= C2)) // closed
- *
- * When initially created, a field range only includes one end of the range.  Additional points may be added via intersect().
- *
- * Note that NE and CMP are not supported.
- *
- * @class FieldRangeExpression
- * @namespace mungedb-aggregate.pipeline.expressions
- * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.Expression
- * @constructor
- * @param pathExpr the field path for extracting the field value
- * @param cmpOp the comparison operator
- * @param value the value to compare against
- * @returns the newly created field range expression
- **/
-var FieldRangeExpression = module.exports = function FieldRangeExpression(pathExpr, cmpOp, value){
-	if (arguments.length !== 3) throw new Error("args expected: pathExpr, cmpOp, and value");
-	this.pathExpr = pathExpr;
-	this.range = new Range({cmpOp:cmpOp, value:value});
-}, klass = FieldRangeExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-// DEPENDENCIES
-var Value = require("../Value"),
-	ConstantExpression = require("./ConstantExpression");
-
-// NESTED CLASSES
-var Range = (function(){
-	/**
-	 * create a new Range; opts is either {cmpOp:..., value:...} or {bottom:..., isBottomOpen:..., top:..., isTopOpen:...}
-	 * @private
-	 **/
-	var klass = function Range(opts){
-		this.isBottomOpen = this.isTopOpen = false;
-		this.bottom = this.top = undefined;
-		if(opts.hasOwnProperty("cmpOp") && opts.hasOwnProperty("value")){
-			switch (opts.cmpOp) {
-				case Expression.CmpOp.EQ:
-					this.bottom = this.top = opts.value;
-					break;
-
-				case Expression.CmpOp.GT:
-					this.isBottomOpen = true;
-					/* falls through */
-				case Expression.CmpOp.GTE:
-					this.isTopOpen = true;
-					this.bottom = opts.value;
-					break;
-
-				case Expression.CmpOp.LT:
-					this.isTopOpen = true;
-					/* falls through */
-				case Expression.CmpOp.LTE:
-					this.isBottomOpen = true;
-					this.top = opts.value;
-					break;
-
-				case Expression.CmpOp.NE:
-				case Expression.CmpOp.CMP:
-					throw new Error("CmpOp not allowed: " + opts.cmpOp);
-
-				default:
-					throw new Error("Unexpected CmpOp: " + opts.cmpOp);
-			}
-		}else{
-			this.bottom = opts.bottom;
-			this.isBottomOpen = opts.isBottomOpen;
-			this.top = opts.top;
-			this.isTopOpen = opts.isTopOpen;
-		}
-	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-	// PROTOTYPE MEMBERS
-	proto.intersect = function intersect(range){
-		// Find the max of the bottom end of ranges
-		var maxBottom = range.bottom,
-			maxBottomOpen = range.isBottomOpen;
-		if(this.bottom !== undefined){
-			if(range.bottom === undefined){
-				maxBottom = this.bottom;
-				maxBottomOpen = this.isBottomOpen;
-			}else{
-				if(Value.compare(this.bottom, range.bottom) === 0){
-					maxBottomOpen = this.isBottomOpen || range.isBottomOpen;
-				}else{
-					maxBottom = this.bottom;
-					maxBottomOpen = this.isBottomOpen;
-				}
-			}
-		}
-		// Find the min of the tops of the ranges
-		var minTop = range.top,
-			minTopOpen = range.isTopOpen;
-		if(this.top !== undefined){
-			if(range.top === undefined){
-				minTop = this.top;
-				minTopOpen = this.isTopOpen;
-			}else{
-				if(Value.compare(this.top, range.top) === 0){
-					minTopOpen = this.isTopOpen || range.isTopOpen;
-				}else{
-					minTop = this.top;
-					minTopOpen = this.isTopOpen;
-				}
-			}
-		}
-		if(Value.compare(maxBottom, minTop) <= 0)
-			return new Range({bottom:maxBottom, isBottomOpen:maxBottomOpen, top:minTop, isTopOpen:minTopOpen});
-		return null; // empty intersection
-	};
-
-	proto.contains = function contains(value){
-		var cmp;
-		if(this.bottom !== undefined){
-			cmp = Value.compare(value, this.bottom);
-			if(cmp < 0) return false;
-			if(this.isBottomOpen && cmp === 0) return false;
-		}
-		if(this.top !== undefined){
-			cmp = Value.compare(value, this.top);
-			if(cmp > 0) return false;
-			if(this.isTopOpen && cmp === 0) return false;
-		}
-		return true;
-	};
-
-	return klass;
-})();
-
-// PROTOTYPE MEMBERS
-proto.evaluate = function evaluate(obj){
-	if(this.range === undefined) return false;
-	var value = this.pathExpr.evaluate(obj);
-	if(value instanceof Array)
-		throw new Error('FieldRangeExpression cannot evaluate an array.');
-	return this.range.contains(value);
-};
-
-proto.optimize = function optimize(){
-	if(this.range === undefined) return new ConstantExpression(false);
-	if(this.range.bottom === undefined && this.range.top === undefined) return new ConstantExpression(true);
-	return this;
-};
-
-proto.addDependencies = function(deps){
-	return this.pathExpr.addDependencies(deps);
-};
-
-/**
- * Add an intersecting range.
- *
- * This can be done any number of times after creation.  The range is
- * internally optimized for each new addition.  If the new intersection
- * extends or reduces the values within the range, the internal
- * representation is adjusted to reflect that.
- *
- * Note that NE and CMP are not supported.
- *
- * @method intersect
- * @param cmpOp the comparison operator
- * @param pValue the value to compare against
- **/
-proto.intersect = function intersect(cmpOp, value){
-	this.range = this.range.intersect(new Range({cmpOp:cmpOp, value:value}));
-};
-
-proto.toJSON = function toJSON(){
-	if (this.range === undefined) return false; //nothing will satisfy this predicate
-	if (this.range.top === undefined && this.range.bottom === undefined) return true; // any value will satisfy this predicate
-
-	// FIXME Append constant values using the $const operator.  SERVER-6769
-
-	var json = {};
-	if (this.range.top === this.range.bottom) {
-		json[Expression.CmpOp.EQ] = [this.pathExpr.toJSON(), this.range.top];
-	}else{
-		var leftOp = {};
-		if (this.range.bottom !== undefined) {
-			leftOp[this.range.isBottomOpen ? Expression.CmpOp.GT : Expression.CmpOp.GTE] = [this.pathExpr.toJSON(), this.range.bottom];
-			if (this.range.top === undefined) return leftOp;
-		}
-
-		var rightOp = {};
-		if(this.range.top !== undefined){
-			rightOp[this.range.isTopOpen ? Expression.CmpOp.LT : Expression.CmpOp.LTE] = [this.pathExpr.toJSON(), this.range.top];
-			if (this.range.bottom === undefined) return rightOp;
-		}
-
-		json.$and = [leftOp, rightOp];
-	}
-	return json;
-};
-
-//TODO: proto.addToBson = ...?
-//TODO: proto.addToBsonObj = ...?
-//TODO: proto.addToBsonArray = ...?
-//TODO: proto.toMatcherBson = ...? WILL PROBABLY NEED THESE...

+ 6 - 7
lib/pipeline/expressions/FixedArityExpressionT.js

@@ -6,8 +6,7 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-
+ */
 var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubClass, nArgs) {
 
 	var FixedArityExpression = function FixedArityExpression() {
@@ -15,12 +14,16 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		base.call(this);
 	}, klass = FixedArityExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
 	/**
 	 * Check that the number of args is what we expected
 	 * @method validateArguments
 	 * @param args Array The array of arguments to the expression
 	 * @throws
-	 **/
+	 */
 	proto.validateArguments = function validateArguments(args) {
 		if(args.length !== nArgs) {
 			throw new Error("Expression " + this.getOpName() + " takes exactly " +
@@ -28,9 +31,5 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		}
 	};
 
-	klass.parse = base.parse; 						// NOTE: Need to explicitly
-	klass.parseArguments = base.parseArguments;		// bubble static members in
-													// our inheritance chain
 	return FixedArityExpression;
 };
-

+ 75 - 68
lib/pipeline/expressions/LetExpression.js

@@ -1,119 +1,126 @@
 "use strict";
 
 var LetExpression = module.exports = function LetExpression(vars, subExpression){
-	//if (arguments.length !== 2) throw new Error("Two args expected");
+	if (arguments.length !== 2) throw new Error(klass.name + ": expected args: vars, subExpression");
 	this._variables = vars;
 	this._subExpression = subExpression;
 }, klass = LetExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-Expression.registerExpression("$let", LetExpression.parse);
 
-// DEPENDENCIES
-var Variables = require("./Variables"),
-	VariablesParseState = require("./VariablesParseState");
+var Value = require("../Value"),
+	Variables = require("./Variables");
 
-// PROTOTYPE MEMBERS
 
+function NameAndExpression(name, expr){
+	this.name = name;
+	this.expression = expr;
+}
 
-proto.parse = function parse(expr, vpsIn){
-	if(!("$let" in expr)) {
-		throw new Error("Tried to create a $let with something other than let. Looks like your parse map went all funny.");
-	}
 
-	if(typeof(expr.$let) !== 'object' || (expr.$let instanceof Array)) {
-		throw new Error("$let only supports an object as its argument: 16874");
-	}
+klass.parse = function parse(expr, vpsIn){
 
-	var args = expr.$let,
-		varsElem = args.vars,
-		inElem = args['in']; // args.in; ??
-
-	//NOTE: DEVIATION FROM MONGO: 1. These if statements are in a loop in the c++ version,
-	// 2. 'vars' and 'in' are each mandatory here. in the c++ code you only need one of the two.
-	// 3. Below, we croak if there are more than 2 arguments.  The original does not have this limitation, specifically.
-	// Upon further review, I think our code is more accurate.  The c++ code will accept if there are multiple 'in'
-	// or 'var' values. The previous ones will be overwritten by newer ones.
-	//
-	// Final note - I think this code is fine.
-	//
-	if(!varsElem) {
-		throw new Error("Missing 'vars' parameter to $let: 16876");
-	}
-	if(!inElem) {
-		throw new Error("Missing 'in' parameter to $let: 16877");
-	}
+	// if (!(exprFieldName === "$let")) throw new Error("Assertion failure"); //NOTE: DEVIATION FROM MONGO: we do not have exprFieldName here
 
-	// Should this be !== 2?  Why would we have fewer than 2 arguments?  Why do we even care what the length of the
-	// array is? It may be an optimization of sorts. But what we're really wanting here is, 'If any keys are not "in"
-	// or "vars" then we need to bugcheck.'
-	if(Object.keys(args).length > 2) {
-		var bogus = Object.keys(args).filter(function(x) {return !(x === 'in' || x === 'vars');});
-		throw new Error("Unrecognized parameter to $let: " + bogus.join(",") + "- 16875");
-	}
+	if (Value.getType(expr) !== "Object")
+		throw new Error("$let only supports an object as it's argument; uassert code 16874");
+	var args = expr;
 
-	var vpsSub = new VariablesParseState(vpsIn),
-		vars = {};
+	// varsElem must be parsed before inElem regardless of BSON order.
+	var varsElem,
+		inElem;
+	for (var argFieldName in args) {
+		var arg = args[argFieldName];
+		if (argFieldName === "vars") {
+			varsElem = arg;
+		} else if (argFieldName === "in") {
+			inElem = arg;
+		} else {
+			throw new Error("Unrecognized parameter to $let: " + argFieldName + "; uasserted code 16875");
+		}
+	}
 
-	for(var varName in varsElem) {
+	if (!varsElem)
+		throw new Error("Missing 'vars' parameter to $let; uassert code 16876");
+	if (!inElem)
+		throw new Error("Missing 'in' parameter to $let; uassert code 16877");
+
+	// parse "vars"
+	var vpsSub = vpsIn, // vpsSub gets our vars, vpsIn doesn't.
+		vars = {}; // using like a VariableMap
+	if (Value.getType(varsElem) !== "Object") //NOTE: emulate varsElem.embeddedObjectUserCheck()
+		throw new Error("invalid parameter: expected an object (vars); uasserted code 10065");
+	for (var varName in varsElem) {
+		var varElem = varsElem[varName];
 		Variables.uassertValidNameForUserWrite(varName);
 		var id = vpsSub.defineVariable(varName);
 
-		vars[id] = {};
-		vars[id][varName] = Expression.parseOperand(varsElem, vpsIn);
+		vars[id] = new NameAndExpression(varName,
+			Expression.parseOperand(varElem, vpsIn)); // only has outer vars
 	}
 
-	var subExpression = Expression.parseOperand(inElem, vpsSub);
+	// parse "in"
+	var subExpression = Expression.parseOperand(inElem, vpsSub); // has our vars
+
 	return new LetExpression(vars, subExpression);
 };
 
+
 proto.optimize = function optimize() {
-	if(this._variables.empty()) {
+	if (Object.keys(this._variables).length === 0) {
+		// we aren't binding any variables so just return the subexpression
 		return this._subExpression.optimize();
 	}
 
-	for(var id in this._variables){
-		for(var name in this._variables[id]) {
-			//NOTE: DEVIATION FROM MONGO: This is actually ok. The c++ code does this with a single map. The js structure
-			// is nested objects.
-			this._variables[id][name] = this._variables[id][name].optimize();
-		}
+	for (var id in this._variables) {
+		this._variables[id].expression = this._variables[id].expression.optimize();
 	}
 
+	// TODO be smarter with constant "variables"
 	this._subExpression = this._subExpression.optimize();
 
 	return this;
 };
 
+
 proto.serialize = function serialize(explain) {
 	var vars = {};
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			vars[name] = this._variables[id][name];
-		}
+	for (var id in this._variables) {
+		vars[this._variables[id].name] = this._variables[id].expression.serialize(explain);
 	}
 
-	return {$let: {vars:vars, 'in':this._subExpression.serialize(explain)}};
+	return {
+		$let: {
+			vars: vars,
+			in : this._subExpression.serialize(explain)
+		}
+	};
 };
 
+
 proto.evaluateInternal = function evaluateInternal(vars) {
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			vars.setValue(id, this._variables[id][name]);
-		}
+	for (var id in this._variables) {
+		var itFirst = +id, //NOTE: using the unary + to coerce it to a Number
+			itSecond = this._variables[itFirst];
+		// It is guaranteed at parse-time that these expressions don't use the variable ids we
+		// are setting
+		vars.setValue(itFirst,
+			itSecond.expression.evaluateInternal(vars));
 	}
 
 	return this._subExpression.evaluateInternal(vars);
 };
 
+
 proto.addDependencies = function addDependencies(deps, path){
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			this._variables[id][name].addDependencies(deps);
-		}
+	for (var id in this._variables) {
+		var itFirst = +id, //NOTE: using the unary + to coerce it to a Number
+			itSecond = this._variables[itFirst];
+			itSecond.expression.addDependencies(deps);
 	}
-	this._subExpression.addDependencies(deps, path);
-	return deps; //NOTE: DEVIATION FROM MONGO: The c++ version does not return a value. We seem to use the returned value
-					// (or something from a different method named
-					// addDependencies) in many places.
 
+	// TODO be smarter when CURRENT is a bound variable
+	this._subExpression.addDependencies(deps);
 };
+
+
+Expression.registerExpression("$let", LetExpression.parse);

+ 3 - 3
lib/pipeline/expressions/MultiplyExpression.js

@@ -2,14 +2,14 @@
 
 /**
  * A $multiply pipeline expression.
- * @see evaluateInternal
  * @class MultiplyExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
  */
-var MultiplyExpression = module.exports = function MultiplyExpression(){
-if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+var MultiplyExpression = module.exports = function MultiplyExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
 }, klass = MultiplyExpression, base = require("./VariadicExpressionT")(MultiplyExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 

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

@@ -3,9 +3,9 @@
 /**
  * Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class.
  * @class NaryBaseExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
  * @constructor
  */
 var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass) {
@@ -15,6 +15,10 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		base.call(this);
 	}, klass = NaryBaseExpression, NaryExpression = require("./NaryExpression"), base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
 	klass.parse = function(objExpr, vps) {
 		var expr = new SubClass(),
 			args = NaryExpression.parseArguments(objExpr, vps);
@@ -23,8 +27,5 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		return expr;
 	};
 
-	klass.parseArguments = base.parseArguments;		// NOTE: Need to explicitly
-													// bubble static members in
-													// our inheritance chain
 	return NaryBaseExpression;
 };

+ 2 - 2
lib/pipeline/expressions/NaryExpression.js

@@ -3,9 +3,9 @@
 /**
  * The base class for all n-ary `Expression`s
  * @class NaryExpression
+ * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
  */
 var NaryExpression = module.exports = function NaryExpression() {
@@ -62,7 +62,7 @@ proto.optimize = function optimize() {
 			// this is commutative and associative.  We detect sameness of
 			// the child operator by checking for equality of the opNames
 			var nary = expr instanceof NaryExpression ? expr : undefined;
-			if (!nary || nary.getOpName() !== this.getOpName) {
+			if (!nary || nary.getOpName() !== this.getOpName()) {
 				nonConstExprs.push(expr);
 			} else {
 				// same expression, so flatten by adding to vpOperand which

+ 1 - 1
lib/pipeline/expressions/ObjectExpression.js

@@ -49,7 +49,7 @@ proto.optimize = function optimize() {
 		if (!this._expressions.hasOwnProperty(key)) continue;
 		var expr = this._expressions[key];
 		if (expr)
-			expr.optimize();
+			this._expressions[key] = expr.optimize();
 	}
 	return this;
 };

+ 53 - 36
lib/pipeline/expressions/OrExpression.js

@@ -2,67 +2,84 @@
 
 /**
  * An $or pipeline expression.
- * @see evaluateInternal
  * @class OrExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var OrExpression = module.exports = function OrExpression(){
-//	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
 }, klass = OrExpression, base = require("./VariadicExpressionT")(OrExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	ConstantExpression = require("./ConstantExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$or";
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
-
-/**
- * Takes an array of one or more values and returns true if any of the values in the array are true. Otherwise $or returns false.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars){
-	for(var i = 0, n = this.operands.length; i < n; ++i){
+	var n = this.operands.length;
+	for (var i = 0; i < n; ++i) {
 		var value = this.operands[i].evaluateInternal(vars);
-		if (Value.coerceToBool(value)) return true;
+		if (Value.coerceToBool(value))
+			return true;
 	}
 	return false;
 };
 
 proto.optimize = function optimize() {
-	var pE = base.prototype.optimize.call(this); // optimize the disjunction as much as possible
+	// optimize the disjunction as much as possible
+	var expr = base.prototype.optimize.call(this);
 
-	if (!(pE instanceof OrExpression)) return pE; // if the result isn't a disjunction, we can't do anything
-	var pOr = pE;
+	// if the result isn't a disjunction, we can't do anything
+	var orExp = expr instanceof OrExpression ? expr : undefined;
+	if (!orExp)
+		return expr;
 
-	// Check the last argument on the result; if it's not const (as promised
-	// by ExpressionNary::optimize(),) then there's nothing we can do.
-	var n = pOr.operands.length;
+	/*
+	 * Check the last argument on the result; if it's not constant (as
+	 * promised by ExpressionNary::optimize(),) then there's nothing
+	 * we can do.
+	 */
+	var n = orExp.operands.length;
 	// ExpressionNary::optimize() generates an ExpressionConstant for {$or:[]}.
-	if (!n) throw new Error("OrExpression must have operands!");
-	var pLast = pOr.operands[n - 1];
-	if (!(pLast instanceof ConstantExpression)) return pE;
+	if (n <= 0) throw new Error("Assertion failuer");
+	var lastExpr = orExp.operands[n - 1],
+		constExpr = lastExpr instanceof ConstantExpression ? lastExpr : undefined;
+	if (!constExpr)
+		return expr;
 
-	// Evaluate and coerce the last argument to a boolean.  If it's true, then we can replace this entire expression.
-	var last = Value.coerceToBool();
-	if (last) return new ConstantExpression(true);
+	/*
+	 * Evaluate and coerce the last argument to a boolean.  If it's true,
+	 * then we can replace this entire expression.
+	 */
+	var last = Value.coerceToBool(constExpr.evaluateInternal());
+	if (last)
+		return ConstantExpression.create(true);
 
-	// If we got here, the final operand was false, so we don't need it anymore.
-	// If there was only one other operand, we don't need the conjunction either.  Note we still need to keep the promise that the result will be a boolean.
-	if (n == 2) return new CoerceToBoolExpression(pOr.operands[0]);
+	/*
+	 * If we got here, the final operand was false, so we don't need it
+	 * anymore.  If there was only one other operand, we don't need the
+	 * conjunction either.  Note we still need to keep the promise that
+	 * the result will be a boolean.
+	 */
+	if (n === 2)
+		return CoerceToBoolExpression.create(orExp.operands[0]);
 
-	// Remove the final "false" value, and return the new expression.
-	pOr.operands.length = n - 1;
-	return pE;
+	/*
+	 * Remove the final "false" value, and return the new expression.
+	 */
+	orExp.operands.length = n - 1;
+	return expr;
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$or", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$or";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 6 - 5
lib/pipeline/expressions/VariadicExpressionT.js

@@ -3,11 +3,11 @@
 /**
  * A factory and base class for all expressions that are variadic (AKA they accept any number of arguments)
  * @class VariadicExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryBaseExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-
+ */
 var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass) {
 
 	var VariadicExpression = function VariadicExpression() {
@@ -15,8 +15,9 @@ var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass
 		base.call(this);
 	}, klass = VariadicExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-	klass.parse = base.parse; 						// NOTE: Need to explicitly
-	klass.parseArguments = base.parseArguments;		// bubble static members in
-													// our inheritance chain
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
 	return VariadicExpression;
 };

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

@@ -12,7 +12,6 @@ module.exports = {
 	DivideExpression: require("./DivideExpression.js"),
 	Expression: require("./Expression.js"),
 	FieldPathExpression: require("./FieldPathExpression.js"),
-	FieldRangeExpression: require("./FieldRangeExpression.js"),
 	HourExpression: require("./HourExpression.js"),
 	IfNullExpression: require("./IfNullExpression.js"),
 	MinuteExpression: require("./MinuteExpression.js"),

+ 20 - 21
lib/pipeline/matcher/ComparisonMatchExpression.js

@@ -1,6 +1,6 @@
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression.js');
-var Value = require('../Value');
+var LeafMatchExpression = require("./LeafMatchExpression.js");
+var Value = require("../Value");
 
 /**
  * ComparisonMatchExpression
@@ -27,31 +27,31 @@ proto._rhs = undefined;
 proto.debugString = function debugString(level) {
 	var retStr = this._debugAddSpace(level) + this.path() + " ";
 	switch (this._matchType) {
-		case 'LT':
-			retStr += '$lt';
+		case "LT":
+			retStr += "$lt";
 			break;
-		case 'LTE':
-			retStr += '$lte';
+		case "LTE":
+			retStr += "$lte";
 			break;
-		case 'EQ':
-			retStr += '==';
+		case "EQ":
+			retStr += "==";
 			break;
-		case 'GT':
-			retStr += '$gt';
+		case "GT":
+			retStr += "$gt";
 			break;
-		case 'GTE':
-			retStr += '$gte';
+		case "GTE":
+			retStr += "$gte";
 			break;
 		default:
 			retStr += "Unknown comparison!";
 			break;
 	}
 
-	retStr += (this._rhs ? this._rhs.toString() : '?');
+	retStr += (this._rhs ? this._rhs.toString() : "?");
 	if (this.getTag()) {
 		retStr += this.getTag().debugString();
 	}
-	return retStr + '\n';
+	return retStr + "\n";
 };
 
 /**
@@ -96,11 +96,11 @@ proto.getRHS = function getRHS() {
  */
 proto.init = function init(path,rhs) {
 	this._rhs = rhs;
-	if ((rhs instanceof Object && Object.keys(rhs).length === 0)) return {'code':'BAD_VALUE', 'description':'Need a real operand'};
+	if ((rhs instanceof Object && Object.keys(rhs).length === 0)) return {"code":"BAD_VALUE", "description":"Need a real operand"};
 
-	if (rhs === undefined) return {'code':'BAD_VALUE', 'desc':'Cannot compare to undefined'};
+	if (rhs === undefined) return {"code":"BAD_VALUE", "desc":"Cannot compare to undefined"};
 	if (!(this._matchType in {"LT":1, "LTE":1, "EQ":1, "GT":1, "GTE":1})) {
-		return {'code':'BAD_VALUE', 'description':'Bad match type for ComparisonMatchExpression'};
+		return {"code":"BAD_VALUE", "description":"Bad match type for ComparisonMatchExpression"};
 	}
 	return this.initPath(path);
 };
@@ -118,7 +118,7 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 			return ["EQ","LTE","GTE"].indexOf(this._matchType) != -1;
 		}
 
-		if (['MaxKey','MinKey'].indexOf(Value.getType(this._rhs)) != -1) {
+		if (["MaxKey","MinKey"].indexOf(Value.getType(this._rhs)) != -1) {
 			return this._matchType !== "EQ";
 		}
 		return false;
@@ -128,17 +128,16 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 
 	switch(this._matchType) {
 		case "LT":
-			return x == -1;
+			return x < 0;
 		case "LTE":
 			return x <= 0;
 		case "EQ":
 			return x === 0;
 		case "GT":
-			return x === 1;
+			return x > 0;
 		case "GTE":
 			return x >= 0;
 		default:
 			throw new Error("Invalid comparison type evaluated.");
 	}
-	return false;
 };

+ 15 - 13
lib/pipeline/matcher/GTEMatchExpression.js

@@ -1,24 +1,26 @@
 "use strict";
 
-var ComparisonMatchExpression = require('./ComparisonMatchExpression');
+var ComparisonMatchExpression = require("./ComparisonMatchExpression");
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * File: matcher/expression_leaf.h
+ * @class GTEMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var GTEMatchExpression = module.exports = function GTEMatchExpression(){
-	base.call(this);
-	this._matchType = 'GTE';
-}, klass = GTEMatchExpression, base =  ComparisonMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	base.call(this, "GTE");
+}, klass = GTEMatchExpression, base = ComparisonMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 /**
- *
- * Return a new instance of this class, with fields set the same as ourself
  * @method shallowClone
- * @param
- *
  */
-proto.shallowClone = function shallowClone( /*  */ ){
-// File: expression_leaf.h lines: 141-144
+proto.shallowClone = function shallowClone(){
 	var e = new GTEMatchExpression();
-	e.init( this.path(), this._rhs );
+	e.init(this.path(), this._rhs);
+	if(this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 14 - 13
lib/pipeline/matcher/GTMatchExpression.js

@@ -1,25 +1,26 @@
 "use strict";
 
-var ComparisonMatchExpression = require('./ComparisonMatchExpression.js');
+var ComparisonMatchExpression = require("./ComparisonMatchExpression.js");
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * File: matcher/expression_leaf.h
+ * @class GTMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var GTMatchExpression = module.exports = function GTMatchExpression(){
-	base.call(this);
-	this._matchType = 'GT';
+	base.call(this, "GT");
 }, klass = GTMatchExpression, base = ComparisonMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-
 /**
- *
- * Return a new instance of this class, with fields set the same as ourself
  * @method shallowClone
- * @param
- *
  */
-proto.shallowClone = function shallowClone( /* */ ){
-	// File: expression_leaf.h lines: 130-133
+proto.shallowClone = function shallowClone(){
 	var e = new GTMatchExpression();
-	e.init( this.path(), this._rhs );
+	e.init(this.path(), this._rhs);
+	if(this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 14 - 12
lib/pipeline/matcher/LTEMatchExpression.js

@@ -1,24 +1,26 @@
 "use strict";
 
-var ComparisonMatchExpression = require('./ComparisonMatchExpression');
+var ComparisonMatchExpression = require("./ComparisonMatchExpression");
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * File: matcher/expression_leaf.h
+ * @class LTEMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var LTEMatchExpression = module.exports = function LTEMatchExpression(){
-	base.call(this);
-	this._matchType = 'LTE';
+	base.call(this, "LTE");
 }, klass = LTEMatchExpression, base = ComparisonMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 /**
- *
- * Return a new instance of this class, with fields set the same as ourself
  * @method shallowClone
- * @param
- *
  */
-proto.shallowClone = function shallowClone( /* */ ){
-	// File: expression_leaf.h lines: 108-111
+proto.shallowClone = function shallowClone(){
 	var e = new LTEMatchExpression();
-	e.init( this.path(), this._rhs );
+	e.init(this.path(), this._rhs);
+	if(this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 14 - 12
lib/pipeline/matcher/LTMatchExpression.js

@@ -1,24 +1,26 @@
 "use strict";
 
-var ComparisonMatchExpression = require('./ComparisonMatchExpression');
+var ComparisonMatchExpression = require("./ComparisonMatchExpression");
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * File: matcher/expression_leaf.h
+ * @class LTMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var LTMatchExpression = module.exports = function LTMatchExpression(){
-	base.call(this);
-	this._matchType = 'LT';
+	base.call(this, "LT");
 }, klass = LTMatchExpression, base = ComparisonMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 /**
- *
- * Return a new instance of this class, with fields set the same as ourself
  * @method shallowClone
- * @param
- *
  */
-proto.shallowClone = function shallowClone( /* */ ){
-	// File: expression_leaf.h lines: 119-122
+proto.shallowClone = function shallowClone(){
 	var e = new LTMatchExpression();
-	e.init( this.path(), this._rhs );
+	e.init(this.path(), this._rhs);
+	if(this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 2 - 1
package.json

@@ -34,7 +34,8 @@
     "mocha": "*",
     "jshint": "*",
     "jscoverage": "*",
-    "jscheckstyle": "*"
+    "jscheckstyle": "*",
+    "bson": "0.2.15"
   },
   "license": "AGPL",
   "private": true,

+ 0 - 0
test/lib/pipeline/accumulators/AddToSetAccumulator.js → test/lib/pipeline/accumulators/AddToSetAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/AvgAccumulator.js → test/lib/pipeline/accumulators/AvgAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/FirstAccumulator.js → test/lib/pipeline/accumulators/FirstAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/LastAccumulator.js → test/lib/pipeline/accumulators/LastAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/MinMaxAccumulator.js → test/lib/pipeline/accumulators/MinMaxAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/PushAccumulator.js → test/lib/pipeline/accumulators/PushAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/SumAccumulator.js → test/lib/pipeline/accumulators/SumAccumulator_test.js


+ 47 - 46
test/lib/pipeline/documentSources/SortDocumentSource.js

@@ -13,32 +13,32 @@ module.exports = {
 
 	"SortDocumentSource": {
 
-		"constructor()": {
+		// "constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SortDocumentSource();
-				});
-			}
+		// 	"should not throw Error when constructing without args": function testConstructor(){
+		// 		assert.doesNotThrow(function(){
+		// 			new SortDocumentSource();
+		// 		});
+		// 	}
 
-		},
+		// },
 
-		"#getSourceName()": {
+		// "#getSourceName()": {
 
-			"should return the correct source name; $sort": function testSourceName(){
-				var sds = new SortDocumentSource();
-				assert.strictEqual(sds.getSourceName(), "$sort");
-			}
+		// 	"should return the correct source name; $sort": function testSourceName(){
+		// 		var sds = new SortDocumentSource();
+		// 		assert.strictEqual(sds.getSourceName(), "$sort");
+		// 	}
 
-		},
+		// },
 
-		"#getFactory()": {
+		// "#getFactory()": {
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new SortDocumentSource().getFactory(), SortDocumentSource);
-			}
+		// 	"should return the constructor for this class": function factoryIsConstructor(){
+		// 		assert.strictEqual(new SortDocumentSource().getFactory(), SortDocumentSource);
+		// 	}
 
-		},
+		// },
 
 		"#getNext()": {
 			/** Assert that iterator state accessors consistently report the source is exhausted. */
@@ -125,11 +125,11 @@ module.exports = {
             * created with.
             */
             "should have equal json representation": function serializeToArrayCheck(){
-				var spec = {"sort":1};
-				var sds = new SortDocumentSource.createFromJson(spec, {});
+				var sds = SortDocumentSource.createFromJson({"sort":1}, {});
 
 				var array = [];
 				sds.serializeToArray(array, false);
+
 				assert.deepEqual(array, [ { '$sort': { '[object Object]': 1 } } ]);
 			},
 
@@ -258,33 +258,34 @@ module.exports = {
 				);
 			},
 
-			// "should sort documents with a compound key": function compoundKeySort(next) {
-			// 	var cwc = new CursorDocumentSource.CursorWithContext();
-			// 	var l = [{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}];
-			// 	cwc._cursor = new Cursor( l );
-			// 	var cds = new CursorDocumentSource(cwc);
-			// 	var sds = new SortDocumentSource();
-			// 	sds.addKey("a", false);
-			// 	sds.addKey("b", false);
-			// 	sds.setSource(cds);
+			"should sort documents with a compound key": function compoundKeySort(next) {
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				var l = [{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}];
+				cwc._cursor = new Cursor( l );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
 
-			// 	var docs = [], i = 0;
-			// 	async.doWhilst(
-			// 		function(cb) {
-			// 			sds.getNext(function(err, val) {
-			// 				docs[i] = val;
-			// 				return cb(err);
-			// 			});
-			// 		},
-			// 		function() {
-			// 			return docs[i++] !== DocumentSource.EOF;
-			// 		},
-			// 		function(err) {
-			// 			assert.deepEqual([{_id:5, a:12, b:7}, {_id:0, a:1, b:3}, {_id:1, a:0, b:2}, DocumentSource.EOF], docs);
-			// 			next();
-			// 		}
-			// 	);
-			// },
+				sds.addKey("a", false);
+				sds.addKey("b", false);
+				sds.setSource(cds);
+
+				var docs = [], i = 0;
+				async.doWhilst(
+					function(cb) {
+						sds.getNext(function(err, val) {
+							docs[i] = val;
+							return cb(err);
+						});
+					},
+					function() {
+						return docs[i++] !== DocumentSource.EOF;
+					},
+					function(err) {
+						assert.deepEqual([{_id:5, a:12, b:7}, {_id:0, a:1, b:3}, {_id:1, a:0, b:2}, DocumentSource.EOF], docs);
+						next();
+					}
+				);
+			},
 
 		// 	"should sort documents with a compound key in ascending order": function compoundAscendingKeySort(next) {
 		// 		var cwc = new CursorDocumentSource.CursorWithContext();

+ 206 - 90
test/lib/pipeline/expressions/AddExpression_test.js

@@ -1,132 +1,248 @@
 "use strict";
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
 
+// 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 TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var expr = new AddExpression();
+			this.populateOperands(expr);
+			var expectedResult = this.expectedResult instanceof Function ? this.expectedResult() : this.expectedResult;
+			if (expectedResult instanceof Date) //NOTE: DEVIATION FROM MONGO: special case for Date
+				return assert.strictEqual(Date(expectedResult), Date(expr.evaluate({})));
+			assert.strictEqual(expectedResult, expr.evaluate({}));
+		};
+		return klass;
+	})(),
+	SingleOperandBase = (function() {
+		var klass = function SingleOperandBase() {
+			base.apply(this, arguments);
+		}, base = ExpectedResultBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.populateOperands = function(expr) {
+			var operand = this.operand instanceof Function ? this.operand() : this.operand;
+			expr.addOperand(ConstantExpression.create(operand));
+		};
+		proto.expectedResult = function() {
+			var operand = this.operand instanceof Function ? this.operand() : this.operand;
+			return operand;
+		};
+		return klass;
+	})(),
+	TwoOperandBase = (function() {
+		var klass = function TwoOperandBase() {
+			base.apply(this, arguments);
+		}, base = ExpectedResultBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			base.prototype.run.call(this);
+            // Now add the operands in the reverse direction.
+            this._reverse = true;
+			base.prototype.run.call(this);
+		};
+		proto.populateOperands = function(expr) {
+			var operand1 = this.operand1 instanceof Function ? this.operand1() : this.operand1,
+				operand2 = this.operand1 instanceof Function ? this.operand2() : this.operand2;
+			expr.addOperand(ConstantExpression.create(this._reverse ? operand2 : operand1));
+			expr.addOperand(ConstantExpression.create(this._reverse ? operand1 : operand2));
+		};
+		proto._reverse = false;
+		return klass;
+	})();
+
+exports.AddExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new AddExpression() instanceof AddExpression);
+			assert(new AddExpression() instanceof Expression);
+		},
 
-//TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
-module.exports = {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AddExpression("bad stuff");
+			});
+		},
 
-	"AddExpression": {
+	},
 
-		"constructor()": {
+	"#getOpName()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AddExpression();
-				});
-			},
+		"should return the correct op name; $add": function() {
+			assert.equal(new AddExpression().getOpName(), "$add");
+		}
+	},
+
+	"#evaluate()": {
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new AddExpression(1);
-				});
-			}
+		"should return the operand if null document is given": function testNullDocument() {
+			/** $add with a NULL Document pointer, as called by ExpressionNary::optimize(). */
+			var expr = new AddExpression();
+			expr.addOperand(ConstantExpression.create(2));
+			assert.strictEqual(expr.evaluate({}), 2);
 		},
 
-		"#getOpName()": {
+		"should return 0 if no operands were given": function testNoOperands() {
+			/** $add without operands. */
+			var expr = new AddExpression();
+			assert.strictEqual(expr.evaluate({}), 0);
+		},
 
-			"should return the correct op name; $add": function testOpName(){
-				assert.equal(new AddExpression().getOpName(), "$add");
-			}
+		"should throw Error if a String operand was given": function testString() {
+			/** String type unsupported. */
+			var expr = new AddExpression();
+			expr.addOperand(ConstantExpression.create("a"));
+			assert.throws(function () {
+				expr.evaluate({});
+			});
+		},
 
+		"should throw Error if a Boolean operand was given": function testBool() {
+			var expr = new AddExpression();
+			expr.addOperand(ConstantExpression.create(true));
+			assert.throws(function () {
+				expr.evaluate({});
+			});
 		},
 
-		"#evaluateInternal()": {
+		"w/ 1 operand": {
 
-			"should return the operand if null document is given": function nullDocument(){
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(2));
-				assert.equal(expr.evaluateInternal(null), 2);
+			"should pass through a single int": function testInt() {
+        		/** Single int argument. */
+				new SingleOperandBase({
+					operand: 1,
+				}).run();
 			},
 
-			"should return 0 if no operands were given": function noOperands(){
-				var expr = new AddExpression();
-				assert.equal(expr.evaluateInternal({}), 0);
-			},
+			//SKIPPED: Long -- would be same as Int above
 
-			"should throw Error if a Date operand was given": function date(){
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(new Date()));
-				assert.throws(function(){
-					expr.evaluateInternal({});
-				});
+			"should pass through a single float": function testDouble() {
+				/** Single double argument. */
+				new SingleOperandBase({
+					operand: 99.99,
+				}).run();
 			},
 
-			"should throw Error if a String operand was given": function string(){
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(""));
-				assert.throws(function(){
-					expr.evaluateInternal({});
-				});
+			"should pass through a single date": function testDate() {
+				/** Single Date argument. */
+				new SingleOperandBase({
+					operand: new Date(12345),
+				}).run();
 			},
 
-			"should throw Error if a Boolean operand was given": function bool() {
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(true));
-				assert.throws(function() {
-					expr.evaluateInternal({});
-				});
+			"should pass through a single null": function testNull() {
+				/** Single null argument. */
+				new SingleOperandBase({
+					operand: null,
+				}).run();
 			},
 
-			"should pass thru a single number": function number() {
-				var expr = new AddExpression(),
-					input = 123,
-					expected = 123;
-				expr.addOperand(new ConstantExpression(input));
-				assert.equal(expr.evaluateInternal({}), expected);
+			"should pass through a single undefined": function testUndefined() {
+				/** Single undefined argument. */
+				new SingleOperandBase({
+					operand: undefined,
+					expectedResult: null,
+				}).run();
 			},
 
-			"should pass thru a single null": function nullSupport() {
-				var expr = new AddExpression(),
-					input = null,
-					expected = 0;
-				expr.addOperand(new ConstantExpression(input));
-				assert.equal(expr.evaluateInternal({}), expected);
+		},
+
+		"w/ 2 operands": {
+
+			"should add two ints": function testIntInt() {
+				/** Add two ints. */
+				new TwoOperandBase({
+					operand1: 1,
+					operand2: 5,
+					expectedResult: 6,
+				}).run();
 			},
 
-			"should pass thru a single undefined": function undefinedSupport() {
-				var expr = new AddExpression(),
-					input,
-					expected = 0;
-				expr.addOperand(new ConstantExpression(input));
-				assert.equal(expr.evaluateInternal({}), expected);
+			//SKIPPED: IntIntNoOverflow
+
+			//SKIPPED: IntLong
+
+			//SKIPPED: IntLongOverflow
+
+			"should add int and double": function testIntDouble() {
+				/** Adding an int and a double produces a double. */
+				new TwoOperandBase({
+					operand1: 9,
+					operand2: 1.1,
+					expectedResult: 10.1,
+				}).run();
 			},
 
-			"should add two numbers": function numbers() {
-				var expr = new AddExpression(),
-					inputs = [1, 5],
-					expected = 6;
-				inputs.forEach(function(input) {
-					expr.addOperand(new ConstantExpression(input));
-				});
-				assert.equal(expr.evaluateInternal({}), expected);
+			"should add int and date": function testIntDate() {
+				/** Adding an int and a Date produces a Date. */
+				new TwoOperandBase({
+					operand1: 6,
+					operand2: new Date(123450),
+					expectedResult: new Date(123456),
+				}).run();
 			},
 
-			"should add a number and a null": function numberAndNull() {
-				var expr = new AddExpression(),
-					inputs = [1, null],
-					expected = 1;
-				inputs.forEach(function(input) {
-					expr.addOperand(new ConstantExpression(input));
-				});
-				assert.equal(expr.evaluateInternal({}), expected);
+			//SKIPPED: LongDouble
+
+			//SKIPPED: LongDoubleNoOverflow
+
+			"should add int and null": function testIntNull() {
+				/** Adding an int and null. */
+				new TwoOperandBase({
+					operand1: 1,
+					operand2: null,
+					expectedResult: null,
+				}).run();
 			},
 
-			"should add a number and an undefined": function numberAndUndefined() {
-				var expr = new AddExpression(),
-					inputs = [1, undefined],
-					expected = 1;
-				inputs.forEach(function(input) {
-					expr.addOperand(new ConstantExpression(input));
-				});
-				assert.equal(expr.evaluateInternal({}), expected);
-			}
+			"should add long and undefined": function testLongUndefined() {
+				/** Adding a long and undefined. */
+				new TwoOperandBase({
+					operand1: 5e11,
+					operand2: undefined,
+					expectedResult: null,
+				}).run();
+			},
 
 		}
 
-	}
+	},
+
+	"optimize": {
+
+		"should understand a single number": function() {
+			var vps = new VariablesParseState(new VariablesIdGenerator()),
+				expr = Expression.parseOperand({$add:[123]}, vps).optimize();
+			assert.strictEqual(expr.operands.length, 0, "should optimize operands away");
+			assert(expr instanceof ConstantExpression);
+			assert.strictEqual(expr.evaluate(), 123);
+		},
+
+		"should optimize strings of numbers without regard to their order": function() {
+			var vps = new VariablesParseState(new VariablesIdGenerator()),
+				expr = Expression.parseOperand({$add:[1,2,3,'$a',4,5,6]}, vps).optimize();
+			assert.strictEqual(expr.operands.length, 2, "should optimize operands away");
+			assert(expr.operands[0] instanceof FieldPathExpression);
+			assert(expr.operands[1] instanceof ConstantExpression);
+			assert.strictEqual(expr.operands[1].evaluate(), 1 + 2 + 3 + 4 + 5 + 6);
+		},
+
+	},
 
 };
 
-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 - 0
test/lib/pipeline/expressions/AllElementsTrueExpression.js → test/lib/pipeline/expressions/AllElementsTrueExpression_test.js


+ 245 - 172
test/lib/pipeline/expressions/AndExpression_test.js

@@ -1,213 +1,286 @@
 "use strict";
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
-	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
-	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// 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 TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var expectedResult = this.expectedResult instanceof Function ? this.expectedResult() : this.expectedResult;
+			assert.strictEqual(expectedResult, expr.evaluate({a:1}));
+			var optimized = expr.optimize();
+			assert.strictEqual(expectedResult, optimized.evaluate({a:1}));
+		};
+		return klass;
+	})(),
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var optimized = expr.optimize(),
+				expectedOptimized = this.expectedOptimized instanceof Function ? this.expectedOptimized() : this.expectedOptimized;
+			assert.deepEqual(expectedOptimized, expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	NoOptimizeBase = (function() {
+		var klass = function NoOptimizeBase() {
+			base.apply(this, arguments);
+		}, base = OptimizeBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function() {
+			return constify(this.spec instanceof Function ? this.spec() : this.spec);
+		};
+		return klass;
+	})();
+
+exports.AndExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new AndExpression() instanceof AndExpression);
+			assert(new AndExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AndExpression("bad stuff");
+			});
+		},
 
+	},
 
-module.exports = {
+	"#getOpName()": {
 
-	"AndExpression": {
+		"should return the correct op name; $and": function() {
+			assert.equal(new AndExpression().getOpName(), "$and");
+		}
 
-		beforeEach: function() {
-			this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
+
+	"#evaluate()": {
+
+		"should return true if no operands": function testNoOperands() {
+			/** $and without operands. */
+			new ExpectedResultBase({
+				spec: {$and:[]},
+				expectedResult: true,
+			}).run();
 		},
 
-		"constructor()": {
+		"should return true if given true": function testTrue() {
+			/** $and passed 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AndExpression();
-				});
-			},
+		"should return false if given false": function testFalse() {
+			/** $and passed 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new AndExpression(1);
-				});
-			}
+		"should return true if given true and true": function testTrueTrue() {
+			/** $and passed 'true', 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, true]},
+				expectedResult: true,
+			}).run();
+		},
 
+		"should return false if given true and false": function testTrueFalse() {
+			/** $and passed 'true', 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, false]},
+				expectedResult: false,
+			}).run();
 		},
 
-		"#getOpName()": {
+		"should return false if given false and true": function testFalseTrue() {
+			/** $and passed 'false', 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[false, true]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return the correct op name; $and": function testOpName(){
-				assert.equal(new AndExpression().getOpName(), "$and");
-			}
+		"should return false if given false and false": function testFalseFalse() {
+			/** $and passed 'false', 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[false, false]},
+				expectedResult: false,
+			}).run();
+		},
 
+		"should return true if given true and true and true": function testTrueTrueTrue() {
+			/** $and passed 'true', 'true', 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, true, true]},
+				expectedResult: true,
+			}).run();
 		},
 
+		"should return false if given true and true and false": function testTrueTrueFalse() {
+			/** $and passed 'true', 'true', 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, true, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-		"#evaluate()": {
+		"should return false if given 0 and 1": function testZeroOne() {
+			/** $and passed '0', '1'. */
+			new ExpectedResultBase({
+				spec: {$and:[0, 1]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return true if no operands were given; {$and:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$and:[]},this.vps).evaluate(), true);
-			},
+		"should return true if given 1 and 2": function testOneTwo() {
+			/** $and passed '1', '2'. */
+			new ExpectedResultBase({
+				spec: {$and:[1, 2]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return true if operands is one true; {$and:[true]}": function testTrue(){
-				assert.equal(Expression.parseOperand({$and:[true]},this.vps).evaluate(), true);
-			},
+		"should return true if given a field path to a truthy value": function testFieldPath() {
+			/** $and passed a field path. */
+			new ExpectedResultBase({
+				spec: {$and:["$a"]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operands is one false; {$and:[false]}": function testFalse(){
-				assert.equal(Expression.parseOperand({$and:[false]},this.vps).evaluate(), false);
-			},
+	},
 
-			"should return true if operands are true and true; {$and:[true,true]}": function testTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true]},this.vps).evaluate(), true);
-			},
+	"#optimize()": {
 
-			"should return false if operands are true and false; {$and:[true,false]}": function testTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,false]},this.vps).evaluate(), false);
-			},
+		"should optimize a constant expression": function testOptimizeConstantExpression() {
+			/** A constant expression is optimized to a constant. */
+			new OptimizeBase({
+				spec: {$and:[1]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should return false if operands are false and true; {$and:[false,true]}": function testFalseTrue(){
-				assert.equal(Expression.parseOperand({$and:[false,true]},this.vps).evaluate(), false);
-			},
+		"should not optimize a non constant": function testNonConstant() {
+			/** A non constant expression is not optimized. */
+			new NoOptimizeBase({
+				spec: {$and:["$a"]},
+			}).run();
+		},
 
-			"should return false if operands are false and false; {$and:[false,false]}": function testFalseFalse(){
-				assert.equal(Expression.parseOperand({$and:[false,false]},this.vps).evaluate(), false);
-			},
+		"should optimize truthy constant and truthy expression": function testConstantNonConstantTrue() {
+			/** An expression beginning with a single constant is optimized. */
+			new OptimizeBase({
+				spec: {$and:[1,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+			// note: using $and as serialization of ExpressionCoerceToBool rather than ExpressionAnd
+		},
 
-			"should return true if operands are true, true, and true; {$and:[true,true,true]}": function testTrueTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true,true]},this.vps).evaluate(), true);
-			},
+		"should optimize falsy constant and truthy expression": function testConstantNonConstantFalse() {
+			new OptimizeBase({
+				spec: {$and:[0,"$a"]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
-			"should return false if operands are true, true, and false; {$and:[true,true,false]}": function testTrueTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,true,false]},this.vps).evaluate(), false);
-			},
+		"should optimize truthy expression and truthy constant": function testNonConstantOne() {
+			/** An expression with a field path and '1'. */
+			new OptimizeBase({
+				spec: {$and:["$a",1]},
+				expectedOptimized: {$and:["$a"]}
+			}).run();
+		},
 
-			"should return false if operands are 0 and 1; {$and:[0,1]}": function testZeroOne(){
-				assert.equal(Expression.parseOperand({$and:[0,1]},this.vps).evaluate(), false);
-			},
+		"should optimize truthy expression and falsy constant": function testNonConstantZero() {
+			/** An expression with a field path and '0'. */
+			new OptimizeBase({
+				spec: {$and:["$a",0]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
-			"should return false if operands are 1 and 2; {$and:[1,2]}": function testOneTwo(){
-				assert.equal(Expression.parseOperand({$and:[1,2]},this.vps).evaluate(), true);
-			},
+		"should optimize truthy expression, falsy expression, and truthy constant": function testNonConstantNonConstantOne() {
+			/** An expression with two field paths and '1'. */
+			new OptimizeBase({
+				spec: {$and:["$a","$b",1]},
+				expectedOptimized: {$and:["$a","$b"]}
+			}).run();
+		},
 
-			"should return true if operand is a path String to a truthy value; {$and:['$a']}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$and:['$a']},this.vps).evaluate({a:1}), true);
-			}
+		"should optimize truthy expression, falsy expression, and falsy constant": function testNonConstantNonConstantZero() {
+			/** An expression with two field paths and '0'. */
+			new OptimizeBase({
+				spec: {$and:["$a","$b",0]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
+		"should optimize to false if [0,1,'$a']": function testZeroOneNonConstant() {
+			/** An expression with '0', '1', and a field path. */
+			new OptimizeBase({
+				spec: {$and:[0,1,"$a"]},
+				expectedOptimized: {$const:false},
+			}).run();
 		},
 
-		"#optimize()": {
+		"should optimize to {$and:'$a'} if [1,1,'$a']": function testOneOneNonConstant() {
+			/** An expression with '1', '1', and a field path. */
+			new OptimizeBase({
+				spec: {$and:[1,1,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+		},
 
-			"should optimize a constant expression to a constant; {$and:[1]} == true": function testOptimizeConstantExpression(){
-				var a = Expression.parseOperand({$and:[1]}, this.vps).optimize();
-				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
-				assert.equal(a.evaluateInternal(), true);
-			},
+		"should optimize away nested truthy $and expressions": function testNested() {
+			/** Nested $and expressions. */
+			new OptimizeBase({
+				spec: {$and:[1, {$and:[1]}, "$a", "$b"]},
+				expectedOptimized: {$and:["$a","$b"]},
+			}).run();
+		},
 
-			"should not optimize a non-constant expression; {$and:['$a']}": function testNonConstant(){
-				var a = Expression.parseOperand({$and:['$a']}, this.vps).optimize();
-				assert.equal(a.operands[0]._fieldPath.fieldNames.length, 2);
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
-			},
-
-			"should not optimize an expression ending with a non-constant. {$and:[1,'$a']};": function testConstantNonConstant(){
-				var a = Expression.parseOperand({$and:[1,'$a']}, this.vps).optimize();
-				assert(a instanceof CoerceToBoolExpression);
-				assert(a.expression instanceof FieldPathExpression);
-
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-
-			"should optimize an expression with a path and a '1'; {$and:['$a',1]}": function testNonConstantOne(){
-				var a = Expression.parseOperand({$and:['$a', 1]}, this.vps).optimize();
-				// The 1 should be removed as it is redundant.
-				assert(a instanceof CoerceToBoolExpression, "The result should be forced to a boolean");
-
-				// This is the '$a' which cannot be optimized.
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-
-			"should optimize an expression with a field path and a '0'; {$and:['$a',0]}": function testNonConstantZero(){
-				var a = Expression.parseOperand({$and:['$a',0]}, this.vps).optimize();
-				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
-				assert.equal(a.evaluateInternal(), false, "The 0 operand should have been converted to false");
-			},
-
-			"should optimize an expression with two field paths and '1'; {$and:['$a','$b',1]}": function testNonConstantNonConstantOne(){
-				var a = Expression.parseOperand({$and:['$a', '$b', 1]}, this.vps).optimize();
-				assert.equal(a.operands.length, 2, "Two operands should remain.");
-
-				// This is the '$a' which cannot be optimized.
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames.length, 2);
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
-
-				// This is the '$b' which cannot be optimized.
-				assert.deepEqual(a.operands[1]._fieldPath.fieldNames.length, 2);
-				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[0], "CURRENT");
-				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[1], "b");
-			},
-
-			"should optimize an expression with two field paths and '0'; {$and:['$a','$b',0]}": function testNonConstantNonConstantZero(){
-				var a = Expression.parseOperand({$and:['$a', '$b', 0]}, this.vps).optimize();
-				assert(a instanceof ConstantExpression, "With that trailing false, we know the result...");
-				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
-				assert.equal(a.evaluateInternal(), false);
-			},
-
-			"should optimize an expression with '0', '1', and a field path; {$and:[0,1,'$a']}": function testZeroOneNonConstant(){
-				var a = Expression.parseOperand({$and:[0,1,'$a']}, this.vps).optimize();
-				assert(a instanceof ConstantExpression);
-				assert.equal(a.evaluateInternal(), false);
-			},
-
-			"should optimize an expression with '1', '1', and a field path; {$and:[1,1,'$a']}": function testOneOneNonConstant(){
-				var a = Expression.parseOperand({$and:[1,1,'$a']}, this.vps).optimize();
-				assert(a instanceof CoerceToBoolExpression);
-				assert(a.expression instanceof FieldPathExpression);
-
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-
-			"should optimize nested $and expressions properly and optimize out values evaluating to true; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				var a = Expression.parseOperand({$and:[1,{$and:[1]},'$a','$b']}, this.vps).optimize();
-				assert.equal(a.operands.length, 2)
-				assert(a.operands[0] instanceof FieldPathExpression);
-				assert(a.operands[1] instanceof FieldPathExpression);
-			},
-
-			"should optimize nested $and expressions containing a nested value evaluating to false; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				//assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize().toJSON(true), {$const:false});
-				var a = Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize();
-				assert(a instanceof ConstantExpression);
-				assert.equal(a.evaluateInternal(), false);
-			},
-
-			"should optimize when the constants are on the right of the operand list. The rightmost is true": function(){
-				// 1, "x", and 1 are all true.  They should be optimized away.
-				var a = Expression.parseOperand({$and:['$a', 1, "x", 1]}, this.vps).optimize();
-				assert(a instanceof CoerceToBoolExpression);
-				assert(a.expression instanceof FieldPathExpression);
-
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-			"should optimize when the constants are on the right of the operand list. The rightmost is false": function(){
-				// 1, "x", and 1 are all true.  They should be optimized away.
-				var a = Expression.parseOperand({$and:['$a', 1, "x", 0]}, this.vps).optimize();
-				assert(a instanceof ConstantExpression, "The rightmost false kills it all");
-				assert.equal(a.evaluateInternal(), false);
-			}
-		}
+		"should optimize to false if nested falsey $and expressions": function testNestedZero() {
+			/** Nested $and expressions containing a nested value evaluating to false. */
+			new OptimizeBase({
+				spec: {$and:[1, {$and:[ {$and:[0]} ]}, "$a", "$b"]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 0
test/lib/pipeline/expressions/AnyElementTrueExpression.js → test/lib/pipeline/expressions/AnyElementTrueExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/CoerceToBoolExpression.js → test/lib/pipeline/expressions/CoerceToBoolExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/CompareExpression.js → test/lib/pipeline/expressions/CompareExpression_test.js


+ 1 - 1
test/lib/pipeline/expressions/ConcatExpression_test.js

@@ -80,7 +80,7 @@ exports.ConcatExpression = {
 		},
 
 		"should throw if an operand is a boolean": function() {
-			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps)
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
 			assert.throws(function() {
 				expr.evaluate({a:true});
 			});

+ 0 - 0
test/lib/pipeline/expressions/DayOfMonthExpression.js → test/lib/pipeline/expressions/DayOfMonthExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/DayOfWeekExpression.js → test/lib/pipeline/expressions/DayOfWeekExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/DayOfYearExpression.js → test/lib/pipeline/expressions/DayOfYearExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/FieldPathExpression.js → test/lib/pipeline/expressions/FieldPathExpression_test.js


+ 0 - 139
test/lib/pipeline/expressions/FieldRangeExpression.js

@@ -1,139 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	FieldRangeExpression = require("../../../../lib/pipeline/expressions/FieldRangeExpression");
-
-
-module.exports = {
-
-	"FieldRangeExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function testInvalid(){
-				assert.throws(function() {
-					new FieldRangeExpression();
-				});
-			}
-
-		},
-
-		"#evaluate()": {
-
-
-			"$eq": {
-
-				"should return false if documentValue < rangeValue": function testEqLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 1).evaluate({a:0}), false);
-				},
-
-				"should return true if documentValue == rangeValue": function testEqEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 1).evaluate({a:1}), true);
-				},
-
-				"should return false if documentValue > rangeValue": function testEqGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 1).evaluate({a:2}), false);
-				}
-
-			},
-
-			"$lt": {
-
-				"should return true if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lt", "y").evaluate({a:"x"}), true);
-				},
-
-				"should return false if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lt", "y").evaluate({a:"y"}), false);
-				},
-
-				"should return false if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lt", "y").evaluate({a:"z"}), false);
-				}
-
-			},
-
-			"$lte": {
-
-				"should return true if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lte", 1.1).evaluate({a:1.0}), true);
-				},
-
-				"should return true if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lte", 1.1).evaluate({a:1.1}), true);
-				},
-
-				"should return false if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lte", 1.1).evaluate({a:1.2}), false);
-				}
-
-			},
-
-			"$gt": {
-
-				"should return false if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gt", 100).evaluate({a:50}), false);
-				},
-
-				"should return false if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gt", 100).evaluate({a:100}), false);
-				},
-
-				"should return true if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gt", 100).evaluate({a:150}), true);
-				}
-
-			},
-
-			"$gte": {
-
-				"should return false if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gte", "abc").evaluate({a:"a"}), false);
-				},
-
-				"should return true if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gte", "abc").evaluate({a:"abc"}), true);
-				},
-
-				"should return true if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gte", "abc").evaluate({a:"abcd"}), true);
-				}
-
-			},
-
-			"should throw Error if given multikey values": function testMultikey(){
-				assert.throws(function(){
-					new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 0).evaluate({a:[1,0,2]});
-				});
-			}
-
-		},
-
-//		"#optimize()": {
-//			"should optimize if ...": function testOptimize(){
-//			},
-//			"should not optimize if ...": function testNoOptimize(){
-//			}
-//		},
-
-		"#addDependencies()": {
-
-			"should return the range's path as a dependency": function testDependencies(){
-				var deps = new FieldRangeExpression(new FieldPathExpression("a.b.c"), "$eq", 0).addDependencies({});
-				assert.strictEqual(Object.keys(deps).length, 1);
-				assert.ok(deps['a.b.c']);
-			}
-
-		},
-
-//		"#intersect()": {
-//		},
-
-//		"#toJSON()": {
-//		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 0
test/lib/pipeline/expressions/HourExpression.js → test/lib/pipeline/expressions/HourExpression_test.js


+ 197 - 0
test/lib/pipeline/expressions/LetExpression_test.js

@@ -0,0 +1,197 @@
+"use strict";
+var assert = require("assert"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	LetExpression = require("../../../../lib/pipeline/expressions/LetExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	MultiplyExpression = require("../../../../lib/pipeline/expressions/MultiplyExpression"), //jshint ignore:line
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"), //jshint ignore:line
+	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"), //jshint ignore:line
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"), //jshint ignore:line
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	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));
+
+exports.LetExpression = {
+
+	beforeEach: function() {
+		this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
+
+	"constructor()": {
+
+		"should throw an Error when constructing without args": function() {
+			assert.throws(function() {
+				new LetExpression();
+			});
+		},
+
+		"should throw Error when constructing with one arg": function() {
+			assert.throws(function() {
+				new LetExpression(1);
+			});
+		},
+
+		"should not throw when constructing with two args": function() {
+			assert.doesNotThrow(function() {
+				new LetExpression(1, 2);
+			});
+		},
+
+	},
+
+	"#parse()": {
+
+		"should throw if $let isn't in expr": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$xlet: ['$$a', 1]}, self.vps);
+			}, /15999/);
+		},
+
+		"should throw if the $let expression isn't an object": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: "this is not an object"}, self.vps);
+			}, /16874/);
+		},
+
+		"should throw if the $let expression is an array": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: [1, 2, 3]}, self.vps);
+			}, /16874/);
+		},
+
+		"should throw if there is no vars parameter to $let": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: {vars: undefined}}, self.vps);
+			}, /16876/);
+		},
+
+		"should throw if there is no input parameter to $let": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: {vars: 1, in: undefined}}, self.vps);
+			}, /16877/);
+		},
+
+		"should throw if any of the arguments to $let are not 'in' or 'vars'": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: {vars: 1, in: 2, zoot:3}}, self.vps);
+			}, /16875/);
+		},
+
+		"should return a Let expression": function() {
+			var x = Expression.parseOperand({$let: {vars: {a:{$const:123}}, in: 2}}, this.vps);
+			assert(x instanceof LetExpression);
+			assert(x._subExpression instanceof ConstantExpression);
+			assert.strictEqual(x._subExpression.getValue(), 2);
+			assert(x._variables[0].expression instanceof ConstantExpression);
+			assert.strictEqual(x._variables[0].expression.getValue(), 123);
+		},
+
+		"should show we collect multiple vars": function() {
+			var x = Expression.parseOperand({$let: {vars: {a:{$const:1}, b:{$const:2}, c:{$const:3}}, in: 2}}, this.vps);
+			assert.strictEqual(x._variables[0].expression.getValue(), 1);
+			assert.strictEqual(x._variables[1].expression.getValue(), 2);
+			assert.strictEqual(x._variables[2].expression.getValue(), 3);
+		},
+
+	},
+
+	"#optimize()": {
+
+		beforeEach: function() {
+			this.testInOpt = function(expr, expected) {
+				assert(expr._subExpression instanceof ConstantExpression, "should have $const subexpr");
+				assert.strictEqual(expr._subExpression.operands.length, 0);
+				assert.strictEqual(expr._subExpression.getValue(), expected);
+			};
+			this.testVarOpt = function(expr, expected) {
+				var varExpr = expr._variables[0].expression;
+				assert(varExpr instanceof ConstantExpression, "should have $const first var");
+				assert.strictEqual(varExpr.getValue(), expected);
+			};
+		},
+
+		"should optimize to subexpression if no variables": function() {
+			var x = Expression.parseOperand({$let:{vars:{}, in:{$multiply:[2,3]}}}, this.vps).optimize();
+			assert(x instanceof ConstantExpression, "should become $const");
+			assert.strictEqual(x.getValue(), 6);
+		},
+
+		"should optimize variables": function() {
+			var x = Expression.parseOperand({$let:{vars:{a:{$multiply:[5,4]}}, in:{$const:6}}}, this.vps).optimize();
+			this.testVarOpt(x, 20);
+		},
+
+		"should optimize subexpressions if there are variables": function() {
+			var x = Expression.parseOperand({$let:{vars:{a:{$multiply:[5,4]}}, in: {$multiply:[2,3]}}}, this.vps).optimize();
+			this.testInOpt(x, 6);
+			this.testVarOpt(x, 20);
+		},
+
+	},
+
+	"#serialize()": {
+
+		"should serialize variables and the subexpression": function() {
+			var s = Expression.parseOperand({$let: {vars: {a:{$const:1}, b:{$const:2}}, in: {$multiply: [2,3]}}}, this.vps).optimize().serialize("zoot");
+			var expected = {$let:{vars:{a:{$const:1},b:{$const:2}},in:{$const:6}}};
+			assert.deepEqual(s, expected);
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should perform the evaluation for variables and the subexpression": function() {
+			var x = Expression.parseOperand({$let: {vars: {a: '$in1', b: '$in2'}, in: { $multiply: ["$$a", "$$b"] }}}, this.vps).optimize();
+			var	y = x.evaluate(new Variables(10, {in1: 6, in2: 7}));
+			assert.equal(y, 42);
+		},
+
+	},
+
+	"#addDependencies()": {
+
+		"should add dependencies": function() {
+			var expr = Expression.parseOperand({$let: {vars: {a: {$multiply:['$a','$b']}}, in: {$multiply: ['$c','$d']}}}, this.vps);
+			var deps = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.equal(Object.keys(deps.fields).length, 4);
+			assert('a' in deps.fields);
+			assert('b' in deps.fields);
+			assert('c' in deps.fields);
+			assert('d' in deps.fields);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
+		},
+
+	},
+
+	"The Gauntlet": {
+
+		"example from http://docs.mongodb.org/manual/reference/operator/aggregation/let/": function() {
+			var x = Expression.parseOperand(
+				{$let: { vars: { total: { $add: [ '$price', '$tax' ] },	discounted: { $cond: { if: '$applyDiscount', then: 0.9, else: 1 } }}, in: { $multiply: [ '$$total', '$$discounted' ] }}},
+				this.vps).optimize();
+			var y;
+			y = x.evaluate(new Variables(10, {price: 90, tax: 0.05}));
+			assert.equal(y, 90.05);
+			y = x.evaluate(new Variables(10, {price: 90, tax: 0.05, applyDiscount: 1}));
+			assert.equal(y, 90.05 * 0.9);
+		},
+
+	},
+
+};
+
+
+if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 9 - 9
test/lib/pipeline/expressions/MapExpression_test.js

@@ -19,16 +19,16 @@ exports.MapExpression = {
 
 	"constructor()": {
 
-		"should accept 4 arguments": function () {
+		"should accept 4 arguments": function() {
 			new MapExpression(1, 2, 3, 4);
 		},
 
-		"should accept only 4 arguments": function () {
-			assert.throws(function () { new MapExpression(); });
-			assert.throws(function () { new MapExpression(1); });
-			assert.throws(function () { new MapExpression(1, 2); });
-			assert.throws(function () { new MapExpression(1, 2, 3); });
-			assert.throws(function () { new MapExpression(1, 2, 3, 4, 5); });
+		"should accept only 4 arguments": function() {
+			assert.throws(function() { new MapExpression(); });
+			assert.throws(function() { new MapExpression(1); });
+			assert.throws(function() { new MapExpression(1, 2); });
+			assert.throws(function() { new MapExpression(1, 2, 3); });
+			assert.throws(function() { new MapExpression(1, 2, 3, 4, 5); });
 		},
 
 	},
@@ -47,7 +47,7 @@ exports.MapExpression = {
 				optimized = expr.optimize();
 			assert.strictEqual(optimized, expr, "should be same reference");
 			assert.deepEqual(expressionToJson(optimized._input), {$const:[1,2,3]});
-			assert.deepEqual(expressionToJson(optimized._each), constify({$add:["$$i","$$i",1,2]}));
+			assert.deepEqual(expressionToJson(optimized._each), constify({$add:["$$i","$$i",3]}));
 		},
 
 	},
@@ -92,7 +92,7 @@ exports.MapExpression = {
 
 	"#addDependencies()": {
 
-		"should add dependencies to both $map.input and $map.in": function () {
+		"should add dependencies to both $map.input and $map.in": function() {
 			var spec = {$map:{
 					input: "$inputArray",
 					as: "i",

+ 0 - 0
test/lib/pipeline/expressions/MillisecondExpression.js → test/lib/pipeline/expressions/MillisecondExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/MinuteExpression.js → test/lib/pipeline/expressions/MinuteExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/ModExpression.js → test/lib/pipeline/expressions/ModExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/MonthExpression.js → test/lib/pipeline/expressions/MonthExpression_test.js


+ 37 - 33
test/lib/pipeline/expressions/NaryExpression_test.js

@@ -3,11 +3,16 @@
 var assert = require("assert"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	NaryExpression = require("../../../../lib/pipeline/expressions/NaryExpression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
-	utils = require("./utils");
+	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"), //jshint ignore:line
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"), //jshint ignore:line
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
 
 
 // Mocha one-liner to make these tests self-hosted
@@ -16,13 +21,11 @@ if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({
 
 // A dummy child of NaryExpression used for testing
 var Testable = (function(){
-	// CONSTRUCTOR
 	var klass = function Testable(isAssociativeAndCommutative){
 		this._isAssociativeAndCommutative = isAssociativeAndCommutative;
 		base.call(this);
 	}, base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-	// MEMBERS
 	proto.evaluateInternal = function evaluateInternal(vars) {
 		// Just put all the values in a list.  This is not associative/commutative so
 		// the results will change if a factory is provided and operations are reordered.
@@ -54,14 +57,15 @@ var Testable = (function(){
 		var idGenerator = new VariablesIdGenerator(),
 			vps = new VariablesParseState(idGenerator),
 			testable = Testable.create(haveFactory);
-		operands.forEach(function(element) {
+		for (var i = 0, l = operands.length; i < l; i++) {
+			var element = operands[i];
 			testable.addOperand(Expression.parseOperand(element, vps));
-		});
+		}
 		return testable;
 	};
 
 	proto.assertContents = function assertContents(expectedContents) {
-		assert.deepEqual(utils.constify({$testable:expectedContents}), utils.expressionToJson(this));
+		assert.deepEqual(constify({$testable:expectedContents}), expressionToJson(this));
 	};
 
 	return klass;
@@ -103,26 +107,23 @@ exports.NaryExpression = {
 		var testable = Testable.create();
 
 		var assertDependencies = function assertDependencies(expectedDeps, expr) {
-			var deps = {}, //TODO: new DepsTracker
-				depsJson = [];
+			var deps = new DepsTracker();
 			expr.addDependencies(deps);
-			deps.forEach(function(dep) {
-				depsJson.push(dep);
-			});
+			var depsJson = Object.keys(deps.fields).sort();
 			assert.deepEqual(depsJson, expectedDeps);
-			assert.equal(deps.needWholeDocument, false);
-			assert.equal(deps.needTextScore, false);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
 		};
 
 		// No arguments.
 		assertDependencies([], testable);
 
 		// Add a constant argument.
-		testable.addOperand(new ConstantExpression(1));
+		testable.addOperand(ConstantExpression.create(1));
 		assertDependencies([], testable);
 
 		// Add a field path argument.
-		testable.addOperand(new FieldPathExpression("ab.c"));
+		testable.addOperand(FieldPathExpression.create("ab.c"));
 		assertDependencies(["ab.c"], testable);
 
 		// Add an object expression.
@@ -131,7 +132,7 @@ exports.NaryExpression = {
 			ctx = new Expression.ObjectCtx({isDocumentOk:true}),
 			vps = new VariablesParseState(new VariablesIdGenerator());
 		testable.addOperand(Expression.parseObject(specElement, ctx, vps));
-		assertDependencies(["ab.c", "r", "x"]);
+		assertDependencies(["ab.c", "r", "x"], testable);
 	},
 
 	/** Serialize to an object. */
@@ -159,7 +160,7 @@ exports.NaryExpression = {
 		var spec = [{$and:[]}, "$abc"],
 			testable = Testable.createFromOperands(spec);
 		testable.assertContents(spec);
-		assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+		assert.strictEqual(testable, testable.optimize());
 		testable.assertContents([true, "$abc"]);
 	},
 
@@ -169,8 +170,8 @@ exports.NaryExpression = {
 			testable = Testable.createFromOperands(spec);
 		testable.assertContents(spec);
 		var optimized = testable.optimize();
-		assert.notDeepEqual(testable.serialize(), optimized.serialize());
-		assert.deepEqual({$const:[1,2]}, utils.expressionToJson(optimized));
+		assert.notStrictEqual(testable, optimized);
+		assert.deepEqual({$const:[1,2]}, expressionToJson(optimized));
 	},
 
 	"NoFactoryOptimize": {
@@ -201,7 +202,7 @@ exports.NaryExpression = {
 		// The constant expressions are evaluated separately and placed at the end.
 		var testable = Testable.createFromOperands([55, 66, "$path"], true),
 			optimized = testable.optimize();
-		assert.deepEqual(utils.constify({$testable:["$path", [55, 66]]}), utils.expressionToJson(optimized));
+		assert.deepEqual(constify({$testable:["$path", [55, 66]]}), expressionToJson(optimized));
 	},
 
 	/** Factory optimization flattens nested operators of the same type. */
@@ -209,18 +210,23 @@ exports.NaryExpression = {
 		var testable = Testable.createFromOperands(
 				[55, "$path", {$add:[5,6,"$q"]}, 66],
 			true);
+		// Add a nested $testable operand.
 		testable.addOperand(Testable.createFromOperands(
 				[99, 100, "$another_path"],
-			true));
+			true)
+		);
 		var optimized = testable.optimize();
 		assert.deepEqual(
-			utils.constify({$testable:[
-					"$path",
-					{$add:["$q", 11]},
-					"$another_path",
-					[55, 66, [99, 100]]
-				]}),
-			utils.expressionToJson(optimized));
+			constify({$testable:[
+				// non constant parts
+				"$path",
+				{$add:["$q", 11]},
+				"$another_path",
+				// constant part last
+				[55, 66, [99, 100]]
+			]}),
+			expressionToJson(optimized)
+		);
 	},
 
 	/** Three layers of factory optimization are flattened. */
@@ -231,10 +237,8 @@ exports.NaryExpression = {
 		top.addOperand(nested);
 		var optimized = top.optimize();
 		assert.deepEqual(
-			utils.constify({
-				$testable: ["$a", "$b", "$c", [1, 2, [3, 4, [5, 6]]]]
-			}),
-			utils.expressionToJson(optimized)
+			constify({$testable: ["$a", "$b", "$c", [1, 2, [3, 4, [5, 6]]]]}),
+			expressionToJson(optimized)
 		);
 	},
 

+ 0 - 0
test/lib/pipeline/expressions/ObjectExpression.js → test/lib/pipeline/expressions/ObjectExpression_test.js


+ 245 - 101
test/lib/pipeline/expressions/OrExpression_test.js

@@ -1,143 +1,287 @@
 "use strict";
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	OrExpression = require("../../../../lib/pipeline/expressions/OrExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// 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 TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var expectedResult = this.expectedResult instanceof Function ? this.expectedResult() : this.expectedResult;
+			assert.strictEqual(expectedResult, expr.evaluate({a:1}));
+			var optimized = expr.optimize();
+			assert.strictEqual(expectedResult, optimized.evaluate({a:1}));
+		};
+		return klass;
+	})(),
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var optimized = expr.optimize(),
+				expectedOptimized = this.expectedOptimized instanceof Function ? this.expectedOptimized() : this.expectedOptimized;
+			assert.deepEqual(expectedOptimized, expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	NoOptimizeBase = (function() {
+		var klass = function NoOptimizeBase() {
+			base.apply(this, arguments);
+		}, base = OptimizeBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function() {
+			return constify(this.spec instanceof Function ? this.spec() : this.spec);
+		};
+		return klass;
+	})();
+
+exports.OrExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new OrExpression() instanceof OrExpression);
+			assert(new OrExpression() instanceof Expression);
+		},
 
+		"should error if given args": function() {
+			assert.throws(function() {
+				new OrExpression("bad stuff");
+			});
+		},
 
-module.exports = {
+	},
 
-	"OrExpression": {
+	"#getOpName()": {
 
-		"constructor()": {
+		"should return the correct op name; $or": function(){
+			assert.equal(new OrExpression().getOpName(), "$or");
+		}
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new OrExpression();
-				});
-			},
+	},
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new OrExpression(1);
-				});
-			}
+	"#evaluate()": {
 
+		"should return false if no operands": function testNoOperands(){
+			/** $or without operands. */
+			new ExpectedResultBase({
+				spec: {$or:[]},
+				expectedResult: false,
+			}).run();
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $or": function testOpName(){
-				assert.equal(new OrExpression().getOpName(), "$or");
-			}
-
+		"should return true if given true": function testTrue(){
+			/** $or passed 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[true]},
+				expectedResult: true,
+			}).run();
 		},
 
-		"#getFactory()": {
-
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.equal(new OrExpression().getFactory(), OrExpression);
-			}
-
+		"should return false if given false": function testFalse(){
+			/** $or passed 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[false]},
+				expectedResult: false,
+			}).run();
 		},
 
-		"#evaluateInternalInternal()": {
-
-			"should return false if no operors were given; {$or:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$or:[]}).evaluateInternal(), false);
-			},
-
-			"should return true if operors is one true; {$or:[true]}": function testTrue(){
-				assert.equal(Expression.parseOperand({$or:[true]}).evaluateInternal(), true);
-			},
+		"should return true if given true and true": function testTrueTrue(){
+			/** $or passed 'true', 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[true, true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors is one false; {$or:[false]}": function testFalse(){
-				assert.equal(Expression.parseOperand({$or:[false]}).evaluateInternal(), false);
-			},
+		"should return true if given true and false": function testTrueFalse(){
+			/** $or passed 'true', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[true, false]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return true if operors are true or true; {$or:[true,true]}": function testTrueTrue(){
-				assert.equal(Expression.parseOperand({$or:[true,true]}).evaluateInternal(), true);
-			},
+		"should return true if given false and true": function testFalseTrue(){
+			/** $or passed 'false', 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return true if operors are true or false; {$or:[true,false]}": function testTrueFalse(){
-				assert.equal(Expression.parseOperand({$or:[true,false]}).evaluateInternal(), true);
-			},
+		"should return false if given false and false": function testFalseFalse(){
+			/** $or passed 'false', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return true if operors are false or true; {$or:[false,true]}": function testFalseTrue(){
-				assert.equal(Expression.parseOperand({$or:[false,true]}).evaluateInternal(), true);
-			},
+		"should return false if given false and false and false": function testFalseFalseFalse(){
+			/** $or passed 'false', 'false', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, false, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return false if operors are false or false; {$or:[false,false]}": function testFalseFalse(){
-				assert.equal(Expression.parseOperand({$or:[false,false]}).evaluateInternal(), false);
-			},
+		"should return true if given false and false and true": function testFalseFalseTrue(){
+			/** $or passed 'false', 'false', 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, false, true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors are false, false, or false; {$or:[false,false,false]}": function testFalseFalseFalse(){
-				assert.equal(Expression.parseOperand({$or:[false,false,false]}).evaluateInternal(), false);
-			},
+		"should return true if given 0 and 1": function testZeroOne(){
+			/** $or passed '0', '1'. */
+			new ExpectedResultBase({
+				spec: {$or:[0, 1]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors are false, false, or false; {$or:[false,false,true]}": function testFalseFalseTrue(){
-				assert.equal(Expression.parseOperand({$or:[false,false,true]}).evaluateInternal(), true);
-			},
+		"should return false if given 0 and false": function testZeroFalse(){
+			/** $or passed '0', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[0, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return true if operors are 0 or 1; {$or:[0,1]}": function testZeroOne(){
-				assert.equal(Expression.parseOperand({$or:[0,1]}).evaluateInternal(), true);
-			},
+		"should return true if given a field path to a truthy value": function testFieldPath(){
+			/** $or passed a field path. */
+			new ExpectedResultBase({
+				spec: {$or:["$a"]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors are 0 or false; {$or:[0,false]}": function testZeroFalse(){
-				assert.equal(Expression.parseOperand({$or:[0,false]}).evaluateInternal(), false);
-			},
+	},
 
-			"should return true if operor is a path String to a truthy value; {$or:['$a']}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$or:['$a']}).evaluateInternal({a:1}), true);
-			}
+	"#optimize()": {
 
+		"should optimize a constant expression": function testOptimizeConstantExpression() {
+			/** A constant expression is optimized to a constant. */
+			new OptimizeBase({
+				spec: {$or:[1]},
+				expectedOptimized: {$const:true},
+			}).run();
 		},
 
-		"#optimize()": {
-
-			"should optimize a constant expression to a constant; {$or:[1]} == true": function testOptimizeConstantExpression(){
-				assert.deepEqual(Expression.parseOperand({$or:[1]}).optimize().toJSON(true), {$const:true});
-			},
+		"should not optimize a non constant": function testNonConstant() {
+			/** A non constant expression is not optimized. */
+			new NoOptimizeBase({
+				spec: {$or:["$a"]},
+			}).run();
+		},
 
-			"should not optimize a non-constant expression; {$or:['$a']}; SERVER-6192": function testNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a']}).optimize().toJSON(), {$or:['$a']});
-			},
+		"should optimize truthy constant and truthy expression": function testConstantNonConstantTrue() {
+			/** An expression beginning with a single constant is optimized. */
+			new OptimizeBase({
+				spec: {$or:[1,"$a"]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize an expression with a path or a '1' (is entirely constant); {$or:['$a',1]}": function testNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a',1]}).optimize().toJSON(true), {$const:true});
-			},
+		"should optimize falsy constant and truthy expression": function testConstantNonConstantFalse() {
+			/** An expression beginning with a single constant is optimized. */
+			new OptimizeBase({
+				spec: {$or:[0,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+			// note: using $and as serialization of ExpressionCoerceToBool rather than ExpressionAnd
+		},
 
-			"should optimize an expression with a field path or a '0'; {$or:['$a',0]}": function testNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a',0]}).optimize().toJSON(), {$and:['$a']});
-			},
+		"should optimize truthy expression and truthy constant": function testNonConstantOne() {
+			/** An expression with a field path and '1'. */
+			new OptimizeBase({
+				spec: {$or:["$a", 1]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize an expression with two field paths or '1' (is entirely constant); {$or:['$a','$b',1]}": function testNonConstantNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a','$b',1]}).optimize().toJSON(true), {$const:true});
-			},
+		"should optimize truthy expression and falsy constant": function testNonConstantZero() {
+			/** An expression with a field path and '0'. */
+			new OptimizeBase({
+				spec: {$or:["$a", 0]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+		},
 
-			"should optimize an expression with two field paths or '0'; {$or:['$a','$b',0]}": function testNonConstantNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a','$b',0]}).optimize().toJSON(), {$or:['$a','$b']});
-			},
+		"should optimize truthy expression, falsy expression, and truthy constant": function testNonConstantNonConstantOne() {
+			/** An expression with two field paths and '1'. */
+			new OptimizeBase({
+				spec: {$or:["$a","$b",1]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize an expression with '0', '1', or a field path; {$or:[0,1,'$a']}": function testZeroOneNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,1,'$a']}).optimize().toJSON(true), {$const:true});
-			},
+		"should optimize truthy expression, falsy expression, and falsy constant": function testNonConstantNonConstantZero() {
+			/** An expression with two field paths and '0'. */
+			new OptimizeBase({
+				spec: {$or:["$a","$b",0]},
+				expectedOptimized: {$or:["$a", "$b"]},
+			}).run();
+		},
 
-			"should optimize an expression with '0', '0', or a field path; {$or:[0,0,'$a']}": function testZeroZeroNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,0,'$a']}).optimize().toJSON(), {$and:['$a']});
-			},
+		"should optimize to true if [0,1,'$a']": function testZeroOneNonConstant() {
+			/** An expression with '0', '1', and a field path. */
+			new OptimizeBase({
+				spec: {$or:[0,1,"$a"]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize nested $or expressions properly or optimize out values evaluating to false; {$or:[0,{$or:[0]},'$a','$b']}": function testNested(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,{$or:[0]},'$a','$b']}).optimize().toJSON(), {$or:['$a','$b']});
-			},
+		"should optimize to {$and:'$a'} if [0,0,'$a']": function testZeroZeroNonConstant() {
+			/** An expression with '0', '0', and a field path. */
+			new OptimizeBase({
+				spec: {$or:[0,0,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+		},
 
-			"should optimize nested $or expressions containing a nested value evaluating to false; {$or:[0,{$or:[{$or:[1]}]},'$a','$b']}": function testNestedOne(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,{$or:[{$or:[1]}]},'$a','$b']}).optimize().toJSON(true), {$const:true});
-			}
+		"should optimize away nested falsey $or expressions": function testNested() {
+			/** Nested $or expressions. */
+			new OptimizeBase({
+				spec: {$or:[0, {$or:[0]}, "$a", "$b"]},
+				expectedOptimized: {$or: ["$a", "$b"]},
+			}).run();
+		},
 
-		}
+		"should optimize to tru if nested truthy $or expressions": function testNestedOne() {
+			/** Nested $or expressions containing a nested value evaluating to false. */
+			new OptimizeBase({
+				spec: {$or:[0, {$or:[ {$or:[1]} ]}, "$a", "$b"]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 0
test/lib/pipeline/expressions/SecondExpression.js → test/lib/pipeline/expressions/SecondExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetDifferenceExpression.js → test/lib/pipeline/expressions/SetDifferenceExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetEqualsExpression.js → test/lib/pipeline/expressions/SetEqualsExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetIntersectionExpression.js → test/lib/pipeline/expressions/SetIntersectionExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetIsSubsetExpression.js → test/lib/pipeline/expressions/SetIsSubsetExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetUnionExpression.js → test/lib/pipeline/expressions/SetUnionExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SizeExpression.js → test/lib/pipeline/expressions/SizeExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/StrcasecmpExpression.js → test/lib/pipeline/expressions/StrcasecmpExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SubtractExpression.js → test/lib/pipeline/expressions/SubtractExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/VariablesIdGenerator.js → test/lib/pipeline/expressions/VariablesIdGenerator_test.js


+ 0 - 0
test/lib/pipeline/expressions/VariablesParseState.js → test/lib/pipeline/expressions/VariablesParseState_test.js


+ 0 - 0
test/lib/pipeline/expressions/Variables.js → test/lib/pipeline/expressions/Variables_test.js


+ 0 - 0
test/lib/pipeline/expressions/WeekExpression.js → test/lib/pipeline/expressions/WeekExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/YearExpression.js → test/lib/pipeline/expressions/YearExpression_test.js


+ 0 - 50
test/lib/pipeline/matcher/ComparisonMatchExpression.js

@@ -1,50 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	ComparisonMatchExpression = require("../../../../lib/pipeline/matcher/ComparisonMatchExpression");
-
-
-module.exports = {
-	"ComparisonMatchExpression": {
-
-		"Should properly initialize with an empty path and a number": function (){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'LT';
-			assert.strictEqual(e.init('', 5 ).code,'OK');
-		},
-		"Should not initialize when given an undefined rhs": function() {
-			var e = new ComparisonMatchExpression();
-			assert.strictEqual(e.init('',5).code,'BAD_VALUE');
-			e._matchType = 'LT';
-			assert.strictEqual(e.init('',{}).code,'BAD_VALUE');	
-			assert.strictEqual(e.init('',undefined).code,'BAD_VALUE');
-			assert.strictEqual(e.init('',{}).code,'BAD_VALUE');
-		},
-		"Should match numbers with GTE": function (){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'GTE';
-			assert.strictEqual(e.init('',5).code,'OK');
-			assert.ok(e.matchesSingleElement(6), "6 ≥ 5");
-			assert.ok(e.matchesSingleElement(5), "5 ≥ 5");
-			assert.ok(!e.matchesSingleElement(4), "4 ≥ 5");
-			assert.ok(!e.matchesSingleElement('foo'), "5 ≥ 'foo'");
-		},
-		"Should match with simple paths and GTE": function(){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'GTE';
-			assert.strictEqual(e.init('a', 5).code,'OK');
-			assert.ok(e.matches({'a':6}));
-		},
-		"Should match arrays with GTE": function (){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'GTE';
-			assert.strictEqual(e.init('a',5).code,'OK');
-			assert.ok(e.matches({'a':[6,10]}),'[6,10] ≥ 5');
-			assert.ok(e.matches({'a':[4,5.5]}), '[4,5.5] ≥ 5');
-			assert.ok(!e.matches({'a':[1,2]}),'[1,2] ≥ 5');
-			assert.ok(e.matches({'a':[1,10]}),'[1,10] ≥ 5');
-		}
-	}
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
-

+ 110 - 0
test/lib/pipeline/matcher/ComparisonMatchExpression_test.js

@@ -0,0 +1,110 @@
+"use strict";
+var assert = require("assert"),
+	bson = require("bson"),
+	MinKey = bson.BSONPure.MinKey,
+	MaxKey = bson.BSONPure.MaxKey,
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
+	ComparisonMatchExpression = require("../../../../lib/pipeline/matcher/ComparisonMatchExpression");
+
+// 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.ComparisonMatchExpression = {
+
+	"should properly initialize with an empty path and a number": function () {
+		var e = new ComparisonMatchExpression('LT');
+		assert.strictEqual(e.init('',5).code,'OK');
+	},
+	"should not initialize when given an invalid operand": function() {
+		var e = new ComparisonMatchExpression('');
+		assert.strictEqual(e.init('',5).code, 'BAD_VALUE');
+	},
+	"should not initialize when given an undefined rhs": function() {
+		var e = new ComparisonMatchExpression();
+		assert.strictEqual(e.init('',5).code,'BAD_VALUE');
+		e._matchType = 'LT';
+		assert.strictEqual(e.init('',{}).code,'BAD_VALUE');
+		assert.strictEqual(e.init('',undefined).code,'BAD_VALUE');
+		assert.strictEqual(e.init('',{}).code,'BAD_VALUE');
+	},
+	"should match numbers with GTE": function () {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('',5).code,'OK');
+		assert.ok(e.matchesSingleElement(6),'6 ≥ 5');
+		assert.ok(e.matchesSingleElement(5),'5 ≥ 5');
+		assert.ok(!e.matchesSingleElement(4),'4 !≥ 5');
+		assert.ok(!e.matchesSingleElement('foo'),"'foo' !≥ 5");
+	},
+	"should match with simple paths and GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',5).code,'OK');
+		assert.ok(e.matches({'a':6}));
+	},
+	"should match array values with GTE": function () {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',5).code,'OK');
+		assert.ok(e.matches({'a':[6,10]}),'[6,10] ≥ 5');
+		assert.ok(e.matches({'a':[4,5.5]}),'[4,5.5] ≥ 5');
+		assert.ok(!e.matches({'a':[1,2]}),'[1,2] !≥ 5');
+		assert.ok(e.matches({'a':[1,10]}),'[1,10] ≥ 5');
+	},
+	"should match entire arrays with GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',[5]).code,'OK');
+		assert.ok(!e.matches({'a':[4]}),'[4] !≥ [5]');
+		assert.ok(e.matches({'a':[5]}),'[5] !≥ [5]');
+		assert.ok(e.matches({'a':[6]}),'[6] !≥ [5]');
+		// documents current behavior
+		assert.ok(e.matches({'a':[[6]]}),'[[4]] ≥ [5]');
+		assert.ok(e.matches({'a':[[6]]}),'[[5]] ≥ [5]');
+		assert.ok(e.matches({'a':[[6]]}),'[[6]] ≥ [5]');
+	},
+	"should match null with GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		e._matchType = 'GTE';
+		assert.strictEqual(e.init('a',null).code,'OK');
+		assert.ok(e.matches({}),'{} ≥ null');
+		assert.ok(e.matches({'a':null}),'null ≥ null');
+		assert.ok(!e.matches({'a':4}),'4 !≥ null');
+		assert.ok(e.matches({'b':null}),'non-existent field ≥ null');
+	},
+	"should match null in dotted paths with GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a.b',null).code,'OK');
+		assert.ok(e.matches({}),'{} ≥ null');
+		assert.ok(e.matches({'a':null}),'{a:null} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':4}),'{a:4} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':{}}),'{a:{}} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':[{'b':null}]}),'{a:[{b:null}]} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':[{'a':4},{'b':4}]}),'{a:[{a:4},{b:4}]} ≥ {a.b:null}');
+		assert.ok(!e.matches({'a':[4]}),'{a:[4]} !≥ {a.b:null}');
+		assert.ok(!e.matches({'a':[{'b':4}]}),'{a:[{b:4}]} !≥ {a.b:null}');
+	},
+	"should match MinKeys": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',new MinKey()).code,'OK');
+		assert.ok(e.matches({'a':new MinKey()}),'minKey ≥ minKey');
+		assert.ok(e.matches({'a':new MaxKey()}),'maxKey ≥ minKey');
+		assert.ok(e.matches({'a':4}),'4 ≥ minKey');
+	},
+	"should match MaxKeys": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',new MaxKey()).code,'OK');
+		assert.ok(e.matches({'a':new MaxKey()}),'maxKey ≥ maxKey');
+		assert.ok(!e.matches({'a':new MinKey()}),'minKey !≥ maxKey');
+		assert.ok(!e.matches({'a':4},null),'4 !≥ maxKey');
+	},
+	"should properly set match keys": function() {
+		var e = new ComparisonMatchExpression('GTE'),
+			d = new MatchDetails();
+		d.requestElemMatchKey();
+		assert.strictEqual(e.init('a',5).code,'OK');
+		assert.ok(!e.matchesJSON({'a':4},d),'4 !≥ 5');
+		assert(!d.hasElemMatchKey());
+		assert.ok(e.matchesJSON({'a':6},d),'6 ≥ 5');
+		assert(!d.hasElemMatchKey());
+		assert.ok(e.matchesJSON({'a':[2,6,5]},d),'[2,6,5] ≥ 5');
+		assert(d.hasElemMatchKey());
+		assert.strictEqual('1',d.elemMatchKey());
+	}
+};

+ 67 - 32
test/lib/pipeline/matcher/GTEMatchExpression.js

@@ -1,6 +1,7 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	GTEMatchExpression = require("../../../../lib/pipeline/matcher/GTEMatchExpression");
 
 
@@ -8,68 +9,102 @@ module.exports = {
 	"GTEMatchExpression": {
 		"should match scalars and strings properly": function (){
 			var e = new GTEMatchExpression();
-			var s = e.init('x',5);
+			var s = e.init("",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'x':5}) );
-			assert.ok( ! e.matches({'x':4}) );
-			assert.ok( e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesSingleElement(5.5) );
+			assert.ok( e.matchesSingleElement(5) );
+			assert.ok( ! e.matchesSingleElement(4) );
+			assert.ok( ! e.matchesSingleElement( "foo" ) );
 		},
 		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
 			var e = new GTEMatchExpression();
-			var s = e.init('',{});
+			var s = e.init("",{});
 
-			assert.strictEqual(s.code, 'BAD_VALUE');
+			assert.strictEqual(s.code, "BAD_VALUE");
 		},
 		"should match a pathed number":function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':5.5}) );
-			assert.ok( ! e.matches({'a':4}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":5.5}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
 		},
 		"should match stuff in an array": function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[4,5.5]}) );
-			assert.ok( ! e.matches({'a':[1,2]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[4,5.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[1,2]}) );
 		},
 		"should not match full array" : function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( e.matchesJSON({"a":[5]}) );
+			assert.ok( e.matchesJSON({"a":[6]}) );
 		},
 		"should not match null" : function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({}) );
-			assert.ok( e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+			var s = e.init("a",null);
+
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({}) );
+			assert.ok( e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			assert.ok( e.matchesJSON({"b":4}) );
+		},
+		"should match dot notation nulls": function() {
+			var e = new GTEMatchExpression();
+			var s = e.init("a.b",null);
+
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({}));
+			assert.ok(e.matchesJSON({a:null}));
+			assert.ok(e.matchesJSON({a:{}}));
+			assert.ok(e.matchesJSON({a:[{b: null}]}));
+			assert.ok(e.matchesJSON({a:[{a:4}, {b:4}]}));
+			assert.ok(!e.matchesJSON({a:[4]}));
+			assert.ok(!e.matchesJSON({a:[{b:4}]}));
+		},
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new GTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new GTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':4}, m) );
+			assert.ok( ! e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':6}, m) );
+			assert.ok( e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[2,6,5]}, m));
+			assert.ok( e.matchesJSON({"a":[2,6,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 	}
 };

+ 70 - 38
test/lib/pipeline/matcher/GTMatchExpression.js

@@ -1,75 +1,107 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	GTMatchExpression = require("../../../../lib/pipeline/matcher/GTMatchExpression");
 
 
 module.exports = {
 	"GTMatchExpression": {
-		"should match scalars and strings properly": function (){
+		"should handle invalid End of Object Operand": function (){
 			var e = new GTMatchExpression();
-			var s = e.init('x',5);
+			var s = e.init("",{});
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( ! e.matches({'x':5}) );
-			assert.ok( ! e.matches({'x':4}) );
-			assert.ok( e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+			assert.strictEqual(s.code, "BAD_VALUE");
 		},
-		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
+		"should match scalars":function() {
 			var e = new GTMatchExpression();
-			var s = e.init('',{});
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'BAD_VALUE');
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":5.5}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
 		},
-		"should match a pathed number":function() {
+		"should match array value": function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':5.5}) );
-			assert.ok( ! e.matches({'a':4}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[3,5.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[2,4]}) );
 		},
-		"should match stuff in an array": function() {
+		"should match whole array": function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[3,5.5]}) );
-			assert.ok( ! e.matches({'a':[2,4]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[5]}) );
+			assert.ok( e.matchesJSON({"a":[6]}) );
+			// Nested array.
+			// XXX: The following assertion documents current behavior.
+			assert.ok( e.matchesJSON({"a":[[4]]}) );
+			assert.ok( e.matchesJSON({"a":[[5]]}) );
+			assert.ok( e.matchesJSON({"a":[[6]]}) );
 		},
-		"should not match full array" : function() {
+		"should match null" : function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( !e.matchesJSON({}) );
+			assert.ok( !e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			// A non-existent field is treated same way as an empty bson object
+			assert.ok( ! e.matchesJSON({"b":4}) );
 		},
-		"should not match null" : function() {
+		"should match dot notation null" : function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( !e.matches({}) );
-			assert.ok( !e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+			var s = e.init("a.b",null);
+
+			assert.strictEqual(s.code, "OK");
+			assert.ok( !e.matchesJSON({}) );
+			assert.ok( !e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			assert.ok( ! e.matchesJSON({"a":{}}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:null}]}) );
+			assert.ok( ! e.matchesJSON({"a":[{a:4},{b:4}]}) );
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:4}]}) );
+		},
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new GTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new GTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':4}, m) );
+			assert.ok( ! e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':6}, m) );
+			assert.ok( e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[2,6,5]}, m));
+			assert.ok( e.matchesJSON({"a":[2,6,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 	}
 };

+ 85 - 42
test/lib/pipeline/matcher/LTEMatchExpression.js

@@ -1,75 +1,118 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	LTEMatchExpression = require("../../../../lib/pipeline/matcher/LTEMatchExpression");
 
 
 module.exports = {
 	"LTEMatchExpression": {
-		"should match scalars and strings properly": function (){
-			var e = new LTEMatchExpression();
-			var s = e.init('x',5);
-			
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'x':5}) );
-			assert.ok( e.matches({'x':4}) );
-			assert.ok( ! e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+		"should match element": function (){
+			var operand = {$lte:5},
+				match = {a:4.5},
+				equalMatch = {a:5},
+				notMatch = {a:6},
+				notMatchWrongType = {a:"foo"},
+				lte = new LTEMatchExpression();
+			var s = lte.init("",operand.$lte);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lte.matchesSingleElement(match.a));
+			assert.ok(lte.matchesSingleElement(equalMatch.a));
+			assert.ok(!lte.matchesSingleElement(notMatch.a));
+			assert.ok(!lte.matchesSingleElement(notMatchWrongType.a));
+		},
+		"should not work for invalid eoo operand": function(){
+			var operand = {},
+				lte = new LTEMatchExpression();
+			assert.ok(lte.init("", operand).code !== "OK");
+		},
+		"should match scalars properly": function (){
+			var operand = {$lte:5},
+				lte = new LTEMatchExpression();
+			var s = lte.init("a",operand.$lte);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lte.matchesJSON({"a":4.5}, null));
+			assert.ok(!lte.matchesJSON({"a":6}), null);
 		},
-		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
+		"should match array value": function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('',{});
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'BAD_VALUE');
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[6,4.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[6,7]}) );
 		},
-		"should match a pathed number":function() {
-			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+		"should match whole array" : function() {
+			var e = new LTEMatchExpression(),
+				s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':4.5}) );
-			assert.ok( ! e.matches({'a':6}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":[4]}));
+			assert.ok(e.matchesJSON({"a":[5]}));
+			assert.ok(!e.matchesJSON({"a":[6]}));
+			assert.ok(e.matchesJSON({"a":[[4]]}));
+			assert.ok(e.matchesJSON({"a":[[5]]}));
+			assert.ok(!e.matchesJSON({"a":[[6]]}));
 		},
-		"should match stuff in an array": function() {
+		"should match null" : function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6,4.5]}) );
-			assert.ok( ! e.matches({'a':[6,7]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({}) );
+			assert.ok( e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			// A non-existent field is treated same way as an empty bson object
+			assert.ok( e.matchesJSON({"b":4}) );
 		},
-		"should not match full array" : function() {
+		"should match dot notation null" : function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a.b",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok(e.matches({'a':[4]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({}) );
+			assert.ok( e.matchesJSON({"a":null}) );
+			assert.ok( e.matchesJSON({"a":4}) );
+			assert.ok( e.matchesJSON({"a":{}}) );
+			assert.ok( e.matchesJSON({"a":[{b:null}]}) );
+			assert.ok( e.matchesJSON({"a":[{a:4},{b:4}]}) );
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:4}]}) );
 		},
-		"should not match null" : function() {
-			var e = new LTEMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({}) );
-			assert.ok( e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new LTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new LTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':6}, m) );
+			assert.ok( ! e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':4}, m) );
+			assert.ok( e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[6,2,5]}, m));
+			assert.ok( e.matchesJSON({"a":[6,2,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 
 	}

+ 93 - 52
test/lib/pipeline/matcher/LTMatchExpression.js

@@ -1,87 +1,128 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	LTMatchExpression = require("../../../../lib/pipeline/matcher/LTMatchExpression");
 
 
 module.exports = {
 	"LTMatchExpression": {
-		"should match scalars and strings properly": function (){
-			var e = new LTMatchExpression();
-			var s = e.init('x',5);
-			
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( ! e.matches({'x':5}) );
-			assert.ok( e.matches({'x':4}) );
-			assert.ok( ! e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+		"should match element": function (){
+			var operand = {$lt:5},
+				match = {a:4.5},
+				notMatch = {a:6},
+				notMatchEqual = {a:5},
+				notMatchWrongType = {a:"foo"},
+				lt = new LTMatchExpression();
+			var s = lt.init("",operand.$lt);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lt.matchesSingleElement(match.a));
+			assert.ok(!lt.matchesSingleElement(notMatch.a));
+			assert.ok(!lt.matchesSingleElement(notMatchEqual.a));
+			assert.ok(!lt.matchesSingleElement(notMatchWrongType.a));
 		},
-		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
-			var e = new LTMatchExpression();
-			var s = e.init('',{});
-
-			assert.strictEqual(s.code, 'BAD_VALUE');
+		"should not work for invalid eoo operand": function(){
+			var operand = {},
+				lt = new LTMatchExpression();
+			assert.ok(lt.init("", operand).code !== "OK");
 		},
-		"should match a pathed number":function() {
+		"should match scalars properly": function (){
+			var operand = {$lt:5},
+				lt = new LTMatchExpression();
+			var s = lt.init("a",operand.$lt);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lt.matchesJSON({"a":4.5}, null));
+			assert.ok(!lt.matchesJSON({"a":6}), null);
+		},
+		"should match scalars with empty keys properly": function (){
+			var operand = {$lt:5},
+				lt = new LTMatchExpression();
+			var s = lt.init("",operand.$lt);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lt.matchesJSON({"":4.5}, null));
+			assert.ok(!lt.matchesJSON({"":6}), null);
+		},
+		"should match array value": function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':4.5}) );
-			assert.ok( ! e.matches({'a':6}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[6,4.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[6,7]}) );
 		},
-		"should match an empty pathed number":function() {
-			var e = new LTMatchExpression();
-			var s = e.init('',5);
+		"should match whole array" : function() {
+			var e = new LTMatchExpression(),
+				s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'':4.5}) );
-			assert.ok( ! e.matches({'':6}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":[4]}));
+			assert.ok(!e.matchesJSON({"a":[5]}));
+			assert.ok(!e.matchesJSON({"a":[6]}));
+			// Nested array.
+			assert.ok(e.matchesJSON({"a":[[4]]}));
+			assert.ok(!e.matchesJSON({"a":[[5]]}));
+			assert.ok(!e.matchesJSON({"a":[[6]]}));
 		},
-		"should match stuff in an array": function() {
+		"should match null" : function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6,4.5]}) );
-			assert.ok( ! e.matches({'a':[6,7]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({}) );
+			assert.ok( ! e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			// A non-existent field is treated same way as an empty bson object
+			assert.ok( ! e.matchesJSON({"b":4}) );
 		},
-		"should not match full array" : function() {
+		"should match dot notation null" : function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a.b",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[4]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({}) );
+			assert.ok( ! e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			assert.ok( ! e.matchesJSON({"a":{}}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:null}]}) );
+			assert.ok( ! e.matchesJSON({"a":[{a:4},{b:4}]}) );
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:4}]}) );
 		},
-		"should not match null" : function() {
-			var e = new LTMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( ! e.matches({}) );
-			assert.ok( ! e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new LTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(!e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new LTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':6}, m) );
+			assert.ok( ! e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':4}, m) );
+			assert.ok( e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[6,2,5]}, m));
+			assert.ok( e.matchesJSON({"a":[6,2,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
-
-
-
 	}
 };