Quellcode durchsuchen

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

Jake Delaney vor 11 Jahren
Ursprung
Commit
19093daa98
50 geänderte Dateien mit 2673 neuen und 1686 gelöschten Zeilen
  1. 113 99
      lib/pipeline/Value.js
  2. 2 2
      lib/pipeline/ValueSet.js
  3. 33 41
      lib/pipeline/accumulators/Accumulator.js
  4. 27 36
      lib/pipeline/accumulators/AddToSetAccumulator.js
  5. 29 33
      lib/pipeline/accumulators/AvgAccumulator.js
  6. 10 18
      lib/pipeline/accumulators/FirstAccumulator.js
  7. 16 13
      lib/pipeline/accumulators/LastAccumulator.js
  8. 25 31
      lib/pipeline/accumulators/MinMaxAccumulator.js
  9. 20 25
      lib/pipeline/accumulators/PushAccumulator.js
  10. 25 18
      lib/pipeline/accumulators/SumAccumulator.js
  11. 40 23
      lib/pipeline/expressions/AddExpression.js
  12. 60 44
      lib/pipeline/expressions/AndExpression.js
  13. 3 4
      lib/pipeline/expressions/ConstantExpression.js
  14. 1 1
      lib/pipeline/expressions/Expression.js
  15. 6 7
      lib/pipeline/expressions/FixedArityExpressionT.js
  16. 3 3
      lib/pipeline/expressions/MultiplyExpression.js
  17. 5 4
      lib/pipeline/expressions/NaryBaseExpressionT.js
  18. 2 2
      lib/pipeline/expressions/NaryExpression.js
  19. 53 36
      lib/pipeline/expressions/OrExpression.js
  20. 1 1
      lib/pipeline/expressions/SetDifferenceExpression.js
  21. 1 1
      lib/pipeline/expressions/SetEqualsExpression.js
  22. 1 1
      lib/pipeline/expressions/SetIntersectionExpression.js
  23. 1 1
      lib/pipeline/expressions/SetIsSubsetExpression.js
  24. 1 1
      lib/pipeline/expressions/SetUnionExpression.js
  25. 6 5
      lib/pipeline/expressions/VariadicExpressionT.js
  26. 20 21
      lib/pipeline/matcher/ComparisonMatchExpression.js
  27. 15 13
      lib/pipeline/matcher/GTEMatchExpression.js
  28. 14 13
      lib/pipeline/matcher/GTMatchExpression.js
  29. 14 12
      lib/pipeline/matcher/LTEMatchExpression.js
  30. 14 12
      lib/pipeline/matcher/LTMatchExpression.js
  31. 2 1
      package.json
  32. 68 71
      test/lib/pipeline/accumulators/AddToSetAccumulator.js
  33. 192 73
      test/lib/pipeline/accumulators/AvgAccumulator.js
  34. 77 55
      test/lib/pipeline/accumulators/FirstAccumulator.js
  35. 71 44
      test/lib/pipeline/accumulators/LastAccumulator.js
  36. 0 78
      test/lib/pipeline/accumulators/MaxAccumulator.js
  37. 0 78
      test/lib/pipeline/accumulators/MinAccumulator.js
  38. 206 0
      test/lib/pipeline/accumulators/MinMaxAccumulator.js
  39. 97 77
      test/lib/pipeline/accumulators/PushAccumulator.js
  40. 240 78
      test/lib/pipeline/accumulators/SumAccumulator.js
  41. 207 90
      test/lib/pipeline/expressions/AddExpression_test.js
  42. 245 172
      test/lib/pipeline/expressions/AndExpression_test.js
  43. 37 33
      test/lib/pipeline/expressions/NaryExpression_test.js
  44. 245 101
      test/lib/pipeline/expressions/OrExpression_test.js
  45. 0 50
      test/lib/pipeline/matcher/ComparisonMatchExpression.js
  46. 110 0
      test/lib/pipeline/matcher/ComparisonMatchExpression_test.js
  47. 67 32
      test/lib/pipeline/matcher/GTEMatchExpression.js
  48. 70 38
      test/lib/pipeline/matcher/GTMatchExpression.js
  49. 85 42
      test/lib/pipeline/matcher/LTEMatchExpression.js
  50. 93 52
      test/lib/pipeline/matcher/LTMatchExpression.js

+ 113 - 99
lib/pipeline/Value.js

@@ -9,9 +9,9 @@
  **/
  **/
 var Value = module.exports = function Value(){
 var Value = module.exports = function Value(){
 	if(this.constructor === Value) throw new Error("Never create instances of this! Use the static helpers only.");
 	if(this.constructor === Value) throw new Error("Never create instances of this! Use the static helpers only.");
-}, klass = Value, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = Value;
 
 
-var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
+var Document; // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
 
 
 //SKIPPED: ValueStorage -- probably not required; use JSON?
 //SKIPPED: ValueStorage -- probably not required; use JSON?
 //SKIPPED: createIntOrLong -- not required; use Number
 //SKIPPED: createIntOrLong -- not required; use Number
@@ -22,7 +22,7 @@ var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate
 //SKIPPED: addToBsonArray -- not required; use arr.push(<val>)
 //SKIPPED: addToBsonArray -- not required; use arr.push(<val>)
 
 
 /** Coerce a value to a bool using BSONElement::trueValue() rules.
 /** Coerce a value to a bool using BSONElement::trueValue() rules.
- * Some types unsupported.  SERVER-6120
+ * Some types unsupported. SERVER-6120
  * @method coerceToBool
  * @method coerceToBool
  * @static
  * @static
  */
  */
@@ -31,9 +31,10 @@ klass.coerceToBool = function coerceToBool(value) {
 	return !!value;	// including null or undefined
 	return !!value;	// including null or undefined
 };
 };
 
 
-/** Coercion operators to extract values with fuzzy type logic.
- *  These currently assert if called on an unconvertible type.
- *  TODO: decided how to handle unsupported types.
+/**
+ * Coercion operators to extract values with fuzzy type logic.
+ * These currently assert if called on an unconvertible type.
+ * TODO: decided how to handle unsupported types.
  */
  */
 klass.coerceToWholeNumber = function coerceToInt(value) {
 klass.coerceToWholeNumber = function coerceToInt(value) {
 	return klass.coerceToNumber(value) | 0;
 	return klass.coerceToNumber(value) | 0;
@@ -42,36 +43,29 @@ klass.coerceToInt = klass.coerceToWholeNumber;
 klass.coerceToLong = klass.coerceToWholeNumber;
 klass.coerceToLong = klass.coerceToWholeNumber;
 klass.coerceToNumber = function coerceToNumber(value) {
 klass.coerceToNumber = function coerceToNumber(value) {
 	if (value === null) return 0;
 	if (value === null) return 0;
-	switch (typeof(value)) {
-	case "undefined":
-		return 0;
-	case "number":
-		return value;
-	case "object":
-		switch (value.constructor.name) {
-			case "Long":
-				return parseInt(value.toString(), 10);
-			case "Double":
-				return parseFloat(value.value, 10);
-			default:
-				throw new Error("can't convert from BSON type " + value.constructor.name + " to int; codes 16003, 16004, 16005");
-		}
-		return value;
-	default:
-		throw new Error("can't convert from BSON type " + typeof(value) + " to int; codes 16003, 16004, 16005");
+	switch (Value.getType(value)) {
+		case "undefined":
+			return 0;
+		case "number":
+			return value;
+		case "Long":
+			return parseInt(value.toString(), 10);
+		case "Double":
+			return parseFloat(value.value, 10);
+		default:
+			throw new Error("can't convert from BSON type " + Value.getType(value) + " to int; codes 16003, 16004, 16005");
 	}
 	}
 };
 };
 klass.coerceToDouble = klass.coerceToNumber;
 klass.coerceToDouble = klass.coerceToNumber;
 klass.coerceToDate = function coerceToDate(value) {
 klass.coerceToDate = function coerceToDate(value) {
 	if (value instanceof Date) return value;
 	if (value instanceof Date) return value;
-	throw new Error("can't convert from BSON type " + typeof(value) + " to Date; uassert code 16006");
+	throw new Error("can't convert from BSON type " + Value.getType(value) + " to Date; uassert code 16006");
 };
 };
 //SKIPPED: coerceToTimeT -- not required; just use Date
 //SKIPPED: coerceToTimeT -- not required; just use Date
 //SKIPPED: coerceToTm -- not required; just use Date
 //SKIPPED: coerceToTm -- not required; just use Date
 //SKIPPED: tmToISODateString -- not required; just use Date
 //SKIPPED: tmToISODateString -- not required; just use Date
 klass.coerceToString = function coerceToString(value) {
 klass.coerceToString = function coerceToString(value) {
-	var type = typeof(value);
-	if (type === "object") type = value === null ? "null" : value.constructor.name;
+	var type = Value.getType(value);
 	switch (type) {
 	switch (type) {
 		//TODO: BSON numbers?
 		//TODO: BSON numbers?
 		case "number":
 		case "number":
@@ -91,7 +85,7 @@ klass.coerceToString = function coerceToString(value) {
 			return "";
 			return "";
 
 
 		default:
 		default:
-			throw new Error("can't convert from BSON type " + typeof(value) + " to String; uassert code 16007");
+			throw new Error("can't convert from BSON type " + Value.getType(value) + " to String; uassert code 16007");
 	}
 	}
 };
 };
 //SKIPPED: coerceToTimestamp
 //SKIPPED: coerceToTimestamp
@@ -101,8 +95,16 @@ klass.coerceToString = function coerceToString(value) {
  * @method cmp
  * @method cmp
  * @static
  * @static
  */
  */
-klass.cmp = function cmp(l, r){
-	return l < r ? -1 : l > r ? 1 : 0;
+var cmp = klass.cmp = function cmp(left, right){
+	// The following is lifted directly from compareElementValues
+	// to ensure identical handling of NaN
+	if (left < right)
+		return -1;
+	if (left === right)
+		return 0;
+	if (isNaN(left))
+		return isNaN(right) ? 0 : -1;
+	return 1;
 };
 };
 
 
 /** Compare two Values.
 /** Compare two Values.
@@ -112,74 +114,84 @@ klass.cmp = function cmp(l, r){
  * Warning: may return values other than -1, 0, or 1
  * Warning: may return values other than -1, 0, or 1
  */
  */
 klass.compare = function compare(l, r) {
 klass.compare = function compare(l, r) {
-	//NOTE: deviation from mongo code: we have to do some coercing for null "types" because of javascript
-	var lt = l === null ? "null" : typeof(l),
-		rt = r === null ? "null" : typeof(r),
+	var lType = Value.getType(l),
+		rType = Value.getType(r),
 		ret;
 		ret;
 
 
-	// NOTE: deviation from mongo code: javascript types do not work quite the same, so for proper results we always canonicalize, and we don't need the "speed" hack
-	ret = (klass.cmp(klass.canonicalize(l), klass.canonicalize(r)));
+	ret = lType === rType ?
+	 	0 // fast-path common case
+		: cmp(klass.canonicalize(l), klass.canonicalize(r));
 
 
-	if(ret !== 0) return ret;
+	if(ret !== 0)
+		return ret;
 
 
-	// Numbers
-	if (lt === "number" && rt === "number"){
-		//NOTE: deviation from Mongo code: they handle NaN a bit differently
-		if (isNaN(l)) return isNaN(r) ? 0 : -1;
-		if (isNaN(r)) return 1;
-		return klass.cmp(l,r);
-	}
-	// Compare MinKey and MaxKey cases
-	if (l instanceof Object && ["MinKey", "MaxKey"].indexOf(l.constructor.name) !== -1) {
-		if (l.constructor.name === r.constructor.name) {
-			return 0;
-		} else if (l.constructor.name === "MinKey") {
-			return -1;
-		} else {
-			return 1; // Must be MaxKey, which is greater than everything but MaxKey (which r cannot be)
-		}
-	}
-	// hack: These should really get converted to their BSON type ids and then compared, we use int vs object in queries
-	if (lt === "number" && rt === "object"){
-		return -1;
-	} else if (lt === "object" && rt === "number") {
-		return 1;
-	}
 	// CW TODO for now, only compare like values
 	// CW TODO for now, only compare like values
-	if (lt !== rt) throw new Error("can't compare values of BSON types [" + lt + " " + l.constructor.name + "] and [" + rt + ":" + r.constructor.name + "]; code 16016");
-	// Compare everything else
-	switch (lt) {
-	case "number":
-		throw new Error("number types should have been handled earlier!");
-	case "string":
-		return klass.cmp(l, r);
-	case "boolean":
-		return l === r ? 0 : l ? 1 : -1;
-	case "undefined": //NOTE: deviation from mongo code: we are comparing null to null or undefined to undefined (otherwise the ret stuff above would have caught it)
-	case "null":
-		return 0;
-	case "object":
-		if (l instanceof Array) {
-			for (var i = 0, ll = l.length, rl = r.length; true ; ++i) {
-				if (i > ll) {
-					if (i > rl) return 0; // arrays are same length
-					return -1; // left array is shorter
-				}
-				if (i > rl) return 1; // right array is shorter
-				var cmp = Value.compare(l[i], r[i]);
-				if (cmp !== 0) return cmp;
+	if (lType !== rType)
+		throw new Error("can't compare values of BSON types [" + lType + "] and [" + rType + "]; code 16016");
+
+	switch (lType) {
+		// Order of types is the same as in compareElementValues() to make it easier to verify
+
+		// These are valueless types
+		//SKIPPED: case "EOO":
+		case "undefined":
+		case "null":
+		//SKIPPED: case "jstNULL":
+		case "MaxKey":
+		case "MinKey":
+			return ret;
+
+		case "boolean":
+			return l - r;
+
+		// WARNING: Timestamp and Date have same canonical type, but compare differently.
+		// Maintaining behavior from normal BSON.
+		//SKIPPED: case "Timestamp": //unsigned-----//TODO: handle case for bson.Timestamp()
+		case "Date": // signed
+			return cmp(l.getTime(), r.getTime());
+
+        // Numbers should compare by equivalence even if different types
+		case "number":
+			return cmp(l, r);
+
+        //SKIPPED: case "jstOID":----//TODO: handle case for bson.ObjectID()
+
+        case "Code":
+        case "Symbol":
+        case "string":
+			l = String(l);
+			r = String(r);
+			return l < r ? -1 : l > r ? 1 : 0;
+
+		case "Object":
+			if (Document === undefined) Document = require("./Document");	//TODO: a dirty hack; need to investigate and clean up
+			return Document.compare(l, r);
+
+		case "Array":
+			var lArr = l,
+				rArr = r;
+
+			var elems = Math.min(lArr.length, rArr.length);
+			for (var i = 0; i < elems; i++) {
+				// compare the two corresponding elements
+				ret = Value.compare(lArr[i], rArr[i]);
+				if (ret !== 0)
+					return ret;
 			}
 			}
+			// if we get here we are either equal or one is prefix of the other
+			return cmp(lArr.length, rArr.length);
 
 
-			throw new Error("logic error in Value.compare for Array types!");
-		}
-		if (l instanceof Date) return klass.cmp(l,r);
-		if (l instanceof RegExp) return klass.cmp(l,r);
-		if (Document === undefined) Document = require("./Document");	//TODO: a dirty hack; need to investigate and clean up
-		return Document.compare(l, r);
-	default:
-		throw new Error("unhandled left hand type:" + lt);
-	}
+		//SKIPPED: case "DBRef":-----//TODO: handle case for bson.DBRef()
+		//SKIPPED: case "BinData":-----//TODO: handle case for bson.BinData()
 
 
+		case "RegExp": // same as String in this impl but keeping order same as compareElementValues
+			l = String(l);
+			r = String(r);
+			return l < r ? -1 : l > r ? 1 : 0;
+
+		//SKIPPED: case "CodeWScope":-----//TODO: handle case for bson.CodeWScope()
+	}
+	throw new Error("Assertion failure");
 };
 };
 
 
 //SKIPPED: hash_combine
 //SKIPPED: hash_combine
@@ -201,22 +213,25 @@ klass.consume = function consume(consumed) {
 };
 };
 
 
 //NOTE: DEVIATION FROM MONGO: many of these do not apply or are inlined (code where relevant)
 //NOTE: DEVIATION FROM MONGO: many of these do not apply or are inlined (code where relevant)
-// missing(val):  val === undefined
-// nullish(val):  val === null || val === undefined
-// numeric(val):  typeof val === "number"
+// missing(val): val === undefined
+// nullish(val): val === null || val === undefined
+// numeric(val): typeof val === "number"
 klass.getType = function getType(v) {
 klass.getType = function getType(v) {
 	var t = typeof v;
 	var t = typeof v;
-	if (t === "object") t = (v === null ? "null" : v.constructor.name || t);
-	return t;
+	if (t !== "object")
+		return t;
+	if (v === null)
+		return "null";
+	return v.constructor.name || t;
 };
 };
 // getArrayLength(arr): arr.length
 // getArrayLength(arr): arr.length
-// getString(val): val.toString()   //NOTE: same for getStringData(val) I think
+// getString(val): val.toString() //NOTE: same for getStringData(val) I think
 // getOid
 // getOid
 // getBool
 // getBool
 // getDate
 // getDate
 // getTimestamp
 // getTimestamp
-// getRegex(re):  re.source
-// getRegexFlags(re):  re.toString().slice(-re.toString().lastIndexOf('/') + 2)
+// getRegex(re): re.source
+// getRegexFlags(re): re.toString().slice(-re.toString().lastIndexOf('/') + 2)
 // getSymbol
 // getSymbol
 // getCode
 // getCode
 // getInt
 // getInt
@@ -225,8 +240,7 @@ klass.getType = function getType(v) {
 
 
 // from bsontypes
 // from bsontypes
 klass.canonicalize = function canonicalize(x) {
 klass.canonicalize = function canonicalize(x) {
-	var xType = typeof(x);
-	if (xType === "object") xType = x === null ? "null" : x.constructor.name;
+	var xType = Value.getType(x);
 	switch (xType) {
 	switch (xType) {
 		case "MinKey":
 		case "MinKey":
 			return -1;
 			return -1;

+ 2 - 2
lib/pipeline/expressions/ValueSet.js → lib/pipeline/ValueSet.js

@@ -1,8 +1,8 @@
 "use strict";
 "use strict";
 
 
 /**
 /**
- * Somewhat mimics the ValueSet in mongo (std::set<Value>)
- * @class valueSet
+ * A set of values (i.e., `typedef unordered_set<Value, Value::Hash> ValueSet;`)
+ * @class ValueSet
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor

+ 33 - 41
lib/pipeline/accumulators/Accumulator.js

@@ -1,8 +1,7 @@
 "use strict";
 "use strict";
 
 
 /**
 /**
- * A base class for all pipeline accumulators. Uses NaryExpression as a base class.
- *
+ * A base class for all pipeline accumulators.
  * @class Accumulator
  * @class Accumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @module mungedb-aggregate
@@ -10,66 +9,59 @@
  **/
  **/
 var Accumulator = module.exports = function Accumulator(){
 var Accumulator = module.exports = function Accumulator(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	if (arguments.length !== 0) throw new Error("zero args expected");
-	this._memUsageBytes = 0;
 	base.call(this);
 	base.call(this);
 }, klass = Accumulator, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = Accumulator, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// DEPENDENCIES
-// var Value = require("../Value"),
-
-proto.memUsageForSorter = function memUsageForSorter() {
-	return this._memUsageBytes;
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
 /** Process input and update internal state.
 /** Process input and update internal state.
- * merging should be true when processing outputs from getValue(true).
+ *  merging should be true when processing outputs from getValue(true).
+ *  @method process
+ *  @param input {Value}
+ *  @param merging {Boolean}
  */
  */
-proto.process = function process(input, merging){
+proto.process = function process(input, merging) {
 	this.processInternal(input, merging);
 	this.processInternal(input, merging);
 };
 };
 
 
-proto.toJSON = function toJSON(isExpressionRequired){
-	var rep = {};
-	rep[this.getOpName()] = this.operands[0].toJSON(isExpressionRequired);
-	return rep;
+/** Marks the end of the evaluate() phase and return accumulated result.
+ *  toBeMerged should be true when the outputs will be merged by process().
+ *  @method getValue
+ *  @param toBeMerged {Boolean}
+ *  @return {Value}
+ */
+proto.getValue = function getValue(toBeMerged) {
+	throw new Error("You need to define this function on your accumulator");
 };
 };
 
 
 /**
 /**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
+ * The name of the op as used in a serialization of the pipeline.
+ * @method getOpName
+ * @return {String}
+ */
 proto.getOpName = function getOpName() {
 proto.getOpName = function getOpName() {
 	throw new Error("You need to define this function on your accumulator");
 	throw new Error("You need to define this function on your accumulator");
 };
 };
 
 
+//NOTE: DEVIATION FROM MONGO: not implementing this
+//int memUsageForSorter() const {}
+
 /**
 /**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
-proto.getValue = function getValue(toBeMerged) {
+ * Reset this accumulator to a fresh state ready to receive input.
+ * @method reset
+ */
+proto.reset = function reset() {
 	throw new Error("You need to define this function on your accumulator");
 	throw new Error("You need to define this function on your accumulator");
 };
 };
 
 
 /**
 /**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
+ * Update subclass's internal state based on input
+ * @method processInternal
+ * @param input {Value}
+ * @param merging {Boolean}
+ */
 proto.processInternal = function processInternal(input, merging) {
 proto.processInternal = function processInternal(input, merging) {
 	throw new Error("You need to define this function on your accumulator");
 	throw new Error("You need to define this function on your accumulator");
 };
 };
 
 
-/**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
-proto.reset = function reset() {
-	throw new Error("You need to define this function on your accumulator");
-};
+//NOTE: DEVIATION FROM MONGO: not implementing this
+// /// subclasses are expected to update this as necessary
+// int _memUsageBytes;

+ 27 - 36
lib/pipeline/accumulators/AddToSetAccumulator.js

@@ -6,54 +6,45 @@
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
-**/
-var AddToSetAccumulator = module.exports = function AddToSetAccumulator(/* ctx */){
+ */
+var AddToSetAccumulator = module.exports = function AddToSetAccumulator() {
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	if (arguments.length !== 0) throw new Error("zero args expected");
-	this.set = [];
-	//this.itr = undefined; /* Shoudln't need an iterator for the set */
-	//this.ctx = undefined; /* Not using the context object currently as it is related to sharding */
+	this.reset();
 	base.call(this);
 	base.call(this);
 }, klass = AddToSetAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = AddToSetAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
-var Value = require("../Value");
-
-
-// MEMBER FUNCTIONS
-
-proto.getOpName = function getOpName(){
-	return "$addToSet";
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
-
-proto.contains = function contains(value) {
-	var set = this.set;
-	for (var i = 0, l = set.length; i < l; ++i) {
-		if (Value.compare(set[i], value) === 0) {
-			return true;
-		}
-	}
-	return false;
-};
+var ValueSet = require("../ValueSet");
 
 
 proto.processInternal = function processInternal(input, merging) {
 proto.processInternal = function processInternal(input, merging) {
-	if (! this.contains(input)) {
-		this.set.push(input);
+	if (!merging) {
+		if (input !== undefined) {
+			this.set.insert(input);
+		}
+	} else {
+		// If we're merging, we need to take apart the arrays we
+		// receive and put their elements into the array we are collecting.
+		// If we didn't, then we'd get an array of arrays, with one array
+		// from each merge source.
+		if (!Array.isArray(input)) throw new Error("Assertion failure");
+
+		for (var i = 0, l = input.length; i < l; i++) {
+			this.set.insert(input[i]);
+		}
 	}
 	}
 };
 };
 
 
-proto.getValue = function getValue(toBeMerged) {
-	return this.set;
+proto.getValue = function getValue(toBeMerged) { //jshint ignore:line
+	return this.set.values();
 };
 };
 
 
 proto.reset = function reset() {
 proto.reset = function reset() {
-	this.set = [];
+	this.set = new ValueSet();
 };
 };
 
 
+klass.create = function create() {
+	return new AddToSetAccumulator();
+};
 
 
+proto.getOpName = function getOpName() {
+	return "$addToSet";
+};

+ 29 - 33
lib/pipeline/accumulators/AvgAccumulator.js

@@ -8,57 +8,53 @@
  * @constructor
  * @constructor
  **/
  **/
 var AvgAccumulator = module.exports = function AvgAccumulator(){
 var AvgAccumulator = module.exports = function AvgAccumulator(){
-	this.subTotalName = "subTotal";
-	this.countName = "count";
-	this.totalIsANumber = true;
-	this.total = 0;
-	this.count = 0;
+	this.reset();
 	base.call(this);
 	base.call(this);
 }, klass = AvgAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = AvgAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
 var Value = require("../Value");
 var Value = require("../Value");
 
 
-// MEMBER FUNCTIONS
+var SUB_TOTAL_NAME = "subTotal";
+var COUNT_NAME = "count";
+
 proto.processInternal = function processInternal(input, merging) {
 proto.processInternal = function processInternal(input, merging) {
 	if (!merging) {
 	if (!merging) {
-		if (typeof input !== "number") {
-			return;
-		}
-		this.total += input;
-		this.count += 1;
+		// non numeric types have no impact on average
+		if (typeof input != "number") return;
+
+		this._total += input;
+		this._count += 1;
 	} else {
 	} else {
-		Value.verifyDocument(input);
-		this.total += input[this.subTotalName];
-		this.count += input[this.countName];
+		// We expect an object that contains both a subtotal and a count.
+		// This is what getValue(true) produced below.
+		if (!(input instanceof Object)) throw new Error("Assertion error");
+		this._total += input[SUB_TOTAL_NAME];
+		this._count += input[COUNT_NAME];
 	}
 	}
 };
 };
 
 
-proto.getValue = function getValue(toBeMerged){
+klass.create = function create() {
+	return new AvgAccumulator();
+};
+
+proto.getValue = function getValue(toBeMerged) {
 	if (!toBeMerged) {
 	if (!toBeMerged) {
-		if (this.totalIsANumber && this.count > 0) {
-			return this.total / this.count;
-		} else if (this.count === 0) {
-			return 0;
-		} else {
-			throw new Error("$sum resulted in a non-numeric type");
-		}
+		if (this._count === 0)
+			return 0.0;
+		return this._total / this._count;
 	} else {
 	} else {
-		var ret = {};
-		ret[this.subTotalName] = this.total;
-		ret[this.countName] = this.count;
-
-		return ret;
+		var doc = {};
+		doc[SUB_TOTAL_NAME] = this._total;
+		doc[COUNT_NAME] = this._count;
+		return doc;
 	}
 	}
 };
 };
 
 
 proto.reset = function reset() {
 proto.reset = function reset() {
-	this.total = 0;
-	this.count = 0;
+	this._total = 0;
+	this._count = 0;
 };
 };
 
 
-proto.getOpName = function getOpName(){
+proto.getOpName = function getOpName() {
 	return "$avg";
 	return "$avg";
 };
 };

+ 10 - 18
lib/pipeline/accumulators/FirstAccumulator.js

@@ -9,30 +9,15 @@
  **/
  **/
 var FirstAccumulator = module.exports = function FirstAccumulator(){
 var FirstAccumulator = module.exports = function FirstAccumulator(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	if (arguments.length !== 0) throw new Error("zero args expected");
+	this.reset();
 	base.call(this);
 	base.call(this);
-	this._haveFirst = false;
-	this._first = undefined;
 }, klass = FirstAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = FirstAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// MEMBER FUNCTIONS
-proto.getOpName = function getOpName(){
-	return "$first";
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
-
 proto.processInternal = function processInternal(input, merging) {
 proto.processInternal = function processInternal(input, merging) {
-	/* only remember the first value seen */
+	// only remember the first value seen
 	if (!this._haveFirst) {
 	if (!this._haveFirst) {
-		// can't use pValue.missing() since we want the first value even if missing
 		this._haveFirst = true;
 		this._haveFirst = true;
 		this._first = input;
 		this._first = input;
-		//this._memUsageBytes = sizeof(*this) + input.getApproximateSize() - sizeof(Value);
 	}
 	}
 };
 };
 
 
@@ -43,5 +28,12 @@ proto.getValue = function getValue(toBeMerged) {
 proto.reset = function reset() {
 proto.reset = function reset() {
 	this._haveFirst = false;
 	this._haveFirst = false;
 	this._first = undefined;
 	this._first = undefined;
-	this._memUsageBytes = 0;
+};
+
+klass.create = function create() {
+	return new FirstAccumulator();
+};
+
+proto.getOpName = function getOpName() {
+	return "$first";
 };
 };

+ 16 - 13
lib/pipeline/accumulators/LastAccumulator.js

@@ -1,32 +1,35 @@
 "use strict";
 "use strict";
 
 
-/** 
- * Constructor for LastAccumulator, wraps SingleValueAccumulator's constructor and finds the last document
+/**
+ * Accumulator for getting last value
  * @class LastAccumulator
  * @class LastAccumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
  **/
  **/
 var LastAccumulator = module.exports = function LastAccumulator(){
 var LastAccumulator = module.exports = function LastAccumulator(){
+	if (arguments.length !== 0) throw new Error("zero args expected");
+	this.reset();
 	base.call(this);
 	base.call(this);
-	this.value = undefined;
 }, klass = LastAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = LastAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// NOTE: Skipping the create function, using the constructor instead
+proto.processInternal = function processInternal(input, merging) {
+	// always remember the last value seen
+	this._last = input;
+};
 
 
-// MEMBER FUNCTIONS
-proto.processInternal = function processInternal(input, merging){
-	this.value = input;
+proto.getValue = function getValue(toBeMerged) {
+	return this._last;
 };
 };
 
 
-proto.getValue = function getValue() {
-	return this.value;
+proto.reset = function reset() {
+	this._last = undefined;
 };
 };
 
 
-proto.getOpName = function getOpName(){
-	return "$last";
+klass.create = function create() {
+	return new LastAccumulator();
 };
 };
 
 
-proto.reset = function reset() {
-	this.value = undefined;
+proto.getOpName = function getOpName(){
+	return "$last";
 };
 };

+ 25 - 31
lib/pipeline/accumulators/MinMaxAccumulator.js

@@ -1,55 +1,49 @@
 "use strict";
 "use strict";
 
 
 /**
 /**
- * Constructor for MinMaxAccumulator, wraps SingleValueAccumulator's constructor and adds flag to track whether we have started or not
+ * Accumulator to get the min or max value
  * @class MinMaxAccumulator
  * @class MinMaxAccumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
  **/
  **/
-var MinMaxAccumulator = module.exports = function MinMaxAccumulator(sense){
-	if (arguments.length > 1) throw new Error("expects a single value");
+var MinMaxAccumulator = module.exports = function MinMaxAccumulator(theSense){
+	if (arguments.length != 1) throw new Error("expects a single value");
+	this._sense = theSense; // 1 for min, -1 for max; used to "scale" comparison
 	base.call(this);
 	base.call(this);
-	this.sense = sense; /* 1 for min, -1 for max; used to "scale" comparison */
-	if (this.sense !== 1 && this.sense !== -1) throw new Error("this should never happen");
+	if (this._sense !== 1 && this._sense !== -1) throw new Error("Assertion failure");
 }, klass = MinMaxAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = MinMaxAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
 var Value = require("../Value");
 var Value = require("../Value");
 
 
-// MEMBER FUNCTIONS
-proto.getOpName = function getOpName(){
-	if (this.sense == 1) return "$min";
-	return "$max";
-};
-
-klass.createMin = function createMin(){
-	return new MinMaxAccumulator(1);
+proto.processInternal = function processInternal(input, merging) {
+	// nullish values should have no impact on result
+	if (!(input === undefined || input === null)) {
+		// compare with the current value; swap if appropriate
+		var cmp = Value.compare(this._val, input) * this._sense;
+		if (cmp > 0 || this._val === undefined) { // missing is lower than all other values
+			this._val = input;
+		}
+	}
 };
 };
 
 
-klass.createMax = function createMax(){
-	return new MinMaxAccumulator(-1);
+proto.getValue = function getValue(toBeMerged) {
+	return this._val;
 };
 };
 
 
 proto.reset = function reset() {
 proto.reset = function reset() {
-	this.value = undefined;
+	this._val = undefined;
 };
 };
 
 
-proto.getValue = function getValue(toBeMerged) {
-	return this.value;
+klass.createMin = function createMin(){
+	return new MinMaxAccumulator(1);
 };
 };
 
 
-proto.processInternal = function processInternal(input, merging) {
-	// if this is the first value, just use it
-	if (!this.hasOwnProperty('value')) {
-		this.value = input;
-	} else {
-		// compare with the current value; swap if appropriate
-		var cmp = Value.compare(this.value, input) * this.sense;
-		if (cmp > 0) this.value = input;
-	}
+klass.createMax = function createMax(){
+	return new MinMaxAccumulator(-1);
+};
 
 
-	return this.value;
+proto.getOpName = function getOpName() {
+	if (this._sense == 1) return "$min";
+	return "$max";
 };
 };

+ 20 - 25
lib/pipeline/accumulators/PushAccumulator.js

@@ -8,44 +8,39 @@
  * @constructor
  * @constructor
  **/
  **/
 var PushAccumulator = module.exports = function PushAccumulator(){
 var PushAccumulator = module.exports = function PushAccumulator(){
+	if (arguments.length !== 0) throw new Error("zero args expected");
 	this.values = [];
 	this.values = [];
 	base.call(this);
 	base.call(this);
 }, klass = PushAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = PushAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// MEMBER FUNCTIONS
-proto.getValue = function getValue(toBeMerged){
-	return this.values;
-};
-
-proto.getOpName = function getOpName(){
-	return "$push";
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
-
 proto.processInternal = function processInternal(input, merging) {
 proto.processInternal = function processInternal(input, merging) {
 	if (!merging) {
 	if (!merging) {
 		if (input !== undefined) {
 		if (input !== undefined) {
 			this.values.push(input);
 			this.values.push(input);
-			//_memUsageBytes += input.getApproximateSize();
 		}
 		}
-	}
-	else {
+	} else {
 		// If we're merging, we need to take apart the arrays we
 		// If we're merging, we need to take apart the arrays we
 		// receive and put their elements into the array we are collecting.
 		// receive and put their elements into the array we are collecting.
 		// If we didn't, then we'd get an array of arrays, with one array
 		// If we didn't, then we'd get an array of arrays, with one array
 		// from each merge source.
 		// from each merge source.
-		if (!(input instanceof Array)) throw new Error("input is not an Array during merge in PushAccumulator:35");
-
-		this.values = this.values.concat(input);
+		if (!Array.isArray(input)) throw new Error("Assertion failure");
 
 
-		//for (size_t i=0; i < vec.size(); i++) {
-			//_memUsageBytes += vec[i].getApproximateSize();
-		//}
+		Array.prototype.push.apply(this.values, input);
 	}
 	}
 };
 };
+
+proto.getValue = function getValue(toBeMerged) {
+	return this.values;
+};
+
+proto.reset = function reset() {
+	this.values = [];
+};
+
+klass.create = function create() {
+	return new PushAccumulator();
+};
+
+proto.getOpName = function getOpName() {
+	return "$push";
+};

+ 25 - 18
lib/pipeline/accumulators/SumAccumulator.js

@@ -1,37 +1,44 @@
 "use strict";
 "use strict";
 
 
-/** 
- * Accumulator for summing a field across documents
+/**
+ * Accumulator for summing values
  * @class SumAccumulator
  * @class SumAccumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
  **/
  **/
-var SumAccumulator = module.exports = function SumAccumulator(){
-	this.total = 0;
-	this.count = 0;
-	this.totalIsANumber = true;
+var SumAccumulator = module.exports = function SumAccumulator() {
+	if (arguments.length !== 0) throw new Error("zero args expected");
+	this.reset();
 	base.call(this);
 	base.call(this);
-}, klass = SumAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-// NOTE: Skipping the create function, using the constructor instead
+}, klass = SumAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// MEMBER FUNCTIONS
 proto.processInternal = function processInternal(input, merging) {
 proto.processInternal = function processInternal(input, merging) {
-	if(typeof input === "number"){ // do nothing with non-numeric types
-		this.totalIsANumber = true;
-		this.total += input;
+	// do nothing with non numeric types
+	if (typeof input !== "number"){
+		if (input !== undefined && input !== null) { //NOTE: DEVIATION FROM MONGO: minor fix for 0-like values
+			this.isNumber = false;
+		}
+		return;
 	}
 	}
-	this.count++;
+	this.total += input;
+};
 
 
-	return 0;
+klass.create = function create() {
+	return new SumAccumulator();
 };
 };
 
 
-proto.getValue = function getValue(toBeMerged){
-	if (this.totalIsANumber) {
+proto.getValue = function getValue(toBeMerged) {
+	if (this.isNumber) {
 		return this.total;
 		return this.total;
+	} else {
+		throw new Error("$sum resulted in a non-numeric type; massert code 16000");
 	}
 	}
-	throw new Error("$sum resulted in a non-numeric type");
+};
+
+proto.reset = function reset() {
+	this.isNumber = true;
+	this.total = 0;
 };
 };
 
 
 proto.getOpName = function getOpName(){
 proto.getOpName = function getOpName(){

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

@@ -3,41 +3,58 @@
 /**
 /**
  * Create an expression that finds the sum of n operands.
  * Create an expression that finds the sum of n operands.
  * @class AddExpression
  * @class AddExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
- **/
+ */
 var AddExpression = module.exports = function AddExpression(){
 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);
 	base.call(this);
 }, klass = AddExpression, base = require("./VariadicExpressionT")(AddExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = AddExpression, base = require("./VariadicExpressionT")(AddExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// DEPENDENCIES
 var Value = require("../Value"),
 var Value = require("../Value"),
 	Expression = require("./Expression");
 	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) {
 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.
  * returns false on the first operand that evaluates to false.
  *
  *
  * @class AndExpression
  * @class AndExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
- **/
+ */
 var AndExpression = module.exports = function AndExpression() {
 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);
 	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"),
 var Value = require("../Value"),
 	ConstantExpression = require("./ConstantExpression"),
 	ConstantExpression = require("./ConstantExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 	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() {
 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 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;
 	var n = andExpr.operands.length;
 	// ExpressionNary::optimize() generates an ExpressionConstant for {$and:[]}.
 	// 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;
 	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";
 "use strict";
 
 
-var Value = require("../Value"),
-    Expression = require("./Expression");
-
 /**
 /**
  * Internal expression for constant values
  * Internal expression for constant values
  * @class ConstantExpression
  * @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");
     if (arguments.length !== 1) throw new Error(klass.name + ": args expected: value");
     this.value = value;
     this.value = value;
     base.call(this);
     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) {
 klass.parse = function parse(exprElement, vps) {
 	return new ConstantExpression(exprElement);
 	return new ConstantExpression(exprElement);

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

@@ -15,7 +15,7 @@
  */
  */
 var Expression = module.exports = function Expression() {
 var Expression = module.exports = function Expression() {
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	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"),
 var Value = require("../Value"),

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

@@ -6,8 +6,7 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
- **/
-
+ */
 var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubClass, nArgs) {
 var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubClass, nArgs) {
 
 
 	var FixedArityExpression = function FixedArityExpression() {
 	var FixedArityExpression = function FixedArityExpression() {
@@ -15,12 +14,16 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		base.call(this);
 		base.call(this);
 	}, klass = FixedArityExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 	}, 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
 	 * Check that the number of args is what we expected
 	 * @method validateArguments
 	 * @method validateArguments
 	 * @param args Array The array of arguments to the expression
 	 * @param args Array The array of arguments to the expression
 	 * @throws
 	 * @throws
-	 **/
+	 */
 	proto.validateArguments = function validateArguments(args) {
 	proto.validateArguments = function validateArguments(args) {
 		if(args.length !== nArgs) {
 		if(args.length !== nArgs) {
 			throw new Error("Expression " + this.getOpName() + " takes exactly " +
 			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;
 	return FixedArityExpression;
 };
 };
-

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

@@ -2,14 +2,14 @@
 
 
 /**
 /**
  * A $multiply pipeline expression.
  * A $multiply pipeline expression.
- * @see evaluateInternal
  * @class MultiplyExpression
  * @class MultiplyExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @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);
 	base.call(this);
 }, klass = MultiplyExpression, base = require("./VariadicExpressionT")(MultiplyExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, 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.
  * Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class.
  * @class NaryBaseExpressionT
  * @class NaryBaseExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
  * @constructor
  * @constructor
  */
  */
 var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass) {
 var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass) {
@@ -15,6 +15,10 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		base.call(this);
 		base.call(this);
 	}, klass = NaryBaseExpression, NaryExpression = require("./NaryExpression"), base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 	}, 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) {
 	klass.parse = function(objExpr, vps) {
 		var expr = new SubClass(),
 		var expr = new SubClass(),
 			args = NaryExpression.parseArguments(objExpr, vps);
 			args = NaryExpression.parseArguments(objExpr, vps);
@@ -23,8 +27,5 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		return expr;
 		return expr;
 	};
 	};
 
 
-	klass.parseArguments = base.parseArguments;		// NOTE: Need to explicitly
-													// bubble static members in
-													// our inheritance chain
 	return NaryBaseExpression;
 	return NaryBaseExpression;
 };
 };

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

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

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

@@ -2,67 +2,84 @@
 
 
 /**
 /**
  * An $or pipeline expression.
  * An $or pipeline expression.
- * @see evaluateInternal
  * @class OrExpression
  * @class OrExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
- **/
+ */
 var OrExpression = module.exports = function OrExpression(){
 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);
 	base.call(this);
 }, klass = OrExpression, base = require("./VariadicExpressionT")(OrExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 }, klass = OrExpression, base = require("./VariadicExpressionT")(OrExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// DEPENDENCIES
 var Value = require("../Value"),
 var Value = require("../Value"),
 	ConstantExpression = require("./ConstantExpression"),
 	ConstantExpression = require("./ConstantExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 	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){
 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);
 		var value = this.operands[i].evaluateInternal(vars);
-		if (Value.coerceToBool(value)) return true;
+		if (Value.coerceToBool(value))
+			return true;
 	}
 	}
 	return false;
 	return false;
 };
 };
 
 
 proto.optimize = function optimize() {
 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:[]}.
 	// 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;
+};

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

@@ -14,7 +14,7 @@ var SetDifferenceExpression = module.exports = function SetDifferenceExpression(
 
 
 var Value = require("../Value"),
 var Value = require("../Value"),
 	Expression = require("./Expression"),
 	Expression = require("./Expression"),
-	ValueSet = require("./ValueSet");
+	ValueSet = require("../ValueSet");
 
 
 proto.evaluateInternal = function evaluateInternal(vars) {
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var lhs = this.operands[0].evaluateInternal(vars),
 	var lhs = this.operands[0].evaluateInternal(vars),

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

@@ -14,7 +14,7 @@ var SetEqualsExpression = module.exports = function SetEqualsExpression() {
 
 
 var Value = require("../Value"),
 var Value = require("../Value"),
 	Expression = require("./Expression"),
 	Expression = require("./Expression"),
-	ValueSet = require("./ValueSet");
+	ValueSet = require("../ValueSet");
 
 
 proto.validateArguments = function validateArguments(args) {
 proto.validateArguments = function validateArguments(args) {
 	if (args.length < 2)
 	if (args.length < 2)

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

@@ -14,7 +14,7 @@ var SetIntersectionExpression = module.exports = function SetIntersectionExpress
 
 
 var Value = require("../Value"),
 var Value = require("../Value"),
 	Expression = require("./Expression"),
 	Expression = require("./Expression"),
-	ValueSet = require("./ValueSet");
+	ValueSet = require("../ValueSet");
 
 
 proto.evaluateInternal = function evaluateInternal(vars) {
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var n = this.operands.length,
 	var n = this.operands.length,

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

@@ -17,7 +17,7 @@ var Value = require("../Value"),
 	Expression = require("./Expression"),
 	Expression = require("./Expression"),
 	NaryExpression = require("./NaryExpression"),
 	NaryExpression = require("./NaryExpression"),
 	ConstantExpression = require("./ConstantExpression"),
 	ConstantExpression = require("./ConstantExpression"),
-	ValueSet = require("./ValueSet");
+	ValueSet = require("../ValueSet");
 
 
 function setIsSubsetHelper(lhs, rhs) { //NOTE: vector<Value> &lhs, ValueSet &rhs
 function setIsSubsetHelper(lhs, rhs) { //NOTE: vector<Value> &lhs, ValueSet &rhs
 	// do not shortcircuit when lhs.size() > rhs.size()
 	// do not shortcircuit when lhs.size() > rhs.size()

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

@@ -14,7 +14,7 @@ var SetUnionExpression = module.exports = function SetUnionExpression() {
 
 
 var Value = require("../Value"),
 var Value = require("../Value"),
 	Expression = require("./Expression"),
 	Expression = require("./Expression"),
-	ValueSet = require("./ValueSet");
+	ValueSet = require("../ValueSet");
 
 
 proto.evaluateInternal = function evaluateInternal(vars) {
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var unionedSet = new ValueSet(),
 	var unionedSet = new ValueSet(),

+ 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)
  * A factory and base class for all expressions that are variadic (AKA they accept any number of arguments)
  * @class VariadicExpressionT
  * @class VariadicExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryBaseExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @module mungedb-aggregate
  * @constructor
  * @constructor
- **/
-
+ */
 var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass) {
 var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass) {
 
 
 	var VariadicExpression = function VariadicExpression() {
 	var VariadicExpression = function VariadicExpression() {
@@ -15,8 +15,9 @@ var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass
 		base.call(this);
 		base.call(this);
 	}, klass = VariadicExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 	}, 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;
 	return VariadicExpression;
 };
 };

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

@@ -1,6 +1,6 @@
 "use strict";
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression.js');
-var Value = require('../Value');
+var LeafMatchExpression = require("./LeafMatchExpression.js");
+var Value = require("../Value");
 
 
 /**
 /**
  * ComparisonMatchExpression
  * ComparisonMatchExpression
@@ -27,31 +27,31 @@ proto._rhs = undefined;
 proto.debugString = function debugString(level) {
 proto.debugString = function debugString(level) {
 	var retStr = this._debugAddSpace(level) + this.path() + " ";
 	var retStr = this._debugAddSpace(level) + this.path() + " ";
 	switch (this._matchType) {
 	switch (this._matchType) {
-		case 'LT':
-			retStr += '$lt';
+		case "LT":
+			retStr += "$lt";
 			break;
 			break;
-		case 'LTE':
-			retStr += '$lte';
+		case "LTE":
+			retStr += "$lte";
 			break;
 			break;
-		case 'EQ':
-			retStr += '==';
+		case "EQ":
+			retStr += "==";
 			break;
 			break;
-		case 'GT':
-			retStr += '$gt';
+		case "GT":
+			retStr += "$gt";
 			break;
 			break;
-		case 'GTE':
-			retStr += '$gte';
+		case "GTE":
+			retStr += "$gte";
 			break;
 			break;
 		default:
 		default:
 			retStr += "Unknown comparison!";
 			retStr += "Unknown comparison!";
 			break;
 			break;
 	}
 	}
 
 
-	retStr += (this._rhs ? this._rhs.toString() : '?');
+	retStr += (this._rhs ? this._rhs.toString() : "?");
 	if (this.getTag()) {
 	if (this.getTag()) {
 		retStr += this.getTag().debugString();
 		retStr += this.getTag().debugString();
 	}
 	}
-	return retStr + '\n';
+	return retStr + "\n";
 };
 };
 
 
 /**
 /**
@@ -96,11 +96,11 @@ proto.getRHS = function getRHS() {
  */
  */
 proto.init = function init(path,rhs) {
 proto.init = function init(path,rhs) {
 	this._rhs = 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})) {
 	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);
 	return this.initPath(path);
 };
 };
@@ -118,7 +118,7 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 			return ["EQ","LTE","GTE"].indexOf(this._matchType) != -1;
 			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 this._matchType !== "EQ";
 		}
 		}
 		return false;
 		return false;
@@ -128,17 +128,16 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 
 
 	switch(this._matchType) {
 	switch(this._matchType) {
 		case "LT":
 		case "LT":
-			return x == -1;
+			return x < 0;
 		case "LTE":
 		case "LTE":
 			return x <= 0;
 			return x <= 0;
 		case "EQ":
 		case "EQ":
 			return x === 0;
 			return x === 0;
 		case "GT":
 		case "GT":
-			return x === 1;
+			return x > 0;
 		case "GTE":
 		case "GTE":
 			return x >= 0;
 			return x >= 0;
 		default:
 		default:
 			throw new Error("Invalid comparison type evaluated.");
 			throw new Error("Invalid comparison type evaluated.");
 	}
 	}
-	return false;
 };
 };

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

@@ -1,24 +1,26 @@
 "use strict";
 "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(){
 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
  * @method shallowClone
- * @param
- *
  */
  */
-proto.shallowClone = function shallowClone( /*  */ ){
-// File: expression_leaf.h lines: 141-144
+proto.shallowClone = function shallowClone(){
 	var e = new GTEMatchExpression();
 	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;
 	return e;
 };
 };
-

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

@@ -1,25 +1,26 @@
 "use strict";
 "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(){
 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}});
 }, 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
  * @method shallowClone
- * @param
- *
  */
  */
-proto.shallowClone = function shallowClone( /* */ ){
-	// File: expression_leaf.h lines: 130-133
+proto.shallowClone = function shallowClone(){
 	var e = new GTMatchExpression();
 	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;
 	return e;
 };
 };
-

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

@@ -1,24 +1,26 @@
 "use strict";
 "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(){
 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}});
 }, 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
  * @method shallowClone
- * @param
- *
  */
  */
-proto.shallowClone = function shallowClone( /* */ ){
-	// File: expression_leaf.h lines: 108-111
+proto.shallowClone = function shallowClone(){
 	var e = new LTEMatchExpression();
 	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;
 	return e;
 };
 };
-

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

@@ -1,24 +1,26 @@
 "use strict";
 "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(){
 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}});
 }, 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
  * @method shallowClone
- * @param
- *
  */
  */
-proto.shallowClone = function shallowClone( /* */ ){
-	// File: expression_leaf.h lines: 119-122
+proto.shallowClone = function shallowClone(){
 	var e = new LTMatchExpression();
 	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;
 	return e;
 };
 };
-

+ 2 - 1
package.json

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

+ 68 - 71
test/lib/pipeline/accumulators/AddToSetAccumulator.js

@@ -2,95 +2,92 @@
 var assert = require("assert"),
 var assert = require("assert"),
 	AddToSetAccumulator = require("../../../../lib/pipeline/accumulators/AddToSetAccumulator");
 	AddToSetAccumulator = require("../../../../lib/pipeline/accumulators/AddToSetAccumulator");
 
 
-
-var createAccumulator = function createAccumulator() {
-	return new AddToSetAccumulator();
+// 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 testData = {
+	nil: null,
+	bF: false, bT: true,
+	numI: 123, numF: 123.456,
+	str: "TesT! mmm π",
+	obj: {foo:{bar:"baz"}},
+	arr: [1, 2, 3, [4, 5, 6]],
+	date: new Date(),
+	re: /foo/gi,
 };
 };
 
 
 //TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
 //TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
-module.exports = {
+exports.AddToSetAccumulator = {
 
 
-	"AddToSetAccumulator": {
+	".constructor()": {
 
 
-		"constructor()": {
+		"should create instance of Accumulator": function() {
+			assert(new AddToSetAccumulator() instanceof AddToSetAccumulator);
+		},
 
 
-			"should error if called with args": function testArgsGivenToCtor() {
-				assert.throws(function() {
-					new AddToSetAccumulator('arg');
-				});
-			},
+		"should error if called with args": function() {
+			assert.throws(function() {
+				new AddToSetAccumulator(123);
+			});
+		}
 
 
-			"should construct object with set property": function testCtorAssignsSet() {
-				var acc = new AddToSetAccumulator();
-				assert.notEqual(acc.set, null);
-				assert.notEqual(acc.set, undefined);
-			}
+	},
 
 
-		},
+	".create()": {
 
 
-		"#getFactory()": {
+		"should return an instance of the accumulator": function() {
+			assert(AddToSetAccumulator.create() instanceof AddToSetAccumulator);
+		}
+
+	},
 
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new AddToSetAccumulator().getFactory(), AddToSetAccumulator);
-			}
+	"#process()": {
 
 
+		"should add input to set": function() {
+			var acc = AddToSetAccumulator.create();
+			acc.process(testData);
+			assert.deepEqual(acc.getValue(), [testData]);
 		},
 		},
 
 
-		"#processInternal()" : {
-			"should add input to set": function testAddsToSet() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([5]));
-			}
+		"should add input iff not already in set": function() {
+			var acc = AddToSetAccumulator.create();
+			acc.process(testData);
+			acc.process(testData);
+			assert.deepEqual(acc.getValue(), [testData]);
+		},
 
 
+		"should merge input into set": function() {
+			var acc = AddToSetAccumulator.create();
+			acc.process(testData);
+			acc.process([testData, 42], true);
+			assert.deepEqual(acc.getValue(), [42, testData]);
 		},
 		},
 
 
-		"#getValue()": {
-
-			"should return empty array": function testEmptySet() {
-				var acc = new createAccumulator();
-				var value = acc.getValue();
-				assert.equal((value instanceof Array), true);
-				assert.equal(value.length, 0);
-			},
-
-			"should return array with one element that equals 5": function test5InSet() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(5);
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([5]));
-			},
-
-			"should produce value that is an array of multiple elements": function testMultipleItems() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal({key: "value"});
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([5, {key: "value"}]));
-			},
-
-			"should return array with one element that is an object containing a key/value pair": function testKeyValue() {
-				var acc = createAccumulator();
-				acc.processInternal({key: "value"});
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([{key: "value"}]));
-			},
-
-			"should coalesce different instances of equivalent objects": function testGetValue_() {
-				var acc = createAccumulator();
-				acc.processInternal({key: "value"});
-				acc.processInternal({key: "value"});
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([{key: "value"}]));
-			}
+	},
 
 
-		}
+	"#getValue()": {
 
 
-	}
+		"should return empty set initially": function() {
+			var acc = new AddToSetAccumulator.create();
+			var value = acc.getValue();
+			assert.equal((value instanceof Array), true);
+			assert.equal(value.length, 0);
+		},
 
 
-};
+		"should return set of added items": function() {
+			var acc = AddToSetAccumulator.create(),
+				expected = [
+					42,
+					{foo:1, bar:2},
+					{bar:2, foo:1},
+					testData
+				];
+			expected.forEach(function(input){
+				acc.process(input);
+			});
+			assert.deepEqual(acc.getValue(), expected);
+		},
 
 
+	}
 
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 192 - 73
test/lib/pipeline/accumulators/AvgAccumulator.js

@@ -2,111 +2,230 @@
 var assert = require("assert"),
 var assert = require("assert"),
 	AvgAccumulator = require("../../../../lib/pipeline/accumulators/AvgAccumulator");
 	AvgAccumulator = require("../../../../lib/pipeline/accumulators/AvgAccumulator");
 
 
-function createAccumulator(){
-	return new AvgAccumulator();
-}
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
 
-module.exports = {
+exports.AvgAccumulator = {
 
 
-	"AvgAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AvgAccumulator();
-				});
-			}
+	".constructor()": {
 
 
+		"should not throw Error when constructing without args": function() {
+			new AvgAccumulator();
 		},
 		},
 
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
 
 
-			"should return the correct op name; $avg": function testOpName(){
-				assert.strictEqual(new AvgAccumulator().getOpName(), "$avg");
-			}
+		"should allow numbers": function() {
+			assert.doesNotThrow(function() {
+				var acc = AvgAccumulator.create();
+				acc.process(1);
+			});
+		},
 
 
+		"should ignore non-numbers": function() {
+			assert.doesNotThrow(function() {
+				var acc = AvgAccumulator.create();
+				acc.process(true);
+				acc.process("Foo");
+				acc.process(new Date());
+				acc.process({});
+				acc.process([]);
+			});
 		},
 		},
 
 
-		"#processInternal()": {
+		"router": {
 
 
-			"should evaluate no documents": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				assert.strictEqual(avgAccumulator.getValue(), 0);
+			"should handle result from one shard": function testOneShard() {
+				var acc = AvgAccumulator.create();
+				acc.process({subTotal:3.0, count:2}, true);
+				assert.deepEqual(acc.getValue(), 3.0 / 2);
 			},
 			},
 
 
-			"should evaluate one document with a field that is NaN": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(Number("foo"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(avgAccumulator.getValue(), avgAccumulator.getValue());
+			"should handle result from two shards": function testTwoShards() {
+				var acc = AvgAccumulator.create();
+				acc.process({subTotal:6.0, count:1}, true);
+				acc.process({subTotal:5.0, count:2}, true);
+				assert.deepEqual(acc.getValue(), 11.0 / 3);
 			},
 			},
 
 
+		},
+
+	},
+
+	".create()": {
+
+		"should create an instance": function() {
+			assert(AvgAccumulator.create() instanceof AvgAccumulator);
+		},
+
+	},
+
+	"#getValue()": {
 
 
-			"should evaluate one document and avg it's value": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(5);
-				assert.strictEqual(avgAccumulator.getValue(), 5);
+		"should return 0 if no inputs evaluated": function testNoDocsEvaluated() {
+			var acc = AvgAccumulator.create();
+			assert.equal(acc.getValue(), 0);
+		},
+
+		"should return one int": function testOneInt() {
+			var acc = AvgAccumulator.create();
+			acc.process(3);
+			assert.equal(acc.getValue(), 3);
+		},
+
+		"should return one long": function testOneLong() {
+			var acc = AvgAccumulator.create();
+			acc.process(-4e24);
+			assert.equal(acc.getValue(), -4e24);
+		},
+
+		"should return one double": function testOneDouble() {
+			var acc = AvgAccumulator.create();
+			acc.process(22.6);
+			assert.equal(acc.getValue(), 22.6);
+		},
+
+		"should return avg for two ints": function testIntInt() {
+			var acc = AvgAccumulator.create();
+			acc.process(10);
+			acc.process(11);
+			assert.equal(acc.getValue(), 10.5);
+		},
+
+		"should return avg for int and double": function testIntDouble() {
+			var acc = AvgAccumulator.create();
+			acc.process(10);
+			acc.process(11.0);
+			assert.equal(acc.getValue(), 10.5);
+		},
 
 
+		"should return avg for two ints w/o overflow": function testIntIntNoOverflow() {
+			var acc = AvgAccumulator.create();
+			acc.process(32767);
+			acc.process(32767);
+			assert.equal(acc.getValue(), 32767);
+		},
+
+		"should return avg for two longs w/o overflow": function testLongLongOverflow() {
+			var acc = AvgAccumulator.create();
+			acc.process(2147483647);
+			acc.process(2147483647);
+			assert.equal(acc.getValue(), (2147483647 + 2147483647) / 2);
+		},
+
+		"shard": {
+
+			"should return avg info for int": function testShardInt() {
+				var acc = AvgAccumulator.create();
+				acc.process(3);
+				assert.deepEqual(acc.getValue(true), {subTotal:3.0, count:1});
 			},
 			},
 
 
+			"should return avg info for long": function testShardLong() {
+				var acc = AvgAccumulator.create();
+				acc.process(5);
+				assert.deepEqual(acc.getValue(true), {subTotal:5.0, count:1});
+			},
 
 
-			"should evaluate and avg two ints": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(5);
-				avgAccumulator.processInternal(7);
-				assert.strictEqual(avgAccumulator.getValue(), 6);
+			"should return avg info for double": function testShardDouble() {
+				var acc = AvgAccumulator.create();
+				acc.process(116.0);
+				assert.deepEqual(acc.getValue(true), {subTotal:116.0, count:1});
 			},
 			},
 
 
-			"should evaluate and avg two ints overflow": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(Number.MAX_VALUE);
-				avgAccumulator.processInternal(Number.MAX_VALUE);
-				assert.strictEqual(Number.isFinite(avgAccumulator.getValue()), false);
+			beforeEach: function() { // used in the tests below
+				this.getAvgValueFor = function(a, b) { // kind of like TwoOperandBase
+					var acc = AvgAccumulator.create();
+					for (var i = 0, l = arguments.length; i < l; i++) {
+						acc.process(arguments[i]);
+					}
+					return acc.getValue(true);
+				};
 			},
 			},
 
 
+			"should return avg info for two ints w/ overflow": function testShardIntIntOverflow() {
+				var operand1 = 32767,
+					operand2 = 3,
+					expected = {subTotal: 32767 + 3.0, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
+			},
 
 
-			"should evaluate and avg two negative ints": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(-5);
-				avgAccumulator.processInternal(-7);
-				assert.strictEqual(avgAccumulator.getValue(), -6);
+			"should return avg info for int and long": function testShardIntLong() {
+				var operand1 = 5,
+					operand2 = 3e24,
+					expected = {subTotal: 5 + 3e24, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 			},
 
 
-//TODO Not sure how to do this in Javascript
-//			"should evaluate and avg two negative ints overflow": function testStuff(){
-//				var avgAccumulator = createAccumulator();
-//				avgAccumulator.processInternal(Number.MIN_VALUE);
-//				avgAccumulator.processInternal(7);
-//				assert.strictEqual(avgAccumulator.getValue(), Number.MAX_VALUE);
-//			},
-//
-
-			"should evaluate and avg int and float": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(8.5);
-				avgAccumulator.processInternal(7);
-				assert.strictEqual(avgAccumulator.getValue(), 7.75);
+			"should return avg info for int and double": function testShardIntDouble() {
+				var operand1 = 5,
+					operand2 = 6.2,
+					expected = {subTotal: 5 + 6.2, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 			},
 
 
-			"should evaluate and avg one Number and a NaN sum to NaN": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(8);
-				avgAccumulator.processInternal(Number("bar"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(avgAccumulator.getValue(), avgAccumulator.getValue());
+			"should return avg info for long and double": function testShardLongDouble() {
+				var operand1 = 5e24,
+					operand2 = 1.0,
+					expected = {subTotal: 5e24 + 1.0, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 			},
 
 
-			"should evaluate and avg a null value to 0": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(null);
-				assert.strictEqual(avgAccumulator.getValue(), 0);
-			}
+			"should return avg info for int and long and double": function testShardIntLongDouble() {
+				var operand1 = 1,
+					operand2 = 2e24,
+					operand3 = 4.0,
+					expected = {subTotal: 1 + 2e24 + 4.0, count: 3};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2, operand3), expected);
+			},
 
 
+		},
+
+		"should handle NaN": function() {
+			var acc = AvgAccumulator.create();
+			acc.process(NaN);
+			acc.process(1);
+			assert(isNaN(acc.getValue()));
+			acc = AvgAccumulator.create();
+			acc.process(1);
+			acc.process(NaN);
+			assert(isNaN(acc.getValue()));
+		},
+
+		"should handle null as 0": function() {
+			var acc = AvgAccumulator.create();
+			acc.process(null);
+			assert.equal(acc.getValue(), 0);
 		}
 		}
 
 
-	}
+	},
 
 
-};
+	"#reset()": {
+
+		"should reset to zero": function() {
+			var acc = AvgAccumulator.create();
+			assert.equal(acc.getValue(), 0);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), 0);
+			acc.reset();
+			assert.equal(acc.getValue(), 0);
+			assert.deepEqual(acc.getValue(true), {subTotal:0, count:0});
+		}
+
+	},
 
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	"#getOpName()": {
+
+		"should return the correct op name; $avg": function() {
+			assert.equal(new AvgAccumulator().getOpName(), "$avg");
+		}
+
+	},
+
+};

+ 77 - 55
test/lib/pipeline/accumulators/FirstAccumulator.js

@@ -2,77 +2,99 @@
 var assert = require("assert"),
 var assert = require("assert"),
 	FirstAccumulator = require("../../../../lib/pipeline/accumulators/FirstAccumulator");
 	FirstAccumulator = require("../../../../lib/pipeline/accumulators/FirstAccumulator");
 
 
-function createAccumulator(){
-	return new FirstAccumulator();
-}
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
 
-module.exports = {
+exports.FirstAccumulator = {
 
 
-	"FirstAccumulator": {
+	".constructor()": {
 
 
-		"constructor()": {
+		"should create instance of Accumulator": function() {
+			assert(new FirstAccumulator() instanceof FirstAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new FirstAccumulator(123);
+			});
+		},
 
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new FirstAccumulator();
-				});
-			}
+	},
 
 
+	".create()": {
+
+		"should return an instance of the accumulator": function() {
+			assert(FirstAccumulator.create() instanceof FirstAccumulator);
 		},
 		},
 
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
 
 
-			"should return the correct op name; $first": function testOpName(){
-				assert.equal(new FirstAccumulator().getOpName(), "$first");
-			}
+		"should return undefined if no inputs evaluated": function testNone() {
+			var acc = FirstAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
 
+		"should return value for one input": function testOne() {
+			var acc = FirstAccumulator.create();
+			acc.process(5);
+			assert.strictEqual(acc.getValue(), 5);
 		},
 		},
 
 
-		"#getFactory()": {
+		"should return missing for one missing input": function testMissing() {
+			var acc = FirstAccumulator.create();
+			acc.process(undefined);
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new FirstAccumulator().getFactory(), FirstAccumulator);
-			}
+		"should return first of two inputs": function testTwo() {
+			var acc = FirstAccumulator.create();
+			acc.process(5);
+			acc.process(7);
+			assert.strictEqual(acc.getValue(), 5);
+		},
 
 
+		"should return first of two inputs (even if first is missing)": function testFirstMissing() {
+			var acc = FirstAccumulator.create();
+			acc.process(undefined);
+			acc.process(7);
+			assert.strictEqual(acc.getValue(), undefined);
 		},
 		},
 
 
-		"#processInternal()": {
-
-			"The accumulator has no value": function none() {
-				// The accumulator returns no value in this case.
-				var acc = createAccumulator();
-				assert.ok(!acc.getValue());
-			},
-
-			"The accumulator uses processInternal on one input and retains its value": function one() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator uses processInternal on one input with the field missing and retains undefined": function missing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			},
-
-			"The accumulator uses processInternal on two inputs and retains the value in the first": function two() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator uses processInternal on two inputs and retains the undefined value in the first": function firstMissing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), undefined);
-			}
+	},
+
+	"#getValue()": {
+
+		"should get value the same for shard and router": function() {
+			var acc = FirstAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+		},
+
+	},
+
+	"#reset()": {
+
+		"should reset to missing": function() {
+			var acc = FirstAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), undefined);
+			acc.reset();
+			assert.strictEqual(acc.getValue(), undefined);
+			assert.strictEqual(acc.getValue(true), undefined);
 		}
 		}
 
 
-	}
+	},
 
 
-};
+	"#getOpName()": {
 
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should return the correct op name; $first": function() {
+			assert.equal(new FirstAccumulator().getOpName(), "$first");
+		}
+
+	},
+
+};

+ 71 - 44
test/lib/pipeline/accumulators/LastAccumulator.js

@@ -2,73 +2,100 @@
 var assert = require("assert"),
 var assert = require("assert"),
 	LastAccumulator = require("../../../../lib/pipeline/accumulators/LastAccumulator");
 	LastAccumulator = require("../../../../lib/pipeline/accumulators/LastAccumulator");
 
 
-function createAccumulator(){
-	return new LastAccumulator();
-}
+// 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.LastAccumulator = {
 
 
-module.exports = {
+	".constructor()": {
 
 
-	"LastAccumulator": {
+		"should create instance of Accumulator": function() {
+			assert(new LastAccumulator() instanceof LastAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new LastAccumulator(123);
+			});
+		},
 
 
-		"constructor()": {
+	},
 
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new LastAccumulator();
-				});
-			}
+	".create()": {
 
 
+		"should return an instance of the accumulator": function() {
+			assert(LastAccumulator.create() instanceof LastAccumulator);
 		},
 		},
 
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
+
+		"should return undefined if no inputs evaluated": function testNone() {
+			var acc = LastAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
 
-			"should return the correct op name; $last": function testOpName(){
-				assert.strictEqual(new LastAccumulator().getOpName(), "$last");
-			}
+		"should return value for one input": function testOne() {
+			var acc = LastAccumulator.create();
+			acc.process(5);
+			assert.strictEqual(acc.getValue(), 5);
+		},
 
 
+		"should return missing for one missing input": function testMissing() {
+			var acc = LastAccumulator.create();
+			acc.process(undefined);
+			assert.strictEqual(acc.getValue(), undefined);
 		},
 		},
 
 
-		"#processInternal()": {
+		"should return last of two inputs": function testTwo() {
+			var acc = LastAccumulator.create();
+			acc.process(5);
+			acc.process(7);
+			assert.strictEqual(acc.getValue(), 7);
+		},
 
 
-			"should evaluate no documents": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				assert.strictEqual(lastAccumulator.getValue(), undefined);
-			},
+		"should return last of two inputs (even if last is missing)": function testFirstMissing() {
+			var acc = LastAccumulator.create();
+			acc.process(7);
+			acc.process(undefined);
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
 
+	},
 
 
-			"should evaluate one document and retains its value": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal(5);
-				assert.strictEqual(lastAccumulator.getValue(), 5);
+	"#getValue()": {
 
 
-			},
+		"should get value the same for shard and router": function() {
+			var acc = LastAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+		},
 
 
+	},
 
 
-			"should evaluate one document with the field missing retains undefined": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal();
-				assert.strictEqual(lastAccumulator.getValue(), undefined);
-			},
+	"#reset()": {
 
 
+		"should reset to missing": function() {
+			var acc = LastAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), undefined);
+			acc.reset();
+			assert.strictEqual(acc.getValue(), undefined);
+			assert.strictEqual(acc.getValue(true), undefined);
+		},
 
 
-			"should evaluate two documents and retains the value in the last": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal(5);
-				lastAccumulator.processInternal(7);
-				assert.strictEqual(lastAccumulator.getValue(), 7);
-			},
+	},
 
 
+	"#getOpName()": {
 
 
-			"should evaluate two documents and retains the undefined value in the last": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal(5);
-				lastAccumulator.processInternal();
-				assert.strictEqual(lastAccumulator.getValue(), undefined);
-			}
-		}
+		"should return the correct op name; $last": function() {
+			assert.equal(new LastAccumulator().getOpName(), "$last");
+		},
 
 
-	}
+	},
 
 
 };
 };
 
 

+ 0 - 78
test/lib/pipeline/accumulators/MaxAccumulator.js

@@ -1,78 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	MaxAccumulator = require("../../../../lib/pipeline/accumulators/MinMaxAccumulator");
-
-function createAccumulator(){
-	return MaxAccumulator.createMax();
-}
-
-
-module.exports = {
-
-	"MaxAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args using createMax": function testConstructor(){
-				assert.doesNotThrow(function(){
-					MaxAccumulator.createMax();
-				});
-			},
-
-			"should throw Error when constructing without args using default constructor": function testConstructor(){
-				assert.throws(function(){
-					new MaxAccumulator();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $max": function testOpName(){
-				var acc = createAccumulator();
-				assert.equal(acc.getOpName(), "$max");
-			}
-
-		},
-
-		"#processInternal()": {
-
-			"The accumulator evaluates no documents": function none() {
-				// The accumulator returns no value in this case.
-				var acc = createAccumulator();
-				assert.ok(!acc.getValue());
-			},
-
-			"The accumulator evaluates one document and retains its value": function one() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator evaluates one document with the field missing retains undefined": function missing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			},
-
-			"The accumulator evaluates two documents and retains the maximum": function two() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), 7);
-			},
-
-			"The accumulator evaluates two documents and retains the defined value in the first": function lastMissing() {
-				var acc = createAccumulator();
-				acc.processInternal(7);
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), 7);
-			}
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 78
test/lib/pipeline/accumulators/MinAccumulator.js

@@ -1,78 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	MinAccumulator = require("../../../../lib/pipeline/accumulators/MinMaxAccumulator");
-
-function createAccumulator(){
-	return MinAccumulator.createMin();
-}
-
-
-module.exports = {
-
-	"MinAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args using createMin": function testConstructor(){
-				assert.doesNotThrow(function(){
-					MinAccumulator.createMin();
-				});
-			},
-
-			"should throw Error when constructing without args using default constructor": function testConstructor(){
-				assert.throws(function(){
-					new MinAccumulator();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $min": function testOpName(){
-				var acc = createAccumulator();
-				assert.equal(acc.getOpName(), "$min");
-			}
-
-		},
-
-		"#processInternal()": {
-
-			"The accumulator evaluates no documents": function none() {
-				// The accumulator returns no value in this case.
-				var acc = createAccumulator();
-				assert.ok(!acc.getValue());
-			},
-
-			"The accumulator evaluates one document and retains its value": function one() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator evaluates one document with the field missing retains undefined": function missing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			},
-
-			"The accumulator evaluates two documents and retains the minimum": function two() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator evaluates two documents and retains the undefined value in the last": function lastMissing() {
-				var acc = createAccumulator();
-				acc.processInternal(7);
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			}
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 206 - 0
test/lib/pipeline/accumulators/MinMaxAccumulator.js

@@ -0,0 +1,206 @@
+"use strict";
+var assert = require("assert"),
+	MinMaxAccumulator = require("../../../../lib/pipeline/accumulators/MinMaxAccumulator");
+
+// 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.MinMaxAccumulator = {
+
+	".constructor()": {
+
+		"should create instance of Accumulator": function() {
+			assert(MinMaxAccumulator.createMax() instanceof MinMaxAccumulator);
+		},
+
+		"should throw error if called without args": function() {
+			assert.throws(function() {
+				new MinMaxAccumulator();
+			});
+		},
+
+		"should create instance of Accumulator if called with valid sense": function() {
+			new MinMaxAccumulator(-1);
+			new MinMaxAccumulator(1);
+		},
+
+		"should throw error if called with invalid sense": function() {
+			assert.throws(function() {
+				new MinMaxAccumulator(0);
+			});
+		},
+
+	},
+
+	".createMin()": {
+
+		"should return an instance of the accumulator": function() {
+			var acc = MinMaxAccumulator.createMin();
+			assert(acc instanceof MinMaxAccumulator);
+			assert.strictEqual(acc._sense, 1);
+		},
+
+	},
+
+	".createMax()": {
+
+		"should return an instance of the accumulator": function() {
+			var acc = MinMaxAccumulator.createMax();
+			assert(acc instanceof MinMaxAccumulator);
+			assert.strictEqual(acc._sense, -1);
+		},
+
+	},
+
+	"#process()": {
+
+		"Min": {
+
+			"should return undefined if no inputs evaluated": function testNone() {
+				var acc = MinMaxAccumulator.createMin();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return value for one input": function testOne() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process(5);
+				assert.strictEqual(acc.getValue(), 5);
+			},
+
+			"should return missing for one missing input": function testMissing() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return minimum of two inputs": function testTwo() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process(5);
+				acc.process(7);
+				assert.strictEqual(acc.getValue(), 5);
+			},
+
+			"should return minimum of two inputs (ignoring undefined once found)": function testLastMissing() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process(7);
+				acc.process(undefined);
+				assert.strictEqual(acc.getValue(), 7);
+			},
+
+		},
+
+		"Max": {
+
+			"should return undefined if no inputs evaluated": function testNone() {
+				var acc = MinMaxAccumulator.createMax();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return value for one input": function testOne() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process(5);
+				assert.strictEqual(acc.getValue(), 5);
+			},
+
+			"should return missing for one missing input": function testMissing() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return maximum of two inputs": function testTwo() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process(5);
+				acc.process(7);
+				assert.strictEqual(acc.getValue(), 7);
+			},
+
+			"should return maximum of two inputs (ignoring undefined once found)": function testLastMissing() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process(7);
+				acc.process(undefined);
+				assert.strictEqual(acc.getValue(), 7);
+			},
+
+		},
+
+	},
+
+	"#getValue()": {
+
+		"Min": {
+
+			"should get value the same for shard and router": function() {
+				var acc = MinMaxAccumulator.createMin();
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+				acc.process(123);
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			},
+
+		},
+
+		"Max": {
+
+			"should get value the same for shard and router": function() {
+				var acc = MinMaxAccumulator.createMax();
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+				acc.process(123);
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			},
+
+		},
+
+	},
+
+	"#reset()": {
+
+		"Min": {
+
+			"should reset to missing": function() {
+				var acc = MinMaxAccumulator.createMin();
+				assert.strictEqual(acc.getValue(), undefined);
+				acc.process(123);
+				assert.notEqual(acc.getValue(), undefined);
+				acc.reset();
+				assert.strictEqual(acc.getValue(), undefined);
+				assert.strictEqual(acc.getValue(true), undefined);
+			},
+
+		},
+
+		"Max": {
+
+			"should reset to missing": function() {
+				var acc = MinMaxAccumulator.createMax();
+				assert.strictEqual(acc.getValue(), undefined);
+				acc.process(123);
+				assert.notEqual(acc.getValue(), undefined);
+				acc.reset();
+				assert.strictEqual(acc.getValue(), undefined);
+				assert.strictEqual(acc.getValue(true), undefined);
+			},
+
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"Min": {
+
+			"should return the correct op name; $min": function() {
+				assert.equal(MinMaxAccumulator.createMin().getOpName(), "$min");
+			},
+
+		},
+		"Max":{
+
+			"should return the correct op name; $max": function() {
+				assert.equal(MinMaxAccumulator.createMax().getOpName(), "$max");
+			},
+
+		},
+
+	},
+
+};

+ 97 - 77
test/lib/pipeline/accumulators/PushAccumulator.js

@@ -2,99 +2,119 @@
 var assert = require("assert"),
 var assert = require("assert"),
 	PushAccumulator = require("../../../../lib/pipeline/accumulators/PushAccumulator");
 	PushAccumulator = require("../../../../lib/pipeline/accumulators/PushAccumulator");
 
 
+// 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));
 
 
-function createAccumulator(){
-	return new PushAccumulator();
-}
 
 
-module.exports = {
+exports.PushAccumulator = {
 
 
-	"PushAccumulator": {
+	".constructor()": {
 
 
-		"constructor()": {
+		"should create instance of accumulator": function() {
+			assert(new PushAccumulator() instanceof PushAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new PushAccumulator(123);
+			});
+		},
+
+	},
+
+	".create()": {
+
+		"should return an instance of the accumulator": function() {
+			assert(PushAccumulator.create() instanceof PushAccumulator);
+		},
+
+	},
+
+	"#process()": {
+
+		"should return empty array if no inputs evaluated": function() {
+			var acc = PushAccumulator.create();
+			assert.deepEqual(acc.getValue(), []);
+		},
+
+		"should return array of one value for one input": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			assert.deepEqual(acc.getValue(), [1]);
+		},
+
+		"should return array of two values for two inputs": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			acc.process(2);
+			assert.deepEqual(acc.getValue(), [1,2]);
+		},
+
+		"should return array of two values for two inputs (including null)": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			acc.process(null);
+			assert.deepEqual(acc.getValue(), [1, null]);
+		},
+
+		"should return array of one value for two inputs if one is undefined": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			acc.process(undefined);
+			assert.deepEqual(acc.getValue(), [1]);
+		},
+
+		"should return array of two values from two separate mergeable inputs": function() {
+			var acc = PushAccumulator.create();
+			acc.process([1], true);
+			acc.process([0], true);
+			assert.deepEqual(acc.getValue(), [1, 0]);
+		},
+
+		"should throw error if merging non-array": function() {
+			var acc = PushAccumulator.create();
+			assert.throws(function() {
+				acc.process(0, true);
+			});
+			assert.throws(function() {
+				acc.process("foo", true);
+			});
+		},
+
+	},
 
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new PushAccumulator();
-				});
-			}
+	"#getValue()": {
 
 
+		"should get value the same for shard and router": function() {
+			var acc = PushAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
 		},
 		},
 
 
-		"#getOpName()": {
+	},
 
 
-			"should return the correct op name; $push": function testOpName(){
-				assert.strictEqual(new PushAccumulator().getOpName(), "$push");
-			}
+	"#reset()": {
 
 
+		"should reset to empty array": function() {
+			var acc = PushAccumulator.create();
+			assert.deepEqual(acc.getValue(), []);
+			acc.process(123);
+			assert.notDeepEqual(acc.getValue(), []);
+			acc.reset();
+			assert.deepEqual(acc.getValue(), []);
+			assert.deepEqual(acc.getValue(true), []);
 		},
 		},
 
 
-		"#getFactory()": {
+	},
 
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new PushAccumulator().getFactory(), PushAccumulator);
-			}
+	"#getOpName()": {
 
 
+		"should return the correct op name; $push": function(){
+			assert.strictEqual(new PushAccumulator().getOpName(), "$push");
 		},
 		},
 
 
-		"#processInternal()": {
-
-			"should processInternal no documents and return []": function testprocessInternal_None(){
-				var accumulator = createAccumulator();
-				assert.deepEqual(accumulator.getValue(), []);
-			},
-
-			"should processInternal a 1 and return [1]": function testprocessInternal_One(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				assert.deepEqual(accumulator.getValue(), [1]);
-			},
-
-			"should processInternal a 1 and a 2 and return [1,2]": function testprocessInternal_OneTwo(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(2);
-				assert.deepEqual(accumulator.getValue(), [1,2]);
-			},
-
-			"should processInternal a 1 and a null and return [1,null]": function testprocessInternal_OneNull(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(null);
-				assert.deepEqual(accumulator.getValue(), [1, null]);
-			},
-
-			"should processInternal a 1 and an undefined and return [1]": function testprocessInternal_OneUndefined(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(undefined);
-				assert.deepEqual(accumulator.getValue(), [1]);
-			},
-
-			"should processInternal a 1 and a 0 and return [1,0]": function testprocessInternal_OneZero(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(0);
-				assert.deepEqual(accumulator.getValue(), [1, 0]);
-			},
-
-			"should processInternal a 1 and a [0] and return [1,0]": function testprocessInternal_OneArrayZeroMerging(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal([0], true);
-				assert.deepEqual(accumulator.getValue(), [1, 0]);
-			},
-
-			"should processInternal a 1 and a 0 and throw an error if merging": function testprocessInternal_OneZeroMerging(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				assert.throws(function() {
-					accumulator.processInternal(0, true);
-				});
-			}
-		}
-
-	}
+	},
 
 
 };
 };
 
 

+ 240 - 78
test/lib/pipeline/accumulators/SumAccumulator.js

@@ -2,113 +2,275 @@
 var assert = require("assert"),
 var assert = require("assert"),
 	SumAccumulator = require("../../../../lib/pipeline/accumulators/SumAccumulator");
 	SumAccumulator = require("../../../../lib/pipeline/accumulators/SumAccumulator");
 
 
+// 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));
 
 
-function createAccumulator(){
-	return new SumAccumulator();
-}
+exports.SumAccumulator = {
 
 
+	".constructor()": {
 
 
-module.exports = {
+		"should create instance of Accumulator": function() {
+			assert(new SumAccumulator() instanceof SumAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new SumAccumulator(123);
+			});
+		},
+
+	},
+
+	".create()": {
+
+		"should return an instance of the accumulator": function() {
+			assert(SumAccumulator.create() instanceof SumAccumulator);
+		},
+
+	},
+
+	"#process()": {
+
+		"should return 0 if no inputs evaluated": function testNone() {
+			var acc = SumAccumulator.create();
+			assert.strictEqual(acc.getValue(), 0);
+		},
+
+		"should return value for one int input": function testOneInt() {
+			var acc = SumAccumulator.create();
+			acc.process(5);
+			assert.strictEqual(acc.getValue(), 5);
+		},
+
+		"should return value for one long input": function testOneLong() {
+			var acc = SumAccumulator.create();
+			acc.process(6e24);
+			assert.strictEqual(acc.getValue(), 6e24);
+		},
+
+		"should return value for one large long input": function testOneLargeLong() {
+			var acc = SumAccumulator.create();
+			acc.process(6e42);
+			assert.strictEqual(acc.getValue(), 6e42);
+		},
+
+		"should return value for one double input": function testOneDouble() {
+			var acc = SumAccumulator.create();
+			acc.process(7.0);
+			assert.strictEqual(acc.getValue(), 7.0);
+		},
+
+		"should return value for one fractional double input": function testNanDouble() {
+			var acc = SumAccumulator.create();
+			acc.process(NaN);
+			assert.notEqual(acc.getValue(), acc.getValue()); // NaN is unequal to itself.
+		},
+
+		beforeEach: function() { // used in the tests below
+			this.getSumValueFor = function(first, second) { // kind of like TwoOperandBase
+				var acc = SumAccumulator.create();
+				for (var i = 0, l = arguments.length; i < l; i++) {
+					acc.process(arguments[i]);
+				}
+				return acc.getValue();
+			};
+		},
+
+		"should return sum for two ints": function testIntInt() {
+			var summand1 = 4,
+				summand2 = 5,
+				expected = 9;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for two ints (overflow)": function testIntIntOverflow() {
+			var summand1 = 32767,
+				summand2 = 10,
+				expected = 32767 + 10;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for two ints (negative overflow)": function testIntIntNegativeOverflow() {
+			var summand1 = 32767,
+				summand2 = -10,
+				expected = 32767 + -10;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-	"SumAccumulator": {
+		"should return sum for int and long": function testIntLong() {
+			var summand1 = 4,
+				summand2 = 5e24,
+				expected = 4 + 5e24;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-		"constructor()": {
+		"should return sum for max int and long (no int overflow)": function testIntLongNoIntOverflow() {
+			var summand1 = 32767,
+				summand2 = 1e24,
+				expected = 32767 + 1e24;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for int and max long (long overflow)": function testIntLongLongOverflow() {
+			var summand1 = 1,
+				summand2 = 9223372036854775807,
+				expected = 1 + 9223372036854775807;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for long and long": function testLongLong() {
+			var summand1 = 4e24,
+				summand2 = 5e24,
+				expected = 4e24 + 5e24;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for max long and max long (overflow)": function testLongLongOverflow() {
+			var summand1 = 9223372036854775807,
+				summand2 = 9223372036854775807,
+				expected = 9223372036854775807 + 9223372036854775807;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for int and double": function testIntDouble() {
+			var summand1 = 4,
+				summand2 = 5.5,
+				expected = 9.5;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SumAccumulator();
-				});
-			}
+		"should return sum for int and NaN as NaN": function testIntNanDouble() {
+			var summand1 = 4,
+				summand2 = NaN,
+				expected = NaN;
+			assert(isNaN(this.getSumValueFor(summand1, summand2)));
+			assert(isNaN(this.getSumValueFor(summand2, summand1)));
+		},
 
 
+		"should return sum for int and double (no int overflow)": function testIntDoubleNoIntOverflow() {
+			var summand1 = 32767,
+				summand2 = 1.0,
+				expected = 32767 + 1.0;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
 		},
 		},
 
 
-		"#getOpName()": {
+		"should return sum for long and double": function testLongDouble() {
+			var summand1 = 4e24,
+				summand2 = 5.5,
+				expected = 4e24 + 5.5;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-			"should return the correct op name; $sum": function testOpName(){
-				assert.strictEqual(new SumAccumulator().getOpName(), "$sum");
-			}
+		"should return sum for max long and double (no long overflow)": function testLongDoubleNoLongOverflow() {
+			var summand1 = 9223372036854775807,
+				summand2 = 1.0,
+				expected = 9223372036854775807 + 1.0;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
+		"should return sum for double and double": function testDoubleDouble() {
+			var summand1 = 2.5,
+				summand2 = 5.5,
+				expected = 8.0;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
 		},
 		},
 
 
-		"#processInternal()": {
+		"should return sum for double and double (overflow)": function testDoubleDoubleOverflow() {
+			var summand1 = Number.MAX_VALUE,
+				summand2 = Number.MAX_VALUE,
+				expected = Infinity;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-			"should evaluate no documents": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				assert.strictEqual(sumAccumulator.getValue(), 0);
-			},
+		"should return sum for int and long and double": function testIntLongDouble() {
+			assert.strictEqual(this.getSumValueFor(5, 99, 0.2), 104.2);
+		},
 
 
-			"should evaluate one document with a field that is NaN": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(Number("foo"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(sumAccumulator.getValue(), sumAccumulator.getValue());
-			},
+		"should return sum for a negative value": function testNegative() {
+			var summand1 = 5,
+				summand2 = -8.8,
+				expected = 5 - 8.8;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
+		"should return sum for long and negative int": function testLongIntNegative() {
+			var summand1 = 5e24,
+				summand2 = -6,
+				expected = 5e24 - 6;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-			"should evaluate one document and sum it's value": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(5);
-				assert.strictEqual(sumAccumulator.getValue(), 5);
+		"should return sum for int and null": function testIntNull() {
+			var summand1 = 5,
+				summand2 = null,
+				expected = 5;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
-			},
+		"should return sum for int and undefined": function testIntUndefined() {
+			var summand1 = 9,
+				summand2, // = undefined,
+				expected = 9;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
 
+		"should return sum for long long max and long long max and 1": function testNoOverflowBeforeDouble() {
+			var actual = this.getSumValueFor(9223372036854775807, 9223372036854775807, 1.0),
+				expected = 9223372036854775807 + 9223372036854775807;
+			assert.strictEqual(actual, expected);
+		},
 
 
-			"should evaluate and sum two ints": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(5);
-				sumAccumulator.processInternal(7);
-				assert.strictEqual(sumAccumulator.getValue(), 12);
-			},
+	},
 
 
-			"should evaluate and sum two ints overflow": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(Number.MAX_VALUE);
-				sumAccumulator.processInternal(Number.MAX_VALUE);
-				assert.strictEqual(Number.isFinite(sumAccumulator.getValue()), false);
-			},
+	"#getValue()": {
 
 
+		"should get value the same for shard and router": function() {
+			var acc = SumAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+		},
 
 
-			"should evaluate and sum two negative ints": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(-5);
-				sumAccumulator.processInternal(-7);
-				assert.strictEqual(sumAccumulator.getValue(), -12);
-			},
+	},
 
 
-//TODO Not sure how to do this in Javascript
-//			"should evaluate and sum two negative ints overflow": function testStuff(){
-//				var sumAccumulator = createAccumulator();
-//				sumAccumulator.processInternal({b:Number.MIN_VALUE});
-//				sumAccumulator.processInternal({b:7});
-//				assert.strictEqual(sumAccumulator.getValue(), Number.MAX_VALUE);
-//			},
-//
+	"#reset()": {
 
 
-			"should evaluate and sum int and float": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(8.5);
-				sumAccumulator.processInternal(7);
-				assert.strictEqual(sumAccumulator.getValue(), 15.5);
-			},
+		"should reset to 0": function() {
+			var acc = SumAccumulator.create();
+			assert.strictEqual(acc.getValue(), 0);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), 0);
+			acc.reset();
+			assert.strictEqual(acc.getValue(), 0);
+			assert.strictEqual(acc.getValue(true), 0);
+		},
 
 
-			"should evaluate and sum one Number and a NaN sum to NaN": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(8);
-				sumAccumulator.processInternal(Number("bar"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(sumAccumulator.getValue(), sumAccumulator.getValue());
-			},
+	},
 
 
-			"should evaluate and sum a null value to 0": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(null);
-				assert.strictEqual(sumAccumulator.getValue(), 0);
-			}
+	"#getOpName()": {
 
 
+		"should return the correct op name; $sum": function() {
+			assert.equal(SumAccumulator.create().getOpName(), "$sum");
 		}
 		}
 
 
-	}
+	},
 
 
 };
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

@@ -1,132 +1,249 @@
 "use strict";
 "use strict";
 var assert = require("assert"),
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"),
 	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");
 	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);
+			debugger
+			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);

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

@@ -1,213 +1,286 @@
 "use strict";
 "use strict";
 var assert = require("assert"),
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
 	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);

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

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

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

@@ -1,143 +1,287 @@
 "use strict";
 "use strict";
 var assert = require("assert"),
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	OrExpression = require("../../../../lib/pipeline/expressions/OrExpression"),
 	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 - 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";
 "use strict";
 var assert = require("assert"),
 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");
 	GTEMatchExpression = require("../../../../lib/pipeline/matcher/GTEMatchExpression");
 
 
 
 
@@ -8,68 +9,102 @@ module.exports = {
 	"GTEMatchExpression": {
 	"GTEMatchExpression": {
 		"should match scalars and strings properly": function (){
 		"should match scalars and strings properly": function (){
 			var e = new GTEMatchExpression();
 			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(){
 		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
 			var e = new GTEMatchExpression();
 			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() {
 		"should match a pathed number":function() {
 			var e = new GTEMatchExpression();
 			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() {
 		"should match stuff in an array": function() {
 			var e = new GTEMatchExpression();
 			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() {
 		"should not match full array" : function() {
 			var e = new GTEMatchExpression();
 			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() {
 		"should not match null" : function() {
 			var e = new GTEMatchExpression();
 			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() {
 		"should handle elemMatchKey":function() {
 			var e = new GTEMatchExpression();
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
 			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( ! m.hasElemMatchKey() );
 
 
-			assert.ok( e.matches({'a':6}, m) );
+			assert.ok( e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 			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.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";
 "use strict";
 var assert = require("assert"),
 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");
 	GTMatchExpression = require("../../../../lib/pipeline/matcher/GTMatchExpression");
 
 
 
 
 module.exports = {
 module.exports = {
 	"GTMatchExpression": {
 	"GTMatchExpression": {
-		"should match scalars and strings properly": function (){
+		"should handle invalid End of Object Operand": function (){
 			var e = new GTMatchExpression();
 			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 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 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 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 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 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() {
 		"should handle elemMatchKey":function() {
 			var e = new GTMatchExpression();
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
 			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( ! m.hasElemMatchKey() );
 
 
-			assert.ok( e.matches({'a':6}, m) );
+			assert.ok( e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 			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.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";
 "use strict";
 var assert = require("assert"),
 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");
 	LTEMatchExpression = require("../../../../lib/pipeline/matcher/LTEMatchExpression");
 
 
 
 
 module.exports = {
 module.exports = {
 	"LTEMatchExpression": {
 	"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 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 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 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() {
 		"should handle elemMatchKey":function() {
 			var e = new LTEMatchExpression();
 			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
 			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( ! m.hasElemMatchKey() );
 
 
-			assert.ok( e.matches({'a':4}, m) );
+			assert.ok( e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 			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.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";
 "use strict";
 var assert = require("assert"),
 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");
 	LTMatchExpression = require("../../../../lib/pipeline/matcher/LTMatchExpression");
 
 
 
 
 module.exports = {
 module.exports = {
 	"LTMatchExpression": {
 	"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 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 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 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() {
 		"should handle elemMatchKey":function() {
 			var e = new LTMatchExpression();
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
 			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( ! m.hasElemMatchKey() );
 
 
-			assert.ok( e.matches({'a':4}, m) );
+			assert.ok( e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 			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.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 		}
-
-
-
 	}
 	}
 };
 };