Przeglądaj źródła

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

Jake Delaney 11 lat temu
rodzic
commit
3fe48b65ea
63 zmienionych plików z 2120 dodań i 1289 usunięć
  1. 105 0
      lib/pipeline/DepsTracker.js
  2. 42 13
      lib/pipeline/Document.js
  3. 30 27
      lib/pipeline/FieldPath.js
  4. 85 0
      lib/pipeline/ParsedDeps.js
  5. 0 114
      lib/pipeline/documentSources/DocumentSource.js
  6. 3 3
      lib/pipeline/expressions/AddExpression.js
  7. 3 4
      lib/pipeline/expressions/AllElementsTrueExpression.js
  8. 2 2
      lib/pipeline/expressions/AndExpression.js
  9. 18 9
      lib/pipeline/expressions/AnyElementTrueExpression.js
  10. 2 2
      lib/pipeline/expressions/CompareExpression.js
  11. 2 2
      lib/pipeline/expressions/ConcatExpression.js
  12. 64 29
      lib/pipeline/expressions/CondExpression.js
  13. 9 9
      lib/pipeline/expressions/DayOfMonthExpression.js
  14. 9 3
      lib/pipeline/expressions/DayOfWeekExpression.js
  15. 9 3
      lib/pipeline/expressions/DayOfYearExpression.js
  16. 9 3
      lib/pipeline/expressions/DivideExpression.js
  17. 129 105
      lib/pipeline/expressions/Expression.js
  18. 114 161
      lib/pipeline/expressions/FieldPathExpression.js
  19. 36 0
      lib/pipeline/expressions/FixedArityExpressionT.js
  20. 9 3
      lib/pipeline/expressions/HourExpression.js
  21. 3 3
      lib/pipeline/expressions/IfNullExpression.js
  22. 3 3
      lib/pipeline/expressions/LetExpression.js
  23. 3 3
      lib/pipeline/expressions/MillisecondExpression.js
  24. 3 3
      lib/pipeline/expressions/MinuteExpression.js
  25. 3 3
      lib/pipeline/expressions/ModExpression.js
  26. 3 3
      lib/pipeline/expressions/MonthExpression.js
  27. 2 2
      lib/pipeline/expressions/MultiplyExpression.js
  28. 30 0
      lib/pipeline/expressions/NaryBaseExpressionT.js
  29. 107 89
      lib/pipeline/expressions/NaryExpression.js
  30. 5 4
      lib/pipeline/expressions/NotExpression.js
  31. 2 2
      lib/pipeline/expressions/OrExpression.js
  32. 3 3
      lib/pipeline/expressions/SecondExpression.js
  33. 3 3
      lib/pipeline/expressions/SetDifferenceExpression.js
  34. 2 9
      lib/pipeline/expressions/SetEqualsExpression.js
  35. 2 9
      lib/pipeline/expressions/SetIntersectionExpression.js
  36. 3 3
      lib/pipeline/expressions/SetIsSubsetExpression.js
  37. 2 9
      lib/pipeline/expressions/SetUnionExpression.js
  38. 3 3
      lib/pipeline/expressions/SizeExpression.js
  39. 3 3
      lib/pipeline/expressions/StrcasecmpExpression.js
  40. 3 3
      lib/pipeline/expressions/SubstrExpression.js
  41. 3 3
      lib/pipeline/expressions/SubtractExpression.js
  42. 6 6
      lib/pipeline/expressions/ToLowerExpression.js
  43. 2 2
      lib/pipeline/expressions/ToUpperExpression.js
  44. 4 1
      lib/pipeline/expressions/VariadicExpressionT.js
  45. 3 3
      lib/pipeline/expressions/WeekExpression.js
  46. 3 3
      lib/pipeline/expressions/YearExpression.js
  47. 1 1
      lib/pipeline/expressions/index.js
  48. 87 0
      test/lib/pipeline/DepsTracker_test.js
  49. 110 53
      test/lib/pipeline/Document.js
  50. 130 143
      test/lib/pipeline/FieldPath.js
  51. 74 0
      test/lib/pipeline/ParsedDeps.js
  52. 0 42
      test/lib/pipeline/documentSources/DocumentSource.js
  53. 79 10
      test/lib/pipeline/expressions/AnyElementTrueExpression.js
  54. 128 0
      test/lib/pipeline/expressions/CondExpression_test.js
  55. 22 29
      test/lib/pipeline/expressions/ConstantExpression_test.js
  56. 147 146
      test/lib/pipeline/expressions/FieldPathExpression.js
  57. 64 0
      test/lib/pipeline/expressions/IfNullExpression_test.js
  58. 0 151
      test/lib/pipeline/expressions/NaryExpression.js
  59. 241 0
      test/lib/pipeline/expressions/NaryExpression_test.js
  60. 0 8
      test/lib/pipeline/expressions/NotExpression.js
  61. 0 36
      test/lib/pipeline/expressions/VariadicExpressionT_test.js
  62. 46 0
      test/lib/pipeline/expressions/utils.js
  63. 102 0
      test/lib/pipeline/expressions/utils_test.js

+ 105 - 0
lib/pipeline/DepsTracker.js

@@ -0,0 +1,105 @@
+"use strict";
+
+/**
+ * Allows components in an aggregation pipeline to report what they need from their input.
+ *
+ * @class DepsTracker
+ * @namespace mungedb-aggregate.pipeline
+ * @module mungedb-aggregate
+ * @constructor
+ */
+var DepsTracker = module.exports = function DepsTracker() {
+	// fields is a set of strings
+	this.fields = {};
+	this.needWholeDocument = false;
+	this.needTextScore = false;
+}, klass = DepsTracker, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+var ParsedDeps = require("./ParsedDeps");
+
+/**
+ * Returns a projection object covering the dependencies tracked by this class.
+ * @method toProjection
+ * @return {Object} projection of caller's dependencies
+ */
+proto.toProjection = function toProjection() {
+	var proj = {};
+
+	// if(this.needTextScore) {
+		// bb.append(Document::metaFieldTextScore, BSON("$meta" << "textScore"));
+	// }
+
+	if (this.needWholeDocument) {
+		return proj;
+	}
+
+	if (Object.keys(this.fields).length === 0) {
+		// Projection language lacks good a way to say no fields needed. This fakes it.
+		proj._id = 0;
+		proj.$noFieldsNeeded = 1;
+		return proj;
+	}
+
+	var last = "",
+		needId = false;
+
+	Object.keys(this.fields).sort().forEach(function (it) {
+		if (it.slice(0,3) == "_id" && (it.length == 3 || it.charAt(3) == ".")) {
+			// _id and subfields are handled specially due in part to SERVER-7502
+			needId = true;
+			return;
+		}
+
+		if (last !== "" && it.slice(0, last.length) === last) {
+			// we are including a parent of *it so we don't need to include this
+			// field explicitly. In fact, due to SERVER-6527 if we included this
+			// field, the parent wouldn't be fully included. This logic relies
+			// on on set iterators going in lexicographic order so that a string
+			// is always directly before of all fields it prefixes.
+			return;
+		}
+
+		last = it + ".";
+		proj[it] = 1;
+	});
+
+	if (needId)
+		proj._id = 1;
+	else
+		proj._id = 0;
+
+	return proj;
+};
+
+/**
+ * Takes a depsTracker and builds a simple recursive lookup table out of it.
+ * @method toParsedDeps
+ * @return {ParsedDeps}
+ */
+proto.toParsedDeps = function toParsedDeps() {
+	var doc = {};
+
+	if (this.needWholeDocument || this.needTextScore) {
+		// can't use ParsedDeps in this case
+		// TODO: not sure what appropriate equivalent to boost::none is
+		return;
+	}
+
+	var last = "";
+	Object.keys(this.fields).sort().forEach(function (it) {
+		if (last !== "" && it.slice(0, last.length) === last) {
+			// we are including a parent of *it so we don't need to include this
+			// field explicitly. In fact, due to SERVER-6527 if we included this
+			// field, the parent wouldn't be fully included. This logic relies
+			// on on set iterators going in lexicographic order so that a string
+			// is always directly before of all fields it prefixes.
+			return;
+		}
+
+		last = it + ".";
+		// TODO: set nested field to true; i.e. a.b.c = true, not a = true
+		doc[it] = true;
+	});
+
+	return new ParsedDeps(doc);
+};

+ 42 - 13
lib/pipeline/Document.js

@@ -11,7 +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}});
 
-var Value = require("./Value");
+var Value = require("./Value"),
+	FieldPath = require("./FieldPath");
 
 /**
  * Shared "_id"
@@ -35,10 +36,31 @@ klass.toJson = function toJson(doc) {
 //SKIPPED: toBsonWithMetaData
 //SKIPPED: fromBsonWithMetaData
 
-//SKIPPED: MutableDocument
-
-//SKIPPED: getNestedFieldHelper
-//SKIPPED: getNestedField -- same as getNestedFieldHelper in our code
+//SKIPPED: most of MutableDocument except for getNestedField and setNestedField, squashed into Document here (because that's how they use it)
+function getNestedFieldHelper(obj, path) {
+	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+		lastKey = keys[keys.length - 1];
+	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
+		var next = cur[keys[i]];
+		if (!(next instanceof Object)) return undefined;
+		cur = next;
+	}
+	return cur[lastKey];
+}
+klass.getNestedField = getNestedFieldHelper;  // NOTE: ours is static so these are the same
+klass.setNestedField = function setNestedField(obj, path, val) {
+	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+		lastKey = keys[keys.length - 1];
+	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
+		var next = cur[keys[i]];
+		if (!(next instanceof Object)) cur[keys[i]] = next = {};
+		cur = next;
+	}
+	cur[lastKey] = val;
+	return val;
+};
 //SKIPPED: getApproximateSize -- not implementing mem usage right now
 //SKIPPED: hash_combine
 
@@ -91,7 +113,7 @@ klass.serializeForSorter = function serializeForSorter(doc) {
 };
 
 klass.deserializeForSorter = function deserializeForSorter(docStr, sorterDeserializeSettings) {
-	JSON.parse(docStr);
+	return JSON.parse(docStr);
 };
 
 //SKIPPED: swap
@@ -107,22 +129,29 @@ klass.empty = function(obj) {
 
 /**
  * Clone a document
+ * This should only be called by MutableDocument and tests
+ * The new document shares all the fields' values with the original.
+ * This is not a deep copy.  Only the fields on the top-level document
+ * are cloned.
  * @static
  * @method clone
  * @param doc
  */
 klass.clone = function clone(doc) {
+	var obj = {};
+	for (var key in doc) {
+		if (doc.hasOwnProperty(key)) {
+			obj[key] = doc[key];
+		}
+	}
+	return obj;
+};
+klass.cloneDeep = function cloneDeep(doc) {	//there are casese this is actually what we want
 	var obj = {};
 	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;
-			}
+			obj[key] = val instanceof Object && val.constructor === Object ? Document.clone(val) : val;
 		}
 	}
 	return obj;

+ 30 - 27
lib/pipeline/FieldPath.js

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

+ 85 - 0
lib/pipeline/ParsedDeps.js

@@ -0,0 +1,85 @@
+"use strict";
+
+/**
+ * This class is designed to quickly extract the needed fields into a Document.
+ * It should only be created by a call to DepsTracker.toParsedDeps.
+ *
+ * @class ParsedDeps
+ * @namespace mungedb-aggregate.pipeline
+ * @module mungedb-aggregate
+ * @constructor
+ * @param {Object} fields	The fields needed in a Document
+ */
+var ParsedDeps = module.exports = function ParsedDeps(fields) {
+	this._fields = fields;
+}, klass = ParsedDeps, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+var Value = require("./Value");
+
+/**
+ * Extracts fields from the input into a new Document, based on the caller.
+ *
+ * @method extractFields
+ * @param {Object} input	The JSON object to extract from
+ * @return {Document}
+ */
+proto.extractFields = function extractFields(input) {
+	return proto._documentHelper(input, this._fields);
+};
+
+/**
+ * Private: Handles array-type values for extractFields()
+ *
+ * @method _arrayHelper
+ * @param {Object} array	Array to iterate over
+ * @param {Object} neededFields
+ * @return {Array}
+ */
+proto._arrayHelper = function _arrayHelper(array, neededFields) {
+	var values = [];
+
+	for (var it in array) {
+		if (it instanceof Array)
+			values.push(_arrayHelper(it, neededFields));
+		else if (it instanceof Object)
+			values.push(proto._documentHelper(it, neededFields));
+	}
+
+	return values;
+};
+
+/**
+ * Private: Handles object-type values for extractFields()
+ *
+ * @method _documentHelper
+ * @param {Object} json	Object to iterate over and filter
+ * @param {Object} neededFields	Fields to not exclude
+ * @return {Document}
+ */
+proto._documentHelper = function _documentHelper(json, neededFields) {
+	var doc = {};
+
+	for (var fieldName in json) {
+		var jsonElement = json[fieldName],
+			isNeeded = neededFields[fieldName];
+
+		if (isNeeded === undefined)
+			continue;
+
+		if (Value.getType(isNeeded) === 'boolean') {
+			doc[fieldName] = jsonElement;
+			continue;
+		}
+
+		if (!isNeeded instanceof Object) throw new Error("dassert failure");
+
+		if (Value.getType(isNeeded) === 'object') {
+			if (jsonElement instanceof Array)
+				doc[fieldName] = proto._arrayHelper(jsonElement, isNeeded);
+			if (jsonElement instanceof Object)
+				doc[fieldName] = proto._documentHelper(jsonElement, isNeeded);
+		}
+	}
+
+	return doc;
+};

+ 0 - 114
lib/pipeline/documentSources/DocumentSource.js

@@ -185,47 +185,6 @@ proto.getDependencies = function getDependencies(deps) {
 	return klass.GetDepsReturn.NOT_SUPPORTED;
 };
 
-/**
- * This takes dependencies from getDependencies and
- * returns a projection that includes all of them
- *
- * @method	depsToProjection
- * @param	{Object} deps	set (unique array) of strings
- * @returns	{Object}	JSONObj
- **/
-klass.depsToProjection = function depsToProjection(deps) {
-	var needId = false,
-		bb = {};
-	if (deps._id === undefined)
-		bb._id = 0;
-
-	var last = "";
-	Object.keys(deps).sort().forEach(function(it){
-		if (it.indexOf('_id') === 0 && (it.length === 3 || it[3] === '.')) {
-			needId = true;
-			return;
-		} else {
-			if (last !== "" && it.slice(0, last.length) === last){
-				// we are including a parent of *it so we don't need to
-				// include this field explicitly. In fact, due to
-				// SERVER-6527 if we included this field, the parent
-				// wouldn't be fully included.
-				return;
-			}
-		}
-		last = it + ".";
-		bb[it] = 1;
-	});
-
-	if (needId) // we are explicit either way
-		bb._id = 1;
-	else
-		bb._id = 0;
-
-
-	return bb;
-};
-
 proto._serialize = function _serialize(explain) {
 	throw new Error("not implemented");
 };
@@ -237,23 +196,6 @@ proto.serializeToArray = function serializeToArray(array, explain) {
 	}
 };
 
-klass.parseDeps = function parseDeps(deps) {
-	var md = {};
-
-	var last,
-		depKeys = Object.keys(deps);
-	for (var i = 0; i < depKeys.length; i++) {
-		var it = depKeys[i],
-			value = deps[it];
-
-		if (!last && it.indexOf(last) >= 0)
-			continue;
-		last = it + '.';
-		md[it] = true;
-	}
-	return md;
-};
-
 /**
  * A function compatible as a getNext for document sources.
  * Does nothing except pass the documents through. To use,
@@ -274,59 +216,3 @@ klass.GET_NEXT_PASS_THROUGH = function GET_NEXT_PASS_THROUGH(callback) {
 	});
 	return out; // For the sync people in da house
 };
-
-klass.documentFromJsonWithDeps = function documentFromJsonWithDeps(bson, neededFields) {
-	var arrayHelper = function(bson, neededFields) {
-		var values = [];
-
-		var bsonKeys = Object.keys(bson);
-		for (var i = 0; i < bsonKeys.length; i++) {
-			var key = bsonKeys[i],
-				bsonElement = bson[key];
-
-			if (bsonElement instanceof Object) {
-				var sub = klass.documentFromJsonWithDeps(bsonElement, isNeeded);
-				values.push(sub);
-			}
-
-			if (bsonElement instanceof Array) {
-				values.push(arrayHelper(bsonElement, neededFields));
-			}
-		}
-
-		return values;
-	};
-
-	var md = {};
-
-	var bsonKeys = Object.keys(bson);
-	for (var i = 0; i < bsonKeys.length; i++) {
-		var fieldName = bsonKeys[i],
-			bsonElement = bson[fieldName],
-			isNeeded = neededFields ? neededFields[fieldName] : null;
-
-		if (!isNeeded)
-			continue;
-
-		if (typeof(isNeeded) === 'boolean') {
-			md[fieldName] = bsonElement;
-			continue;
-		}
-
-		if (!isNeeded instanceof Object)
-			throw new Error("instanceof should be an instance of Object");
-
-		if (bsonElement instanceof Object) {
-			var sub = klass.documentFromJsonWithDeps(bsonElement, isNeeded);
-
-			md[fieldName] = sub;
-		}
-
-		if (bsonElement instanceof Array) {
-			md[fieldName] = arrayHelper(bsonElement, isNeeded);
-		}
-	}
-
-	return md;
-
-};

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

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

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

@@ -8,12 +8,11 @@
  * @constructor
  **/
 var AllElementsTrueExpression = module.exports = function AllElementsTrueExpression() {
-	this.nargs = 1;
 	base.call(this);
 },
 	klass = AllElementsTrueExpression,
-	NaryExpression = require("./NaryExpression"),
-	base = NaryExpression,
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -46,4 +45,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$allElementsTrue", base.parse(AllElementsTrueExpression));
+Expression.registerExpression("$allElementsTrue", base.parse);

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

@@ -14,7 +14,7 @@
 var AndExpression = module.exports = function AndExpression() {
 //	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = AndExpression, base = require(".VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+}, klass = AndExpression, base = require("./VariadicExpressionT")(AndExpression), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -70,6 +70,6 @@ proto.optimize = function optimize() {
 };
 
 /** Register Expression */
-Expression.registerExpression(klass.opName, base.parse(klass));
+Expression.registerExpression(klass.opName, base.parse);
 
 //TODO: proto.toMatcherBson

+ 18 - 9
lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -8,9 +8,16 @@
  * @constructor
  **/
 var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression(){
-	this.nargs = (1);
 	base.call(this);
-}, klass = AnyElementTrueExpression, NaryExpression = require("./NaryExpression"), base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+},
+	klass = AnyElementTrueExpression,
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
+	proto = klass.prototype = Object.create(base.prototype,{
+		constructor:{
+			value:klass
+		}
+	});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -26,16 +33,18 @@ proto.getOpName = function getOpName(){
  * @method @evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	if (!vars instanceof Array) throw new Error("$anyElementTrue requires an array");
-
-	var total = 0;
-	for (var i = 0, n = vars.length; i < n; ++i) {
-		var value = vars[i].evaluateInternal([i]);
-		if ( value.coerceToBool() )
+	var arr = this.operands[0].evaluateInternal(vars);
+	if (!(arr instanceof Array)) {
+		throw new Error("uassert 17041: $anyElementTrue's " +
+						"argument must be an array, but is " +
+						typeof arr);
+	}
+	for (var i=0, n=arr.length; i<n; ++i) {
+		if (Value.coerceToBool(arr[i]))
 			return true;
 	}
 	return false;
 };
 
 /** Register Expression */
-Expression.registerExpression("$anyElementTrue",base.parse(AnyElementTrueExpression));
+Expression.registerExpression("$anyElementTrue",base.parse);

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

@@ -8,11 +8,11 @@
  * @constructor
  **/
 var CompareExpression = module.exports = function CompareExpression(cmpOp) {
-    this.nargs = 2;
     this.cmpOp = cmpOp;
     base.call(this);
 }, klass = CompareExpression,
-    base = require("./NaryExpression"),
+    FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
     proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass

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

@@ -12,7 +12,7 @@ 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("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = ConcatExpression, base = require("./VariadicExpressionT")(ConcatExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value");
@@ -38,4 +38,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
     }).join("");
 };
 
-Expression.registerExpression(klass.opName, base.parse(klass));
+Expression.registerExpression(klass.opName, base.parse);

+ 64 - 29
lib/pipeline/expressions/CondExpression.js

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

+ 9 - 9
lib/pipeline/expressions/DayOfMonthExpression.js

@@ -8,15 +8,15 @@
  * @constructor
  **/
 var DayOfMonthExpression = module.exports = function DayOfMonthExpression() {
-    this.nargs = 1;
-    base.call(this);
+	base.call(this);
 }, klass = DayOfMonthExpression,
-    base = require("./NaryExpression"),
-    proto = klass.prototype = Object.create(base.prototype, {
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
+	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
 		}
-    });
+	});
 
 // DEPENDENCIES
 var Expression = require("./Expression");
@@ -24,7 +24,7 @@ var Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
-    return "$dayOfMonth";
+	return "$dayOfMonth";
 };
 
 /**
@@ -32,9 +32,9 @@ proto.getOpName = function getOpName() {
  * @method evaluate
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-    var date = this.operands[0].evaluateInternal(vars);
-    return date.getUTCDate();
+	var date = this.operands[0].evaluateInternal(vars);
+	return date.getUTCDate();
 };
 
 /** Register Expression */
-Expression.registerExpression("$dayOfMonth", base.parse(DayOfMonthExpression));
+Expression.registerExpression("$dayOfMonth", base.parse);

+ 9 - 3
lib/pipeline/expressions/DayOfWeekExpression.js

@@ -8,9 +8,15 @@
  * @constructor
  **/
 var DayOfWeekExpression = module.exports = function DayOfWeekExpression(){
-	this.nargs = 1;
 	base.call(this);
-}, klass = DayOfWeekExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = DayOfWeekExpression,
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
+	proto = klass.prototype = Object.create(base.prototype, {
+		constructor:{
+			value:klass
+		}
+	});
 
 // DEPENDENCIES
 var Expression = require("./Expression");
@@ -30,4 +36,4 @@ proto.evaluateInternal = function evaluateInternal(vars){
 };
 
 /** Register Expression */
-Expression.registerExpression("$dayOfWeek",base.parse(DayOfWeekExpression));
+Expression.registerExpression("$dayOfWeek",base.parse);

+ 9 - 3
lib/pipeline/expressions/DayOfYearExpression.js

@@ -8,9 +8,15 @@
  * @constructor
  **/
 var DayOfYearExpression = module.exports = function DayOfYearExpression(){
-	this.nargs = 1;
 	base.call(this);
-}, klass = DayOfYearExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = DayOfYearExpression,
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
+	proto = klass.prototype = Object.create(base.prototype, {
+		constructor:{
+			value:klass
+		}
+	});
 
 // DEPENDENCIES
 var Expression = require("./Expression");
@@ -38,4 +44,4 @@ klass.getDateDayOfYear = function getDateDayOfYear(d){
 };
 
 /** Register Expression */
-Expression.registerExpression("$dayOfYear",base.parse(DayOfYearExpression));
+Expression.registerExpression("$dayOfYear",base.parse);

+ 9 - 3
lib/pipeline/expressions/DivideExpression.js

@@ -9,9 +9,15 @@
  * @constructor
  **/
 var DivideExpression = module.exports = function DivideExpression(){
-    this.nargs = 2;
     base.call(this);
-}, klass = DivideExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = DivideExpression,
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
+	proto = klass.prototype = Object.create(base.prototype, {
+		constructor:{
+			value:klass
+		}
+	});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -37,4 +43,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$divide",base.parse(DivideExpression));
+Expression.registerExpression("$divide",base.parse);

+ 129 - 105
lib/pipeline/expressions/Expression.js

@@ -12,26 +12,22 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-
-
+ */
 var Expression = module.exports = function Expression() {
 	if (arguments.length !== 0) throw new Error("zero args expected");
-}, klass = Expression,
-    base = Object,
-    proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-    });
+}, klass = Expression, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+
+var Value = require("../Value"),
+	Document = require("../Document"),
+	Variables = require("./Variables");
 
 
-// NESTED CLASSES
 /**
  * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.ObjectCtx` class
  * @static
  * @property ObjectCtx
- **/
+ */
 var ObjectCtx = Expression.ObjectCtx = (function() {
 	// CONSTRUCTOR
 	/**
@@ -47,9 +43,9 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 	 *      @param [opts.isDocumentOk]      {Boolean}
 	 *      @param [opts.isTopLevel]        {Boolean}
 	 *      @param [opts.isInclusionOk]     {Boolean}
-	 **/
+	 */
 	var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/ ) {
-		if (!(opts instanceof Object && opts.constructor == Object)) throw new Error("opts is required and must be an Object containing named args");
+		if (!(opts instanceof Object && opts.constructor === Object)) throw new Error("opts is required and must be an Object containing named args");
 		for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
 			if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
 		}
@@ -69,37 +65,32 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 })();
 
 
-/**
- * Produce a field path string with the field prefix removed.
- * Throws an error if the field prefix is not present.
- *
- * @static
- * @param prefixedField the prefixed field
- * @returns the field path with the prefix removed
- **/
-klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
-	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
-	return prefixedField.substr(1);
-};
+//
+// Diagram of relationship between parse functions when parsing a $op:
+//
+// { someFieldOrArrayIndex: { $op: [ARGS] } }
+//                             ^ parseExpression on inner $op BSONElement
+//                          ^ parseObject on BSONObject
+//             ^ parseOperand on outer BSONElement wrapping the $op Object
+//
 
 /**
- * Parse an Object.  The object could represent a functional expression or a Document expression.
- *
- * An object expression can take any of the following forms:
- *
- *      f0: {f1: ..., f2: ..., f3: ...}
- *      f0: {$operator:[operand1, operand2, ...]}
- *
- * @static
+ * Parses a JSON Object that could represent a functional expression or a Document expression.
  * @method parseObject
+ * @static
  * @param obj   the element representing the object
  * @param ctx   a MiniCtx representing the options above
  * @param vps	Variables Parse State
  * @returns the parsed Expression
- **/
+ */
 klass.parseObject = function parseObject(obj, ctx, vps) {
 	if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
+	/*
+	  An object expression can take any of the following forms:
+
+	  f0: {f1: ..., f2: ..., f3: ...}
+	  f0: {$operator:[operand1, operand2, ...]}
+	*/
 
 	var expression, // the result
 		expressionObject, // the alt result
@@ -120,24 +111,27 @@ klass.parseObject = function parseObject(obj, ctx, vps) {
 			if (ctx.isTopLevel)
 				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
+			// we've determined this "object" is an operator expression
+			kind = OPERATOR;
 
 			expression = Expression.parseExpression(fieldName, obj[fieldName], vps); //NOTE: DEVIATION FROM MONGO: c++ code uses 2 arguments. See #parseExpression
 		} else {
 			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)
+			if (!ctx.isTopLevel && fieldName.indexOf(".") !== -1)
 				throw new Error("dotted field names are only allowed at the top level; uassert code 16405");
 
-			if (expression === undefined) { // if it's our first time, create the document expression
-				if (!ctx.isDocumentOk)
-					throw new Error("Assertion failure"); // CW TODO error: document not allowed in this context
+			// if it's our first time, create the document expression
+			if (expression === undefined) {
+				if (!ctx.isDocumentOk) 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.
+				expressionObject = ctx.isTopLevel ? ObjectExpression.createRoot() : ObjectExpression.create();
 				expression = expressionObject;
 
-				kind = NOTOPERATOR; //this "object" is not an operator expression
+				// this "object" is not an operator expression
+				kind = NOTOPERATOR;
 			}
 
 			var fieldValue = obj[fieldName];
@@ -153,8 +147,9 @@ klass.parseObject = function parseObject(obj, ctx, vps) {
 
 					break;
 				case "string":
-					// it's a renamed field         // CW TODO could also be a constant
-					expressionObject.addField(fieldName, new FieldPathExpression.parse(fieldValue, vps));
+					// it's a renamed field
+					// CW TODO could also be a constant
+					expressionObject.addField(fieldName, FieldPathExpression.parse(fieldValue, vps));
 					break;
 				case "boolean":
 				case "number":
@@ -170,7 +165,7 @@ klass.parseObject = function parseObject(obj, ctx, vps) {
 					}
 					break;
 				default:
-					throw new Error("disallowed field type " + (fieldValue instanceof Object ? fieldValue.constructor.name + ":" : typeof fieldValue) + typeof(fieldValue) + " in object expression (at '" + fieldName + "') uassert code 15992");
+					throw new Error("disallowed field type " + Value.getType(fieldValue) + " in object expression (at '" + fieldName + "') uassert code 15992");
 			}
 		}
 	}
@@ -181,10 +176,11 @@ klass.parseObject = function parseObject(obj, ctx, vps) {
 
 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);
+
+/**
+ * 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) {
@@ -194,6 +190,8 @@ klass.registerExpression = function registerExpression(key, parserFunc) {
 	return 1;
 };
 
+
+//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
 /**
  * Parses a BSONElement which has already been determined to be functional expression.
  * @static
@@ -202,15 +200,17 @@ klass.registerExpression = function registerExpression(key, parserFunc) {
  *    That is the field name should be the $op for the expression.
  * @param vps the variable parse state
  * @returns the parsed Expression
- **/
-//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
+ */
 klass.parseExpression = function parseExpression(exprElementKey, exprElementValue, vps) {
-	if (!(exprElementKey in Expression.expressionParserMap)) {
-		throw new Error("Invalid operator : " + exprElementKey + "; code 15999");
-	}
-	return Expression.expressionParserMap[exprElementKey](exprElementValue, vps);
+	var opName = exprElementKey,
+		op = Expression.expressionParserMap[opName];
+	if (!op) throw new Error("invalid operator : " + exprElementKey + "; uassert code 15999");
+
+	// make the expression node
+	return op(exprElementValue, vps);
 };
 
+
 /**
  * Parses a BSONElement which is an operand in an Expression.
  *
@@ -224,89 +224,113 @@ klass.parseExpression = function parseExpression(exprElementKey, exprElementValu
  *    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);
+	if (t === "string" && exprElement[0] === "$") {
+		//if we got here, this is a field path expression
+	    return FieldPathExpression.parse(exprElement, vps);
 	} else if (t === "object" && exprElement && exprElement.constructor === Object) {
-		return Expression.parseObject(exprElement, new ObjectCtx({
+		var oCtx = new ObjectCtx({
 			isDocumentOk: true
-		}), vps);
+		});
+		return Expression.parseObject(exprElement, oCtx, vps);
 	} else {
 		return ConstantExpression.parse(exprElement, vps);
 	}
 };
 
-// PROTOTYPE MEMBERS
-/**
- * Evaluate the Expression using the given document as input.
- *
- * @method evaluate
- * @returns the computed value
- **/
-proto.evaluateInternal = function evaluateInternal(obj) {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
-};
-
-
-/**
- * Evaluate expression with specified inputs and return result.
- *
- * While vars is non-const, if properly constructed, subexpressions modifications to it
- * should not effect outer expressions due to unique variable Ids.
- */
-proto.evaluate = function(vars) {
-	return this.evaluateInternal(vars);
-};
-
 
 /**
  * Optimize the Expression.
  *
  * This provides an opportunity to do constant folding, or to collapse nested
- *  operators that have the same precedence, such as $add, $and, or $or.
+ * operators that have the same precedence, such as $add, $and, or $or.
  *
  * The Expression should be replaced with the return value, which may or may
- *  not be the same object.  In the case of constant folding, a computed
- *  expression may be replaced by a constant.
+ * not be the same object.  In the case of constant folding, a computed
+ * expression may be replaced by a constant.
  *
  * @method optimize
  * @returns the optimized Expression
- **/
+ */
 proto.optimize = function optimize() {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+	return this;
 };
 
+
 /**
- * Add this expression's field dependencies to the set Expressions are trees, so this is often recursive.
- *
- * Top-level ExpressionObject gets pointer to empty vector.
- * If any other Expression is an ancestor, or in other cases where {a:1} inclusion objects aren't allowed, they get NULL.
+ * Add this expression's field dependencies to the set.
+ * Expressions are trees, so this is often recursive.
  *
  * @method addDependencies
- * @param deps  output parameter
- * @param path  path to self if all ancestors are ExpressionObjects.
- **/
+ * @param deps Fully qualified paths to depended-on fields are added to this set.
+ *             Empty string means need full document.
+ * @param path path to self if all ancestors are ExpressionObjects.
+ *             Top-level ExpressionObject gets pointer to empty vector.
+ *             If any other Expression is an ancestor, or in other cases
+ *             where {a:1} inclusion objects aren't allowed, they get
+ *             NULL.
+ */
 proto.addDependencies = function addDependencies(deps, path) {
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
+
 /**
  * simple expressions are just inclusion exclusion as supported by ExpressionObject
- * @method getIsSimple
- **/
-proto.getIsSimple = function getIsSimple() {
+ * @method isSimple
+ */
+proto.isSimple = function isSimple() {
 	return false;
 };
 
-proto.toMatcherBson = function toMatcherBson() {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!"); //verify(false && "Expression::toMatcherBson()");
+/**
+ * Serialize the Expression tree recursively.
+ * If explain is false, returns a Value parsable by parseOperand().
+ * @method serialize
+ */
+proto.serialize = function serialize(explain) {
+	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
+/**
+ * Evaluate expression with specified inputs and return result.
+ *
+ * While vars is non-const, if properly constructed, subexpressions modifications to it
+ * should not effect outer expressions due to unique variable Ids.
+ *
+ * @method evaluate
+ * @param vars
+ */
+proto.evaluate = function evaluate(vars) {
+	if (!(vars instanceof Variables)) vars = new Variables(0, vars); /// Evaluate expression with specified inputs and return result. (only used by tests)
+	return this.evaluateInternal(vars);
+};
+
+/**
+ * Produce a field path string with the field prefix removed.
+ * Throws an error if the field prefix is not present.
+ * @method removeFieldPrefix
+ * @static
+ * @param prefixedField the prefixed field
+ * @returns the field path with the prefix removed
+ */
+klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
+	if (prefixedField.indexOf("\0") !== -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
+	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
+	return prefixedField.substr(1);
+};
+
+/**
+ * Evaluate the subclass Expression using the given Variables as context and return result.
+ * @method evaluate
+ * @returns the computed value
+ */
+proto.evaluateInternal = function evaluateInternal(vars) {
+	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+};
 
-// DEPENDENCIES
-var Document = require("../Document");
-var ObjectExpression = require("./ObjectExpression");
-var FieldPathExpression = require("./FieldPathExpression");
-var ConstantExpression = require("./ConstantExpression");
+var ObjectExpression = require("./ObjectExpression"),
+	FieldPathExpression = require("./FieldPathExpression"),
+	ConstantExpression = require("./ConstantExpression");

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

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

+ 36 - 0
lib/pipeline/expressions/FixedArityExpressionT.js

@@ -0,0 +1,36 @@
+"use strict";
+
+/**
+ * A factory and base class for expressions that take a fixed number of arguments
+ * @class FixedArityExpressionT
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ **/
+
+var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubClass, nArgs) {
+
+	var FixedArityExpression = function FixedArityExpression() {
+		if (arguments.length !== 0) throw new Error(klass.name + "<" + SubClass.name + ">: zero args expected");
+		base.call(this);
+	}, klass = FixedArityExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+
+	/**
+	 * Check that the number of args is what we expected
+	 * @method validateArguments
+	 * @param args Array The array of arguments to the expression
+	 * @throws
+	 **/
+	proto.validateArguments = function validateArguments(args) {
+		if(args.length !== nArgs) {
+			throw new Error("Expression " + this.getOpName() + " takes exactly " +
+				nArgs + " arguments. " + args.length + " were passed in.");
+		}
+	};
+
+	klass.parse = base.parse; 						// NOTE: Need to explicitly
+	klass.parseArguments = base.parseArguments;		// bubble static members in
+													// our inheritance chain
+	return FixedArityExpression;
+};
+

+ 9 - 3
lib/pipeline/expressions/HourExpression.js

@@ -9,9 +9,15 @@
  * @constructor
  **/
 var HourExpression = module.exports = function HourExpression(){
-	this.nargs = 1;
 	base.call(this);
-}, klass = HourExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = HourExpression,
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
+	proto = klass.prototype = Object.create(base.prototype, {
+		constructor:{
+			value:klass
+		}
+	});
 
 // PROTOTYPE MEMBERS
 proto.getOpName = function getOpName(){
@@ -32,4 +38,4 @@ proto.evaluateInternal = function evaluateInternal(vars){
 
 
 /** Register Expression */
-Expression.registerExpression("$hour",base.parse(HourExpression));
+Expression.registerExpression("$hour",base.parse);

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

@@ -9,11 +9,11 @@
  * @constructor
  **/
 var IfNullExpression = module.exports = function IfNullExpression() {
-	this.nargs = 2;
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
 }, klass = IfNullExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -42,4 +42,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$ifNull", base.parse(IfNullExpression));
+Expression.registerExpression("$ifNull", base.parse);

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

@@ -1,12 +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");
 	this._variables = vars;
 	this._subExpression = subExpression;
-}, klass = LetExpression, Expression = require("./FixedArityExpressionT")(klass, 2), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = LetExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+Expression.registerExpression("$let", LetExpression.parse);
 
 // DEPENDENCIES
 var Variables = require("./Variables"),

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var MillisecondExpression = module.exports = function MillisecondExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = MillisecondExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -39,4 +39,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$millisecond", base.parse(MillisecondExpression));
+Expression.registerExpression("$millisecond", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var MinuteExpression = module.exports = function MinuteExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = MinuteExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -37,4 +37,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$minute", base.parse(MinuteExpression));
+Expression.registerExpression("$minute", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var ModExpression = module.exports = function ModExpression() {
-	this.nargs = 2;
 	base.call(this);
 }, klass = ModExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -51,4 +51,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$mod", base.parse(ModExpression));
+Expression.registerExpression("$mod", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var MonthExpression = module.exports = function MonthExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = MonthExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -37,4 +37,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$month", base.parse(MonthExpression));
+Expression.registerExpression("$month", base.parse);

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

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

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

@@ -0,0 +1,30 @@
+"use strict";
+
+/**
+ * Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class.
+ * @class NaryBaseExpressionT
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
+ * @constructor
+ */
+var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass) {
+
+	var NaryBaseExpression = function NaryBaseExpression() {
+		if (arguments.length !== 0) throw new Error(klass.name + "<" + SubClass.name + ">: zero args expected");
+		base.call(this);
+	}, klass = NaryBaseExpression, NaryExpression = require("./NaryExpression"), 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;
+	};
+
+	klass.parseArguments = base.parseArguments;		// NOTE: Need to explicitly
+													// bubble static members in
+													// our inheritance chain
+	return NaryBaseExpression;
+};

+ 107 - 89
lib/pipeline/expressions/NaryExpression.js

@@ -7,128 +7,146 @@
  * @module mungedb-aggregate
  * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
- **/
-var Expression = require("./Expression");
-
-var NaryExpression = module.exports = function NaryExpression(){
+ */
+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}});
-
-klass.parse = function(SubClass) {
-	return function parse(expr, vps) {
-		var outExpr = new SubClass(),
-			args = NaryExpression.parseArguments(expr, vps);
-		outExpr.validateArguments(args);
-		outExpr.operands = args;
-		return outExpr;
-	};
-};
-
-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 = NaryExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-function partitionBy(fn, coll) {
-	var ret = {pass:[],
-			   fail:[]};
-	coll.forEach(function(x) {
-		if(fn(x)) {
-			ret.pass.push(x);
-		} else {
-			ret.fail.push(x);
-		}
-	});
-	return ret;
-}
-// DEPENDENCIES
-var ConstantExpression = require("./ConstantExpression");
+var Variables = require("./Variables"),
+	ConstantExpression = require("./ConstantExpression");
 
-// PROTOTYPE MEMBERS
-proto.evaluate = undefined; // evaluate(doc){ ... defined by inheritor ... }
+proto.optimize = function optimize() {
+	var n = this.operands.length;
 
-proto.getOpName = function getOpName(doc){
-	throw new Error("NOT IMPLEMENTED BY INHERITOR");
-};
+	// optimize sub-expressions and count constants
+	var constCount = 0;
+	for (var i = 0; i < n; ++i) {
+		var optimized = this.operands[i].optimize();
 
-proto.optimize = function optimize(){
-	var n = this.operands.length,
-		constantCount = 0;
+		// substitute the optimized expression
+		this.operands[i] = optimized;
 
-	for(var ii = 0; ii < n; ii++) {
-		if(this.operands[ii] instanceof ConstantExpression) {
-			constantCount++;
-		} else {
-			this.operands[ii] = this.operands[ii].optimize();
-						}
-					}
+		// check to see if the result was a constant
+		if (optimized instanceof ConstantExpression) {
+			constCount++;
+		}
+	}
 
-	if(constantCount === n) {
-		return new ConstantExpression(this.evaluateInternal({}));
-				}
+	// 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;
+	}
 
-	if(!this.isAssociativeAndCommutative) {
+	// Remaining optimizations are only for associative and commutative expressions.
+	if(!this.isAssociativeAndCommutative()) {
 		return this;
 	}
 
-	// Flatten and inline nested operations of the same type
-
-	var similar = partitionBy(function(x){ return x.getOpName() === this.getOpName();}, this.operands);
-
-	this.operands = similar.fail;
-	similar.pass.forEach(function(x){
-		this.operands.concat(x.operands);
-	});
-
-	// Partial constant folding
+	// 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);
+			}
+		}
+	}
 
-	var constantOperands = partitionBy(function(x) {return x instanceof ConstantExpression;}, this.operands);
+	// collapse all constant expressions (if any)
+	var constValue;
+	if (constExprs.length > 0) {
+		this.operands = constExprs;
+		var emptyVars2 = new Variables();
+		constValue = this.evaluateInternal(emptyVars2);
+	}
 
-	this.operands = constantOperands.pass;
-	this.operands = [new ConstantExpression(this.evaluateInternal({}))].concat(constantOperands.fail);
+	// 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){
-	for(var i = 0, l = this.operands.length; i < l; ++i)
+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
- **/
+ * @param expr the expression to add
+ */
 proto.addOperand = function addOperand(expr) {
 	this.operands.push(expr);
 };
 
-proto.serialize = function serialize() {
-	var ret = {}, subret = [];
-	for(var ii = 0; ii < this.operands.length; ii++) {
-		subret.push(this.operands[ii].serialize());
+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));
 	}
-	ret[this.getOpName()] = subret;
-	return ret;
+
+	var obj = {};
+	obj[this.getOpName()] = array;
+	return obj;
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return false;
 };
 
-proto.fixedArity = function(nargs) {
-	this.nargs = nargs;
+/**
+ * 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");
 };
 
-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.");
+/**
+ * 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;
 };

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

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

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

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

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var SecondExpression = module.exports = function SecondExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = SecondExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -39,4 +39,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$second", base.parse(SecondExpression));
+Expression.registerExpression("$second", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var SetDifferenceExpression = module.exports = function SetDifferenceExpression() {
-	this.nargs = 2;
 	base.call(this);
 }, klass = SetDifferenceExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -49,4 +49,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$setdifference", base.parse(SetDifferenceExpression));
+Expression.registerExpression("$setdifference", base.parse);

+ 2 - 9
lib/pipeline/expressions/SetEqualsExpression.js

@@ -9,15 +9,8 @@
  * @constructor
  **/
 var SetEqualsExpression = module.exports = function SetEqualsExpression() {
-	this.nargs = 2;
 	base.call(this);
-}, klass = SetEqualsExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetEqualsExpression, base = require("./NaryBaseExpressionT")(SetEqualsExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -42,4 +35,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$setequals", base.parse(SetEqualsExpression));
+Expression.registerExpression("$setequals", base.parse);

+ 2 - 9
lib/pipeline/expressions/SetIntersectionExpression.js

@@ -9,15 +9,8 @@
  * @constructor
  **/
 var SetIntersectionExpression = module.exports = function SetIntersectionExpression() {
-	this.nargs = 2;
 	base.call(this);
-}, klass = SetIntersectionExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetIntersectionExpression, base = require("./NaryBaseExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -74,4 +67,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$setIntersection", base.parse(SetIntersectionExpression));
+Expression.registerExpression("$setIntersection", base.parse);

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

@@ -9,11 +9,11 @@
  * @constructor
  **/
 var SetIsSubsetExpression = module.exports = function SetIsSubsetExpression() {
-	this.nargs = 2;
 	if (arguments.length !== 2) throw new Error("two args expected");
 	base.call(this);
 }, klass = SetIsSubsetExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -85,4 +85,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$setissubset", base.parse(SetIsSubsetExpression));
+Expression.registerExpression("$setissubset", base.parse);

+ 2 - 9
lib/pipeline/expressions/SetUnionExpression.js

@@ -9,15 +9,8 @@
  * @constructor
  **/
 var SetUnionExpression = module.exports = function SetUnionExpression() {
-	this.nargs = 2;
 	base.call(this);
-}, klass = SetUnionExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetUnionExpression, base = require("./NaryBaseExpressionT")(SetUnionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -50,4 +43,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$setUnion", base.parse(SetUnionExpression));
+Expression.registerExpression("$setUnion", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var SizeExpression = module.exports = function SizeExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = SizeExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -38,4 +38,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$size", base.parse(SizeExpression));
+Expression.registerExpression("$size", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var StrcasecmpExpression = module.exports = function StrcasecmpExpression() {
-	this.nargs = 2;
 	base.call(this);
 }, klass = StrcasecmpExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -43,4 +43,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$strcasecmp", base.parse(StrcasecmpExpression));
+Expression.registerExpression("$strcasecmp", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var SubstrExpression = module.exports = function SubstrExpression() {
-	this.nargs = 3;
 	base.call(this);
 }, klass = SubstrExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 3),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -46,4 +46,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$substr", base.parse(SubstrExpression));
+Expression.registerExpression("$substr", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var SubtractExpression = module.exports = function SubtractExpression(){
-	this.nargs = 2;
 	base.call(this);
 }, klass = SubtractExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -39,4 +39,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$subtract", base.parse(SubtractExpression));
+Expression.registerExpression("$subtract", base.parse);

+ 6 - 6
lib/pipeline/expressions/ToLowerExpression.js

@@ -1,6 +1,6 @@
 "use strict";
-	
-/** 
+
+/**
  * A $toLower pipeline expression.
  * @see evaluateInternal
  * @class ToLowerExpression
@@ -10,7 +10,7 @@
  **/
 var ToLowerExpression = module.exports = function ToLowerExpression(){
 	base.call(this);
-}, klass = ToLowerExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+}, klass = ToLowerExpression, base = require("./FixedArityExpressionT")(ToLowerExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -23,8 +23,8 @@ 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),
@@ -33,4 +33,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression(klass.opName, base.parse(klass));
+Expression.registerExpression(klass.opName, base.parse);

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

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

+ 4 - 1
lib/pipeline/expressions/VariadicExpressionT.js

@@ -13,7 +13,10 @@ 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}});
+	}, klass = VariadicExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
+	klass.parse = base.parse; 						// NOTE: Need to explicitly
+	klass.parseArguments = base.parseArguments;		// bubble static members in
+													// our inheritance chain
 	return VariadicExpression;
 };

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var WeekExpression = module.exports = function WeekExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = WeekExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -47,4 +47,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$week", base.parse(WeekExpression));
+Expression.registerExpression("$week", base.parse);

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

@@ -9,10 +9,10 @@
  * @constructor
  **/
 var YearExpression = module.exports = function YearExpression() {
-	this.nargs = 1;
 	base.call(this);
 }, klass = YearExpression,
-	base = require("./NaryExpression"),
+	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
+	base = FixedArityExpression,
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -40,4 +40,4 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 /** Register Expression */
-Expression.registerExpression("$year", base.parse(YearExpression));
+Expression.registerExpression("$year", base.parse);

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

@@ -19,7 +19,7 @@ module.exports = {
 	ModExpression: require("./ModExpression.js"),
 	MonthExpression: require("./MonthExpression.js"),
 	MultiplyExpression: require("./MultiplyExpression.js"),
-	NaryExpression: require("./NaryExpression.js"),
+	NaryBaseExpressionT: require("./NaryBaseExpressionT.js"),
 	NotExpression: require("./NotExpression.js"),
 	ObjectExpression: require("./ObjectExpression.js"),
 	OrExpression: require("./OrExpression.js"),

+ 87 - 0
test/lib/pipeline/DepsTracker_test.js

@@ -0,0 +1,87 @@
+"use strict";
+var assert = require("assert"),
+	DepsTracker = require("../../../lib/pipeline/DepsTracker");
+
+// 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.DepsTracker = {
+
+	"#toProjection()": {
+
+		"should be able to convert dependencies to a projection": function(){
+			var deps = new DepsTracker(),
+				expected = {_id:0,a:1,b:1};
+			deps.fields = {a:1,b:1};
+			assert.deepEqual(expected, deps.toProjection());
+		},
+
+		"should be able to convert dependencies with subfields to a projection": function(){
+			var deps = new DepsTracker(),
+				expected = {_id:0,a:1};
+			deps.fields = {a:1,"a.b":1};
+			assert.deepEqual(expected, deps.toProjection());
+		},
+
+		"should be able to convert dependencies with _id to a projection": function(){
+			var deps = new DepsTracker(),
+				expected = {a:1,b:1,_id:1};
+			deps.fields = {_id:1,a:1,b:1};
+			assert.deepEqual(expected, deps.toProjection());
+		},
+
+		"should be able to convert dependencies with id and subfields to a projection": function(){
+			var deps = new DepsTracker(),
+				expected = {_id:1,b:1};
+			deps.fields = {"_id.a":1,b:1};
+			assert.deepEqual(expected, deps.toProjection());
+		},
+
+		"should return empty object if needWholeDocument is true": function() {
+			var deps = new DepsTracker(),
+				expected = {};
+			deps.needWholeDocument = true;
+			assert.deepEqual(expected, deps.toProjection());
+		},
+
+		"should return $noFieldsNeeded if there are no dependencies": function() {
+			var deps = new DepsTracker(),
+				expected = {_id:0,$noFieldsNeeded:1};
+			assert.deepEqual(expected, deps.toProjection());
+		},
+
+	},
+
+	"#toParsedDeps()": {
+
+		"should not parse if needWholeDocument is true": function() {
+			var deps = new DepsTracker(),
+				expected; // undefined;
+			deps.needWholeDocument = true;
+			assert.strictEqual(expected, deps.toParsedDeps());
+		},
+
+		"should not parse if needTextScore is true": function() {
+			var deps = new DepsTracker(),
+				expected; // undefined;
+			deps.needTextScore = true;
+			assert.strictEqual(expected, deps.toParsedDeps());
+		},
+
+		"should be able to parse dependencies": function() {
+			var deps = new DepsTracker(),
+				expected = {_fields:{a:true,b:true}};
+			deps.fields = {a:1,b:1};
+			assert.deepEqual(expected, deps.toParsedDeps());
+		},
+
+		"should be able to parse dependencies with subfields": function() {
+			var deps = new DepsTracker(),
+				expected = {_fields:{a:true}};
+			deps.fields = {a:1,"a.b":1};
+			assert.deepEqual(expected, deps.toParsedDeps());
+		},
+
+	},
+
+};

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

@@ -1,91 +1,148 @@
 "use strict";
 var assert = require("assert"),
-	Document = require("../../../lib/pipeline/Document");
+	Document = require("../../../lib/pipeline/Document"),
+	FieldPath = require("../../../lib/pipeline/FieldPath");
 
 // Mocha one-liner to make these tests self-hosted
 if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
 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"}');
+	//SKIPPED: Create -- ours is a static class so we have no constructor
+
+	//SKIPPED: CreateFromBsonObj -- no ctor again, would just use JSON.parse
+
+	//SKIPPED: AddField - no need because we use:  obj[key] = val
+
+	//SKIPPED: GetValue - no need because we use:  val = obj[key]
+
+	//SKIPPED: SetField - no need because we usually use:  obj[key] = val  though setNestedField *does* is implemented now
+
+	".compare()": {
+
+		"should work": function testCompare() {
+            assertComparison(0, {}, {});
+            assertComparison(0, {a:1}, {a:1});
+            assertComparison(-1, {}, {a:1});
+            assertComparison(-1, {a:1}, {c:1});
+            assertComparison(0, {a:1,r:2}, {a:1,r:2});
+            assertComparison(-1, {a:1}, {a:1,r:2});
+            assertComparison(0, {a:2}, {a:2});
+            assertComparison(-1, {a:1}, {a:2});
+            assertComparison(-1, {a:1,b:1}, {a:1,b:2});
+            // numbers sort before strings
+            assertComparison(-1, {a:1}, {a:"foo"});
+			// helpers for the above
+			function cmp(a, b) {
+				var result = Document.compare(a, b);
+				return result < 0 ? -1 : // sign
+					result > 0 ? 1 :
+					0;
+			}
+			function assertComparison(expectedResult, a, b) {
+				assert.strictEqual(expectedResult, cmp(a, b));
+				assert.strictEqual(-expectedResult, cmp(b, a));
+				if (expectedResult === 0) {
+					var hash = JSON.stringify; // approximating real hash
+					assert.strictEqual(hash(a), hash(b));
+				}
+			}
 		},
 
-		"convert from Json": function fromJsonWithMetaData() {
-			var aDocumentString = '{\"prop1\":0,\"metadata\":1}',
-				jsonDocument = {"prop1":0,"metadata":1},
-				result = Document.fromJsonWithMetaData(aDocumentString);
-			assert.deepEqual(result, jsonDocument);
+		"should work for a null": function testCompareNamedNull(){
+			var obj1 = {z:null},
+				obj2 = {a:1};
+            //// Comparsion with type precedence.
+			// assert(obj1.woCompare(obj2) < 0); //NOTE: probably will not need this
+            // Comparison with field name precedence.
+			assert(Document.compare(obj1, obj2) > 0);
 		},
 
-	},
-
-	"compare 2 Documents": {
-
-		"should return 0 if Documents are identical": function compareDocumentsIdentical() {
-			var lDocument = {"prop1": 0},
-				rDocument = {"prop1": 0},
+		"should return 0 if Documents are identical": function() {
+			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},
+		"should return -1 if left Document is shorter": function() {
+			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},
+		"should return 1 if right Document is shorter": function() {
+			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},
+		"should return nameCmp result -1 if left Document field value is less": function() {
+			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},
+		"should return nameCmp result 1 if right Document field value is less": function() {
+			var lDocument = {prop1: 1},
+				rDocument = {prop1: 0},
 				result = Document.compare(lDocument, rDocument);
 			assert.equal(result, 1);
 		},
 
 	},
 
-	"clone a Document": {
+	".clone()": {
+
+		"should shallow clone a single field document": function testClone() {
+			var doc = {a:{b:1}},
+				clone = doc;
+
+			//NOTE: silly since we use static helpers but here for consistency
+			// Check equality
+			assert.strictEqual(clone, doc);
+			// Check pointer equality of sub document
+			assert.strictEqual(clone.a, doc.a);
+
+			// Change field in clone and ensure the original document's field is unchanged.
+			clone = Document.clone(doc);
+			clone.a = 2;
+			assert.strictEqual(Document.getNestedField(doc, new FieldPath("a.b")), 1);
+
+			// setNestedField and ensure the original document is unchanged.
+			clone = Document.cloneDeep(doc);
+			assert.strictEqual(Document.getNestedField(doc, "a.b"), 1);
 
-		"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);
+			Document.setNestedField(clone, "a.b", 2);
+
+			assert.strictEqual(Document.getNestedField(doc, "a.b"), 1);
+			assert.strictEqual(Document.getNestedField(clone, "a.b"), 2);
+			assert.deepEqual(doc, {a:{b:1}});
+			assert.deepEqual(clone, {a:{b:2}});
+		},
+
+		"should shallow clone a multi field document": function testCloneMultipleFields() {
+			var doc = {a:1,b:['ra',4],c:{z:1},d:'lal'},
+				clone = Document.clone(doc);
+			assert.deepEqual(doc, clone);
 		},
 
-		"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");
+	},
+
+	//SKIPPED: FieldIteratorEmpty
+
+	//SKIPPED: FieldIteratorSingle
+
+	//SKIPPED: FieldIteratorMultiple
+
+	".toJson()": {
+
+		"should convert to JSON Object": function() {
+			var doc = {prop1:0};
+			assert.deepEqual(Document.toJson(doc), {prop1:0});
 		},
 
 	},
@@ -93,14 +150,14 @@ exports.Document = {
 	"serialize and deserialize for sorter": {
 
 		"should return a string": function serializeDocument() {
-			var doc = {"prop1":1},
+			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},
+				doc = {prop1:1},
 				res = Document.deserializeForSorter(str);
 			assert.deepEqual(res, doc);
 		},

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

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

+ 74 - 0
test/lib/pipeline/ParsedDeps.js

@@ -0,0 +1,74 @@
+"use strict";
+var assert = require("assert"),
+	ParsedDeps = require("../../../lib/pipeline/ParsedDeps");
+
+module.exports = {
+	"ParsedDeps": {
+		"#extractFields": {
+			"should be able to convert a document to its projected form": function() {
+				var deps = {'a': true, 'b': true},
+					doc = {a:23, b:64, c:92},
+					parse = new ParsedDeps(deps);
+
+				var proj = parse.extractFields(doc);
+				assert.deepEqual({a:23,b:64}, proj);
+			}
+		},
+		"#_documentHelper": {
+			"should skip fields that are not needed": function() {
+				var json = {'foo':'bar'},
+					neededFields = {},
+					parse = new ParsedDeps(),
+					expected = {};
+				assert.deepEqual(expected, parse._documentHelper(json, neededFields));
+			},
+			"should return values that are booleans": function() {
+				var json = {'foo':'bar'},
+					neededFields = {'foo':true},
+					parse = new ParsedDeps(),
+					expected = {'foo':'bar'};
+				assert.deepEqual(expected, parse._documentHelper(json, neededFields));
+			},
+			"should call _arrayHelper on values that are arrays": function() {
+				var json = {'foo':[{'bar':'baz'}]},
+					neededFields = {'foo':true},
+					parse = new ParsedDeps(),
+					expected = {'foo':true};
+				// TODO: mock out _arrayHelper to return true
+				parse._arrayHelper = function() {
+					return true;
+				};
+				assert.deepEqual(expected, parse._documentHelper(json, neededFields));
+			},
+			"should recurse on values that are objects": function() {
+				var json = {'foo':{'bar':'baz'}},
+					neededFields = {'foo':true},
+					parse = new ParsedDeps(),
+					expected = {'foo':{'bar':'baz'}};
+				assert.deepEqual(expected, parse._documentHelper(json, neededFields));
+			}
+		},
+		"#_arrayHelper": {
+			"should call _documentHelper on values that are objects": function() {
+				var array = [{'foo':'bar'}],
+					neededFields = {'foo':true},
+					parse = new ParsedDeps(),
+					expected = [true];
+				// TODO: mock out _documentHelper to return true
+				parse._documentHelper = function() {
+					return true;
+				};
+				assert.deepEqual(expected, parse._arrayHelper(array, neededFields));
+			},
+			"should recurse on values that are arrays": function() {
+				var array = [[{'foo':'bar'}]],
+					neededFields = {'foo':true},
+					parse = new ParsedDeps(),
+					expected = [[{'foo':'bar'}]];
+				assert.deepEqual(expected, parse._arrayHelper(array, neededFields));
+			}
+		}
+	}
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run();

+ 0 - 42
test/lib/pipeline/documentSources/DocumentSource.js

@@ -7,50 +7,8 @@ module.exports = {
 
 	"DocumentSource": {
 
-		"#depsToProjection()": {
-			"should be able to convert dependencies to a projection": function(){
-				var array = {'a':1,'b':1},
-					expected = '{"_id":0,"a":1,"b":1}',
-					proj = DocumentSource.depsToProjection(array);
-
-				assert.equal(expected, JSON.stringify(proj));
-			},
-			"should be able to convert dependencies with subfields to a projection": function(){
-				var array = {'a':1,'a.b':1},
-					expected = '{"_id":0,"a":1}',
-					proj = DocumentSource.depsToProjection(array);
-
-				assert.equal(expected, JSON.stringify(proj));
-			},
-			"should be able to convert dependencies with _id to a projection": function(){
-				var array = {"_id":1,'a':1,'b':1},
-					expected = '{"a":1,"b":1,"_id":1}',
-					proj = DocumentSource.depsToProjection(array);
-
-				assert.equal(expected, JSON.stringify(proj));
-			},
-			"should be able to convert dependencies with id and subfields to a projection": function(){
-				var array = {'_id.a':1,'b':1},
-					expected = '{"_id":1,"b":1}',
-					proj = DocumentSource.depsToProjection(array);
-
-				assert.equal(expected, JSON.stringify(proj));
-			},
-		},
-
-		"#documentFromJsonWithDeps()": {
-			"should be able to convert a document to its projected form": function() {
-				var deps = {'a': true, 'b': true},
-					doc = {a:23, b:64, c:92};
-
-				var proj = DocumentSource.documentFromJsonWithDeps(doc, deps);
-				assert.deepEqual({a:23,b:64}, proj);
-			}
-		}
-
 	}
 
 };
 
 if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run();
-

+ 79 - 10
test/lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -1,10 +1,20 @@
 "use strict";
 var assert = require("assert"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	AnyElementTrueExpression = require("../../../../lib/pipeline/expressions/AnyElementTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
 var anyElementTrueExpression = new AnyElementTrueExpression();
 
+function errMsg(expr, args, tree, expected, result) {
+	return 	"for expression " + expr +
+			" with argument " + args +
+			" full tree: " + JSON.stringify(tree) +
+			" expected: " + expected +
+			" result: " + result;
+}
+
 module.exports = {
 
 	"AnyElementTrueExpression": {
@@ -27,25 +37,84 @@ module.exports = {
 
 		},
 
-		"#evaluateInternal()": {
+		"integration": {
+
+			"JustFalse": function JustFalse(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[false]],
+					expr = Expression.parseExpression("$anyElementTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
+			},
+
+			"JustTrue": function JustTrue(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[true]],
+					expr = Expression.parseExpression("$anyElementTrue", input),
+					result = expr.evaluate({}),
+					expected = true,
+					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
+			},
 
-			"should return error if parameter is not an array": function testEmpty(){
-				assert.throws(function(){
-					anyElementTrueExpression.evaluateInternal("TEST");});
+			"OneTrueOneFalse": function OneTrueOneFalse(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[true, false]],
+					expr = Expression.parseExpression("$anyElementTrue", input),
+					result = expr.evaluate({}),
+					expected = true,
+					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"should return true if only true was given a; {true}": function testEmpty(){
-				assert.equal(anyElementTrueExpression.evaluateInternal({$anyElementTrue:[1,2,3,4]}), false);
+			"Empty": function Empty(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[]],
+					expr = Expression.parseExpression("$anyElementTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"should return false if no element is true": function testEmpty(){
-				assert.equal(anyElementTrueExpression.evaluateInternal({$anyElementTrue:[1,2,3,4]}), false);
+			"TrueViaInt": function TrueViaInt(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[1]],
+					expr = Expression.parseExpression("$anyElementTrue", input),
+					result = expr.evaluate({}),
+					expected = true,
+					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
-			"should return true if any element is true": function testEmpty(){
-				assert.equal(anyElementTrueExpression.evaluateInternal({$anyElementTrue:[1,true,2,3,4]}), true);
+			"FalseViaInt": function FalseViaInt(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [[0]],
+					expr = Expression.parseExpression("$anyElementTrue", input),
+					result = expr.evaluate({}),
+					expected = false,
+					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
+				assert.equal(result, expected, msg);
 			},
 
+			"Null": function FalseViaInt(){
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					input = [null],
+					expr = Expression.parseExpression("$anyElementTrue", input);
+				assert.throws(function() {
+					var result = expr.evaluate({});
+				});
+			}
+
 		}
 
 	}

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

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

+ 22 - 29
test/lib/pipeline/expressions/ConstantExpression_test.js

@@ -2,31 +2,31 @@
 var assert = require("assert"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState");
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker");
 
 // 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));
+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.ConstantExpression = {
 
 	".constructor()": {
 
-		"should accept one argument": function() {
+		"should accept one argument": function () {
 			new ConstantExpression(5);
 		},
 
-		"should not accept 0 arguments": function() {
-			assert.throws(function() {
-				 new ConstantExpression();
+		"should not accept 0 arguments": function () {
+			assert.throws(function () {
+				new ConstantExpression();
 			});
 		},
 
-		"should not accept 2 arguments": function() {
-			assert.throws(function() {
+		"should not accept 2 arguments": function () {
+			assert.throws(function () {
 				new ConstantExpression(1, 2);
 			});
-		},
-
+		}
 	},
 
 	".parse()": {
@@ -36,18 +36,15 @@ exports.ConstantExpression = {
 				vps = new VariablesParseState(idGenerator),
 				expression = ConstantExpression.parse("foo", vps);
 			assert.deepEqual("foo", expression.evaluate({}));
-		},
-
+		}
 	},
 
 	".create()": {
 
 		"should create an expression": function testCreate() {
 			assert(ConstantExpression.create() instanceof ConstantExpression);
-		},
-
+		}
 		//SKIPPED: testCreateFronBsonElement
-
 	},
 
 	"#optimize()": {
@@ -55,21 +52,19 @@ exports.ConstantExpression = {
 		"should not optimize anything": function testOptimize() {
 			var expr = new ConstantExpression(5);
 			assert.strictEqual(expr, expr.optimize());
-		},
-
+		}
 	},
 
 	"#addDependencies()": {
 
 		"should return nothing": function testDependencies() {
 			var expr = ConstantExpression.create(5),
-				deps = {}; //TODO: new DepsTracker
+				deps = new DepsTracker();
 			expr.addDependencies(deps);
-			assert.strictEqual(deps.fields.length, 0);
+			assert.deepEqual(deps.fields, {});
 			assert.strictEqual(deps.needWholeDocument, false);
 			assert.strictEqual(deps.needTextScore, false);
-		},
-
+		}
 	},
 
 	//TODO: AddToBsonObj
@@ -78,30 +73,28 @@ exports.ConstantExpression = {
 
 	"#evaluate()": {
 
-		"should do what comes natural with an int": function() {
+		"should do what comes natural with an int": function () {
 			var c = 567;
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
 		},
 
-		"should do what comes natural with a float": function() {
+		"should do what comes natural with a float": function () {
 			var c = 567.123;
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
 		},
 
-		"should do what comes natural with a String": function() {
+		"should do what comes natural with a String": function () {
 			var c = "Quoth the raven";
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
 		},
 
-		"should do what comes natural with a date": function() {
+		"should do what comes natural with a date": function () {
 			var c = new Date();
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
-		},
-
-	},
-
+		}
+	}
 };

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

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

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

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

+ 0 - 151
test/lib/pipeline/expressions/NaryExpression.js

@@ -1,151 +0,0 @@
-"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");
-
-
-// A dummy child of NaryExpression used for testing
-var TestableExpression = (function(){
-	// CONSTRUCTOR
-	var klass = function TestableExpression(operands, haveFactory){
-		base.call(this);
-		if (operands) {
-			var self = this;
-			operands.forEach(function(operand) {
-				self.addOperand(operand);
-			});
-		}
-		this.haveFactory = !!haveFactory;
-	}, base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-	// PROTOTYPE MEMBERS
-	proto.evaluateInternal = function evaluateInternal(vps) {
-		// 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(vps);
-		});
-	};
-
-	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative(){
-		return this.isAssociativeAndCommutative;
-	};
-
-	proto.getOpName = function getOpName() {
-		return "$testable";
-	};
-
-	klass.createFromOperands = function(operands) {
-		var vps = new VariablesParseState(new VariablesIdGenerator()),
-			testable = new TestableExpression();
-		operands.forEach(function(x) {
-			testable.addOperand(Expression.parseOperand(x, vps));
-		});
-		return testable;
-	};
-
-	return klass;
-})();
-
-
-module.exports = {
-
-	"NaryExpression": {
-
-		"constructor()": {
-
-		},
-
-		"#optimize()": {
-
-			"should suboptimize": function() {
-				var testable = TestableExpression.createFromOperands([{"$and": []}, "$abc"], true);
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$testable: [true,"$abc"]});
-			},
-			"should fold constants": function() {
-				var testable = TestableExpression.createFromOperands([1,2], true);
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$const: [1,2]});
-			},
-
-			"should place constants at the end of operands array": function() {
-				var testable = TestableExpression.createFromOperands([55,65, "$path"], true);
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$testable:["$path", [55,66]]});
-			},
-
-			"should flatten two layers" : function() {
-				var testable = TestableExpression.createFromOperands([55, "$path", {$add: [5,6,"$q"]}], true);
-				testable.addOperand(TestableExpression.createFromOperands([99,100,"$another_path"], true));
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$testable: ["$path", {$add: [5,6,"$q"]}, "$another_path", [55,66,[99,100]]]});
-			},
-
-			"should flatten three layers": function(){
-				var bottom = TestableExpression.createFromOperands([5,6,"$c"], true),
-					middle = TestableExpression.createFromOperands([3,4,"$b"], true).addOperand(bottom),
-					top = TestableExpression.createFromOperands([1,2,"$a"], true);
-				var testable = top.optimize();
-				assert.deepEqual(testable.serialize(), {$testable: ["$a", "$b", "$c", [1,2,[3,4,[5,6]]]]});
-			}
-
-		},
-
-		"#addOperand() should be able to add operands to expressions": function testAddOperand(){
-			var foo = new TestableExpression([new ConstantExpression(9)]).serialize();
-			var bar = new TestableExpression([new ConstantExpression(9)]).serialize();
-			var baz = {"$testable":[{"$const":9}]};
-
-			assert.deepEqual(foo,bar);
-			assert.deepEqual(foo, baz);
-			assert.deepEqual(baz,foo);
-			assert.deepEqual(new TestableExpression([new ConstantExpression(9)]).serialize(), {"$testable":[{"$const":9}]});
-			assert.deepEqual(new TestableExpression([new FieldPathExpression("ab.c")]).serialize(), {$testable:["$ab.c"]});
-		},
-
-
-		"#serialize() should convert an object to json": function(){
-			var testable = new TestableExpression();
-			testable.addOperand(new ConstantExpression(5));
-			assert.deepEqual({foo: testable.serialize()}, {foo:{$testable:[{$const: 5}]}});
-		},
-
-
-		//the following test case is eagerly awaiting ObjectExpression
-		"#addDependencies()": function testDependencies(){
-			var testableExpr = new TestableExpression();
-			var deps = {};
-			// no arguments
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {});
-
-			// add a constant argument
-			testableExpr.addOperand(new ConstantExpression(1));
-
-			deps = {};
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {});
-
-			// add a field path argument
-			testableExpr.addOperand(new FieldPathExpression("ab.c"));
-			deps = {};
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {"ab.c":1});
-
-			// add an object expression
-			testableExpr.addOperand(Expression.parseObject({a:"$x",q:"$r"}, new Expression.ObjectCtx({isDocumentOk:1})));
-			deps = {};
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {"ab.c":1, "x":1, "r":1});
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

@@ -0,0 +1,241 @@
+"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.
+		var values = [];
+		for (var i = 0, l = this.operands.length; i < l; i++) {
+			values.push(this.operands[i].evaluateInternal(vars));
+		}
+		return values;
+	};
+
+	proto.getOpName = function getOpName() {
+		return "$testable";
+	};
+
+	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative(){
+		return this._isAssociativeAndCommutative;
+	};
+
+	klass.create = function create(associativeAndCommutative) {
+		return new Testable(Boolean(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) {
+		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(ConstantExpression.create(9));
+		testable.assertContents([9]);
+		testable.addOperand(FieldPathExpression.create("ab.c"));
+		testable.assertContents([9, "$ab.c"]);
+	},
+
+	/** Dependencies of the expression. */
+	"Dependencies": function testDependencies() {
+		var testable = Testable.create();
+
+		var assertDependencies = function assertDependencies(expectedDeps, expr) {
+			var deps = {}, //TODO: 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.strictEqual(testable, testable.optimize());
+		},
+
+		/** A single (instead of multiple) constant prevents optimization.  SERVER-6192 */
+		"SingleConstant": function testSingleConstant() {
+			var testable = Testable.createFromOperands([55, "$path"], true);
+			assert.strictEqual(testable, testable.optimize());
+		},
+
+		/** Factory optimization is not used without a factory. */
+		"NoFactory": function testNoFactory() {
+			var testable = Testable.createFromOperands([55, 66, "$path"], false);
+			assert.strictEqual(testable, testable.optimize());
+		},
+
+	},
+
+	/** 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"], true),
+			optimized = testable.optimize();
+		assert.deepEqual(utils.constify({$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)
+		);
+	},
+
+};

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

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

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

@@ -1,36 +0,0 @@
-"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);
+		},
+
+	},
+
+};