Browse Source

Merge branch 'feature/mongo_2.6.5_expressions_Nary' into feature/mongo_2.6.5_expressions_FixedArity

Conflicts:
	lib/pipeline/expressions/ToLowerExpression.js
	lib/pipeline/expressions/ToUpperExpression.js
Kyle P Davis 11 years ago
parent
commit
ee725b9f19
29 changed files with 1531 additions and 740 deletions
  1. 79 31
      lib/pipeline/Document.js
  2. 166 55
      lib/pipeline/Value.js
  3. 5 4
      lib/pipeline/expressions/AddExpression.js
  4. 5 10
      lib/pipeline/expressions/AndExpression.js
  5. 8 10
      lib/pipeline/expressions/ConcatExpression.js
  6. 99 79
      lib/pipeline/expressions/Expression.js
  7. 30 16
      lib/pipeline/expressions/LetExpression.js
  8. 5 4
      lib/pipeline/expressions/MultiplyExpression.js
  9. 153 0
      lib/pipeline/expressions/NaryExpression.js
  10. 29 0
      lib/pipeline/expressions/NaryExpressionBaseT.js
  11. 0 162
      lib/pipeline/expressions/NaryExpressionT.js
  12. 5 4
      lib/pipeline/expressions/OrExpression.js
  13. 12 17
      lib/pipeline/expressions/ToLowerExpression.js
  14. 5 10
      lib/pipeline/expressions/ToUpperExpression.js
  15. 19 0
      lib/pipeline/expressions/VariadicExpressionT.js
  16. 110 0
      test/lib/pipeline/Document.js
  17. 327 0
      test/lib/pipeline/Value.js
  18. 6 1
      test/lib/pipeline/expressions/AddExpression_test.js
  19. 6 0
      test/lib/pipeline/expressions/AndExpression_test.js
  20. 12 2
      test/lib/pipeline/expressions/ConcatExpression_test.js
  21. 6 0
      test/lib/pipeline/expressions/MultiplyExpression_test.js
  22. 0 333
      test/lib/pipeline/expressions/NaryExpressionT.js
  23. 240 0
      test/lib/pipeline/expressions/NaryExpression_test.js
  24. 6 0
      test/lib/pipeline/expressions/OrExpression_test.js
  25. 7 1
      test/lib/pipeline/expressions/ToLowerExpression_test.js
  26. 7 1
      test/lib/pipeline/expressions/ToUpperExpression_test.js
  27. 36 0
      test/lib/pipeline/expressions/VariadicExpressionT_test.js
  28. 46 0
      test/lib/pipeline/expressions/utils.js
  29. 102 0
      test/lib/pipeline/expressions/utils_test.js

+ 79 - 31
lib/pipeline/Document.js

@@ -11,10 +11,8 @@ var Document = module.exports = function Document(){
 	if(this.constructor == Document) throw new Error("Never create instances! Use static helpers only.");
 }, klass = Document, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("./Value");
 
-// STATIC MEMBERS
 /**
  * Shared "_id"
  * @static
@@ -22,18 +20,45 @@ var Value = require("./Value");
  **/
 klass.ID_PROPERTY_NAME = "_id";
 
+//SKIPPED: DocumentStorage
+
 /**
- * Compare two documents.
+ * Return JSON representation of this Document
+ * @method toJson
+ * @returns {Object} JSON representation of this Document
+ **/
+klass.toJson = function toJson(doc) {
+ 	return JSON.parse(JSON.stringify(doc));
+};
+
+//SKIPPED: metaFieldTextScore
+//SKIPPED: toBsonWithMetaData
+//SKIPPED: fromBsonWithMetaData
+
+//SKIPPED: MutableDocument
+
+//SKIPPED: getNestedFieldHelper
+//SKIPPED: getNestedField -- same as getNestedFieldHelper in our code
+//SKIPPED: getApproximateSize -- not implementing mem usage right now
+//SKIPPED: hash_combine
+
+/** Compare two documents.
  *
- * BSON document field order is significant, so this just goes through the fields in order.
- * The comparison is done in roughly the same way as strings are compared, but comparing one field at a time instead of one character at a time.
+ *  BSON document field order is significant, so this just goes through
+ *  the fields in order.  The comparison is done in roughly the same way
+ *  as strings are compared, but comparing one field at a time instead
+ *  of one character at a time.
+ *
+ *  Note: This does not consider metadata when comparing documents.
  *
- * @static
  * @method compare
- * @param rL left document
- * @param rR right document
- * @returns an integer less than zero, zero, or an integer greater than zero, depending on whether rL < rR, rL == rR, or rL > rR
- **/
+ * @static
+ * @param l {Object}  left document
+ * @param r {Object} right document
+ * @returns an integer less than zero, zero, or an integer greater than
+ *           zero, depending on whether lhs < rhs, lhs == rhs, or lhs > rhs
+ *  Warning: may return values other than -1, 0, or 1
+ */
 klass.compare = function compare(l, r){	//TODO: might be able to replace this with a straight compare of docs using JSON.stringify()
 	var lPropNames = Object.getOwnPropertyNames(l),
 		lPropNamesLength = lPropNames.length,
@@ -48,42 +73,65 @@ klass.compare = function compare(l, r){	//TODO: might be able to replace this wi
 
 		if (i >= rPropNamesLength) return 1; // right document is shorter
 
-		var nameCmp = Value.compare(lPropNames[i], rPropNames[i]);
+		var rField = rPropNames[i],
+			lField = lPropNames[i];
+		var nameCmp = Value.compare(lField, rField);
 		if (nameCmp !== 0) return nameCmp; // field names are unequal
 
-		var valueCmp = Value.compare(l[lPropNames[i]], r[rPropNames[i]]);
+		var valueCmp = Value.compare(l[lPropNames[i]], r[rField]);
 		if (valueCmp) return valueCmp; // fields are unequal
 	}
+};
+
+//SKIPPED: toString
+
+klass.serializeForSorter = function serializeForSorter(doc) {
+	//NOTE: DEVIATION FROM MONGO: they take a buffer to output the current instance into, ours is static and takes a doc and returns the serialized output
+	return JSON.stringify(doc);
+};
+
+klass.deserializeForSorter = function deserializeForSorter(docStr, sorterDeserializeSettings) {
+	JSON.parse(docStr);
+};
 
-	/* NOTREACHED */
-	throw new Error("This should never happen");	//verify(false)
-//		return 0;
+//SKIPPED: swap
+//SKIPPED: []
+//SKIPPED: getField -- inline as:  obj[key]
+//SKIPPED: getNestedField -- use fieldPath? might need to implement this...
+//SKIPPED: size -- need this? Number of fields in this document. O(n) -- recursive
+klass.empty = function(obj) {
+	return Object.keys(obj).length === 0;
 };
+//SKIPPED: operator <<
+//SKIPPED: positionOf
 
 /**
  * Clone a document
  * @static
  * @method clone
- * @param document
- **/
-klass.clone = function(document){
+ * @param doc
+ */
+klass.clone = function clone(doc) {
 	var obj = {};
-	for(var key in document){
-		if(document.hasOwnProperty(key)){
-			var withObjVal = document[key];
-			if(withObjVal === null) { // necessary to handle null values without failing
-				obj[key] = withObjVal;
-			}
-			else if(withObjVal.constructor === Object){
-				obj[key] = Document.clone(withObjVal);
-			}else{
-				obj[key] = withObjVal;
+	for (var key in doc) {
+		if (doc.hasOwnProperty(key)) {
+			var val = doc[key];
+			if (val === undefined || val === null) { // necessary to handle null values without failing
+				obj[key] = val;
+			} else if (val instanceof Object && val.constructor === Object) {
+				obj[key] = Document.clone(val);
+			} else {
+				obj[key] = val;
 			}
 		}
 	}
 	return obj;
 };
 
-//	proto.addField = function addField(){ throw new Error("Instead of `Document#addField(key,val)` you should just use `obj[key] = val`"); }
-//	proto.setField = function addField(){ throw new Error("Instead of `Document#setField(key,val)` you should just use `obj[key] = val`"); }
-//  proto.getField = function getField(){ throw new Error("Instead of `Document#getField(key)` you should just use `var val = obj[key];`"); }
+//SKIPPED: hasTextScore
+//SKIPPED: getTextScore
+
+//SKIPPED: memUsageForSorter -- not implementing mem usage right now
+//SKIPPED: getOwned -- not implementing mem usage right now
+
+//SKIPPED: getPtr

+ 166 - 55
lib/pipeline/Value.js

@@ -11,34 +11,36 @@ var Value = module.exports = function Value(){
 	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}});
 
-// PRIVATE STUFF
-function getTypeVerifier(type, IClass, isStrict) {
-	return function verifyType(value) {
-		if (typeof(value) != type) throw new Error("typeof value is not: " + type + "; actual: " + typeof(value));
-		if (typeof(IClass) == "function" && !(isStrict ? value.constructor == IClass : value instanceof IClass)) throw new Error("instanceof value is not: " + IClass.name + "; actual: " + value.constructor.name);
-		return value;
-	};
-}
-
-// STATIC MEMBERS
-klass.verifyNumber = getTypeVerifier("number", Number);	//NOTE: replaces #getDouble(), #getInt(), and #getLong()
-klass.verifyString = getTypeVerifier("string", String);
-klass.verifyDocument = getTypeVerifier("object", Object, true);	//TODO: change to verifyObject? since we're not using actual Document instances
-klass.verifyArray = getTypeVerifier("object", Array, true);
-klass.verifyDate = getTypeVerifier("object", Date, true);
-klass.verifyRegExp = getTypeVerifier("object", RegExp, true);	//NOTE: renamed from #getRegex()
-//TODO:	klass.verifyOid = ...?
-//TODO:	klass.VerifyTimestamp = ...?
-klass.verifyBool = getTypeVerifier("boolean", Boolean, true);
+var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
+
+//SKIPPED: ValueStorage -- probably not required; use JSON?
+//SKIPPED: createIntOrLong -- not required; use Number
+//SKIPPED: operator <Array>[] -- not required; use arr[i]
+//SKIPPED: operator <Object>[] -- not required; use obj[key]
+//SKIPPED: operator << -- not required
+//SKIPPED: addToBsonObj -- not required; use obj[key] = <val>
+//SKIPPED: addToBsonArray -- not required; use arr.push(<val>)
 
+/** Coerce a value to a bool using BSONElement::trueValue() rules.
+ * Some types unsupported.  SERVER-6120
+ * @method coerceToBool
+ * @static
+ */
 klass.coerceToBool = function coerceToBool(value) {
 	if (typeof(value) == "string") return true;
 	return !!value;	// including null or undefined
 };
-klass.coerceToInt =
-klass.coerceToLong =
-klass.coerceToDouble =
-klass._coerceToNumber = function _coerceToNumber(value) { //NOTE: replaces .coerceToInt(), .coerceToLong(), and .coerceToDouble()
+
+/** 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) {
+	return klass.coerceToNumber(value) | 0;
+};
+klass.coerceToInt = klass.coerceToWholeNumber;
+klass.coerceToLong = klass.coerceToWholeNumber;
+klass.coerceToNumber = function coerceToNumber(value) {
 	if (value === null) return 0;
 	switch (typeof(value)) {
 	case "undefined":
@@ -59,38 +61,56 @@ klass._coerceToNumber = function _coerceToNumber(value) { //NOTE: replaces .coer
 		throw new Error("can't convert from BSON type " + typeof(value) + " to int; codes 16003, 16004, 16005");
 	}
 };
+klass.coerceToDouble = klass.coerceToNumber;
 klass.coerceToDate = function coerceToDate(value) {
-	//TODO: Support Timestamp BSON type?
 	if (value instanceof Date) return value;
 	throw new Error("can't convert from BSON type " + typeof(value) + " to Date; uassert code 16006");
 };
-//TODO: klass.coerceToTimeT = ...?   try to use as Date first rather than having coerceToDate return Date.parse  or dateObj.getTime() or similar
-//TODO:	klass.coerceToTm = ...?
+//SKIPPED: coerceToTimeT -- not required; just use Date
+//SKIPPED: coerceToTm -- not required; just use Date
+//SKIPPED: tmToISODateString -- not required; just use Date
 klass.coerceToString = function coerceToString(value) {
-	if (value === null) return "";
-	switch (typeof(value)) {
-	case "undefined":
-		return "";
-	case "number":
-		return value.toString();
-	case "string":
-		return value;
-	default:
-		throw new Error("can't convert from BSON type " + typeof(value) + " to String; uassert code 16007");
+	var type = typeof(value);
+	if (type == "object") type = value === null ? "null" : value.constructor.name;
+	switch (type) {
+		//TODO: BSON numbers?
+		case "number":
+			return value.toString();
+
+		//TODO: BSON Code?
+		//TODO: BSON Symbol?
+		case "string":
+			return value;
+
+		//TODO: BSON Timestamp?
+		case "Date":
+			return value.toISOString().split(".")[0];
+
+		case "null":
+		case "undefined":
+			return "";
+
+		default:
+			throw new Error("can't convert from BSON type " + typeof(value) + " to String; uassert code 16007");
 	}
 };
-//TODO:	klass.coerceToTimestamp = ...?
+//SKIPPED: coerceToTimestamp
 
 /**
- * Compare two Values.
- *
+ * Helper function for Value.compare
+ * @method cmp
+ * @static
+ */
+klass.cmp = function cmp(l, r){
+	return l < r ? -1 : l > r ? 1 : 0;
+};
+
+/** Compare two Values.
  * @static
  * @method compare
- * @param rL left value
- * @param rR right value
- * @returns an integer less than zero, zero, or an integer greater than zero, depending on whether rL < rR, rL == rR, or rL > rR
- **/
-var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
+ * @returns an integer less than zero, zero, or an integer greater than zero, depending on whether lhs < rhs, lhs == rhs, or lhs > rhs
+ * Warning: may return values other than -1, 0, or 1
+ */
 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),
@@ -110,14 +130,14 @@ klass.compare = function compare(l, r) {
 		return klass.cmp(l,r);
 	}
 	// Compare MinKey and MaxKey cases
-	if(l.constructor && l.constructor.name in {'MinKey':1,'MaxKey':1} ){
-		if(l.constructor.name == r.constructor.name) { 
-			return 0; 
-		} else if (l.constructor.name === 'MinKey'){
+	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"){
@@ -132,7 +152,7 @@ klass.compare = function compare(l, r) {
 	case "number":
 		throw new Error("number types should have been handled earlier!");
 	case "string":
-		return klass.cmp(l,r);
+		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)
@@ -162,8 +182,99 @@ klass.compare = function compare(l, r) {
 
 };
 
-//TODO:	klass.hashCombine = ...?
-//TODO:	klass.getWidestNumeric = ...?
-//TODO:	klass.getApproximateSize = ...?
-//TODO:	klass.addRef = ...?
-//TODO:	klass.release = ...?
+//SKIPPED: hash_combine
+//SKIPPED: getWidestNumeric
+//SKIPPED: getApproximateSize
+//SKIPPED: toString
+//SKIPPED: operator <<
+//SKIPPED: serializeForSorter
+//SKIPPED: deserializeForSorter
+
+/**
+ * Takes an array and removes items and adds them to returned array.
+ * @method consume
+ * @static
+ * @param consumed {Array} The array to be copied, emptied.
+ **/
+klass.consume = function consume(consumed) {
+	return consumed.splice(0);
+};
+
+//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"
+klass.getType = function getType(v) {
+	var t = typeof v;
+	if (t == "object") t = (v === null ? "null" : v.constructor.name || t);
+	return t;
+};
+// getArrayLength(arr): arr.length
+// getString(val): val.toString()   //NOTE: same for getStringData(val) I think
+// getOid
+// getBool
+// getDate
+// getTimestamp
+// getRegex(re):  re.source
+// getRegexFlags(re):  re.toString().slice(-re.toString().lastIndexOf('/') + 2)
+// getSymbol
+// getCode
+// getInt
+// getLong
+//NOTE: also, because of this we are not throwing if the type does not match like the mongo code would but maybe that's okay
+
+// from bsontypes
+klass.canonicalize = function canonicalize(x) {
+	var xType = typeof(x);
+	if (xType == "object") xType = x === null ? "null" : x.constructor.name;
+	switch (xType) {
+		case "MinKey":
+			return -1;
+		case "MaxKey":
+			return 127;
+		case "EOO":
+		case "undefined":
+		case undefined:
+			return 0;
+		case "jstNULL":
+		case "null":
+		case "Null":
+			return 5;
+		case "NumberDouble":
+		case "NumberInt":
+		case "NumberLong":
+		case "number":
+			return 10;
+		case "Symbol":
+		case "string":
+			return 15;
+		case "Object":
+			return 20;
+		case "Array":
+			return 25;
+		case "Binary":
+			return 30;
+		case "ObjectId":
+			return 35;
+		case "ObjectID":
+			return 35;
+		case "boolean":
+		case "Boolean":
+			return 40;
+		case "Date":
+		case "Timestamp":
+			return 45;
+		case "RegEx":
+		case "RegExp":
+			return 50;
+		case "DBRef":
+			return 55;
+		case "Code":
+			return 60;
+		case "CodeWScope":
+			return 65;
+		default:
+			// Default value for Object
+			return 20;
+	}
+};

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

@@ -8,17 +8,18 @@
  * @constructor
  **/
 var AddExpression = module.exports = function AddExpression(){
-	if (arguments.length !== 0) throw new Error("zero args expected");
+//	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = AddExpression, NaryExpression = require("./NaryExpression"), base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = AddExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$add";
 proto.getOpName = function getOpName(){
-	return "$add";
+	return klass.opName
 };
 
 /**
@@ -39,4 +40,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 
 
 /** Register Expression */
-Expression.registerExpression("$add",base.parse(AddExpression));
+Expression.registerExpression(klass.opName,base.parse(klass));

+ 5 - 10
lib/pipeline/expressions/AndExpression.js

@@ -12,15 +12,9 @@
  * @constructor
  **/
 var AndExpression = module.exports = function AndExpression() {
-	if (arguments.length !== 0) throw new Error("zero args expected");
+//	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = AndExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = AndExpression, base = require(".VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -29,8 +23,9 @@ var Value = require("../Value"),
 	Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$and";
 proto.getOpName = function getOpName() {
-	return "$and";
+	return klass.opName;
 };
 
 /**
@@ -75,6 +70,6 @@ proto.optimize = function optimize() {
 };
 
 /** Register Expression */
-Expression.registerExpression("$and", base.parse(AndExpression));
+Expression.registerExpression(klass.opName, base.parse(klass));
 
 //TODO: proto.toMatcherBson

+ 8 - 10
lib/pipeline/expressions/ConcatExpression.js

@@ -12,15 +12,16 @@ var Expression = require("./Expression");
 var ConcatExpression = module.exports = function ConcatExpression(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = ConcatExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = ConcatExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value");
 var Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$concat";
 proto.getOpName = function getOpName(){
-	return "$concat";
+	return klass.opName;
 };
 
 /**
@@ -28,16 +29,13 @@ proto.getOpName = function getOpName(){
  * @method evaluate
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-    var n = this.operands.length;
-
     return this.operands.map(function(x) {
-	var y = x.evaluateInternal(vars);
-	if(typeof(y) !== "string") {
-	    throw new Error("$concat only supports strings - 16702");
-	}
+		var y = x.evaluateInternal(vars);
+		if(typeof(y) !== "string") {
+	    	throw new Error("$concat only supports strings - 16702");
+		}
 	return y;
     }).join("");
 };
 
-
-Expression.registerExpression("$concat", base.parse(ConcatExpression));
+Expression.registerExpression(klass.opName, base.parse(klass));

+ 99 - 79
lib/pipeline/expressions/Expression.js

@@ -26,12 +26,6 @@ var Expression = module.exports = function Expression() {
     });
 
 
-
-function fn(){
-	return;
-}
-
-
 // NESTED CLASSES
 /**
  * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.ObjectCtx` class
@@ -74,18 +68,21 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 	return klass;
 })();
 
-proto.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") !== -1) {
-		// field path must not contain embedded null characters - 16419
-	}
-	if (prefixedField[0] !== '$') {
-		// "field path references must be prefixed with a '$'"
-	}
-	return prefixedField.slice(1);
+
+/**
+ * Produce a field path string with the field prefix removed.
+ * Throws an error if the field prefix is not present.
+ *
+ * @static
+ * @param prefixedField the prefixed field
+ * @returns the field path with the prefix removed
+ **/
+klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
+	if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
+	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
+	return prefixedField.substr(1);
 };
-var KIND_UNKNOWN = 0,
-	KIND_NOTOPERATOR = 1,
-	KIND_OPERATOR = 2;
+
 /**
  * Parse an Object.  The object could represent a functional expression or a Document expression.
  *
@@ -98,42 +95,52 @@ var KIND_UNKNOWN = 0,
  * @method parseObject
  * @param obj   the element representing the object
  * @param ctx   a MiniCtx representing the options above
+ * @param vps	Variables Parse State
  * @returns the parsed Expression
  **/
 klass.parseObject = function parseObject(obj, ctx, vps) {
 	if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
-	var kind = KIND_UNKNOWN,
-		pExpression, // the result
-		pExpressionObject; // the alt result
-	if (obj === undefined || obj == {}) return new ObjectExpression();
+
+	var expression, // the result
+		expressionObject, // the alt result
+		UNKNOWN = 0,
+		NOTOPERATOR = 1,
+		OPERATOR = 2,
+		kind = UNKNOWN;
+
+	if (obj === undefined || obj === null || (obj instanceof Object && Object.keys(obj).length === 0)) return new ObjectExpression();
 	var fieldNames = Object.keys(obj);
-	if (fieldNames.length === 0) { //NOTE: Added this for mongo 2.5 port of document sources. Should reconsider when porting the expressions themselves
-		return new ObjectExpression();
-	}
 	for (var fieldCount = 0, n = fieldNames.length; fieldCount < n; ++fieldCount) {
-		var pFieldName = fieldNames[fieldCount];
+		var fieldName = fieldNames[fieldCount];
 
-		if (pFieldName[0] === "$") {
+		if (fieldName[0] === "$") {
 			if (fieldCount !== 0)
-				throw new Error("the operator must be the only field in a pipeline object (at '" + pFieldName + "'.; code 16410");
+				throw new Error("the operator must be the only field in a pipeline object (at '" + fieldName + "'.; uassert code 15983");
 
 			if (ctx.isTopLevel)
-				throw new Error("$expressions are not allowed at the top-level of $project; code 16404");
-			kind = KIND_OPERATOR; //we've determined this "object" is an operator expression
-			pExpression = Expression.parseExpression(pFieldName, obj[pFieldName], vps);
+				throw new Error("$expressions are not allowed at the top-level of $project; uassert code 16404");
+
+			kind = OPERATOR; //we've determined this "object" is an operator expression
+
+			expression = Expression.parseExpression(fieldName, obj[fieldName], vps); //NOTE: DEVIATION FROM MONGO: c++ code uses 2 arguments. See #parseExpression
 		} else {
-			if (kind === KIND_OPERATOR)
-				throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + pFieldName + "'.; code 15990");
+			if (kind === OPERATOR)
+				throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + fieldName + "'.; uassert code 15990");
+
+			if (!ctx.isTopLevel && fieldName.indexOf(".") != -1)
+				throw new Error("dotted field names are only allowed at the top level; uassert code 16405");
 
-			if (!ctx.isTopLevel && pFieldName.indexOf(".") != -1)
-				throw new Error("dotted field names are only allowed at the top level; code 16405");
-			if (pExpression === undefined) { // if it's our first time, create the document expression
+			if (expression === undefined) { // if it's our first time, create the document expression
 				if (!ctx.isDocumentOk)
-					throw new Error("document not allowed in this context"); // CW TODO error: document not allowed in this context
-				pExpression = pExpressionObject = new ObjectExpression(); //check for top level?
-				kind = KIND_NOTOPERATOR; //this "object" is not an operator expression
+					throw new Error("Assertion failure"); // CW TODO error: document not allowed in this context
+
+				expressionObject = new ObjectExpression(); //check for top level? //NOTE: DEVIATION FROM MONGO: the c++ calls createRoot() or create() here.
+				expression = expressionObject;
+
+				kind = NOTOPERATOR; //this "object" is not an operator expression
 			}
-			var fieldValue = obj[pFieldName];
+
+			var fieldValue = obj[fieldName];
 			switch (typeof(fieldValue)) {
 				case "object":
 					// it's a nested document
@@ -141,95 +148,96 @@ klass.parseObject = function parseObject(obj, ctx, vps) {
 						isDocumentOk: ctx.isDocumentOk,
 						isInclusionOk: ctx.isInclusionOk
 					});
-					pExpressionObject.addField(pFieldName, Expression.parseObject(fieldValue, subCtx, vps));
+
+					expressionObject.addField(fieldName, Expression.parseObject(fieldValue, subCtx, vps));
+
 					break;
 				case "string":
 					// it's a renamed field         // CW TODO could also be a constant
-					var pathExpr = new FieldPathExpression.parse(fieldValue);
-					pExpressionObject.addField(pFieldName, pathExpr);
+					expressionObject.addField(fieldName, new FieldPathExpression.parse(fieldValue, vps));
 					break;
 				case "boolean":
 				case "number":
 					// it's an inclusion specification
 					if (fieldValue) {
 						if (!ctx.isInclusionOk)
-							throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
-						pExpressionObject.includePath(pFieldName);
+							throw new Error("field inclusion is not allowed inside of $expressions; uassert code 16420");
+						expressionObject.includePath(fieldName);
 					} else {
-						if (!(ctx.isTopLevel && fn == Document.ID_PROPERTY_NAME))
-							throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; code 16406");
-						pExpressionObject.excludeId = true;
+						if (!(ctx.isTopLevel && fieldName === Document.ID_PROPERTY_NAME))
+							throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; uassert code 16406");
+						expressionObject.excludeId = true;
 					}
 					break;
 				default:
-					throw new Error("disallowed field type " + (fieldValue ? fieldValue.constructor.name + ":" : "") + typeof(fieldValue) + " in object expression (at '" + pFieldName + "')");
+					throw new Error("disallowed field type " + (fieldValue instanceof Object ? fieldValue.constructor.name + ":" : typeof fieldValue) + typeof(fieldValue) + " in object expression (at '" + fieldName + "') uassert code 15992");
 			}
 		}
 	}
-	return pExpression;
+
+	return expression;
 };
 
 
 klass.expressionParserMap = {};
 
+/** Registers an ExpressionParser so it can be called from parseExpression and friends.
+ *
+ *  As an example, if your expression looks like {"$foo": [1,2,3]} you would add this line:
+ *  REGISTER_EXPRESSION("$foo", ExpressionFoo::parse);
+ */
 klass.registerExpression = function registerExpression(key, parserFunc) {
 	if (key in klass.expressionParserMap) {
-		throw new Error("Duplicate expression registrarion for " + key);
+		throw new Error("Duplicate expression (" + key + ") detected; massert code 17064");
 	}
 	klass.expressionParserMap[key] = parserFunc;
-	return 0; // Should
+	return 1;
 };
 
 /**
- * Parse a BSONElement Object which has already been determined to be functional expression.
- *
+ * Parses a BSONElement which has already been determined to be functional expression.
  * @static
  * @method parseExpression
- * @param opName        the name of the (prefix) operator
- * @param obj   the BSONElement to parse
+ * @param exprElement should be the only element inside the expression object.
+ *    That is the field name should be the $op for the expression.
+ * @param vps the variable parse state
  * @returns the parsed Expression
  **/
-klass.parseExpression = function parseExpression(exprKey, exprValue, vps) {
-	if (!(exprKey in Expression.expressionParserMap)) {
-		throw new Error("Invalid operator : " + exprKey);
+//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
+klass.parseExpression = function parseExpression(exprElementKey, exprElementValue, vps) {
+	if (!(exprElementKey in Expression.expressionParserMap)) {
+		throw new Error("Invalid operator : " + exprElementKey + "; code 15999");
 	}
-	return Expression.expressionParserMap[exprKey](exprValue, vps);
+	return Expression.expressionParserMap[exprElementKey](exprElementValue, vps);
 };
 
 /**
- * Parse a BSONElement which is an operand in an Expression.
+ * Parses a BSONElement which is an operand in an Expression.
+ *
+ * This is the most generic parser and can parse ExpressionFieldPath, a literal, or a $op.
+ * If it is a $op, exprElement should be the outer element whose value is an Object
+ * containing the $op.
  *
+ * @method parseOperand
  * @static
- * @param pBsonElement the expected operand's BSONElement
+ * @param exprElement should be the only element inside the expression object.
+ *    That is the field name should be the $op for the expression.
+ * @param vps the variable parse state
  * @returns the parsed operand, as an Expression
  **/
 klass.parseOperand = function parseOperand(exprElement, vps) {
 	var t = typeof(exprElement);
 	if (t === "string" && exprElement[0] == "$") { //if we got here, this is a field path expression
 	    return new FieldPathExpression.parse(exprElement, vps);
-	} else
-	if (t === "object" && exprElement && exprElement.constructor === Object)
+	} else if (t === "object" && exprElement && exprElement.constructor === Object) {
 		return Expression.parseObject(exprElement, new ObjectCtx({
 			isDocumentOk: true
 		}), vps);
-	else return ConstantExpression.parse(exprElement, vps);
-};
-
-/**
- * Produce a field path string with the field prefix removed.
- * Throws an error if the field prefix is not present.
- *
- * @static
- * @param prefixedField the prefixed field
- * @returns the field path with the prefix removed
- **/
-klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; code 16419");
-	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); code 15982");
-	return prefixedField.substr(1);
+	} else {
+		return ConstantExpression.parse(exprElement, vps);
+	}
 };
 
-
 // PROTOTYPE MEMBERS
 /**
  * Evaluate the Expression using the given document as input.
@@ -241,6 +249,18 @@ proto.evaluateInternal = function evaluateInternal(obj) {
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
+
+/**
+ * Evaluate expression with specified inputs and return result.
+ *
+ * While vars is non-const, if properly constructed, subexpressions modifications to it
+ * should not effect outer expressions due to unique variable Ids.
+ */
+proto.evaluate = function(vars) {
+	return this.evaluateInternal(vars);
+};
+
+
 /**
  * Optimize the Expression.
  *

+ 30 - 16
lib/pipeline/expressions/LetExpression.js

@@ -1,10 +1,12 @@
 "use strict";
 
+Expression.registerExpression("$let", LetExpression.parse);
+
 var LetExpression = module.exports = function LetExpression(vars, subExpression){
-	if (arguments.length !== 2) throw new Error("Two args expected");
+	//if (arguments.length !== 2) throw new Error("Two args expected");
 	this._variables = vars;
 	this._subExpression = subExpression;
-}, klass = LetExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = LetExpression, Expression = require("./FixedArityExpressionT")(klass, 2), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Variables = require("./Variables"),
@@ -19,13 +21,21 @@ proto.parse = function parse(expr, vpsIn){
 	}
 
 	if(typeof(expr.$let) !== 'object' || (expr.$let instanceof Array)) {
-		throw new Error("$let only supports an object as it's argument:16874");
+		throw new Error("$let only supports an object as its argument: 16874");
 	}
 
 	var args = expr.$let,
 		varsElem = args.vars,
-		inElem = args['in'];
-
+		inElem = args['in']; // args.in; ??
+
+	//NOTE: DEVIATION FROM MONGO: 1. These if statements are in a loop in the c++ version,
+	// 2. 'vars' and 'in' are each mandatory here. in the c++ code you only need one of the two.
+	// 3. Below, we croak if there are more than 2 arguments.  The original does not have this limitation, specifically.
+	// Upon further review, I think our code is more accurate.  The c++ code will accept if there are multiple 'in'
+	// or 'var' values. The previous ones will be overwritten by newer ones.
+	//
+	// Final note - I think this code is fine.
+	//
 	if(!varsElem) {
 		throw new Error("Missing 'vars' parameter to $let: 16876");
 	}
@@ -33,6 +43,9 @@ proto.parse = function parse(expr, vpsIn){
 		throw new Error("Missing 'in' parameter to $let: 16877");
 	}
 
+	// Should this be !== 2?  Why would we have fewer than 2 arguments?  Why do we even care what the length of the
+	// array is? It may be an optimization of sorts. But what we're really wanting here is, 'If any keys are not "in"
+	// or "vars" then we need to bugcheck.'
 	if(Object.keys(args).length > 2) {
 		var bogus = Object.keys(args).filter(function(x) {return !(x === 'in' || x === 'vars');});
 		throw new Error("Unrecognized parameter to $let: " + bogus.join(",") + "- 16875");
@@ -60,6 +73,8 @@ proto.optimize = function optimize() {
 
 	for(var id in this._variables){
 		for(var name in this._variables[id]) {
+			//NOTE: DEVIATION FROM MONGO: This is actually ok. The c++ code does this with a single map. The js structure
+			// is nested objects.
 			this._variables[id][name] = this._variables[id][name].optimize();
 		}
 	}
@@ -69,15 +84,15 @@ proto.optimize = function optimize() {
 	return this;
 };
 
-proto.addDependencies = function addDependencies(deps, path){
+proto.serialize = function serialize(explain) {
+	var vars = {};
 	for(var id in this._variables) {
 		for(var name in this._variables[id]) {
-			this._variables[id][name].addDependencies(deps);
+			vars[name] = this._variables[id][name];
 		}
 	}
-	this._subExpression.addDependencies(deps, path);
-	return deps;
 
+	return {$let: {vars:vars, 'in':this._subExpression.serialize(explain)}};
 };
 
 proto.evaluateInternal = function evaluateInternal(vars) {
@@ -90,16 +105,15 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 	return this._subExpression.evaluateInternal(vars);
 };
 
-
-proto.serialize = function serialize(explain) {
-	var vars = {};
+proto.addDependencies = function addDependencies(deps, path){
 	for(var id in this._variables) {
 		for(var name in this._variables[id]) {
-			vars[name] = this._variables[id][name];
+			this._variables[id][name].addDependencies(deps);
 		}
 	}
+	this._subExpression.addDependencies(deps, path);
+	return deps; //NOTE: DEVIATION FROM MONGO: The c++ version does not return a value. We seem to use the returned value
+					// (or something from a different method named
+					// addDependencies) in many places.
 
-	return {$let: {vars:vars, 'in':this._subExpression.serialize(explain)}};
 };
-
-Expression.registerExpression("$let", LetExpression.parse);

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

@@ -9,17 +9,18 @@
  * @constructor
  **/
 var MultiplyExpression = module.exports = function MultiplyExpression(){
-	if (arguments.length !== 0) throw new Error("Zero args expected");
+	//if (arguments.length !== 0) throw new Error("Zero args expected");
 	base.call(this);
-}, klass = MultiplyExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = MultiplyExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
  Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$multiply";
 proto.getOpName = function getOpName(){
-	return "$multiply";
+	return klass.opName;
 };
 
 /**
@@ -38,4 +39,4 @@ proto.evaluateInternal = function evaluateInternal(vars){
 };
 
 /** Register Expression */
-Expression.registerExpression("$multiply", base.parse(MultiplyExpression));
+Expression.registerExpression(klass.opName, base.parse(klass));

+ 153 - 0
lib/pipeline/expressions/NaryExpression.js

@@ -0,0 +1,153 @@
+"use strict";
+
+var Expression = require("./Expression"),
+	Variables = require("./Variables"),
+	ConstantExpression = require("./ConstantExpression");
+
+/**
+ * The base class for all n-ary `Expression`s
+ * @class NaryExpression
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @extends mungedb-aggregate.pipeline.expressions.Expression
+ * @constructor
+ **/
+var NaryExpression = module.exports = function NaryExpression() {
+	if (arguments.length !== 0) throw new Error("Zero args expected");
+	this.operands = [];
+	base.call(this);
+}, klass = NaryExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+proto.optimize = function optimize() {
+	var n = this.operands.length;
+
+	// optimize sub-expressions and count constants
+	var constCount = 0;
+	for (var i = 0; i < n; ++i) {
+		var optimized = this.operands[i].optimize();
+
+		// substitute the optimized expression
+		this.operands[i] = optimized;
+
+		// check to see if the result was a constant
+		if (optimized instanceof ConstantExpression) {
+			constCount++;
+		}
+	}
+
+	// If all the operands are constant, we can replace this expression with a constant. Using
+	// an empty Variables since it will never be accessed.
+	if (constCount === n) {
+		var emptyVars = new Variables(),
+			result = this.evaluateInternal(emptyVars),
+			replacement = ConstantExpression.create(result);
+		return replacement;
+	}
+
+	// Remaining optimizations are only for associative and commutative expressions.
+	if(!this.isAssociativeAndCommutative()) {
+		return this;
+	}
+
+	// Process vpOperand to split it into constant and nonconstant vectors.
+	// This can leave vpOperand in an invalid state that is cleaned up after the loop.
+	var constExprs = [],
+		nonConstExprs = [];
+	for (i = 0; i < this.operands.length; ++i) { // NOTE: vpOperand grows in loop
+		var expr = this.operands[i];
+		if (expr instanceof ConstantExpression) {
+			constExprs.push(expr);
+		} else {
+			// If the child operand is the same type as this, then we can
+			// extract its operands and inline them here because we know
+			// this is commutative and associative.  We detect sameness of
+			// the child operator by checking for equality of the opNames
+			var nary = expr instanceof NaryExpression ? expr : undefined;
+			if (!nary || nary.getOpName() !== this.getOpName) {
+				nonConstExprs.push(expr);
+			} else {
+				// same expression, so flatten by adding to vpOperand which
+				// will be processed later in this loop.
+				Array.prototype.push.apply(this.operands, nary.operands);
+			}
+		}
+	}
+
+	// collapse all constant expressions (if any)
+	var constValue;
+	if (constExprs.length > 0) {
+		this.operands = constExprs;
+		var emptyVars2 = new Variables();
+		constValue = this.evaluateInternal(emptyVars2);
+	}
+
+	// now set the final expression list with constant (if any) at the end
+	this.operands = nonConstExprs;
+	if (constExprs.length > 0) {
+		this.operands.push(ConstantExpression.create(constValue));
+	}
+
+	return this;
+};
+
+proto.addDependencies = function addDependencies(deps, path) {
+	for (var i = 0, l = this.operands.length; i < l; ++i) {
+		this.operands[i].addDependencies(deps);
+	}
+};
+
+/**
+ * Add an operand to the n-ary expression.
+ * @method addOperand
+ * @param expr the expression to add
+ */
+proto.addOperand = function addOperand(expr) {
+	this.operands.push(expr);
+};
+
+proto.serialize = function serialize(explain) {
+	var nOperand = this.operands.length,
+		array = [];
+	// build up the array
+	for (var i = 0; i < nOperand; i++) {
+		array.push(this.operands[i].serialize(explain));
+	}
+
+	var obj = {};
+	obj[this.getOpName()] = array;
+	return obj;
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return false;
+};
+
+/**
+ * Get the name of the operator.
+ * @method getOpName
+ * @returns the name of the operator; this string belongs to the class
+ *  implementation, and should not be deleted
+ *  and should not
+ */
+proto.getOpName = function getOpName() {
+	throw new Error("NOT IMPLEMENTED BY INHERITOR");
+};
+
+/**
+ * Allow subclasses the opportunity to validate arguments at parse time.
+ * @method validateArguments
+ * @param {[type]} args [description]
+ */
+proto.validateArguments = function(args) {};
+
+klass.parseArguments = function(exprElement, vps) {
+	var out = [];
+	if (exprElement instanceof Array) {
+		for (var ii = 0; ii < exprElement.length; ii++) {
+			out.push(Expression.parseOperand(exprElement[ii], vps));
+		}
+	} else {
+		out.push(Expression.parseOperand(exprElement, vps));
+	}
+	return out;
+};

+ 29 - 0
lib/pipeline/expressions/NaryExpressionBaseT.js

@@ -0,0 +1,29 @@
+"use strict";
+
+var NaryExpression = require("./NaryExpression");
+
+/**
+* Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class.
+* @class NaryExpressionBaseT
+* @namespace mungedb-aggregate.pipeline.expressions
+* @module mungedb-aggregate
+* @extends mungedb-aggregate.pipeline.expressions.Expression
+* @constructor
+**/
+var NaryExpressionBaseT = module.exports = function NaryExpressionBaseT(SubClass) {
+
+	var NaryExpressionBase = function NaryExpressionBase() {
+		if (arguments.length !== 0) throw new Error("Zero args expected");
+		base.call(this);
+	}, klass = NaryExpressionBase, base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	klass.parse = function(objExpr, vps) {
+		var expr = new SubClass(),
+			args = NaryExpression.parseArguments(objExpr, vps);
+		expr.validateArguments(args);
+		expr.operands = args;
+		return expr;
+	};
+
+	return NaryExpressionBase;
+};

+ 0 - 162
lib/pipeline/expressions/NaryExpressionT.js

@@ -1,162 +0,0 @@
-"use strict";
-
-/**
- * The base class for all n-ary `Expression`s
- * @class NaryExpression
- * @namespace mungedb-aggregate.pipeline.expressions
- * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.Expression
- * @constructor
- **/
-var Expression = require("./Expression"),
-	Variables = require("./Variables");
-
-var NaryExpressionT = module.exports = function NaryExpressionT(SubClass) {
-
-	var NaryExpression = function NaryExpression(){
-		if (arguments.length !== 0) throw new Error("Zero args expected");
-		this.operands = [];
-		base.call(this);
-	}, klass = NaryExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-	klass.parseArguments = function(exprElement, vps) {
-		var out = [];
-		if(exprElement instanceof Array) {
-			for(var ii = 0; ii < exprElement.length; ii++) {
-				out.push(Expression.parseOperand(exprElement[ii], vps));
-			}
-		} else {
-			out.push(Expression.parseOperand(exprElement, vps));
-		}
-		return out;
-	};
-
-	klass.parse = function(expr, vps) {
-		var outExpr = new SubClass(),
-			args = NaryExpression.parseArguments(expr, vps);
-		outExpr.validateArguments(args);
-		outExpr.operands = args;
-		return outExpr;
-	};
-
-	proto.optimize = function optimize(){
-		var n = this.operands.length,
-			constCount = 0;
-
-		// optimize sub-expressions and count constants
-		for(var ii = 0; ii < n; ii++) {
-			var optimized = this.operands[ii].optimize();
-
-			// substitute the optimized expression
-			this.operands[ii] = optimized;
-
-			// check to see if the result was a constant
-			if(optimized instanceof ConstantExpression) {
-				constCount++;
-			}
-		}
-
-		// If all the operands are constant, we can replace this expression with a constant. Using
-		// an empty Variables since it will never be accessed.
-		if(constCount === n) {
-			var emptyVars = new Variables(),
-				result = this.evaluateInternal(emptyVars),
-				replacement = new ConstantExpression(result);
-			return replacement;
-		}
-
-		// Remaining optimizations are only for associative and commutative expressions.
-		if(!this.isAssociativeAndCommutative()) {
-			return this;
-		}
-
-		// Process vpOperand to split it into constant and nonconstant vectors.
-		// This can leave vpOperand in an invalid state that is cleaned up after the loop.
-		var constExprs = [],
-			nonConstExprs = [];
-		for(var i=0; i<this.operands.length; i++) { // NOTE: vpOperand grows in loop
-			var expr = this.operands[i];
-			if(expr instanceof ConstantExpression) {
-				constExprs.push(expr);
-			} else {
-				// If the child operand is the same type as this, then we can
-				// extract its operands and inline them here because we know
-				// this is commutative and associative.  We detect sameness of
-				// the child operator by checking for equality of the opNames
-				var nary = expr;
-				if(!(nary instanceof NaryExpression) || nary.getOpName() !== this.getOpName) {
-					nonConstExprs.push(expr);
-				} else {
-					// same expression, so flatten by adding to vpOperand which
-					// will be processed later in this loop.
-					for(var j=0; j<nary.operands.length; j++) {
-						this.operands.push(nary.operands[j]);
-					}
-				}
-			}
-		}
-
-		// collapse all constant expressions (if any)
-		var constValue;
-		if(constExprs.length > 0) {
-			this.operands = constExprs;
-			var emptyVars = new Variables();
-			constValue = this.evaluateInternal(emptyVars);
-		}
-
-		// now set the final expression list with constant (if any) at the end
-		this.operands = nonConstExprs;
-		if (constExprs.length > 0) {
-			this.operands.push(new ConstantExpression(constValue));
-		}
-
-		return this;
-	};
-
-	// DEPENDENCIES
-	var ConstantExpression = require("./ConstantExpression");
-
-	proto.getOpName = function getOpName(doc){
-		throw new Error("NOT IMPLEMENTED BY INHERITOR");
-	};
-
-	proto.addDependencies = function addDependencies(deps, path){
-		for(var i = 0, l = this.operands.length; i < l; ++i)
-			this.operands[i].addDependencies(deps);
-	};
-
-	/**
-	 * Add an operand to the n-ary expression.
-	 * @method addOperand
-	 * @param pExpression the expression to add
-	 **/
-	proto.addOperand = function addOperand(expr) {
-		this.operands.push(expr);
-	};
-
-	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
-		return false;
-	};
-
-	proto.serialize = function serialize(explain) {
-		var nOperand = this.operands.length,
-			array = [];
-
-		for(var i=0; i<nOperand; i++){
-			array.push(this.operands[i].serialize(explain));
-		}
-
-		var obj = {};
-		obj[this.getOpName()] = array;
-		return obj;
-	};
-
-	proto.validateArguments = function(args) {
-		if(this.nargs !== args.length) {
-			throw new Error("Expression " + this.getOpName() + " takes exactly " + this.nargs + " arguments. " + args.length + " were passed in.");
-		}
-	};
-
-	return NaryExpression;
-};
-

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

@@ -9,9 +9,9 @@
  * @constructor
  **/
 var OrExpression = module.exports = function OrExpression(){
-	if (arguments.length !== 0) throw new Error("zero args expected");
+//	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = OrExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = OrExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -20,8 +20,9 @@ var Value = require("../Value"),
 	Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$or";
 proto.getOpName = function getOpName(){
-	return "$or";
+	return klass.opName;
 };
 
 /**
@@ -64,4 +65,4 @@ proto.optimize = function optimize() {
 };
 
 /** Register Expression */
-Expression.registerExpression("$or", base.parse(OrExpression));
+Expression.registerExpression(klass.opName, base.parse(klass));

+ 12 - 17
lib/pipeline/expressions/ToLowerExpression.js

@@ -1,6 +1,6 @@
 "use strict";
-
-/**
+	
+/** 
  * A $toLower pipeline expression.
  * @see evaluateInternal
  * @class ToLowerExpression
@@ -8,29 +8,24 @@
  * @module mungedb-aggregate
  * @constructor
  **/
-var ToLowerExpression = module.exports = function ToLowerExpression() {
+var ToLowerExpression = module.exports = function ToLowerExpression(){
 	base.call(this);
-}, klass = ToLowerExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = ToLowerExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
+klass.opName = "$toLower";
+
 // PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$toLower";
+proto.getOpName = function getOpName(){
+	return klass.opName;
 };
 
-/**
- * Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase.
- **/
+/** 
+* Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase. 
+**/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var val = this.operands[0].evaluateInternal(vars),
 		str = Value.coerceToString(val);
@@ -38,4 +33,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$toLower", base.parse);
+Expression.registerExpression(klass.opName, base.parse(klass));

+ 5 - 10
lib/pipeline/expressions/ToUpperExpression.js

@@ -10,22 +10,17 @@
  **/
 var ToUpperExpression = module.exports = function ToUpperExpression() {
 	base.call(this);
-}, klass = ToUpperExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = ToUpperExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass }});
 
 // DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
+klass.opName = "$toUpper";
+
 // PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
-	return "$toUpper";
+	return klass.opName;
 };
 
 /**
@@ -38,4 +33,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$toUpper", base.parse);
+Expression.registerExpression(klass.opName, base.parse(klass));

+ 19 - 0
lib/pipeline/expressions/VariadicExpressionT.js

@@ -0,0 +1,19 @@
+"use strict";
+
+/**
+ * A factory and base class for all expressions that are variadic (AKA they accept any number of arguments)
+ * @class VariadicExpressionT
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ **/
+
+var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass) {
+
+	var VariadicExpression = function VariadicExpression() {
+		if (arguments.length !== 0) throw new Error(klass.name + "<" + SubClass.name + ">: zero args expected");
+		base.call(this);
+	}, klass = VariadicExpression, base = require("./NaryExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+
+	return VariadicExpression;
+};

+ 110 - 0
test/lib/pipeline/Document.js

@@ -0,0 +1,110 @@
+"use strict";
+var assert = require("assert"),
+	Document = require("../../../lib/pipeline/Document");
+
+// 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.Document = {
+
+	"Json conversion": {
+
+		"convert to Json": function toJson() {
+			var aDocument = {"prop1":0},
+				result = Document.toJson(aDocument);
+			assert.equal(result, '{"prop1":0}');
+		},
+
+		"convert to Json with metadata": function toJsonWithMetaData() {
+			var aDocument = {"prop1": 0,"metadata":"stuff"},
+				result = Document.toJsonWithMetaData(aDocument);
+			assert.equal(result, '{"prop1":0,"metadata":"stuff"}');
+		},
+
+		"convert from Json": function fromJsonWithMetaData() {
+			var aDocumentString = '{\"prop1\":0,\"metadata\":1}',
+				jsonDocument = {"prop1":0,"metadata":1},
+				result = Document.fromJsonWithMetaData(aDocumentString);
+			assert.deepEqual(result, jsonDocument);
+		},
+
+	},
+
+	"compare 2 Documents": {
+
+		"should return 0 if Documents are identical": function compareDocumentsIdentical() {
+			var lDocument = {"prop1": 0},
+				rDocument = {"prop1": 0},
+				result = Document.compare(lDocument, rDocument);
+			assert.equal(result, 0);
+		},
+
+		"should return -1 if left Document is shorter": function compareLeftDocumentShorter() {
+			var lDocument = {"prop1": 0},
+				rDocument = {"prop1": 0, "prop2": 0},
+				result = Document.compare(lDocument, rDocument);
+			assert.equal(result, -1);
+		},
+
+		"should return 1 if right Document is shorter": function compareRightDocumentShorter() {
+			var lDocument = {"prop1": 0, "prop2": 0},
+				rDocument = {"prop1": 0},
+				result = Document.compare(lDocument, rDocument);
+			assert.equal(result, 1);
+		},
+
+		"should return nameCmp result -1 if left Document field value is less": function compareLeftDocumentFieldLess() {
+			var lDocument = {"prop1": 0},
+				rDocument = {"prop1": 1},
+				result = Document.compare(lDocument, rDocument);
+			assert.equal(result, -1);
+		},
+
+		"should return nameCmp result 1 if right Document field value is less": function compareRightDocumentFieldLess() {
+			var lDocument = {"prop1": 1},
+				rDocument = {"prop1": 0},
+				result = Document.compare(lDocument, rDocument);
+			assert.equal(result, 1);
+		},
+
+	},
+
+	"clone a Document": {
+
+		"should return same field and value from cloned Document ": function clonedDocumentSingleFieldValue() {
+			var doc = {"prop1": 17},
+				res = Document.clone(doc);
+			assert(res instanceof Object);
+			assert.deepEqual(doc, res);
+			assert.equal(res.prop1, 17);
+		},
+
+		"should return same fields and values from cloned Document ": function clonedDocumentMultiFieldValue() {
+			var doc = {"prop1": 17, "prop2": "a string"},
+				res = Document.clone(doc);
+			assert.deepEqual(doc, res);
+			assert(res instanceof Object);
+			assert.equal(res.prop1, 17);
+			assert.equal(res.prop2, "a string");
+		},
+
+	},
+
+	"serialize and deserialize for sorter": {
+
+		"should return a string": function serializeDocument() {
+			var doc = {"prop1":1},
+				res = Document.serializeForSorter(doc);
+			assert.equal(res, "{\"prop1\":1}");
+		},
+
+		"should return a Document": function deserializeToDocument() {
+			var str = "{\"prop1\":1}",
+				doc = {"prop1":1},
+				res = Document.deserializeForSorter(str);
+			assert.deepEqual(res, doc);
+		},
+
+	},
+
+};

+ 327 - 0
test/lib/pipeline/Value.js

@@ -0,0 +1,327 @@
+"use strict";
+var assert = require("assert"),
+	Value = require("../../../lib/pipeline/Value");
+
+// 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.Value = {
+
+	".constructor()": {
+
+		"should throw an error when used": function() {
+			assert.throws(function() {
+				new Value();
+			});
+		}
+
+	},
+
+	".coerceToBool()": {
+
+		"should coerce 0 to false": function testZeroIntToBool() {
+			assert.strictEqual(Value.coerceToBool(0), false);
+		},
+
+		"should coerce -1 to true": function testNonZeroIntToBool() {
+			assert.strictEqual(Value.coerceToBool(-1), true);
+		},
+
+		"should coerce 0L to false": function testZeroLongToBool() {
+			assert.strictEqual(Value.coerceToBool(0e11), false);
+		},
+
+		"should coerce 5L to true": function testNonZeroLongToBool() {
+			assert.strictEqual(Value.coerceToBool(5e11), true);
+		},
+
+		"should coerce 0.0 to false": function testZeroDoubleToBool() {
+			assert.strictEqual(Value.coerceToBool(0.0), false);
+		},
+
+		"should coerce -1.3 to true": function testNonZeroDoubleToBool() {
+			assert.strictEqual(Value.coerceToBool(-1.3), true);
+		},
+
+		"should coerce {} to true": function testObjectToBool() {
+			assert.strictEqual(Value.coerceToBool({}), true);
+		},
+
+		"should coerce [] to true": function testArrayToBool() {
+			assert.strictEqual(Value.coerceToBool([]), true);
+		},
+
+		"should coerce Date(0) to true": function testDateToBool() {
+			assert.strictEqual(Value.coerceToBool(new Date(0)), true);
+		},
+
+		"should coerce Regex to true": function testRegexToBool() {
+			assert.strictEqual(Value.coerceToBool(new RegExp("")), true);
+		},
+
+		"should coerce true to true": function testTrueToBool() {
+			assert.strictEqual(Value.coerceToBool(true), true);
+		},
+
+		"should coerce false to false": function testFalseToBool() {
+			assert.strictEqual(Value.coerceToBool(false), false);
+		},
+
+		"should coerce null to false": function testNullToBool() {
+			assert.strictEqual(Value.coerceToBool(null), false);
+		},
+
+		"should coerce undefined to false": function testUndefinedToBool() {
+			assert.strictEqual(Value.coerceToBool(null), false);
+		},
+
+	},
+
+	".coerceToWholeNumber()": {
+
+		"should coerce int to int": function testIntToInt() {
+			assert.strictEqual(Value.coerceToWholeNumber(-5), -5);
+		},
+
+		"should coerce long to int": function testLongToInt() {
+			assert.strictEqual(Value.coerceToWholeNumber(0xff00000007), 7);
+		},
+
+		"should coerce double to int": function testDoubleToInt() {
+			assert.strictEqual(Value.coerceToWholeNumber(9.8), 9);
+		},
+
+		"should coerce null to int": function testNullToInt() {
+			assert.strictEqual(Value.coerceToWholeNumber(null), 0);
+		},
+
+		"should coerce undefined to int": function testUndefinedToInt() {
+			assert.strictEqual(Value.coerceToWholeNumber(undefined), 0);
+		},
+
+		"should error if coerce \"\" to int": function testStringToInt() {
+			assert.throws(function(){
+				Value.coerceToWholeNumber("");
+			});
+		},
+
+		//SKIPPED: ...ToLong tests because they are the same here
+
+	},
+
+	".coerceToNumber()": {
+
+		"should coerce int to double": function testIntToDouble() {
+			assert.strictEqual(Value.coerceToNumber(-5), -5.0);
+		},
+
+		"should coerce long to double": function testLongToDouble() {
+			assert.strictEqual(Value.coerceToNumber(0x8fffffffffffffff), 0x8fffffffffffffff);
+		},
+
+		"should coerce double to double": function testDoubleToDouble() {
+			assert.strictEqual(Value.coerceToNumber(9.8), 9.8);
+		},
+
+		"should coerce null to double": function testNullToDouble() {
+			assert.strictEqual(Value.coerceToNumber(null), 0);
+		},
+
+		"should coerce undefined to double": function testUndefinedToDouble() {
+			assert.strictEqual(Value.coerceToNumber(undefined), 0);
+		},
+
+		"should error if coerce \"\" to double": function testStringToDouble() {
+			assert.throws(function() {
+				Value.coerceToNumber("");
+			});
+		},
+
+	},
+
+	".coerceToDate()": {
+
+		"should coerce date to date": function testDateToDate() {
+			assert.deepEqual(Value.coerceToDate(new Date(888)), new Date(888));
+		},
+
+		//SKIPPED: TimestampToDate because we don't have a Timestamp
+
+		"should error if string to date": function testStringToDate() {
+			assert.throws(function() {
+				Value.coerceToDate("");
+			});
+		},
+
+	},
+
+	".coerceToString()": {
+
+		"should coerce double to string": function testDoubleToString() {
+			assert.strictEqual(Value.coerceToString(-0.2), "-0.2");
+		},
+
+		"should coerce int to string": function testIntToString() {
+			assert.strictEqual(Value.coerceToString(-4), "-4");
+		},
+
+		"should coerce long to string": function testLongToString() {
+			assert.strictEqual(Value.coerceToString(10000e11), "1000000000000000");
+		},
+
+		"should coerce string to string": function testStringToString() {
+			assert.strictEqual(Value.coerceToString("fO_o"), "fO_o");
+		},
+
+		//SKIPPED: TimestampToString because we don't have a Timestamp
+
+		"should coerce date to string": function testDateToString() {
+			assert.strictEqual(Value.coerceToString(new Date(1234567890 * 1000)), "2009-02-13T23:31:30");
+		},
+
+		"should coerce null to string": function testNullToString() {
+			assert.strictEqual(Value.coerceToString(null), "");
+		},
+
+		"should coerce undefined to string": function testUndefinedToString() {
+			assert.strictEqual(Value.coerceToString(undefined), "");
+		},
+
+		"should throw if coerce document to string": function testDocumentToString() {
+			assert.throws(function() {
+				Value.coerceToString({});
+			});
+		},
+
+	},
+
+	".compare()": {
+
+		"should test things": function testCompare() {
+            // BSONObjBuilder undefinedBuilder;
+            // undefinedBuilder.appendUndefined( "" );
+            // BSONObj undefined = undefinedBuilder.obj();
+
+            // Undefined / null.
+            assert.strictEqual(Value.compare(undefined, undefined), 0);
+            assert.strictEqual(Value.compare(undefined, null), -1);
+            assert.strictEqual(Value.compare(null, null), 0);
+
+            // Undefined / null with other types.
+			assert.strictEqual(Value.compare(undefined, 1), -1);
+			assert.strictEqual(Value.compare(undefined, "bar"), -1);
+			assert.strictEqual(Value.compare(null, -1), -1);
+			assert.strictEqual(Value.compare(null, "bar"), -1);
+
+            // Numeric types.
+            assert.strictEqual(Value.compare(5, 5e11 / 1e11), 0);
+            assert.strictEqual(Value.compare(-2, -2.0), 0);
+            assert.strictEqual(Value.compare(90e11 / 1e11, 90.0), 0);
+            assert.strictEqual(Value.compare(5, 6e11 / 1e11), -1);
+            assert.strictEqual(Value.compare(-2, 2.1), -1);
+            assert.strictEqual(Value.compare(90e11 / 1e11, 89.999), 1);
+            assert.strictEqual(Value.compare(90, 90.1), -1);
+            assert.strictEqual(Value.compare(NaN, NaN), 0);
+            assert.strictEqual(Value.compare(NaN, 5), -1);
+
+            // strings compare between numbers and objects
+            assert.strictEqual(Value.compare("abc", 90), 1);
+            assert.strictEqual(Value.compare("abc", {a:"b"}), -1);
+
+            // String comparison.
+            assert.strictEqual(Value.compare("", "a"), -1);
+			assert.strictEqual(Value.compare("a", "a"), 0);
+			assert.strictEqual(Value.compare("a", "b"), -1);
+			assert.strictEqual(Value.compare("aa", "b"), -1);
+			assert.strictEqual(Value.compare("bb", "b"), 1);
+			assert.strictEqual(Value.compare("bb", "b"), 1);
+			assert.strictEqual(Value.compare("b-", "b"), 1);
+			assert.strictEqual(Value.compare("b-", "ba"), -1);
+            // With a null character.
+            assert.strictEqual(Value.compare("a\0", "a"), 1);
+
+            // Object.
+            assert.strictEqual(Value.compare({}, {}), 0);
+            assert.strictEqual(Value.compare({x:1}, {x:1}), 0);
+            assert.strictEqual(Value.compare({}, {x:1}), -1);
+
+            // Array.
+            assert.strictEqual(Value.compare([], []), 0);
+			assert.strictEqual(Value.compare([0], [1]), -1);
+			assert.strictEqual(Value.compare([0, 0], [1]), -1);
+			assert.strictEqual(Value.compare([0], [0, 0]), -1);
+			assert.strictEqual(Value.compare([0], [""]), -1);
+
+            //TODO: OID?
+            // assert.strictEqual(Value.compare(OID("abcdefabcdefabcdefabcdef"), OID("abcdefabcdefabcdefabcdef")), 0);
+            // assert.strictEqual(Value.compare(OID("abcdefabcdefabcdefabcdef"), OID("010101010101010101010101")), 1);
+
+            // Bool.
+            assert.strictEqual(Value.compare(true, true), 0);
+            assert.strictEqual(Value.compare(false, false), 0);
+            assert.strictEqual(Value.compare(true, false), 1);
+
+            // Date.
+            assert.strictEqual(Value.compare(new Date(555), new Date(555)), 0);
+            assert.strictEqual(Value.compare(new Date(555), new Date(554)), 1);
+            // Negative date.
+            assert.strictEqual(Value.compare(new Date(0), new Date(-1)), 1);
+
+            // Regex.
+            assert.strictEqual(Value.compare(/a/, /a/), 0);
+            assert.strictEqual(Value.compare(/a/, /a/i), -1);
+            assert.strictEqual(Value.compare(/a/, /aa/), -1);
+
+            //TODO: Timestamp?
+            // assert.strictEqual(Value.compare(OpTime(1234), OpTime(1234)), 0);
+            // assert.strictEqual(Value.compare(OpTime(4), OpTime(1234)), -1);
+
+            // Cross-type comparisons. Listed in order of canonical types.
+            // assert.strictEqual(Value.compare(MINKEY, undefined), -1);
+            assert.strictEqual(Value.compare(undefined, undefined), 0);
+            // assert.strictEqual(Value.compare(undefined, BSONUndefined), 0);
+            assert.strictEqual(Value.compare(undefined, null), -1);
+            assert.strictEqual(Value.compare(null, 1), -1);
+			assert.strictEqual(Value.compare(1, 1 /*LL*/ ), 0);
+            assert.strictEqual(Value.compare(1, 1.0), 0);
+            assert.strictEqual(Value.compare(1, "string"), -1);
+            // assert.strictEqual(Value.compare("string", BSONSymbol("string")), 0);
+            assert.strictEqual(Value.compare("string", {}), -1);
+            assert.strictEqual(Value.compare({}, []), -1);
+            // assert.strictEqual(Value.compare([], BSONBinData("", 0, MD5Type)), -1);
+            // assert.strictEqual(Value.compare(BSONBinData("", 0, MD5Type), OID()), -1);
+            // assert.strictEqual(Value.compare(OID(), false), -1);
+            // assert.strictEqual(Value.compare(false, OpTime()), -1);
+            // assert.strictEqual(Value.compare(OpTime(), Date_t(0)), 0, );
+            // assert.strictEqual(Value.compare(Date_t(0), BSONRegEx("")), -1);
+            // assert.strictEqual(Value.compare(BSONRegEx(""), BSONDBRef("", OID())), -1);
+            // assert.strictEqual(Value.compare(BSONDBRef("", OID()), BSONCode("")), -1);
+            // assert.strictEqual(Value.compare(BSONCode(""), BSONCodeWScope("", BSONObj())), -1);
+            // assert.strictEqual(Value.compare(BSONCodeWScope("", BSONObj()), MAXKEY), -1);
+		},
+
+	},
+
+	".consume()": {
+
+		"should return an equivalent array, empty the original": function() {
+			var inputs = [5, 6, "hi"],
+				expected = [].concat(inputs), // copy
+				actual = Value.consume(inputs);
+			assert.deepEqual(actual, expected, "should equal input array");
+			assert.notEqual(actual, inputs, "should be different array");
+			assert.strictEqual(inputs.length, 0, "should be empty");
+		},
+
+		"should work given an empty array": function() {
+			var inputs = [],
+				expected = [].concat(inputs), // copy
+				actual = Value.consume(inputs);
+			assert.deepEqual(actual, expected, "should equal input array");
+			assert.notEqual(actual, inputs, "should be different array");
+			assert.strictEqual(inputs.length, 0, "should be empty");
+		}
+
+	},
+
+};

+ 6 - 1
test/lib/pipeline/expressions/AddExpression.js → test/lib/pipeline/expressions/AddExpression_test.js

@@ -15,8 +15,13 @@ module.exports = {
 				assert.doesNotThrow(function(){
 					new AddExpression();
 				});
-			}
+			},
 
+			"should throw Error when constructing with args": function testConstructor(){
+				assert.throws(function(){
+					new AddExpression(1);
+				});
+			}
 		},
 
 		"#getOpName()": {

+ 6 - 0
test/lib/pipeline/expressions/AndExpression.js → test/lib/pipeline/expressions/AndExpression_test.js

@@ -14,6 +14,12 @@ module.exports = {
 				assert.doesNotThrow(function(){
 					new AndExpression();
 				});
+			},
+
+			"should throw Error when constructing with args": function testConstructor(){
+				assert.throws(function(){
+					new AndExpression(1);
+				});
 			}
 
 		},

+ 12 - 2
test/lib/pipeline/expressions/ConcatExpression.js → test/lib/pipeline/expressions/ConcatExpression_test.js

@@ -14,8 +14,12 @@ module.exports = {
 				assert.doesNotThrow(function(){
 					new ConcatExpression();
 				});
+			},
+			"should throw Error when constructing with args": function testConstructor(){
+				assert.throws(function(){
+					new ConcatExpression("should die");
+				});
 			}
-
 		},
 
 		"#getOpName()": {
@@ -50,8 +54,14 @@ module.exports = {
 
 			"should return null if an operand evaluates to null; {$concat:[my,$a]}": function testNull(){
 				assert.equal(Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:null}), null);
-			}
+			},
+
+			"should throw if a non-string is passed in: {$concat:[my,$a]}": function testNull(){
+				assert.throws(function(){
+					Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:100});
+				});
 
+			}
 		}
 
 	}

+ 6 - 0
test/lib/pipeline/expressions/MultiplyExpression.js → test/lib/pipeline/expressions/MultiplyExpression_test.js

@@ -14,6 +14,12 @@ module.exports = {
 				assert.doesNotThrow(function(){
 					new MultiplyExpression();
 				});
+			},
+
+			"should throw Error when constructing with args": function testConstructor(){
+				assert.throws(function(){
+					new MultiplyExpression(1);
+				});
 			}
 
 		},

+ 0 - 333
test/lib/pipeline/expressions/NaryExpressionT.js

@@ -1,333 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
-	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	NaryExpressionT = require("../../../../lib/pipeline/expressions/NaryExpressionT"),
-	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
-	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-function constify(obj, parentIsArray) {
-	parentIsArray = !parentIsArray ? false : true;
-	var bob = parentIsArray ? [] : {};
-	Object.keys(obj).forEach(function(key) {
-		var elem = obj[key];
-		if(elem.constructor === Object) {
-			bob[key] = constify(elem, false);
-		}
-		else if(elem.constructor === Array && !parentIsArray) {
-			bob[key] = constify(elem, true);
-		}
-		else if(key === "$const" ||
-			elem.constructor === String && elem[0] === '$') {
-			bob[key] = obj[key];
-		}
-		else {
-			bob[key] = {$const:obj[key]}
-		}
-	});
-	return bob;
-};
-
-function expressionToJson(expr) {
-	return expr.serialize(false);
-};
-
-function assertDependencies(expectedDeps, expr) {
-	var deps = new DepsTracker(),
-		depsJson = [];
-	expr.addDependencies(deps);
-	deps.forEach(function(dep) {
-		depsJson.push(dep);
-	});
-	assert.deepEqual(depsJson, expectedDeps);
-	assert.equal(deps.needWholeDocument, false);
-	assert.equal(deps.needTextScore, false);
-};
-
-// A dummy child of NaryExpression used for testing
-var TestableExpression = (function(){
-		// CONSTRUCTOR
-	var klass = function TestableExpression(isAssociativeAndCommutative){
-		this._isAssociativeAndCommutative = isAssociativeAndCommutative;
-		base.call(this);
-	}, base = NaryExpressionT(TestableExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-	// PROTOTYPE MEMBERS
-	proto.evaluateInternal = function evaluateInternal(vars) {
-		// Just put all the values in a list.  This is not associative/commutative so
-		// the results will change if a factory is provided and operations are reordered.
-		return this.operands.map(function(operand) {
-			return operand.evaluateInternal(vars);
-		});
-	};
-
-	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative(){
-		return this._isAssociativeAndCommutative;
-	};
-
-	klass.create = function create(associativeAndCommutative) {
-		associativeAndCommutative = !associativeAndCommutative ? false : true; //NOTE: coercing to bool -- defaults to false
-		return new TestableExpression(associativeAndCommutative);
-	};
-
-	klass.factory = function factory() {
-		return new TestableExpression(true);
-	};
-
-	proto.getOpName = function getOpName() {
-		return "$testable";
-	};
-
-	proto.assertContents = function assertContents(expectedContents) {
-		assert.deepEqual(constify({$testable:expectedContents}), expressionToJson(this));
-	};
-
-	klass.createFromOperands = function(operands, haveFactory) {
-		haveFactory = !haveFactory ? false : true; //NOTE: coercing to bool -- defaults to false
-		var vps = new VariablesParseState(new VariablesIdGenerator()),
-			testable = new TestableExpression(haveFactory);
-		operands.forEach(function(x) {
-			testable.addOperand(Expression.parseOperand(x, vps));
-		});
-		return testable;
-	};
-
-	return klass;
-})();
-
-
-module.exports = {
-
-	"NaryExpressionT": {
-
-		"generator": {
-
-			"can generate a NaryExpression class": function() {
-				assert.doesNotThrow(function() {
-					var NaryExpressionClass = NaryExpressionT(String),
-						naryEpressionIntance = new NaryExpressionClass();
-				});
-			}
-
-		}
-
-	},
-
-	"NaryExpression": {
-
-		"statics": {
-
-			"parseArguments":{
-
-				"should parse a fieldPathExpression": function parsesFieldPathExpression() {
-					var NaryExpressionClass = NaryExpressionT(String),
-						vps = new VariablesParseState(new VariablesIdGenerator()),
-						parsedArguments = NaryExpressionClass.parseArguments("$field.path.expression", vps);
-						assert.equal(parsedArguments.length, 1);
-						assert(parsedArguments[0] instanceof FieldPathExpression);
-				},
-
-				"should parse an array of fieldPathExpressions": function parsesFieldPathExpression() {
-					var NaryExpressionClass = NaryExpressionT(String),
-						vps = new VariablesParseState(new VariablesIdGenerator()),
-						parsedArguments = NaryExpressionClass.parseArguments(["$field.path.expression", "$another.FPE"], vps);
-						assert.equal(parsedArguments.length, 2);
-						assert(parsedArguments[0] instanceof FieldPathExpression);
-						assert(parsedArguments[1] instanceof FieldPathExpression);
-				}
-			}
-
-		},
-
-		"addOperand": {
-			"run" : function run() {
-				var testable = new TestableExpression.create();
-				testable.addOperand(new ConstantExpression(9));
-				debugger;
-				testable.assertContents([9]);
-				testable.addOperand(new FieldPathExpression("ab.c"));
-				testable.assertContents([9, "$ab.c"]); //NOTE: Broken, not sure if problem with assertConents or FPE serialize
-			}
-		},
-
-		"Dependencies": {
-			"run": function run() {
-				var testable = new TestableExpression.create();
-
-				// No arguments.
-				assertDependencies([], testable);
-
-				// Add a constant argument.
-				testable.addOperand(new ConstantExpression(1));
-				assertDependencies([], testable);
-
-				// Add a field path argument.
-				testable.addOperand(new FieldPathExpression("ab.c"));
-				assertDependencies(["ab.c"], testable);
-
-				// Add an object expression.
-				var spec = {a:"$x", q:"$r"},
-					specElement = spec,
-					ctx = new Expression.ObjectCtx({isDocumentOk:true}),
-					vps = new VariablesParseState(new VariablesIdGenerator());
-				testable.addOperand(Expression.parseObject(specElement, ctx, vps));
-				assertDependencies(["ab.c", "r", "x"]);
-			}
-		},
-
-		"AddToJsonObj": {
-			"run": function run() {
-				var testable = new TestableExpression.create();
-				testable.addOperand(new ConstantExpression(5));
-				assert.deepEqual(
-						{foo:{$testable:[{$const:5}]}},
-						{foo:testable.serialize(false)}
-					);
-			}
-		},
-
-		"AddToJsonArray": {
-			"run": function run() {
-				var testable = new TestableExpression.create();
-				testable.addOperand(new ConstantExpression(5));
-				assert.deepEqual(
-						[{$testable:[{$const:5}]}],
-						[testable.serialize(false)]
-					);
-			}
-		},
-
-		"OptimizeOneOperand": {
-			"run": function run() {
-				var spec = [{$and:[]},"$abc"],
-					testable = TestableExpression.createFromOperands(spec);
-				testable.assertContents(spec);
-				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
-				assertContents([true, "$abc"])
-			}
-		},
-
-		"EvaluateAllConstantOperands": {
-			"run": function run() {
-				var spec = [1,2],
-					testable = TestableExpression.createFromOperands(spec);
-				testable.assertContents(spec);
-				var optimized = testable.optimize();
-				assert.notDeepEqual(testable.serialize(), optimized.serialize());
-				assert.deepEqual({$const:[1,2]}, expressionToJson(optimized));
-			}
-		},
-
-		"NoFactoryOptimize": {
-			// Without factory optimization, optimization will not produce a new expression.
-
-			/** A string constant prevents factory optimization. */
-			"StringConstant": function run() {
-				var testable = TestableExpression.createFromOperands(["abc","def","$path"], true);
-				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
-			},
-
-			/** A single (instead of multiple) constant prevents optimization.  SERVER-6192 */
-			"SingleConstant": function run() {
-				var testable = TestableExpression.createFromOperands([55,"$path"], true);
-				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
-			},
-
-			/** Factory optimization is not used without a factory. */
-			"NoFactory": function run() {
-				var testable = TestableExpression.createFromOperands([55,66,"$path"], false);
-				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
-			}
-		},
-
-		/** Factory optimization separates constant from non constant expressions. */
-		"FactoryOptimize": {
-
-			// The constant expressions are evaluated separately and placed at the end.
-			"run": function run() {
-				var testable = TestableExpression.createFromOperands([55,66,"$path"], false),
-					optimized = testable.optimize();	
-				assert.deepEqual({$testable:["$path", [55,66]]}, expressionToJson(optimized));
-			}
-		},
-
-		/** Factory optimization flattens nested operators of the same type. */
-		"FlattenOptimize": {
-			"run": function run() {
-				var testable = TestableExpression.createFromOperands(
-						[55,"$path",{$add:[5,6,"$q"]},66],
-					true);
-				testable.addOperand(Testable.createFromOperands(
-						[99,100,"$another_path"],
-					true));
-				var optimized = testable.optimize();
-				assert.deepEqual(
-					constify({$testable:[
-							"$path",
-							{$add:["$q", 11]},
-							"$another_path",
-							[55, 66, [99, 100]]
-						]}),
-					expressionToJson(optimized));
-			}
-		},
-
-		/** Three layers of factory optimization are flattened. */
-		"FlattenThreeLayers": {
-			"run": function run() {
-				var top = TestableExpression.createFromOperands([1,2,"$a"], true),
-					nested = TestableExpression.createFromOperands([3,4,"$b"], true);
-				nested.addOperand(TestableExpression.createFromOperands([5,6,"$c"],true));	
-				top.addOperand(nested);
-				var optimized = top.optimize();
-				assert.deepEqual(
-					constify({$testable:[
-						"$a",
-						"$b",
-						"$c",
-						[1,2,[3,4,[5,6]]]]}),
-					expressionToJson(optimized));
-			}
-		},
-
-		"constify": {
-			"simple": function simple() {
-				var obj = {a:'s'},
-					constified = constify(obj);
-				assert.deepEqual(constified, { a: { '$const': 's' } });
-			},
-			"array": function array() {
-				var obj = {a:['s']},
-					constified = constify(obj);
-				assert.deepEqual(constified, { a: [ { '$const': 's' } ] });
-			},
-			"array2": function array2() {
-				var obj = {a:['s', [5], {a:5}]},
-					constified = constify(obj);
-				assert.deepEqual(constified,
-					{ a: 
-						[{ '$const': 's' },
-						 { '$const': [ 5 ] },
-						 { a: { '$const': 5 } }]
-					});
-			},
-			"object": function object() {
-				var obj = {a:{b:{c:5}, d:'hi'}},
-					constified = constify(obj);
-				assert.deepEqual(constified, 
-					{ a: 
-						{ b: { c: { '$const': 5 } },
-							d: { '$const': 'hi' } } });
-			},
-			"fieldPathExpression": function fieldPathExpression() {
-				var obj = {a:"$field.path"},
-					constified = constify(obj);
-				assert.deepEqual(constified, obj);
-			}
-		}
-
-	}
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 240 - 0
test/lib/pipeline/expressions/NaryExpression_test.js

@@ -0,0 +1,240 @@
+"use strict";
+
+var assert = require("assert"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	NaryExpression = require("../../../../lib/pipeline/expressions/NaryExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	utils = require("./utils");
+
+// 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));
+
+// A dummy child of NaryExpression used for testing
+var Testable = (function(){
+	// CONSTRUCTOR
+	var klass = function Testable(isAssociativeAndCommutative){
+		this._isAssociativeAndCommutative = isAssociativeAndCommutative;
+		base.call(this);
+	}, base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// MEMBERS
+	proto.evaluateInternal = function evaluateInternal(vars) {
+		// Just put all the values in a list.  This is not associative/commutative so
+		// the results will change if a factory is provided and operations are reordered.
+		return this.operands.map(function(operand) {
+			return operand.evaluateInternal(vars);
+		});
+	};
+
+	proto.getOpName = function getOpName() {
+		return "$testable";
+	};
+
+	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative(){
+		return this._isAssociativeAndCommutative;
+	};
+
+	klass.create = function create(associativeAndCommutative) {
+		return new Testable(!!associativeAndCommutative);
+	};
+
+	klass.factory = function factory() {
+		return new Testable(true);
+	};
+
+	klass.createFromOperands = function(operands, haveFactory) {
+		if (haveFactory === undefined) haveFactory = false;
+		var idGenerator = new VariablesIdGenerator(),
+			vps = new VariablesParseState(idGenerator),
+			testable = Testable.create(haveFactory);
+		operands.forEach(function(element) {
+			testable.addOperand(Expression.parseOperand(element, vps));
+		});
+		return testable;
+	};
+
+	proto.assertContents = function assertContents(expectedContents) {
+		debugger;
+		assert.deepEqual(utils.constify({$testable:expectedContents}), utils.expressionToJson(this));
+	};
+
+	return klass;
+})();
+
+exports.NaryExpression = {
+
+	".parseArguments()": {
+
+		"should parse a fieldPathExpression": function() {
+			var vps = new VariablesParseState(new VariablesIdGenerator()),
+				parsedArguments = NaryExpression.parseArguments("$field.path.expression", vps);
+			assert.equal(parsedArguments.length, 1);
+			assert(parsedArguments[0] instanceof FieldPathExpression);
+		},
+
+		"should parse an array of fieldPathExpressions": function() {
+			var vps = new VariablesParseState(new VariablesIdGenerator()),
+				parsedArguments = NaryExpression.parseArguments(["$field.path.expression", "$another.FPE"], vps);
+			assert.equal(parsedArguments.length, 2);
+			assert(parsedArguments[0] instanceof FieldPathExpression);
+			assert(parsedArguments[1] instanceof FieldPathExpression);
+		},
+
+	},
+
+	/** Adding operands to the expression. */
+	"AddOperand": function testAddOperand() {
+		var testable = Testable.create();
+		testable.addOperand(new ConstantExpression(9));
+		testable.assertContents([9]);
+		testable.addOperand(new FieldPathExpression("ab.c"));
+		testable.assertContents([9, "$ab.c"]); //NOTE: Broken, not sure if problem with assertConents or FPE serialize
+	},
+
+	/** Dependencies of the expression. */
+	"Dependencies": function testDependencies() {
+		var testable = Testable.create();
+
+		var assertDependencies = function assertDependencies(expectedDeps, expr) {
+			var deps = new DepsTracker(),
+				depsJson = [];
+			expr.addDependencies(deps);
+			deps.forEach(function(dep) {
+				depsJson.push(dep);
+			});
+			assert.deepEqual(depsJson, expectedDeps);
+			assert.equal(deps.needWholeDocument, false);
+			assert.equal(deps.needTextScore, false);
+		};
+
+		// No arguments.
+		assertDependencies([], testable);
+
+		// Add a constant argument.
+		testable.addOperand(new ConstantExpression(1));
+		assertDependencies([], testable);
+
+		// Add a field path argument.
+		testable.addOperand(new FieldPathExpression("ab.c"));
+		assertDependencies(["ab.c"], testable);
+
+		// Add an object expression.
+		var spec = {a:"$x", q:"$r"},
+			specElement = spec,
+			ctx = new Expression.ObjectCtx({isDocumentOk:true}),
+			vps = new VariablesParseState(new VariablesIdGenerator());
+		testable.addOperand(Expression.parseObject(specElement, ctx, vps));
+		assertDependencies(["ab.c", "r", "x"]);
+	},
+
+	/** Serialize to an object. */
+	"AddToJsonObj": function testAddToJsonObj() {
+		var testable = Testable.create();
+		testable.addOperand(new ConstantExpression(5));
+		assert.deepEqual(
+			{foo:{$testable:[{$const:5}]}},
+			{foo:testable.serialize(false)}
+		);
+	},
+
+	/** Serialize to an array. */
+	"AddToJsonArray": function testAddToJsonArray() {
+		var testable = Testable.create();
+		testable.addOperand(new ConstantExpression(5));
+		assert.deepEqual(
+			[{$testable:[{$const:5}]}],
+			[testable.serialize(false)]
+		);
+	},
+
+	/** One operand is optimized to a constant, while another is left as is. */
+	"OptimizeOneOperand": function testOptimizeOneOperand() {
+		var spec = [{$and:[]},"$abc"],
+			testable = Testable.createFromOperands(spec);
+		testable.assertContents(spec);
+		assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+		testable.assertContents([true, "$abc"]);
+	},
+
+	/** All operands are constants, and the operator is evaluated with them. */
+	"EvaluateAllConstantOperands": function testEvaluateAllConstantOperands() {
+		var spec = [1,2],
+			testable = Testable.createFromOperands(spec);
+		testable.assertContents(spec);
+		var optimized = testable.optimize();
+		assert.notDeepEqual(testable.serialize(), optimized.serialize());
+		assert.deepEqual({$const:[1,2]}, utils.expressionToJson(optimized));
+	},
+
+	"NoFactoryOptimize": {
+		// Without factory optimization, optimization will not produce a new expression.
+
+		/** A string constant prevents factory optimization. */
+		"StringConstant": function testStringConstant() {
+			var testable = Testable.createFromOperands(["abc","def","$path"], true);
+			assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+		},
+
+		/** A single (instead of multiple) constant prevents optimization.  SERVER-6192 */
+		"SingleConstant": function testSingleConstant() {
+			var testable = Testable.createFromOperands([55,"$path"], true);
+			assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+		},
+
+		/** Factory optimization is not used without a factory. */
+		"NoFactory": function testNoFactory() {
+			var testable = Testable.createFromOperands([55,66,"$path"], false);
+			assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+		},
+
+	},
+
+	/** Factory optimization separates constant from non constant expressions. */
+	"FactoryOptimize": function testFactoryOptimize() {
+		// The constant expressions are evaluated separately and placed at the end.
+		var testable = Testable.createFromOperands([55,66,"$path"], false),
+			optimized = testable.optimize();
+		assert.deepEqual({$testable:["$path", [55,66]]}, utils.expressionToJson(optimized));
+	},
+
+	/** Factory optimization flattens nested operators of the same type. */
+	"FlattenOptimize": function testFlattenOptimize() {
+		var testable = Testable.createFromOperands(
+				[55,"$path",{$add:[5,6,"$q"]},66],
+			true);
+		testable.addOperand(Testable.createFromOperands(
+				[99,100,"$another_path"],
+			true));
+		var optimized = testable.optimize();
+		assert.deepEqual(
+			utils.constify({$testable:[
+					"$path",
+					{$add:["$q", 11]},
+					"$another_path",
+					[55, 66, [99, 100]]
+				]}),
+			utils.expressionToJson(optimized));
+	},
+
+	/** Three layers of factory optimization are flattened. */
+	"FlattenThreeLayers": function testFlattenThreeLayers() {
+		var top = Testable.createFromOperands([1,2,"$a"], true),
+			nested = Testable.createFromOperands([3,4,"$b"], true);
+		nested.addOperand(Testable.createFromOperands([5,6,"$c"],true));
+		top.addOperand(nested);
+		var optimized = top.optimize();
+		assert.deepEqual(
+			utils.constify({$testable:[
+				"$a",
+				"$b",
+				"$c",
+				[1,2,[3,4,[5,6]]]]}),
+			utils.expressionToJson(optimized));
+	},
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 6 - 0
test/lib/pipeline/expressions/OrExpression.js → test/lib/pipeline/expressions/OrExpression_test.js

@@ -14,6 +14,12 @@ module.exports = {
 				assert.doesNotThrow(function(){
 					new OrExpression();
 				});
+			},
+
+			"should throw Error when constructing with args": function testConstructor(){
+				assert.throws(function(){
+					new OrExpression(1);
+				});
 			}
 
 		},

+ 7 - 1
test/lib/pipeline/expressions/ToLowerExpression.js → test/lib/pipeline/expressions/ToLowerExpression_test.js

@@ -14,7 +14,13 @@ module.exports = {
 								assert.doesNotThrow(function() {
 										new ToLowerExpression();
 								});
-						}
+						},
+
+					"should throw Error when constructing with args": function testConstructor(){
+						assert.throws(function(){
+							new ToLowerExpression(1);
+						});
+					}
 
 				},
 

+ 7 - 1
test/lib/pipeline/expressions/ToUpperExpression.js → test/lib/pipeline/expressions/ToUpperExpression_test.js

@@ -14,7 +14,13 @@ module.exports = {
 								assert.doesNotThrow(function() {
 										new ToUpperExpression();
 								});
-						}
+						},
+
+					"should throw Error when constructing with args": function testConstructor(){
+						assert.throws(function(){
+							new ToUpperExpression(1);
+						});
+					}
 
 				},
 

+ 36 - 0
test/lib/pipeline/expressions/VariadicExpressionT_test.js

@@ -0,0 +1,36 @@
+"use strict";
+
+var assert = require("assert"),
+	VariadicExpressionT = require("../../../../lib/pipeline/expressions/VariadicExpressionT"),
+	NaryExpressionT = require("../../../../lib/pipeline/expressions/NaryExpressionT");
+
+
+//TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
+module.exports = {
+
+	"VariadicExpression": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor() {
+				assert.doesNotThrow(function () {
+					new VariadicExpressionT({});
+				});
+			},
+
+			"should be an instance of NaryExpression": function () {
+				var VariadicExpressionString = VariadicExpressionT(String);
+				assert.doesNotThrow(function() {
+					var ves = new VariadicExpressionString();
+				});
+				var ves = new VariadicExpressionString();
+				assert(ves.addOperand);
+				assert(ves.validateArguments);
+				//.... and so on. These prove we have a NaryExpression
+			}
+		}
+	}
+};
+
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 46 - 0
test/lib/pipeline/expressions/utils.js

@@ -0,0 +1,46 @@
+"use strict";
+
+var utils = module.exports = {
+
+	/**
+	 * Convert BSONObj to a BSONObj with our $const wrappings.
+	 * @method constify
+	 */
+	constify: function constify(obj, parentIsArray) {
+		if (parentIsArray === undefined) parentIsArray = false;
+		var bob = parentIsArray ? [] : {};
+		for (var key in obj) {
+			if (!obj.hasOwnProperty(key)) continue;
+			var elem = obj[key];
+			if (elem instanceof Object && elem.constructor === Object) {
+				bob[key] = utils.constify(elem, false);
+			} else if (Array.isArray(elem) && !parentIsArray) {
+				// arrays within arrays are treated as constant values by the real parser
+				bob[key] = utils.constify(elem, true);
+			} else if (key == "$const" ||
+					(typeof elem == "string" && elem[0] == "$")) {
+				bob[key] = obj[key];
+			} else {
+				bob[key] = {$const: obj[key]};
+			}
+		}
+		return bob;
+	},
+
+	//SKIPPED: assertBinaryEqual
+
+	//SKIPPED: toJson
+
+    /**
+     * Convert Expression to BSON.
+     * @method expressionToJson
+     */
+	expressionToJson: function expressionToJson(expr) {
+		return expr.serialize(false);
+	},
+
+	//SKIPPED: fromJson
+
+	//SKIPPED: valueFromJson
+
+};

+ 102 - 0
test/lib/pipeline/expressions/utils_test.js

@@ -0,0 +1,102 @@
+"use strict";
+
+var assert = require("assert"),
+	utils = require("./utils");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.utils = {
+
+	".constify()": {
+
+		"simple": function() {
+			var original = {
+					a: 1,
+					b: "s"
+				},
+				expected = {
+					a: {
+						$const: 1
+					},
+					b: {
+						$const: "s"
+					}
+				};
+			assert.deepEqual(utils.constify(original), expected);
+		},
+
+		"array": function() {
+			var original = {
+					a: ["s"]
+				},
+				expected = {
+					a: [
+						{
+							$const: "s"
+						}
+					]
+				};
+			assert.deepEqual(utils.constify(original), expected);
+		},
+
+		"array2": function() {
+			var original = {
+					a: [
+						"s",
+						[5],
+						{
+							a: 5
+						}
+					]
+				},
+				expected = {
+					a: [{
+							$const: "s"
+					},
+						{
+							$const: [5]
+					},
+						{
+							a: {
+								$const: 5
+							}
+					}]
+				};
+			assert.deepEqual(utils.constify(original), expected);
+		},
+
+		"object": function() {
+			var original = {
+					a: {
+						b: {
+							c: 5
+						},
+						d: "hi"
+					}
+				},
+				expected = {
+					a: {
+						b: {
+							c: {
+								"$const": 5
+							}
+						},
+						d: {
+							"$const": "hi"
+						}
+					}
+				};
+			assert.deepEqual(utils.constify(original), expected);
+		},
+
+		"fieldPathExpression": function() {
+			var original = {
+				a: "$field.path"
+			};
+			assert.deepEqual(utils.constify(original), original);
+		},
+
+	},
+
+};