Browse Source

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

Kyle P Davis 11 năm trước cách đây
mục cha
commit
907cef7ca3
100 tập tin đã thay đổi với 6698 bổ sung4457 xóa
  1. 107 0
      lib/pipeline/DepsTracker.js
  2. 110 33
      lib/pipeline/Document.js
  3. 30 27
      lib/pipeline/FieldPath.js
  4. 85 0
      lib/pipeline/ParsedDeps.js
  5. 169 58
      lib/pipeline/Value.js
  6. 0 114
      lib/pipeline/documentSources/DocumentSource.js
  7. 5 4
      lib/pipeline/expressions/AddExpression.js
  8. 14 30
      lib/pipeline/expressions/AllElementsTrueExpression.js
  9. 8 11
      lib/pipeline/expressions/AndExpression.js
  10. 14 22
      lib/pipeline/expressions/AnyElementTrueExpression.js
  11. 20 24
      lib/pipeline/expressions/CoerceToBoolExpression.js
  12. 83 78
      lib/pipeline/expressions/CompareExpression.js
  13. 23 24
      lib/pipeline/expressions/ConcatExpression.js
  14. 33 53
      lib/pipeline/expressions/CondExpression.js
  15. 34 34
      lib/pipeline/expressions/ConstantExpression.js
  16. 12 24
      lib/pipeline/expressions/DayOfMonthExpression.js
  17. 13 18
      lib/pipeline/expressions/DayOfWeekExpression.js
  18. 17 26
      lib/pipeline/expressions/DayOfYearExpression.js
  19. 22 17
      lib/pipeline/expressions/DivideExpression.js
  20. 190 152
      lib/pipeline/expressions/Expression.js
  21. 115 161
      lib/pipeline/expressions/FieldPathExpression.js
  22. 36 0
      lib/pipeline/expressions/FixedArityExpressionT.js
  23. 13 20
      lib/pipeline/expressions/HourExpression.js
  24. 10 25
      lib/pipeline/expressions/IfNullExpression.js
  25. 29 15
      lib/pipeline/expressions/LetExpression.js
  26. 62 59
      lib/pipeline/expressions/MapExpression.js
  27. 12 26
      lib/pipeline/expressions/MillisecondExpression.js
  28. 12 24
      lib/pipeline/expressions/MinuteExpression.js
  29. 25 36
      lib/pipeline/expressions/ModExpression.js
  30. 12 24
      lib/pipeline/expressions/MonthExpression.js
  31. 30 22
      lib/pipeline/expressions/MultiplyExpression.js
  32. 30 0
      lib/pipeline/expressions/NaryBaseExpressionT.js
  33. 107 89
      lib/pipeline/expressions/NaryExpression.js
  34. 11 23
      lib/pipeline/expressions/NotExpression.js
  35. 170 165
      lib/pipeline/expressions/ObjectExpression.js
  36. 5 4
      lib/pipeline/expressions/OrExpression.js
  37. 12 26
      lib/pipeline/expressions/SecondExpression.js
  38. 33 33
      lib/pipeline/expressions/SetDifferenceExpression.js
  39. 33 27
      lib/pipeline/expressions/SetEqualsExpression.js
  40. 43 29
      lib/pipeline/expressions/SetIntersectionExpression.js
  41. 69 61
      lib/pipeline/expressions/SetIsSubsetExpression.js
  42. 25 34
      lib/pipeline/expressions/SetUnionExpression.js
  43. 10 21
      lib/pipeline/expressions/SizeExpression.js
  44. 22 28
      lib/pipeline/expressions/StrcasecmpExpression.js
  45. 24 31
      lib/pipeline/expressions/SubstrExpression.js
  46. 36 25
      lib/pipeline/expressions/SubtractExpression.js
  47. 11 24
      lib/pipeline/expressions/ToLowerExpression.js
  48. 10 23
      lib/pipeline/expressions/ToUpperExpression.js
  49. 88 0
      lib/pipeline/expressions/ValueSet.js
  50. 94 74
      lib/pipeline/expressions/Variables.js
  51. 8 8
      lib/pipeline/expressions/VariablesIdGenerator.js
  52. 26 23
      lib/pipeline/expressions/VariablesParseState.js
  53. 22 0
      lib/pipeline/expressions/VariadicExpressionT.js
  54. 14 31
      lib/pipeline/expressions/WeekExpression.js
  55. 9 24
      lib/pipeline/expressions/YearExpression.js
  56. 1 1
      lib/pipeline/expressions/index.js
  57. 87 0
      test/lib/pipeline/DepsTracker_test.js
  58. 167 0
      test/lib/pipeline/Document.js
  59. 130 143
      test/lib/pipeline/FieldPath.js
  60. 74 0
      test/lib/pipeline/ParsedDeps.js
  61. 327 0
      test/lib/pipeline/Value.js
  62. 0 42
      test/lib/pipeline/documentSources/DocumentSource.js
  63. 6 1
      test/lib/pipeline/expressions/AddExpression_test.js
  64. 148 49
      test/lib/pipeline/expressions/AllElementsTrueExpression.js
  65. 0 135
      test/lib/pipeline/expressions/AndExpression.js
  66. 213 0
      test/lib/pipeline/expressions/AndExpression_test.js
  67. 146 31
      test/lib/pipeline/expressions/AnyElementTrueExpression.js
  68. 56 34
      test/lib/pipeline/expressions/CoerceToBoolExpression.js
  69. 409 308
      test/lib/pipeline/expressions/CompareExpression.js
  70. 0 61
      test/lib/pipeline/expressions/ConcatExpression.js
  71. 91 0
      test/lib/pipeline/expressions/ConcatExpression_test.js
  72. 0 72
      test/lib/pipeline/expressions/CondExpression.js
  73. 117 0
      test/lib/pipeline/expressions/CondExpression_test.js
  74. 0 55
      test/lib/pipeline/expressions/ConstantExpression.js
  75. 100 0
      test/lib/pipeline/expressions/ConstantExpression_test.js
  76. 29 59
      test/lib/pipeline/expressions/DayOfMonthExpression.js
  77. 23 28
      test/lib/pipeline/expressions/DayOfWeekExpression.js
  78. 23 28
      test/lib/pipeline/expressions/DayOfYearExpression.js
  79. 47 0
      test/lib/pipeline/expressions/DivideExpression_test.js
  80. 147 146
      test/lib/pipeline/expressions/FieldPathExpression.js
  81. 23 28
      test/lib/pipeline/expressions/HourExpression.js
  82. 0 58
      test/lib/pipeline/expressions/IfNullExpression.js
  83. 59 0
      test/lib/pipeline/expressions/IfNullExpression_test.js
  84. 115 0
      test/lib/pipeline/expressions/MapExpression_test.js
  85. 28 43
      test/lib/pipeline/expressions/MillisecondExpression.js
  86. 23 28
      test/lib/pipeline/expressions/MinuteExpression.js
  87. 62 35
      test/lib/pipeline/expressions/ModExpression.js
  88. 23 28
      test/lib/pipeline/expressions/MonthExpression.js
  89. 0 45
      test/lib/pipeline/expressions/MultiplyExpression.js
  90. 112 0
      test/lib/pipeline/expressions/MultiplyExpression_test.js
  91. 0 151
      test/lib/pipeline/expressions/NaryExpression.js
  92. 241 0
      test/lib/pipeline/expressions/NaryExpression_test.js
  93. 0 53
      test/lib/pipeline/expressions/NotExpression.js
  94. 47 0
      test/lib/pipeline/expressions/NotExpression_test.js
  95. 637 631
      test/lib/pipeline/expressions/ObjectExpression.js
  96. 6 0
      test/lib/pipeline/expressions/OrExpression_test.js
  97. 23 34
      test/lib/pipeline/expressions/SecondExpression.js
  98. 321 83
      test/lib/pipeline/expressions/SetDifferenceExpression.js
  99. 321 83
      test/lib/pipeline/expressions/SetEqualsExpression.js
  100. 59 0
      test/lib/pipeline/expressions/SetExpectedResultBase.js

+ 107 - 0
lib/pipeline/DepsTracker.js

@@ -0,0 +1,107 @@
+"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"),
+	Document = require("./Document");
+
+/**
+ * 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 needId = false,
+		last = "";
+	Object.keys(this.fields).sort().forEach(function(it) {
+		if (it.indexOf("_id") === 0 && (it.length === 3 || it[3] === ".")) {
+			// _id and subfields are handled specially due in part to SERVER-7502
+			needId = true;
+			return;
+		}
+
+		if (last !== "" && it.indexOf(last) === 0) {
+			// we are including a parent of *it so we don't need to include this
+			// field explicitly. In fact, due to SERVER-6527 if we included this
+			// field, the parent wouldn't be fully included. This logic relies
+			// 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) // we are explicit either way
+		proj._id = 1;
+	else
+		proj._id = 0;
+
+	return proj;
+};
+
+// ParsedDeps::_fields is a simple recursive look-up table. For each field:
+//      If the value has type==Bool, the whole field is needed
+//      If the value has type==Object, the fields in the subobject are needed
+//      All other fields should be missing which means not needed
+/**
+ * Takes a depsTracker and builds a simple recursive lookup table out of it.
+ * @method toParsedDeps
+ * @return {ParsedDeps}
+ */
+proto.toParsedDeps = function toParsedDeps() {
+	var obj = {};
+
+	if (this.needWholeDocument || this.needTextScore) {
+		// can't use ParsedDeps in this case
+		return undefined; // TODO: is this equivalent to boost::none ?
+	}
+
+	var last = "";
+	Object.keys(this.fields).sort().forEach(function (it) {
+		if (last !== "" && it.indexOf(last) === 0) {
+			// we are including a parent of *it so we don't need to include this
+			// field explicitly. In fact, due to SERVER-6527 if we included this
+			// field, the parent wouldn't be fully included. This logic relies
+			// on on set iterators going in lexicographic order so that a string
+			// is always directly before of all fields it prefixes.
+			return;
+		}
+
+		last = it + ".";
+		Document.setNestedField(obj, it, true);
+	});
+
+	return new ParsedDeps(obj);
+};

+ 110 - 33
lib/pipeline/Document.js

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

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

+ 169 - 58
lib/pipeline/Value.js

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

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

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

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

+ 14 - 30
lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -6,44 +6,28 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var AllElementsTrueExpression = module.exports = function AllElementsTrueExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-},
-	klass = AllElementsTrueExpression,
-	NaryExpression = require("./NaryExpression"),
-	base = NaryExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = AllElementsTrueExpression, base = require("./FixedArityExpressionT")(AllElementsTrueExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$allElementsTrue";
-};
-
-/**
- * Takes an array of one or more numbers and returns true if all.
- * @method @evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var value = evaluateInternal(vars);
-	if (!vars instanceof Array) throw new Error("$allElementsTrue requires an array");
-
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var checkValue = this.operands[i].evaluateInternal(vars);
-		if (!checkValue.coerceToBool()) return false;
+	var arr = this.operands[0].evaluateInternal(vars);
+	if (!(arr instanceof Array)) throw new Error(this.getOpName() + "'s argument must be an array, but is " + Value.getType(arr) + "; uassert code 17040");
+	for (var i = 0, l = arr.length; i < l; ++i) {
+		if (!Value.coerceToBool(arr[i])) {
+			return false;
+		}
 	}
 	return true;
 };
 
-/** Register Expression */
-Expression.registerExpression("$allElementsTrue", base.parse(AllElementsTrueExpression));
+Expression.registerExpression("$allElementsTrue", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$allElementsTrue";
+};

+ 8 - 11
lib/pipeline/expressions/AndExpression.js

@@ -14,13 +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("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = AndExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -29,8 +23,9 @@ var Value = require("../Value"),
 	Expression = require("./Expression");
 
 // PROTOTYPE MEMBERS
+klass.opName = "$and";
 proto.getOpName = function getOpName() {
-	return "$and";
+	return klass.opName;
 };
 
 /**
@@ -40,11 +35,13 @@ proto.getOpName = function getOpName() {
 proto.evaluateInternal = function evaluateInternal(vars) {
 	for (var i = 0, n = this.operands.length; i < n; ++i) {
 		var value = this.operands[i].evaluateInternal(vars);
-		if (!Value.coerceToBool()) return false;
+		if (!Value.coerceToBool(value)) return false;
 	}
 	return true;
 };
 
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() { return true; };
+
 proto.optimize = function optimize() {
 	var expr = base.prototype.optimize.call(this); //optimize the conjunction as much as possible
 
@@ -60,7 +57,7 @@ proto.optimize = function optimize() {
 	if (!(lastExpr instanceof ConstantExpression)) return expr;
 
 	// Evaluate and coerce the last argument to a boolean.  If it's false, then we can replace this entire expression.
-	var last = Value.coerceToBool(lastExpr.evaluate());
+	var last = Value.coerceToBool(lastExpr.evaluateInternal());
 	if (!last) return new ConstantExpression(false);
 
 	// If we got here, the final operand was true, so we don't need it anymore.
@@ -75,6 +72,6 @@ proto.optimize = function optimize() {
 };
 
 /** Register Expression */
-Expression.registerExpression("$and", base.parse(AndExpression));
+Expression.registerExpression(klass.opName, base.parse);
 
 //TODO: proto.toMatcherBson

+ 14 - 22
lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -6,36 +6,28 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression(){
-	this.nargs = (1);
+ */
+var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = AnyElementTrueExpression, NaryExpression = require("./NaryExpression"), base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = AnyElementTrueExpression, base = require("./FixedArityExpressionT")(AnyElementTrueExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$anyElementTrue";
-};
-
-/**
- * Takes an array of one or more numbers and returns true if any.
- * @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(this.getOpName() + "'s argument must be an array, but is " + Value.getType(arr) + "; uassert code 17041");
+	for (var i = 0, l = arr.length; i < l; ++i) {
+		if (Value.coerceToBool(arr[i])) {
 			return true;
+		}
 	}
 	return false;
 };
 
-/** Register Expression */
-Expression.registerExpression("$anyElementTrue",base.parse(AnyElementTrueExpression));
+Expression.registerExpression("$anyElementTrue",base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$anyElementTrue";
+};

+ 20 - 24
lib/pipeline/expressions/CoerceToBoolExpression.js

@@ -6,27 +6,32 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var CoerceToBoolExpression = module.exports = function CoerceToBoolExpression(expression){
-	if (arguments.length !== 1) throw new Error("args expected: expression");
-	this.expression = expression;
+ */
+var CoerceToBoolExpression = module.exports = function CoerceToBoolExpression(theExpression){
+	if (arguments.length !== 1) throw new Error(klass.name + ": expected args: expr");
+	this.expression = theExpression;
 	base.call(this);
 }, klass = CoerceToBoolExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	AndExpression = require("./AndExpression"),
 	OrExpression = require("./OrExpression"),
 	NotExpression = require("./NotExpression"),
 	Expression = require("./Expression");
 
+klass.create = function create(expression) {
+	var newExpr = new CoerceToBoolExpression(expression);
+	return newExpr;
+};
+
 proto.optimize = function optimize() {
-	this.expression = this.expression.optimize();   // optimize the operand
+	// optimize the operand
+	this.expression = this.expression.optimize();
 
 	// if the operand already produces a boolean, then we don't need this
 	// LATER - Expression to support a "typeof" query?
 	var expr = this.expression;
-	if(expr instanceof AndExpression ||
+	if (expr instanceof AndExpression ||
 			expr instanceof OrExpression ||
 			expr instanceof NotExpression ||
 			expr instanceof CoerceToBoolExpression)
@@ -35,28 +40,19 @@ proto.optimize = function optimize() {
 };
 
 proto.addDependencies = function addDependencies(deps, path) {
-	return this.expression.addDependencies(deps);
+	this.expression.addDependencies(deps);
 };
 
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars){
+proto.evaluateInternal = function evaluateInternal(vars) {
 	var result = this.expression.evaluateInternal(vars);
 	return Value.coerceToBool(result);
 };
 
 proto.serialize = function serialize(explain) {
-	if ( explain ) {
-		return {$coerceToBool:[this.expression.toJSON()]};
-	}
-	else {
-		return {$and:[this.expression.toJSON()]};
-	}
+	// When not explaining, serialize to an $and expression. When parsed, the $and expression
+	// will be optimized back into a ExpressionCoerceToBool.
+	var name = explain ? "$coerceToBool" : "$and",
+		obj = {};
+	obj[name] = [this.expression.serialize(explain)];
+	return obj;
 };
-
-proto.toJSON = function toJSON() {
-	// Serializing as an $and expression which will become a CoerceToBool
-	return {$and:[this.expression.toJSON()]};
-};
-
-//TODO: proto.addToBsonObj   --- may be required for $project to work
-//TODO: proto.addToBsonArray

+ 83 - 78
lib/pipeline/expressions/CompareExpression.js

@@ -6,108 +6,113 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var CompareExpression = module.exports = function CompareExpression(cmpOp) {
-    this.nargs = 2;
+	if (!(arguments.length === 1 && typeof cmpOp === "string")) throw new Error(klass.name + ": args expected: cmpOp");
     this.cmpOp = cmpOp;
     base.call(this);
-}, klass = CompareExpression,
-    base = require("./NaryExpression"),
-    proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-    });
-
-// DEPENDENCIES
-var Value = require("../Value");
-var Expression = require("./Expression");
-var ConstantExpression = require("./ConstantExpression");
-var FieldPathExpression = require("./FieldPathExpression");
-var FieldRangeExpression = require("./FieldRangeExpression");
-var NaryExpression = require("./NaryExpression");
-
-// NESTED CLASSES
+}, klass = CompareExpression, base = require("./FixedArityExpressionT")(CompareExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+
+var Value = require("../Value"),
+	Expression = require("./Expression");
+
+
+klass.parse = function parse(jsonExpr, vps, op) {
+	var expr = new CompareExpression(op),
+		args = base.parseArguments(jsonExpr, vps);
+	expr.validateArguments(args);
+	expr.operands = args;
+	return expr;
+};
+
+
 /**
  * Lookup table for truth value returns
- *
  * @param truthValues   truth value for -1, 0, 1
  * @param reverse               reverse comparison operator
  * @param name                  string name
- **/
-var CmpLookup = (function() { // emulating a struct
-	// CONSTRUCTOR
-	var klass = function CmpLookup(truthValues, reverse, name) {
-		if (arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
-		this.truthValues = truthValues;
-		this.reverse = reverse;
-		this.name = name;
-	}, base = Object,
-		proto = klass.prototype = Object.create(base.prototype, {
-			constructor: {
-				value: klass
-			}
-		});
-	return klass;
-})();
-
-// verify we need this below
-// PRIVATE STATIC MEMBERS
+ */
+var CmpLookup = function CmpLookup(truthValues, reverse, name) { // emulating a struct
+	if (arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
+	this.truthValues = truthValues;
+	this.reverse = reverse;
+	this.name = name;
+};
+
+
+/**
+ * Enumeration of comparison operators. Any changes to these values require adjustment of
+ * the lookup table in the implementation.
+ */
+var CmpOp = klass.CmpOp = {
+	EQ: "$eq",
+	NE: "$ne",
+	GT: "$gt",
+	GTE: "$gte",
+	LT: "$lt",
+	LTE: "$lte",
+	CMP: "$cmp",
+};
+
+
 /**
  * a table of cmp type lookups to truth values
  * @private
- **/
+ */
 var cmpLookupMap = [ //NOTE: converted from this Array to a Dict/Object below using CmpLookup#name as the key
-    //              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
-    new CmpLookup([false, true, false], CompareExpression.EQ, CompareExpression.EQ),
-    new CmpLookup([true, false, true], CompareExpression.NE, CompareExpression.NE),
-    new CmpLookup([false, false, true], CompareExpression.LT, CompareExpression.GT),
-    new CmpLookup([false, true, true], CompareExpression.LTE, CompareExpression.GTE),
-    new CmpLookup([true, false, false], CompareExpression.GT, CompareExpression.LT),
-    new CmpLookup([true, true, false], CompareExpression.GTE, CompareExpression.LTE),
-    new CmpLookup([false, false, false], CompareExpression.CMP, CompareExpression.CMP)
+	//              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
+	new CmpLookup([false, true, false], CmpOp.EQ, CmpOp.EQ),
+	new CmpLookup([true, false, true], CmpOp.NE, CmpOp.NE),
+	new CmpLookup([false, false, true], CmpOp.LT, CmpOp.GT),
+	new CmpLookup([false, true, true], CmpOp.LTE, CmpOp.GTE),
+	new CmpLookup([true, false, false], CmpOp.GT, CmpOp.LT),
+	new CmpLookup([true, true, false], CmpOp.GTE, CmpOp.LTE),
+
+	// CMP is special. Only name is used.
+	new CmpLookup([false, false, false], CmpOp.CMP, CmpOp.CMP)
 ].reduce(function(r, o) {
 	r[o.name] = o;
 	return r;
 }, {});
 
 
-klass.parse = function parse(bsonExpr, vps, op) {
-    var expr = new CompareExpression(op);
-    var args = NaryExpression.parseArguments(bsonExpr, vps);
-    expr.validateArguments(args);
-    expr.vpOperand = args;
-    return expr;
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var left = this.operands[0].evaluateInternal(vars),
+		right = this.operands[1].evaluateInternal(vars),
+		cmp = Value.compare(left, right);
 
-};
+    // Make cmp one of 1, 0, or -1.
+	if (cmp === 0) {
+		//leave as 0
+	} else if (cmp < 0) {
+		cmp = -1;
+	} else if (cmp > 0) {
+		cmp = 1;
+	}
 
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars) {
-	//debugger;
-    var left = this.operands[0].evaluateInternal(vars),
-        right = this.operands[1].evaluateInternal(vars),
-        cmp = Expression.signum(Value.compare(left, right));
-    if (this.cmpOp == Expression.CmpOp.CMP) return cmp;
-    return cmpLookupMap[this.cmpOp].truthValues[cmp + 1] || false;
+	if (this.cmpOp === CmpOp.CMP)
+		return cmp;
+
+	var returnValue = cmpLookupMap[this.cmpOp].truthValues[cmp + 1];
+	return returnValue;
 };
 
-klass.EQ = "$eq";
-klass.NE = "$ne";
-klass.GT = "$gt";
-klass.GTE = "$gte";
-klass.LT = "$lt";
-klass.LTE = "$lte";
-klass.CMP = "$cmp";
 
 proto.getOpName = function getOpName() {
 	return this.cmpOp;
 };
 
-/** Register Expression */
-Expression.registerExpression("$eq", klass.parse);
-Expression.registerExpression("$ne", klass.parse);
-Expression.registerExpression("$gt", klass.parse);
-Expression.registerExpression("$gte", klass.parse);
-Expression.registerExpression("$lt", klass.parse);
-Expression.registerExpression("$lte", klass.parse);
-Expression.registerExpression("$cmp", klass.parse);
+
+function bindLast(fn, lastArg) { // similar to the boost::bind used in the mongo code
+	return function() {
+		return fn.apply(this, Array.prototype.slice.call(arguments).concat([lastArg]));
+	};
+}
+Expression.registerExpression("$cmp", bindLast(klass.parse, CmpOp.CMP));
+Expression.registerExpression("$eq", bindLast(klass.parse, CmpOp.EQ));
+Expression.registerExpression("$gt", bindLast(klass.parse, CmpOp.GT));
+Expression.registerExpression("$gte", bindLast(klass.parse, CmpOp.GTE));
+Expression.registerExpression("$lt", bindLast(klass.parse, CmpOp.LT));
+Expression.registerExpression("$lte", bindLast(klass.parse, CmpOp.LTE));
+Expression.registerExpression("$ne", bindLast(klass.parse, CmpOp.NE));

+ 23 - 24
lib/pipeline/expressions/ConcatExpression.js

@@ -1,43 +1,42 @@
 "use strict";
 
-var Expression = require("./Expression");
-
 /**
  * Creates an expression that concatenates a set of string operands.
  * @class ConcatExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ConcatExpression = module.exports = function ConcatExpression(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = ConcatExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = ConcatExpression, base = require("./VariadicExpressionT")(ConcatExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Value = require("../Value");
-var Expression = require("./Expression");
+var Value = require("../Value"),
+	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$concat";
-};
-
-/**
- * Concats a string of values together.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-    var n = this.operands.length;
+	var n = this.operands.length;
+
+	var result = "";
+	for (var i = 0; i < n; ++i) {
+		var val = this.operands[i].evaluateInternal(vars);
 
-    return this.operands.map(function(x) {
-	var y = x.evaluateInternal(vars);
-	if(typeof(y) !== "string") {
-	    throw new Error("$concat only supports strings - 16702");
+		if (val === undefined || val === null)
+			return null;
+
+		if (typeof val !== "string")
+			throw new Error(this.getOpName() + " only supports strings, not " +
+				Value.getType(val) + "; uassert code 16702");
+
+		result += val;
 	}
-	return y;
-    }).join("");
+
+	return result;
 };
 
+Expression.registerExpression("$concat", base.parse);
 
-Expression.registerExpression("$concat", base.parse(ConcatExpression));
+proto.getOpName = function getOpName(){
+	return "$concat";
+};

+ 33 - 53
lib/pipeline/expressions/CondExpression.js

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

+ 34 - 34
lib/pipeline/expressions/ConstantExpression.js

@@ -1,64 +1,64 @@
 "use strict";
 
+var Value = require("../Value"),
+    Expression = require("./Expression");
+
 /**
  * Internal expression for constant values
  * @class ConstantExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ConstantExpression = module.exports = function ConstantExpression(value){
-    if (arguments.length !== 1) throw new Error("args expected: value");
-    this.value = value; //TODO: actually make read-only in terms of JS?
+    if (arguments.length !== 1) throw new Error(klass.name + ": args expected: value");
+    this.value = value;
     base.call(this);
-}, klass = ConstantExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-
-// DEPENDENCIES
-var Value = require("../Value"),
-    Expression = require("./Expression");
+}, klass = ConstantExpression, base = require("./FixedArityExpressionT")(klass,1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$const";
+klass.parse = function parse(exprElement, vps) {
+	return new ConstantExpression(exprElement);
 };
 
-/**
- * Get the constant value represented by this Expression.
- * @method getValue
- * @returns the value
- **/
-proto.getValue = function getValue(){   //TODO: convert this to an instance field rather than a property
-    return this.value;
+klass.create = function create(value) {
+	var constExpr = new ConstantExpression(value);
+	return constExpr;
 };
 
-proto.addDependencies = function addDependencies(deps, path) {
+proto.optimize = function optimize() {
 	// nothing to do
+	return this;
 };
 
-klass.parse = function parse(expr, vps){
-    return new ConstantExpression(expr);
+proto.addDependencies = function addDependencies(deps, path) {
+	// nothing to do
 };
 
 /**
  * Get the constant value represented by this Expression.
  * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
+ */
+proto.evaluateInternal = function evaluateInternal(vars) {
 	return this.value;
 };
 
-proto.optimize = function optimize() {
-	return this; // nothing to do
-};
+/// Helper function to easily wrap constants with $const.
+function serializeConstant(val) {
+    return {$const: val};
+}
 
-proto.serialize = function(rawValue){
-	return rawValue ? {$const: this.value} : this.value;
+proto.serialize = function serialize(explain) {
+	return serializeConstant(this.value);
 };
 
-//TODO: proto.addToBsonObj   --- may be required for $project to work -- my hope is that we can implement toJSON methods all around and use that instead
-//TODO: proto.addToBsonArray
+Expression.registerExpression("$const", klass.parse);
+
+Expression.registerExpression("$literal", klass.parse); // alias
 
-/** Register Expression */
-Expression.registerExpression("$const",klass.parse(ConstantExpression));
-Expression.registerExpression("$literal", klass.parse(ConstantExpression)); // alias
+proto.getOpName = function getOpName() {
+	return "$const";
+};
+
+proto.getValue = function getValue() {
+    return this.value;
+};

+ 12 - 24
lib/pipeline/expressions/DayOfMonthExpression.js

@@ -6,35 +6,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var DayOfMonthExpression = module.exports = function DayOfMonthExpression() {
-    this.nargs = 1;
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
     base.call(this);
-}, klass = DayOfMonthExpression,
-    base = require("./NaryExpression"),
-    proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-    });
+}, klass = DayOfMonthExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-    return "$dayOfMonth";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCDate();
 };
 
-/**
- * Takes a date and returns the day of the month as a number between 1 and 31.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-    var date = this.operands[0].evaluateInternal(vars);
-    return date.getUTCDate();
+proto.getOpName = function getOpName() {
+	return "$dayOfMonth";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfMonth", base.parse(DayOfMonthExpression));
+Expression.registerExpression("$dayOfMonth", base.parse);

+ 13 - 18
lib/pipeline/expressions/DayOfWeekExpression.js

@@ -6,28 +6,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var DayOfWeekExpression = module.exports = function DayOfWeekExpression(){
-	this.nargs = 1;
+ */
+var DayOfWeekExpression = module.exports = function DayOfWeekExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = DayOfWeekExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = DayOfWeekExpression, base = require("./FixedArityExpressionT")(DayOfWeekExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$dayOfWeek";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCDay() + 1;
 };
 
-/**
- * Takes a date and returns the day of the week as a number between 1 (Sunday) and 7 (Saturday.)
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCDay()+1;
+proto.getOpName = function getOpName() {
+	return "$dayOfWeek";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfWeek",base.parse(DayOfWeekExpression));
+Expression.registerExpression("$dayOfWeek", base.parse);

+ 17 - 26
lib/pipeline/expressions/DayOfYearExpression.js

@@ -6,36 +6,27 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var DayOfYearExpression = module.exports = function DayOfYearExpression(){
-	this.nargs = 1;
+ */
+var DayOfYearExpression = module.exports = function DayOfYearExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = DayOfYearExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = DayOfYearExpression, base = require("./FixedArityExpressionT")(DayOfYearExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-    return "$dayOfYear";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate),
+		//NOTE: DEVIATION FROM MONGO: our calculations are a little different but are equivalent
+		y11 = new Date(date.getUTCFullYear(), 0, 1), // same year, first month, first day; time omitted
+		ymd = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1), // same y,m,d; time omitted, add 1 because days start at 1
+		yday = Math.ceil((ymd - y11) / 86400000); // count days
+	return yday;
 };
 
-/**
- * Takes a date and returns the day of the year as a number between 1 and 366.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	//NOTE: the below silliness is to deal with the leap year scenario when we should be returning 366
-    var date = this.operands[0].evaluateInternal(vars);
-    return klass.getDateDayOfYear(date);
-};
-
-// STATIC METHODS
-klass.getDateDayOfYear = function getDateDayOfYear(d){
-    var y11 = new Date(d.getUTCFullYear(), 0, 1),       // same year, first month, first day; time omitted
-	ymd = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()+1);  // same y,m,d; time omitted, add 1 because days start at 1
-    return Math.ceil((ymd - y11) / 86400000);   //NOTE: 86400000 ms is 1 day
+proto.getOpName = function getOpName() {
+	return "$dayOfYear";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfYear",base.parse(DayOfYearExpression));
+Expression.registerExpression("$dayOfYear", base.parse);

+ 22 - 17
lib/pipeline/expressions/DivideExpression.js

@@ -4,37 +4,42 @@
  * A $divide pipeline expression.
  * @see evaluateInternal
  * @class DivideExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
  **/
 var DivideExpression = module.exports = function DivideExpression(){
-    this.nargs = 2;
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
     base.call(this);
-}, klass = DivideExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = DivideExpression, base = require("./FixedArityExpressionT")(DivideExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){ //TODO: try to move this to a static and/or instance field instead of a getter function
-	return "$divide";
-};
-
 /**
  * Takes an array that contains a pair of numbers and returns the value of the first number divided by the second number.
  * @method evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (!(left instanceof Date) && (!right instanceof Date)) throw new Error("$divide does not support dates; code 16373");
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-	left = Value.coerceToDouble(left);
-	return left / right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+        var numer = lhs,
+            denom = rhs;
+        if (denom === 0) throw new Error("can't $divide by zero; uassert code 16608");
+
+        return numer / denom;
+    } else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+        return null;
+    } else{
+        throw new Error("User assertion: 16609: $divide only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+    }
 };
 
-/** Register Expression */
-Expression.registerExpression("$divide",base.parse(DivideExpression));
+Expression.registerExpression("$divide", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$divide";
+};

+ 190 - 152
lib/pipeline/expressions/Expression.js

@@ -12,32 +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");
 
-function fn(){
-	return;
-}
 
-
-// NESTED CLASSES
 /**
  * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.ObjectCtx` class
  * @static
  * @property ObjectCtx
- **/
+ */
 var ObjectCtx = Expression.ObjectCtx = (function() {
 	// CONSTRUCTOR
 	/**
@@ -53,18 +43,13 @@ 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];
 		}
-	}, base = Object,
-		proto = klass.prototype = Object.create(base.prototype, {
-			constructor: {
-				value: klass
-			}
-		});
+	}, proto = klass.prototype;
 
 	// PROTOTYPE MEMBERS
 	proto.isDocumentOk =
@@ -74,66 +59,77 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 	return klass;
 })();
 
-proto.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") !== -1) {
-		// field path must not contain embedded null characters - 16419
-	}
-	if (prefixedField[0] !== '$') {
-		// "field path references must be prefixed with a '$'"
-	}
-	return prefixedField.slice(1);
-};
-var KIND_UNKNOWN = 0,
-	KIND_NOTOPERATOR = 1,
-	KIND_OPERATOR = 2;
+
+//
+// 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");
-	var kind = KIND_UNKNOWN,
-		pExpression, // the result
-		pExpressionObject; // the alt result
-	if (obj === undefined || obj == {}) return new ObjectExpression();
+	/*
+	  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
+		UNKNOWN = 0,
+		NOTOPERATOR = 1,
+		OPERATOR = 2,
+		kind = UNKNOWN;
+
+	if (obj === undefined || obj === null || (obj instanceof Object && Object.keys(obj).length === 0)) return new ObjectExpression();
 	var fieldNames = Object.keys(obj);
-	if (fieldNames.length === 0) { //NOTE: Added this for mongo 2.5 port of document sources. Should reconsider when porting the expressions themselves
-		return new ObjectExpression();
-	}
 	for (var fieldCount = 0, n = fieldNames.length; fieldCount < n; ++fieldCount) {
-		var pFieldName = fieldNames[fieldCount];
+		var fieldName = fieldNames[fieldCount];
 
-		if (pFieldName[0] === "$") {
+		if (fieldName[0] === "$") {
 			if (fieldCount !== 0)
-				throw new Error("the operator must be the only field in a pipeline object (at '" + pFieldName + "'.; code 16410");
+				throw new Error("the operator must be the only field in a pipeline object (at '" + fieldName + "'.; uassert code 15983");
 
 			if (ctx.isTopLevel)
-				throw new Error("$expressions are not allowed at the top-level of $project; code 16404");
-			kind = KIND_OPERATOR; //we've determined this "object" is an operator expression
-			pExpression = Expression.parseExpression(pFieldName, obj[pFieldName], vps);
+				throw new Error("$expressions are not allowed at the top-level of $project; uassert code 16404");
+
+			// 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 === KIND_OPERATOR)
-				throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + pFieldName + "'.; code 15990");
-
-			if (!ctx.isTopLevel && pFieldName.indexOf(".") != -1)
-				throw new Error("dotted field names are only allowed at the top level; code 16405");
-			if (pExpression === undefined) { // if it's our first time, create the document expression
-				if (!ctx.isDocumentOk)
-					throw new Error("document not allowed in this context"); // CW TODO error: document not allowed in this context
-				pExpression = pExpressionObject = new ObjectExpression(); //check for top level?
-				kind = KIND_NOTOPERATOR; //this "object" is not an operator expression
+			if (kind === OPERATOR)
+				throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + fieldName + "'.; uassert code 15990");
+
+			if (!ctx.isTopLevel && fieldName.indexOf(".") !== -1)
+				throw new Error("dotted field names are only allowed at the top level; uassert code 16405");
+
+			// if 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 = ctx.isTopLevel ? ObjectExpression.createRoot() : ObjectExpression.create();
+				expression = expressionObject;
+
+				// this "object" is not an operator expression
+				kind = NOTOPERATOR;
 			}
-			var fieldValue = obj[pFieldName];
+
+			var fieldValue = obj[fieldName];
 			switch (typeof(fieldValue)) {
 				case "object":
 					// it's a nested document
@@ -141,152 +137,194 @@ klass.parseObject = function parseObject(obj, ctx, vps) {
 						isDocumentOk: ctx.isDocumentOk,
 						isInclusionOk: ctx.isInclusionOk
 					});
-					pExpressionObject.addField(pFieldName, Expression.parseObject(fieldValue, subCtx, vps));
+
+					expressionObject.addField(fieldName, Expression.parseObject(fieldValue, subCtx, vps));
+
 					break;
 				case "string":
-					// it's a renamed field         // CW TODO could also be a constant
-					var pathExpr = new FieldPathExpression.parse(fieldValue);
-					pExpressionObject.addField(pFieldName, pathExpr);
+					// it's a renamed field
+					// CW TODO could also be a constant
+					expressionObject.addField(fieldName, FieldPathExpression.parse(fieldValue, vps));
 					break;
 				case "boolean":
 				case "number":
 					// it's an inclusion specification
 					if (fieldValue) {
 						if (!ctx.isInclusionOk)
-							throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
-						pExpressionObject.includePath(pFieldName);
+							throw new Error("field inclusion is not allowed inside of $expressions; uassert code 16420");
+						expressionObject.includePath(fieldName);
 					} else {
-						if (!(ctx.isTopLevel && fn == Document.ID_PROPERTY_NAME))
-							throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; code 16406");
-						pExpressionObject.excludeId = true;
+						if (!(ctx.isTopLevel && fieldName === Document.ID_PROPERTY_NAME))
+							throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; uassert code 16406");
+						expressionObject.excludeId = true;
 					}
 					break;
 				default:
-					throw new Error("disallowed field type " + (fieldValue ? fieldValue.constructor.name + ":" : "") + typeof(fieldValue) + " in object expression (at '" + pFieldName + "')");
+					throw new Error("disallowed field type " + Value.getType(fieldValue) + " in object expression (at '" + fieldName + "') uassert code 15992");
 			}
 		}
 	}
-	return pExpression;
+
+	return expression;
 };
 
 
 klass.expressionParserMap = {};
 
+
+/**
+ * Registers an ExpressionParser so it can be called from parseExpression and friends.
+ * As an example, if your expression looks like {"$foo": [1,2,3]} you would add this line:
+ * REGISTER_EXPRESSION("$foo", ExpressionFoo::parse);
+ */
 klass.registerExpression = function registerExpression(key, parserFunc) {
-	if (key in klass.expressionParserMap) {
-		throw new Error("Duplicate expression registrarion for " + key);
-	}
+	if (key in klass.expressionParserMap)
+		throw new Error("Duplicate expression (" + key + ") detected; massert code 17064");
 	klass.expressionParserMap[key] = parserFunc;
-	return 0; // Should
+	return 1;
 };
 
+
+//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
 /**
- * Parse a BSONElement Object which has already been determined to be functional expression.
- *
+ * Parses a BSONElement which has already been determined to be functional expression.
  * @static
  * @method parseExpression
- * @param opName        the name of the (prefix) operator
- * @param obj   the BSONElement to parse
+ * @param exprElement should be the only element inside the expression object.
+ *    That is the field name should be the $op for the expression.
+ * @param vps the variable parse state
  * @returns the parsed Expression
- **/
-klass.parseExpression = function parseExpression(exprKey, exprValue, vps) {
-	if (!(exprKey in Expression.expressionParserMap)) {
-		throw new Error("Invalid operator : " + exprKey);
-	}
-	return Expression.expressionParserMap[exprKey](exprValue, vps);
+ */
+klass.parseExpression = function parseExpression(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);
 };
 
+
 /**
- * Parse a BSONElement which is an operand in an Expression.
+ * Parses a BSONElement which is an operand in an Expression.
+ *
+ * This is the most generic parser and can parse ExpressionFieldPath, a literal, or a $op.
+ * If it is a $op, exprElement should be the outer element whose value is an Object
+ * containing the $op.
  *
+ * @method parseOperand
  * @static
- * @param pBsonElement the expected operand's BSONElement
+ * @param exprElement should be the only element inside the expression object.
+ *    That is the field name should be the $op for the expression.
+ * @param vps the variable parse state
  * @returns the parsed operand, as an Expression
- **/
+ */
 klass.parseOperand = function parseOperand(exprElement, vps) {
 	var t = typeof(exprElement);
-	if (t === "string" && exprElement[0] == "$") { //if we got here, this is a field path expression
-	    return new FieldPathExpression.parse(exprElement, vps);
-	} else
-	if (t === "object" && exprElement && exprElement.constructor === Object)
-		return Expression.parseObject(exprElement, new ObjectCtx({
+	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) {
+		var oCtx = new ObjectCtx({
 			isDocumentOk: true
-		}), vps);
-	else return ConstantExpression.parse(exprElement, vps);
-};
-
-/**
- * Produce a field path string with the field prefix removed.
- * Throws an error if the field prefix is not present.
- *
- * @static
- * @param prefixedField the prefixed field
- * @returns the field path with the prefix removed
- **/
-klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; code 16419");
-	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); code 15982");
-	return prefixedField.substr(1);
+		});
+		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!");
-};
-
 /**
  * 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.
- **/
-proto.addDependencies = function addDependencies(deps, path) {
+ * @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) { //jshint ignore:line
 	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) { //jshint ignore:line
+	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 Object && vars.constructor === Object) 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) { //jshint ignore:line
+	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");

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

@@ -1,207 +1,161 @@
 "use strict";
 
+var Expression = require("./Expression"),
+    Variables = require("./Variables"),
+    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);
-};
-
-
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars){
+ * @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}});
 
-    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 $$
+            dotIndex = fieldPath.indexOf("."),
+            varName = fieldPath.substr(0, dotIndex !== -1 ? dotIndex : fieldPath.length);
+        Variables.uassertValidNameForUserRead(varName);
+        return new FieldPathExpression(fieldPath, vps.getVariable(varName));
     } else {
-        return new FieldPathExpression("CURRENT." + raw.slice(1), vps.getVariable("CURRENT"));
+        return new FieldPathExpression("CURRENT." + raw.substr(1), // strip the "$" prefix
+            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;
+};
+

+ 13 - 20
lib/pipeline/expressions/HourExpression.js

@@ -2,34 +2,27 @@
 
 /**
  * An $hour pipeline expression.
- * @see evaluateInternal
  * @class HourExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var HourExpression = module.exports = function HourExpression(){
-	this.nargs = 1;
+ */
+var HourExpression = module.exports = function HourExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = HourExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = HourExpression, base = require("./FixedArityExpressionT")(HourExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$hour";
-};
-
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-/**
- * Takes a date and returns the hour between 0 and 23.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var date = this.operands[0].evaluateInternal(vars);
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
 	return date.getUTCHours();
 };
 
+proto.getOpName = function getOpName() {
+	return "$hour";
+};
 
-/** Register Expression */
-Expression.registerExpression("$hour",base.parse(HourExpression));
+Expression.registerExpression("$hour", base.parse);

+ 10 - 25
lib/pipeline/expressions/IfNullExpression.js

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

+ 29 - 15
lib/pipeline/expressions/LetExpression.js

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

+ 62 - 59
lib/pipeline/expressions/MapExpression.js

@@ -1,107 +1,110 @@
 "use strict";
 
 var MapExpression = module.exports = function MapExpression(varName, varId, input, each){
-	if (arguments.length !== 4) throw new Error("Four args expected");
+	if (arguments.length !== 4) throw new Error(klass.name + ": args expected: varName, varId, input, each");
 	this._varName = varName;
 	this._varId = varId;
 	this._input = input;
 	this._each = each;
 }, klass = MapExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Variables = require("./Variables"),
-	VariablesParseState = require("./VariablesParseState");
+var Value = require("../Value"),
+	Variables = require("./Variables");
 
-// PROTOTYPE MEMBERS
+klass.parse = function parse(expr, vpsIn) {
 
+	// if (!(exprFieldName)) throw new Error("Assertion failure"); //NOTE: DEVIATION FROM MONGO: we do not have exprFieldName here
 
-klass.parse = function parse(expr, vpsIn){
-	if(!("$map" in expr)) {
-		throw new Error("Tried to create a $let with something other than let. Looks like your parse map went all funny.");
+	if (Value.getType(expr) !== "Object") {
+		throw new Error("$map only supports an object as it's argument; uassert code 16878");
 	}
 
-	if(typeof(expr.$map) !== 'object' || (expr.$map instanceof Array)) {
-		throw new Error("$map only supports an object as it's argument:16878");
+	// "in" must be parsed after "as" regardless of BSON order
+	var inputElem,
+		asElem,
+		inElem,
+		args = expr;
+	for (var argFieldName in args) {
+		var arg = args[argFieldName];
+		if (argFieldName === "input") {
+			inputElem = arg;
+		} else if (argFieldName === "as") {
+			asElem = arg;
+		} else if (argFieldName === "in") {
+			inElem = arg;
+		} else {
+			throw new Error("Unrecognized parameter to $map: " + argFieldName + "; uassert code 16879");
+		}
 	}
 
-	var args = expr.$map,
-		inputElem = args.input,
-		inElem = args['in'],
-		asElem = args.as;
-
-	if(!inputElem) {
-		throw new Error("Missing 'input' parameter to $map: 16880");
-	}
-	if(!asElem) {
-		throw new Error("Missing 'as' parameter to $map: 16881");
-	}
-	if(!inElem) {
-		throw new Error("Missing 'in' parameter to $let: 16882");
-	}
+	if (!inputElem) throw new Error("Missing 'input' parameter to $map; uassert code 16880");
+	if (!asElem) throw new Error("Missing 'as' parameter to $map; uassert code 16881");
+	if (!inElem) throw new Error("Missing 'in' parameter to $map; uassert code 16882");
 
+	// parse "input"
+	var input = Expression.parseOperand(inputElem, vpsIn); // only has outer vars
 
-	if(Object.keys(args).length > 3) {
-		var bogus = Object.keys(args).filter(function(x) {return !(x === 'in' || x === 'as' || x === 'input');});
-		throw new Error("Unrecognized parameter to $map: " + bogus.join(",") + "- 16879");
-	}
-
-	var input = Expression.parseOperand(inputElem, vpsIn);
-
-	var vpsSub = new VariablesParseState(vpsIn),
+	// parse "as"
+	var vpsSub = vpsIn, // vpsSub gets our vars, vpsIn doesn't.
 		varName = asElem;
-
 	Variables.uassertValidNameForUserWrite(varName);
 	var varId = vpsSub.defineVariable(varName);
 
-	var invert = Expression.parseOperand(inElem, vpsSub);
+	// parse "in"
+	var inExpr = Expression.parseOperand(inElem, vpsSub); // has access to map variable
 
-	return new MapExpression(varName, varId, input, invert);
+	return new MapExpression(varName, varId, input, inExpr);
 };
 
-
 proto.optimize = function optimize() {
+	// TODO handle when _input is constant
 	this._input = this._input.optimize();
 	this._each = this._each.optimize();
 	return this;
 };
 
 proto.serialize = function serialize(explain) {
-	return {$map: {input:this._input.serialize(explain),
-				   as: this._varName,
-				   'in': this._each.serialize(explain)}};
+	return {
+		$map: {
+			input: this._input.serialize(explain),
+			as: this._varName,
+			in : this._each.serialize(explain)
+		}
+	};
 };
 
 proto.evaluateInternal = function evaluateInternal(vars) {
+	// guaranteed at parse time that this isn't using our _varId
 	var inputVal = this._input.evaluateInternal(vars);
-	if( inputVal === null) {
+	if (inputVal === null || inputVal === undefined)
 		return null;
-	}
 
-	if(!(inputVal instanceof Array)) {
-		throw new Error("Input to $map must be an Array, not a ____ 16883");
+	if (!(inputVal instanceof Array)){
+		throw new Error("input to $map must be an Array not " +
+			Value.getType(inputVal) + "; uassert code 16883");
 	}
 
-	if(inputVal.length === 0) {
-		return [];
-	}
+	if (inputVal.length === 0)
+		return inputVal;
+
+	var output = new Array(inputVal.length);
+	for (var i = 0, l = inputVal.length; i < l; i++) {
+		vars.setValue(this._varId, inputVal[i]);
 
-	// Diverge from Mongo source here, as Javascript has a builtin map operator.
-	return inputVal.map(function(x) {
-	   vars.setValue(this._varId, x);
-	   var toInsert = this._each.evaluateInternal(vars);
-	   if(toInsert === undefined) {
-		   toInsert = null;
-	   }
+		var toInsert = this._each.evaluateInternal(vars);
+		if (toInsert === undefined)
+			toInsert = null; // can't insert missing values into array
 
-	   return toInsert;
-   });
+		output[i] = toInsert;
+	}
+
+	return output;
 };
 
-proto.addDependencies = function addDependencies(deps, path){
-	this._input.addDependencies(deps, path);
-	this._each.addDependencies(deps, path);
+proto.addDependencies = function addDependencies(deps, path) { //jshint ignore:line
+	this._input.addDependencies(deps);
+	this._each.addDependencies(deps);
 	return deps;
 };
 
-
 Expression.registerExpression("$map", klass.parse);

+ 12 - 26
lib/pipeline/expressions/MillisecondExpression.js

@@ -2,41 +2,27 @@
 
 /**
  * An $millisecond pipeline expression.
- * @see evaluateInternal
  * @class MillisecondExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MillisecondExpression = module.exports = function MillisecondExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MillisecondExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MillisecondExpression, base = require("./FixedArityExpressionT")(MillisecondExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$millisecond";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCMilliseconds(); //NOTE: no leap milliseconds in JS - http://code.google.com/p/v8/issues/detail?id=1944
 };
 
-/**
- * Takes a date and returns the millisecond between 0 and 999, but can be 1000 to account for leap milliseconds.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCMilliseconds(); //TODO: incorrect for last millisecond of leap year, need to fix...
-	// currently leap milliseconds are unsupported in v8
-	// http://code.google.com/p/v8/issues/detail?id=1944
+proto.getOpName = function getOpName() {
+	return "$millisecond";
 };
 
-/** Register Expression */
-Expression.registerExpression("$millisecond", base.parse(MillisecondExpression));
+Expression.registerExpression("$millisecond", base.parse);

+ 12 - 24
lib/pipeline/expressions/MinuteExpression.js

@@ -2,39 +2,27 @@
 
 /**
  * An $minute pipeline expression.
- * @see evaluateInternal
  * @class MinuteExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MinuteExpression = module.exports = function MinuteExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MinuteExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MinuteExpression, base = require("./FixedArityExpressionT")(MinuteExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$minute";
-};
-
-/**
- * Takes a date and returns the minute between 0 and 59.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
 	return date.getUTCMinutes();
 };
 
-/** Register Expression */
-Expression.registerExpression("$minute", base.parse(MinuteExpression));
+proto.getOpName = function getOpName() {
+	return "$minute";
+};
+
+Expression.registerExpression("$minute", base.parse);

+ 25 - 36
lib/pipeline/expressions/ModExpression.js

@@ -4,51 +4,40 @@
  * An $mod pipeline expression.
  * @see evaluate
  * @class ModExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ModExpression = module.exports = function ModExpression() {
-	this.nargs = 2;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = ModExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
-
-// DEPENDENCIES
+}, klass = ModExpression, base = require("./FixedArityExpressionT")(ModExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$mod";
-};
-
-/**
- * Takes an array that contains a pair of numbers and returns the remainder of the first number divided by the second number.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	this.checkArgCount(2);
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$mod does not support dates; code 16374");
-
-	// pass along jstNULLs and Undefineds
-	if (left === undefined || left === null) return left;
-	if (right === undefined || right === null) return right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	var leftType = Value.getType(lhs),
+		rightType = Value.getType(rhs);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		// ensure we aren't modding by 0
+		if(rhs === 0) throw new Error("can't $mod by 0; uassert code 16610");
+
+		return lhs % rhs;
+	} else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	} else {
+		throw new Error("$mod only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+	}
+};
 
-	// ensure we aren't modding by 0
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
+Expression.registerExpression("$mod", base.parse);
 
-	left = Value.coerceToDouble(left);
-	return left % right;
+proto.getOpName = function getOpName() {
+	return "$mod";
 };
-
-/** Register Expression */
-Expression.registerExpression("$mod", base.parse(ModExpression));

+ 12 - 24
lib/pipeline/expressions/MonthExpression.js

@@ -2,39 +2,27 @@
 
 /**
  * A $month pipeline expression.
- * @see evaluate
  * @class MonthExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MonthExpression = module.exports = function MonthExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MonthExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MonthExpression, base = require("./FixedArityExpressionT")(MonthExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$month";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCMonth() + 1;
 };
 
-/**
- * Takes a date and returns the month as a number between 1 and 12.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCMonth();
+proto.getOpName = function getOpName() {
+	return "$month";
 };
 
-/** Register Expression */
-Expression.registerExpression("$month", base.parse(MonthExpression));
+Expression.registerExpression("$month", base.parse);

+ 30 - 22
lib/pipeline/expressions/MultiplyExpression.js

@@ -7,35 +7,43 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MultiplyExpression = module.exports = function MultiplyExpression(){
-	if (arguments.length !== 0) throw new Error("Zero args expected");
+if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MultiplyExpression, base = require("./NaryExpression"), 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"),
- Expression = require("./Expression");
+	Expression = require("./Expression");
+
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var product = 1; //NOTE: DEVIATION FROM MONGO: no need to track narrowest so just use one var
+
+	var n = this.operands.length;
+	for (var i = 0; i < n; ++i) {
+		var val = this.operands[i].evaluateInternal(vars);
+
+		if (typeof val === "number") {
+			product *= Value.coerceToDouble(val);
+		} else if (val === undefined || val === null) {
+			return null;
+		} else {
+			throw new Error("$multiply only supports numeric types, not " +
+			 	Value.getType(val) + "; uasserted code 16555");
+		}
+	}
+
+	if (typeof product === "number")
+		return product;
+	throw new Error("$multiply resulted in a non-numeric type; massert code 16418");
+};
+
+Expression.registerExpression("$multiply", base.parse);
 
-// PROTOTYPE MEMBERS
 proto.getOpName = function getOpName(){
 	return "$multiply";
 };
 
-/**
- * Takes an array of one or more numbers and multiples them, returning the resulting product.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var product = 1;
-	for(var i = 0, n = this.operands.length; i < n; ++i){
-		var value = this.operands[i].evaluateInternal(vars);
-		if(value instanceof Date) throw new Error("$multiply does not support dates; code 16375");
-		product *= Value.coerceToDouble(value);
-	}
-	if(typeof(product) != "number") throw new Error("$multiply resulted in a non-numeric type; code 16418");
-	return product;
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
 };
-
-/** Register Expression */
-Expression.registerExpression("$multiply", base.parse(MultiplyExpression));

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

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

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

+ 170 - 165
lib/pipeline/expressions/ObjectExpression.js

@@ -7,129 +7,114 @@
  * @module mungedb-aggregate
  * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
- **/
-var ObjectExpression = module.exports = function ObjectExpression(atRoot){
-	if (arguments.length !== 1) throw new Error("one arg expected");
-	this.excludeId = false;	/// <Boolean> for if _id is to be excluded
-	this.atRoot = atRoot;
-	this._expressions = {};	/// <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
-	this._order = []; /// <Array<String>> this is used to maintain order for generated fields not in the source document
+ */
+var ObjectExpression = module.exports = function ObjectExpression(atRoot) {
+	if (arguments.length !== 1) throw new Error(klass.name + ": expected args: atRoot");
+	this.excludeId = false;
+	this._atRoot = atRoot;
+	this._expressions = {};
+	this._order = [];
 }, klass = ObjectExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-klass.create = function create() {
-	return new ObjectExpression(false);
-};
-
-klass.createRoot = function createRoot() {
-	return new ObjectExpression(true);
-};
 
-// DEPENDENCIES
 var Document = require("../Document"),
-	FieldPath = require("../FieldPath");
+	Value = require("../Value"),
+	FieldPath = require("../FieldPath"),
+	ConstantExpression = require("./ConstantExpression");
 
-// INSTANCE VARIABLES
-/**
- * <Boolean> for if _id is to be excluded
- * @property excludeId
- **/
-proto.excludeId = undefined;
-
-/**
- * <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
- **/
-proto._expressions = undefined;
 
-//TODO: might be able to completely ditch _order everywhere in here since `Object`s are mostly ordered anyhow but need to come back and revisit that later
 /**
- * <Array<String>> this is used to maintain order for generated fields not in the source document
- **/
-proto._order = [];
-
+ * Create an empty expression.
+ * Until fields are added, this will evaluate to an empty document.
+ * @method create
+ * @static
+ */
+klass.create = function create() {
+	return new ObjectExpression(false);
+};
 
-// PROTOTYPE MEMBERS
 
 /**
- * evaluateInternal(), but return a Document instead of a Value-wrapped Document.
- * @method evaluateDocument
- * @param currentDoc the input Document
- * @returns the result document
- **/
-proto.evaluateDocument = function evaluateDocument(vars) {
-	// create and populate the result
-	var pResult = {_id:0};
-	this.addToDocument(pResult, pResult, vars); // No inclusion field matching.
-	return pResult;
+ * Like create but uses special handling of _id for root object of $project.
+ * @method createRoot
+ * @static
+ */
+klass.createRoot = function createRoot() {
+	return new ObjectExpression(true);
 };
 
-proto.evaluateInternal = function evaluateInternal(vars) { //TODO: collapse with #evaluateDocument()?
-	return this.evaluateDocument(vars);
-};
 
-proto.optimize = function optimize(){
+proto.optimize = function optimize() {
 	for (var key in this._expressions) {
+		if (!this._expressions.hasOwnProperty(key)) continue;
 		var expr = this._expressions[key];
-		if (expr !== undefined && expr !== null) this._expressions[key] = expr.optimize();
+		if (expr)
+			expr.optimize();
 	}
 	return this;
 };
 
-proto.isSimple = function isSimple(){
+
+proto.isSimple = function isSimple() {
 	for (var key in this._expressions) {
+		if (!this._expressions.hasOwnProperty(key)) continue;
 		var expr = this._expressions[key];
-		if (expr !== undefined && expr !== null && !expr.isSimple()) return false;
+		if (expr && !expr.isSimple())
+			return false;
 	}
 	return true;
 };
 
-proto.addDependencies = function addDependencies(deps, path){
+
+proto.addDependencies = function addDependencies(deps, path) {
 	var pathStr = "";
-	if (path instanceof Array) {
+	if (path) {
 		if (path.length === 0) {
 			// we are in the top level of a projection so _id is implicit
-			if (!this.excludeId) {
-							deps[Document.ID_PROPERTY_NAME] = 1;
-						}
+			if (!this.excludeId)
+				deps.fields[Document.ID_PROPERTY_NAME] = 1;
 		} else {
-			pathStr = new FieldPath(path).getPath() + ".";
+			var f = new FieldPath(path);
+			pathStr = f.getPath(false);
+			pathStr += ".";
 		}
 	} else {
-		if (this.excludeId) throw new Error("excludeId is true!");
+		if (this.excludeId) throw new Error("Assertion error");
 	}
 	for (var key in this._expressions) {
 		var expr = this._expressions[key];
-		if (expr !== undefined && expr !== null) {
-			if (path instanceof Array) path.push(key);
+		if (expr instanceof Expression) {
+			if (path) path.push(key);
 			expr.addDependencies(deps, path);
-			if (path instanceof Array) path.pop();
+			if (path) path.pop();
 		} else { // inclusion
-			if (path === undefined || path === null) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
-			deps[pathStr + key] = 1;
+			if (!path) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
+			deps.fields[pathStr + key] = 1;
 		}
 	}
 
 	return deps;	// NOTE: added to munge as a convenience
 };
 
-/**
- * evaluateInternal(), but add the evaluated fields to a given document instead of creating a new one.
- * @method addToDocument
- * @param pResult the Document to add the evaluated expressions to
- * @param currentDoc the input Document for this level
- * @param vars the root of the whole input document
- **/
-proto.addToDocument = function addToDocument(out, currentDoc, vars){
 
 
+/**
+* evaluateInternal(), but add the evaluated fields to a given document instead of creating a new one.
+* @method addToDocument
+* @param pResult the Document to add the evaluated expressions to
+* @param currentDoc the input Document for this level
+* @param vars the root of the whole input document
+*/
+proto.addToDocument = function addToDocument(out, currentDoc, vars) {
 	var doneFields = {};	// This is used to mark fields we've done so that we can add the ones we haven't
 
-	for(var fieldName in currentDoc){
+	for (var fieldName in currentDoc) {
 		if (!currentDoc.hasOwnProperty(fieldName)) continue;
 		var fieldValue = currentDoc[fieldName];
 
 		// This field is not supposed to be in the output (unless it is _id)
 		if (!this._expressions.hasOwnProperty(fieldName)) {
-			if (!this.excludeId && this.atRoot && fieldName == Document.ID_PROPERTY_NAME) {
+			if (!this.excludeId && this._atRoot && fieldName === Document.ID_PROPERTY_NAME) {
 				// _id from the root doc is always included (until exclusion is supported)
 				// not updating doneFields since "_id" isn't in _expressions
 				out[fieldName] = fieldValue;
@@ -140,63 +125,97 @@ proto.addToDocument = function addToDocument(out, currentDoc, vars){
 		// make sure we don't add this field again
 		doneFields[fieldName] = true;
 
-		// This means pull the matching field from the input document
 		var expr = this._expressions[fieldName];
-		if (!(expr instanceof Expression)) {
+		if (!(expr instanceof Expression)) expr = undefined;
+		if (!expr) {
+			// This means pull the matching field from the input document
 			out[fieldName] = fieldValue;
 			continue;
 		}
 
-		// Check if this expression replaces the whole field
-		if (!(fieldValue instanceof Object) || (fieldValue.constructor !== Object && fieldValue.constructor !== Array) || !(expr instanceof ObjectExpression)) {
+		var objExpr = expr instanceof ObjectExpression ? expr : undefined,
+			valueType = Value.getType(fieldValue);
+		if ((valueType !== "Object" && valueType !== "Array") || !objExpr) {
+			// This expression replace the whole field
 			var pValue = expr.evaluateInternal(vars);
 
 			// don't add field if nothing was found in the subobject
-			if (expr instanceof ObjectExpression && pValue instanceof Object && Object.getOwnPropertyNames(pValue).length === 0) continue;
+			if (objExpr && Object.getOwnPropertyNames(pValue).length === 0)
+				continue;
+
+			/*
+			 * Don't add non-existent values (note:  different from NULL or Undefined);
+			 * this is consistent with existing selection syntax which doesn't
+			 * force the appearance of non-existent fields.
+			 */
+			// if (pValue !== undefined)
+				out[fieldName] = pValue; //NOTE: DEVIATION FROM MONGO: we want to keep these in JS
 
-			// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
-			// TODO make missing distinct from Undefined
-			if (pValue !== undefined) out[fieldName] = pValue;
 			continue;
 		}
 
-		// Check on the type of the input value.  If it's an object, just walk down into that recursively, and add it to the result.
-		if (fieldValue instanceof Object && fieldValue.constructor === Object) {
-			out[fieldName] = expr.addToDocument({}, fieldValue, vars);	//TODO: pretty sure this is broken;
-		} else if (fieldValue instanceof Object && fieldValue.constructor === Array) {
-			// If it's an array, we have to do the same thing, but to each array element.  Then, add the array of results to the current document.
-			var result = [];
-			for(var fvi = 0, fvl = fieldValue.length; fvi < fvl; fvi++){
-				var subValue = fieldValue[fvi];
-				if (subValue.constructor !== Object) continue;	// can't look for a subfield in a non-object value.
-				result.push(expr.addToDocument({}, subValue, vars));
+		/*
+		 * Check on the type of the input value.  If it's an
+		 * object, just walk down into that recursively, and
+		 * add it to the result.
+		 */
+		if (valueType === "Object") {
+			var sub = {};
+			objExpr.addToDocument(sub, fieldValue, vars);
+			out[fieldName] = sub;
+		} else if (valueType === "Array") {
+			/*
+			 * If it's an array, we have to do the same thing,
+			 * but to each array element.  Then, add the array
+			 * of results to the current document.
+			 */
+			var result = [],
+				input = fieldValue;
+			for (var fvi = 0, fvl = input.length; fvi < fvl; fvi++) {
+				// can't look for a subfield in a non-object value.
+				if (Value.getType(input[fvi]) !== "Object")
+					continue;
+
+				var doc = {};
+				objExpr.addToDocument(doc, input[fvi], vars);
+				result.push(doc);
 			}
+
 			out[fieldName] = result;
 		} else {
-			throw new Error("should never happen");	//verify( false );
+			throw new Error("Assertion failure");
 		}
 	}
 
-	if (Object.getOwnPropertyNames(doneFields).length == Object.getOwnPropertyNames(this._expressions).length) return out;	//NOTE: munge returns result as a convenience
+	if (Object.getOwnPropertyNames(doneFields).length === Object.getOwnPropertyNames(this._expressions).length)
+		return out;	//NOTE: munge returns result as a convenience
 
 	// add any remaining fields we haven't already taken care of
-	for(var i = 0, l = this._order.length; i < l; i++){
-		var fieldName2 = this._order[i];
-		var expr2 = this._expressions[fieldName2];
+	for (var i = 0, l = this._order.length; i < l; i++) {
+		var fieldName2 = this._order[i],
+			expr2 = this._expressions[fieldName2];
 
 		// if we've already dealt with this field, above, do nothing
-		if (doneFields.hasOwnProperty(fieldName2)) continue;
+		if (doneFields.hasOwnProperty(fieldName2))
+			continue;
 
 		// this is a missing inclusion field
-		if (!expr2) continue;
+		if (expr2 === null || expr2 === undefined)
+			continue;
 
 		var value = expr2.evaluateInternal(vars);
 
-		// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
-		if (value === undefined || (typeof(value) == 'object' && value !== null && Object.keys(value).length === 0)) continue;
+		/*
+		 * Don't add non-existent values (note:  different from NULL or Undefined);
+		 * this is consistent with existing selection syntax which doesn't
+		 * force the appearnance of non-existent fields.
+		 */
+		if (value === undefined && !(expr2 instanceof ConstantExpression)) //NOTE: DEVIATION FROM MONGO: only if not {$const:undefined}
+			continue;
 
 		// don't add field if nothing was found in the subobject
-		if (expr2 instanceof ObjectExpression && value && value instanceof Object && Object.getOwnPropertyNames(value) == {} ) continue;
+		if (expr2 instanceof ObjectExpression && Object.getOwnPropertyNames(value).length === 0)
+			continue;
 
 		out[fieldName2] = value;
 	}
@@ -204,22 +223,31 @@ proto.addToDocument = function addToDocument(out, currentDoc, vars){
 	return out;	//NOTE: munge returns result as a convenience
 };
 
+
 /**
- * estimated number of fields that will be output
- * @method getSizeHint
- **/
-proto.getSizeHint = function getSizeHint(){
+* estimated number of fields that will be output
+* @method getSizeHint
+*/
+proto.getSizeHint = function getSizeHint() {
 	// Note: this can overestimate, but that is better than underestimating
 	return Object.getOwnPropertyNames(this._expressions).length + (this.excludeId ? 0 : 1);
 };
 
 
+/**
+* evaluateInternal(), but return a Document instead of a Value-wrapped Document.
+* @method evaluateDocument
+* @param currentDoc the input Document
+* @returns the result document
+*/
 proto.evaluateDocument = function evaluateDocument(vars) {
+	// create and populate the result
 	var out = {};
-	this.addToDocument(out, {}, vars);
+	this.addToDocument(out, {}, vars);	// No inclusion field matching.
 	return out;
 };
 
+
 proto.evaluateInternal = function evaluateInternal(vars) {
 	return this.evaluateDocument(vars);
 };
@@ -230,45 +258,52 @@ proto.evaluateInternal = function evaluateInternal(vars) {
  * @method addField
  * @param fieldPath the path the evaluated expression will have in the result Document
  * @param pExpression the expression to evaluateInternal obtain this field's Value in the result Document
- **/
-proto.addField = function addField(fieldPath, pExpression){
-	if(!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
+ */
+proto.addField = function addField(fieldPath, pExpression) {
+	if (!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
 	var fieldPart = fieldPath.getFieldName(0),
 		haveExpr = this._expressions.hasOwnProperty(fieldPart),
-		subObj = this._expressions[fieldPart];	// inserts if !haveExpr //NOTE: not in munge & JS it doesn't, handled manually below
+		expr = this._expressions[fieldPart],
+		subObj = expr instanceof ObjectExpression ? expr : undefined;	// inserts if !haveExpr
 
 	if (!haveExpr) {
 		this._order.push(fieldPart);
 	} else { // we already have an expression or inclusion for this field
-		if (fieldPath.getPathLength() == 1) { // This expression is for right here
-			if (!(subObj instanceof ObjectExpression && typeof pExpression == "object" && pExpression instanceof ObjectExpression)){
-				throw new Error("can't add an expression for field `" + fieldPart + "` because there is already an expression for that field or one of its sub-fields; uassert code 16400"); // we can merge them
-			}
+		if (fieldPath.getPathLength() === 1) {
+			// This expression is for right here
+
+			var newSubObj = pExpression instanceof ObjectExpression ? pExpression : undefined;
+			if (!(subObj && newSubObj))
+				throw new Error("can't add an expression for field " + fieldPart + " because there is already an expression for that field or one of its sub-fields; uassert code 16400"); // we can merge them
 
 			// Copy everything from the newSubObj to the existing subObj
 			// This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
-			for (var key in pExpression._expressions) {
-				if (pExpression._expressions.hasOwnProperty(key)) {
-					subObj.addField(key, pExpression._expressions[key]); // asserts if any fields are dupes
-				}
+			for (var i = 0, l = newSubObj._order.length; i < l; ++i) {
+				var key = newSubObj._order[i];
+				// asserts if any fields are dupes
+				subObj.addField(key, newSubObj._expressions[key]);
 			}
 			return;
-		} else { // This expression is for a subfield
-			if(!subObj) throw new Error("can't add an expression for a subfield of `" + fieldPart + "` because there is already an expression that applies to the whole field; uassert code 16401");
+		} else {
+			// This expression is for a subfield
+			if(!subObj)
+				throw new Error("can't add an expression for a subfield of " + fieldPart + " because there is already an expression that applies to the whole field; uassert code 16401");
 		}
 	}
 
-	if (fieldPath.getPathLength() == 1) {
-		if(haveExpr) throw new Error("Internal error."); // haveExpr case handled above.
+	if (fieldPath.getPathLength() === 1) {
+		if (haveExpr) throw new Error("Assertion error."); // haveExpr case handled above.
 		this._expressions[fieldPart] = pExpression;
 		return;
 	}
 
-	if (!haveExpr) subObj = this._expressions[fieldPart] = new ObjectExpression(false);
+	if (!haveExpr)
+		this._expressions[fieldPart] = subObj = ObjectExpression.create();
 
 	subObj.addField(fieldPath.tail(), pExpression);
 };
 
+
 /**
  * Add a field path to the set of those to be included.
  *
@@ -276,68 +311,38 @@ proto.addField = function addField(fieldPath, pExpression){
  *
  * @method includePath
  * @param fieldPath the name of the field to be included
- **/
-proto.includePath = function includePath(path){
-	this.addField(path, null);
+ */
+proto.includePath = function includePath(theFieldPath) {
+	this.addField(theFieldPath, null);
 };
 
 
 proto.serialize = function serialize(explain) {
 	var valBuilder = {};
 
-	if(this._excludeId) {
-		valBuilder._id = false;
-	}
+	if (this.excludeId)
+		valBuilder[Document.ID_PROPERTY_NAME] = false;
 
-	for(var ii = 0; ii < this._order.length; ii ++) {
-		var fieldName = this._order[ii],
-			expr = this._expressions[fieldName];
+	for (var i = 0, l = this._order.length; i < l; ++i) {
+		var fieldName = this._order[i];
+		if (!this._expressions.hasOwnProperty(fieldName)) throw new Error("Assertion failure");
+		var expr = this._expressions[fieldName];
 
-		if(expr === undefined || expr === null) {
-			valBuilder[fieldName] = {$const:expr};
+		if (!expr) {
+			valBuilder[fieldName] = true;
 		} else {
 			valBuilder[fieldName] = expr.serialize(explain);
 		}
-
 	}
 	return valBuilder;
 };
 
 
-
 /**
  * Get a count of the added fields.
  * @method getFieldCount
  * @returns how many fields have been added
- **/
-proto.getFieldCount = function getFieldCount(){
+ */
+proto.getFieldCount = function getFieldCount() {
 	return Object.getOwnPropertyNames(this._expressions).length;
 };
-
-///**
-//* Specialized BSON conversion that allows for writing out a $project specification.
-//* This creates a standalone object, which must be added to a containing object with a name
-//*
-//* @param pBuilder where to write the object to
-//* @param requireExpression see Expression::addToBsonObj
-//**/
-//TODO:	proto.documentToBson = ...?
-//TODO:	proto.addToBsonObj = ...?
-//TODO: proto.addToBsonArray = ...?
-
-//NOTE: in `munge` we're not passing the `Object`s in and allowing `toJSON` (was `documentToBson`) to modify it directly and are instead building and returning a new `Object` since that's the way it's actually used
-proto.toJSON = function toJSON(requireExpression){
-	var o = {};
-	if (this.excludeId) o[Document.ID_PROPERTY_NAME] = false;
-	for (var i = 0, l = this._order.length; i < l; i++) {
-		var fieldName = this._order[i];
-		if (!this._expressions.hasOwnProperty(fieldName)) throw new Error("internal error: fieldName from _ordered list not found in _expressions");
-		var fieldValue = this._expressions[fieldName];
-		if (fieldValue === undefined) {
-			o[fieldName] = true; // this is inclusion, not an expression
-		} else {
-			o[fieldName] = fieldValue.toJSON(requireExpression);
-		}
-	}
-	return o;
-};

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

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

+ 12 - 26
lib/pipeline/expressions/SecondExpression.js

@@ -2,41 +2,27 @@
 
 /**
  * An $second pipeline expression.
- * @see evaluateInternal
  * @class SecondExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SecondExpression = module.exports = function SecondExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SecondExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SecondExpression, base = require("./FixedArityExpressionT")(SecondExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$second";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCSeconds();  //NOTE: no leap seconds in JS - http://code.google.com/p/v8/issues/detail?id=1944
 };
 
-/**
- * Takes a date and returns the second between 0 and 59, but can be 60 to account for leap seconds.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCSeconds(); //TODO: incorrect for last second of leap year, need to fix...
-	// currently leap seconds are unsupported in v8
-	// http://code.google.com/p/v8/issues/detail?id=1944
+proto.getOpName = function getOpName() {
+	return "$second";
 };
 
-/** Register Expression */
-Expression.registerExpression("$second", base.parse(SecondExpression));
+Expression.registerExpression("$second", base.parse);

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

@@ -1,52 +1,52 @@
 "use strict";
 
 /**
- * A $setdifference pipeline expression.
- * @see evaluateInternal
+ * A $setDifference pipeline expression.
  * @class SetDifferenceExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetDifferenceExpression = module.exports = function SetDifferenceExpression() {
-	this.nargs = 2;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetDifferenceExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetDifferenceExpression, base = require("./FixedArityExpressionT")(SetDifferenceExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("./ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setdifference";
-};
-
-/**
- * Takes 2 arrays. Assigns the second array to the first array.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-	if (array1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an array");
-	if (array2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an array");
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
 
-	var returnVec = [];
+	if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	}
 
-	array1.forEach(function(key) {
-		if (-1 === array2.indexOf(key)) {
-			returnVec.push(key);
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. First " +
+			"argument is of type: " + Value.getType(lhs) + "; uassert code 17048");
+	if (!(rhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. Second " +
+			"argument is of type: " + Value.getType(rhs) + "; uassert code 17049");
+
+	var rhsSet = new ValueSet(rhs),
+		lhsArray = lhs,
+		returnVec = [];
+	for (var i = 0, l = lhsArray.length; i < l; ++i) {
+		// rhsSet serves the dual role of filtering out elements that were originally present
+		// in RHS and of eleminating duplicates from LHS
+		var it = lhsArray[i];
+		if (rhsSet.insert(it) !== undefined) {
+			returnVec.push(it);
 		}
-	}, this);
+	}
 	return returnVec;
 };
 
-/** Register Expression */
-Expression.registerExpression("$setdifference", base.parse(SetDifferenceExpression));
+Expression.registerExpression("$setDifference", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setDifference";
+};

+ 33 - 27
lib/pipeline/expressions/SetEqualsExpression.js

@@ -2,44 +2,50 @@
 
 /**
  * A $setequals pipeline expression.
- * @see evaluateInternal
  * @class SetEqualsExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetEqualsExpression = module.exports = function SetEqualsExpression() {
-	this.nargs = 2;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetEqualsExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetEqualsExpression, base = require("./VariadicExpressionT")(SetEqualsExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("./ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setequals";
+proto.validateArguments = function validateArguments(args) {
+	if (args.length < 2)
+		throw new Error(this.getOpName() + " needs at least two arguments had: " +
+			args.length + "; uassert code 17045");
 };
 
-/**
- * Takes 2 arrays. Assigns the second array to the first array.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-	if (array1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an array");
-	if (array2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an array");
-	array1 = array2;
-	return array1;
+	var n = this.operands.length,
+		lhs;
+
+	for (var i = 0; i < n; i++) {
+		var nextEntry = this.operands[i].evaluateInternal(vars);
+		if (!(nextEntry instanceof Array))
+			throw new Error("All operands of " + this.getOpName() +" must be arrays. One " +
+				"argument is of type: " + Value.getType(nextEntry) + "; uassert code 17044");
+
+		if (i === 0) {
+			lhs = new ValueSet(nextEntry);
+		} else {
+			var rhs = new ValueSet(nextEntry);
+			if (!lhs.equals(rhs)) {
+				return false;
+			}
+		}
+	}
+	return true;
 };
 
-/** Register Expression */
-Expression.registerExpression("$setequals", base.parse(SetEqualsExpression));
+Expression.registerExpression("$setEquals", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setEquals";
+};

+ 43 - 29
lib/pipeline/expressions/SetIntersectionExpression.js

@@ -2,46 +2,60 @@
 
 /**
  * A $setintersection pipeline expression.
- * @see evaluateInternal
  * @class SetIntersectionExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetIntersectionExpression = module.exports = function SetIntersectionExpression() {
-	this.nargs = 2;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetIntersectionExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetIntersectionExpression, base = require("./VariadicExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("./ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setIntersection";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var n = this.operands.length,
+		currentIntersection = new ValueSet();
+	for (var i = 0; i < n; i++){
+		var nextEntry = this.operands[i].evaluateInternal(vars);
+		if (nextEntry === undefined || nextEntry === null){
+			return null;
+		}
+		if (!(nextEntry instanceof Array))
+			 throw new Error("All operands of " + this.getOpName() + "must be arrays. One " +
+				"argument is of type: " + Value.getType(nextEntry) + "; uassert code 17047");
+
+		if (i === 0){
+			currentIntersection.insertRange(nextEntry);
+		} else {
+			var nextSet = new ValueSet(nextEntry);
+			if (currentIntersection.size() > nextSet.size()) {
+				// to iterate over whichever is the smaller set
+				nextSet.swap(currentIntersection);
+			}
+			for (var itKey in currentIntersection.set) {
+				if (!nextSet.hasKey(itKey)) {
+					currentIntersection.eraseKey(itKey);
+				}
+			}
+		}
+		if (currentIntersection.empty()) {
+			break;
+		}
+	}
+	var result = currentIntersection.values();
+	return result;
 };
 
-/**
- * Takes 2 objects. Returns the intersects of the objects.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var object1 = this.operands[0].evaluateInternal(vars),
-		object2 = this.operands[1].evaluateInternal(vars);
-	if (object1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an object");
-	if (object2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an object");
+Expression.registerExpression("$setIntersection", base.parse);
 
-	var result = object1.filter(function(n) {
-		return object2.indexOf(n) > -1;
-	});
+proto.getOpName = function getOpName() {
+	return "$setIntersection";
 };
 
-/** Register Expression */
-Expression.registerExpression("$setIntersection", base.parse(SetIntersectionExpression));
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 69 - 61
lib/pipeline/expressions/SetIsSubsetExpression.js

@@ -7,82 +7,90 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetIsSubsetExpression = module.exports = function SetIsSubsetExpression() {
-	this.nargs = 2;
-	if (arguments.length !== 2) throw new Error("two args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetIsSubsetExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetIsSubsetExpression, base = require("./FixedArityExpressionT")(SetIsSubsetExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
-
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setissubset";
-};
+	Expression = require("./Expression"),
+	NaryExpression = require("./NaryExpression"),
+	ConstantExpression = require("./ConstantExpression"),
+	ValueSet = require("./ValueSet");
+
+function setIsSubsetHelper(lhs, rhs) { //NOTE: vector<Value> &lhs, ValueSet &rhs
+	// do not shortcircuit when lhs.size() > rhs.size()
+	// because lhs can have redundant entries
+	for (var i = 0; i < lhs.length; i++) {
+		if (!rhs.has(lhs[i])) {
+			return false;
+		}
+	}
+	return true;
+}
 
-proto.optimize = function optimize(cachedRhsSet, operands) {
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
 
-// This optimize needs to be done, eventually
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + ": must be arrays. First " +
+			"argument is of type " + Value.getType(lhs) + "; uassert code 17046");
+	if (!(rhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + ": must be arrays. Second " +
+			"argument is of type " + Value.getType(rhs) + "; code 17042");
 
-// // perfore basic optimizations
-//     intrusive_ptr<Expression> optimized = ExpressionNary::optimize();
+	return setIsSubsetHelper(lhs, new ValueSet(rhs));
+};
 
-//     // if ExpressionNary::optimize() created a new value, return it directly
-//     if (optimized.get() != this)
-//         return optimized;
 
-//     if (ExpressionConstant* ec = dynamic_cast<ExpressionConstant*>(vpOperand[1].get())) {
-//         const Value rhs = ec->getValue();
-//         uassert(17311, str::stream() << "both operands of $setIsSubset must be arrays. Second "
-//                                      << "argument is of type: " << typeName(rhs.getType()),
-//                 rhs.getType() == Array);
+/**
+ * This class handles the case where the RHS set is constant.
+ *
+ * Since it is constant we can construct the hashset once which makes the runtime performance
+ * effectively constant with respect to the size of RHS. Large, constant RHS is expected to be a
+ * major use case for $redact and this has been verified to improve performance significantly.
+ */
+function Optimized(cachedRhsSet, operands) {
+	this._cachedRhsSet = cachedRhsSet;
+	this.operands = operands;
+}
+Optimized.prototype = Object.create(SetIsSubsetExpression.prototype, {constructor:{value:Optimized}});
+Optimized.prototype.evaluateInternal = function evaluateInternal(vars){
+	var lhs = this.operands[0].evaluateInternal(vars);
+
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. First " +
+			"argument is of type: " + Value.getType(lhs) + "; uassert code 17310");
+
+	return setIsSubsetHelper(lhs, this._cachedRhsSet);
+};
 
-//         return new Optimized(arrayToSet(rhs), vpOperand);
-//     }
 
-//     return optimized;
+proto.optimize = function optimize(cachedRhsSet, operands) { //jshint ignore:line
+	// perform basic optimizations
+	var optimized = NaryExpression.optimize();
 
-};
+	// if ExpressionNary::optimize() created a new value, return it directly
+	if(optimized !== this)
+		return optimized;
 
-/**
- * Takes 2 arrays. Assigns the second array to the first array.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-	if (array1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an array");
-	if (array2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an array");
-
-	var sizeOfArray1 = array1.length;
-	var sizeOfArray2 = array2.length;
-	var outerLoop = 0;
-	var innerLoop = 0;
-	for (outerLoop = 0; outerLoop < sizeOfArray1; outerLoop++) {
-		for (innerLoop = 0; innerLoop < sizeOfArray2; innerLoop++) {
-			if (array2[outerLoop] == array1[innerLoop])
-				break;
-		}
+	var ce;
+	if ((ce = this.operands[1] instanceof ConstantExpression ? this.operands[1] : undefined)){
+		var rhs = ce.getValue();
+		if (!(rhs instanceof Array))
+			throw new Error("both operands of " + this.getOpName() + " must be arrays. Second " +
+				"argument is of type " + Value.getType(rhs) + "; uassert code 17311");
 
-		/* If the above inner loop was not broken at all then
-		 array2[i] is not present in array1[] */
-		if (innerLoop == sizeOfArray2)
-			return false;
+		return new Optimized(new ValueSet(rhs), this.operands);
 	}
 
-	/* If we reach here then all elements of array2[]
-	 are present in array1[] */
-	return true;
+	return optimized;
 };
 
-/** Register Expression */
-Expression.registerExpression("$setissubset", base.parse(SetIsSubsetExpression));
+Expression.registerExpression("$setIsSubset", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setIsSubset";
+};

+ 25 - 34
lib/pipeline/expressions/SetUnionExpression.js

@@ -2,52 +2,43 @@
 
 /**
  * A $setunion pipeline expression.
- * @see evaluateInternal
  * @class SetUnionExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetUnionExpression = module.exports = function SetUnionExpression() {
-	this.nargs = 2;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetUnionExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetUnionExpression, base = require("./VariadicExpressionT")(SetUnionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("./ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setUnion";
-};
-
-/**
- * Takes 2 objects. Unions the objects
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var object1 = this.operands[0].evaluateInternal(vars),
-		object2 = this.operands[1].evaluateInternal(vars);
-	if (object1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an object");
-	if (object2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an object");
+	var unionedSet = new ValueSet(),
+		n = this.operands.length;
+	for (var i = 0; i < n; i++){
+		var newEntries = this.operands[i].evaluateInternal(vars);
+		if (newEntries === undefined || newEntries === null){
+			return null;
+		}
+		if (!(newEntries instanceof Array))
+			throw new Error("All operands of " + this.getOpName() + "must be arrays. One argument" +
+				" is of type: " + Value.getType(newEntries) + "; uassert code 17043");
 
-	var object3 = {};
-	for (var attrname1 in object1) {
-		object3[attrname1] = object1[attrname1];
-	}
-	for (var attrname2 in object2) {
-		object3[attrname2] = object2[attrname2];
+		unionedSet.insertRange(newEntries);
 	}
+	return unionedSet.values();
+};
+
+Expression.registerExpression("$setUnion", base.parse);
 
-	return object3;
+proto.getOpName = function getOpName() {
+	return "$setUnion";
 };
 
-/** Register Expression */
-Expression.registerExpression("$setUnion", base.parse(SetUnionExpression));
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 10 - 21
lib/pipeline/expressions/SizeExpression.js

@@ -6,36 +6,25 @@
  * @class SizeExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
+ * @extends mungedb-aggregate.pipeline.FixedArityExpressionT
  * @constructor
- **/
+ */
 var SizeExpression = module.exports = function SizeExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
-}, klass = SizeExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SizeExpression, base = require("./FixedArityExpressionT")(SizeExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$size";
-};
-
-/**
- * Takes an array and return the size.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var array = this.operands[0].evaluateInternal(vars);
-	if (array instanceof Date) throw new Error("$size does not support dates; code 16376");
+	if (!(array instanceof Array)) throw new Error("The argument to $size must be an Array but was of type" + Value.getType(array) + "; uassert code 16376");
 	return array.length;
 };
 
-/** Register Expression */
-Expression.registerExpression("$size", base.parse(SizeExpression));
+Expression.registerExpression("$size", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$size";
+};

+ 22 - 28
lib/pipeline/expressions/StrcasecmpExpression.js

@@ -7,40 +7,34 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var StrcasecmpExpression = module.exports = function StrcasecmpExpression() {
-	this.nargs = 2;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = StrcasecmpExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = StrcasecmpExpression, base = require("./FixedArityExpressionT")(StrcasecmpExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	NaryExpression = require("./NaryExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$strcasecmp";
-};
-
-/**
- * Takes in two strings. Returns a number. $strcasecmp is positive if the first string is “greater than” the second and negative if the first string is “less than” the second. $strcasecmp returns 0 if the strings are identical.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val1 = this.operands[0].evaluateInternal(vars),
-		val2 = this.operands[1].evaluateInternal(vars),
-		str1 = Value.coerceToString(val1).toUpperCase(),
-		str2 = Value.coerceToString(val2).toUpperCase(),
-		cmp = Value.compare(str1, str2);
-	return cmp;
+	var string1 = this.operands[0].evaluateInternal(vars),
+		string2 = this.operands[1].evaluateInternal(vars);
+
+	var str1 = Value.coerceToString(string1).toUpperCase(),
+		str2 = Value.coerceToString(string2).toUpperCase(),
+		result = Value.compare(str1, str2);
+
+	if (result === 0) {
+		return 0;
+	} else if (result > 0) {
+		return 1;
+	} else {
+		return -1;
+	}
 };
 
-/** Register Expression */
-Expression.registerExpression("$strcasecmp", base.parse(StrcasecmpExpression));
+Expression.registerExpression("$strcasecmp", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$strcasecmp";
+};

+ 24 - 31
lib/pipeline/expressions/SubstrExpression.js

@@ -7,43 +7,36 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SubstrExpression = module.exports = function SubstrExpression() {
-	this.nargs = 3;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SubstrExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubstrExpression, base = require("./FixedArityExpressionT")(klass, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$substr";
-};
-
-/**
- * Takes a string and two numbers. The first number represents the number of bytes in the string to skip, and the second number specifies the number of bytes to return from the string.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		idx = this.operands[1].evaluateInternal(vars),
-		len = this.operands[2].evaluateInternal(vars),
-		str = Value.coerceToString(val);
-	if (typeof(idx) != "number") throw new Error(this.getOpName() + ": starting index must be a numeric type; code 16034");
-	if (typeof(len) != "number") throw new Error(this.getOpName() + ": length must be a numeric type; code 16035");
-	if (idx >= str.length) return "";
-	//TODO: Need to handle -1
-	len = (len === -1 ? undefined : len);
-	return str.substr(idx, len);
+	var string = this.operands[0].evaluateInternal(vars),
+		pLower = this.operands[1].evaluateInternal(vars),
+		pLength = this.operands[2].evaluateInternal(vars);
+
+	var str = Value.coerceToString(string);
+	if (typeof pLower !== "number") throw new Error(this.getOpName() + ":  starting index must be a numeric type (is type " + Value.getType(pLower) + "); uassert code 16034");
+	if (typeof pLength !== "number") throw new Error(this.getOpName() + ":  length must be a numeric type (is type " + Value.getType(pLength) + "); uassert code 16035");
+
+	var lower = Value.coerceToLong(pLower),
+		length = Value.coerceToLong(pLength);
+	if (lower >= str.length) {
+		// If lower > str.length() then string::substr() will throw out_of_range, so return an
+		// empty string if lower is not a valid string index.
+		return "";
+	}
+	return str.substr(lower, length);
 };
 
-/** Register Expression */
-Expression.registerExpression("$substr", base.parse(SubstrExpression));
+Expression.registerExpression("$substr", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$substr";
+};

+ 36 - 25
lib/pipeline/expressions/SubtractExpression.js

@@ -4,39 +4,50 @@
  * A $subtract pipeline expression.
  * @see evaluateInternal
  * @class SubtractExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var SubtractExpression = module.exports = function SubtractExpression(){
-	this.nargs = 2;
+ */
+var SubtractExpression = module.exports = function SubtractExpression() {
 	base.call(this);
-}, klass = SubtractExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubtractExpression, base = require("./FixedArityExpressionT")(SubtractExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$subtract";
-};
-
-/**
-* Takes an array that contains a pair of numbers and subtracts the second from the first, returning their difference.
-**/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$subtract does not support dates; code 16376");
-	return left - right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		return lhs - rhs;
+	} else if (lhs === null || lhs === undefined || rhs === null || rhs === undefined) {
+		return null;
+	} else if (lhs instanceof Date) {
+		if (rhs instanceof Date) {
+			var timeDelta = lhs - rhs;
+			return timeDelta;
+		} else if (typeof rhs === "number") {
+			var millisSinceEpoch = lhs - Value.coerceToLong(rhs);
+			return millisSinceEpoch;
+		} else {
+			throw new Error("can't $subtract a " +
+				Value.getType(rhs) +
+				" from a Date" +
+				"; uassert code 16613");
+		}
+	} else {
+		throw new Error("can't $subtract a " +
+			Value.getType(rhs) +
+			" from a " +
+			Value.getType(lhs) +
+			"; uassert code 16556");
+	}
 };
 
-/** Register Expression */
-Expression.registerExpression("$subtract", base.parse(SubtractExpression));
+Expression.registerExpression("$subtract", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$subtract";
+};

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

@@ -2,40 +2,27 @@
 
 /**
  * A $toLower pipeline expression.
- * @see evaluateInternal
  * @class ToLowerExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var ToLowerExpression = module.exports = function ToLowerExpression() {
-	this.nargs = 1;
+ */
+var ToLowerExpression = module.exports = function ToLowerExpression(){
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
-}, klass = ToLowerExpression,
-	base = require("./NaryExpression"),
-	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"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$toLower";
-};
-
-/**
- * Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		str = Value.coerceToString(val);
+	var pString = this.operands[0].evaluateInternal(vars),
+		str = Value.coerceToString(pString);
 	return str.toLowerCase();
 };
 
-/** Register Expression */
-Expression.registerExpression("$toLower", base.parse(ToLowerExpression));
+Expression.registerExpression("$toLower", base.parse);
+
+proto.getOpName = function getOpName(){
+	return "$toLower";
+};

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

@@ -2,40 +2,27 @@
 
 /**
  * A $toUpper pipeline expression.
- * @see evaluateInternal
  * @class ToUpperExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ToUpperExpression = module.exports = function ToUpperExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
-}, klass = ToUpperExpression,
-	base = require("./NaryExpression"),
-	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"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$toUpper";
-};
-
-/**
- * Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		str = Value.coerceToString(val);
+	var pString = this.operands[0].evaluateInternal(vars),
+		str = Value.coerceToString(pString);
 	return str.toUpperCase();
 };
 
-/** Register Expression */
-Expression.registerExpression("$toUpper", base.parse(ToUpperExpression));
+Expression.registerExpression("$toUpper", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$toUpper";
+};

+ 88 - 0
lib/pipeline/expressions/ValueSet.js

@@ -0,0 +1,88 @@
+"use strict";
+
+/**
+ * Somewhat mimics the ValueSet in mongo (std::set<Value>)
+ * @class valueSet
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ */
+var ValueSet = module.exports = function ValueSet(vals) {
+	this.set = {};
+	if (vals instanceof Array)
+		this.insertRange(vals);
+}, klass = ValueSet, proto = klass.prototype;
+
+proto._getKey = JSON.stringify;
+
+proto.hasKey = function hasKey(key) {
+	return key in this.set;
+};
+//SKIPPED: proto.count -- use hasKey instead
+
+proto.has = function has(val) {
+	return this._getKey(val) in this.set;
+};
+
+proto.insert = function insert(val) {
+	var valKey = this._getKey(val);
+	if (!this.hasKey(valKey)) {
+		this.set[valKey] = val;
+		return valKey;
+	}
+	return undefined;
+};
+
+proto.insertRange = function insertRange(vals) {
+	var results = [];
+	for (var i = 0, l = vals.length; i < l; i++)
+		results.push(this.insert(vals[i]));
+	return results;
+};
+
+proto.equals = function equals(other) {
+	for (var key in this.set) {
+		if (!other.hasKey(key))
+			return false;
+	}
+	for (var otherKey in other.set) {
+		if (!this.hasKey(otherKey))
+			return false;
+	}
+	return true;
+};
+
+proto.values = function values() {
+	var vals = [];
+	for (var key in this.set)
+		vals.push(this.set[key]);
+	return vals;
+};
+
+proto.size = function values() {
+	var n = 0;
+	for (var key in this.set) //jshint ignore:line
+		n++;
+	return n;
+};
+
+proto.swap = function swap(other) {
+	var tmp = this.set;
+	this.set = other.set;
+	other.set = tmp;
+};
+
+proto.eraseKey = function eraseKey(key) {
+	delete this.set[key];
+};
+
+proto.erase = function erase(val) {
+	var key = this._getKey(val);
+	this.eraseKey(key);
+};
+
+proto.empty = function empty() {
+	for (var key in this.set) //jshint ignore:line
+		return false;
+	return true;
+};

+ 94 - 74
lib/pipeline/expressions/Variables.js

@@ -1,57 +1,72 @@
 "use strict";
 
+// TODO: Look into merging with ExpressionContext and possibly ObjectCtx.
 /**
- * Class that stores/tracks variables
+ * The state used as input and working space for Expressions.
  * @class Variables
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var Variables = module.exports = function Variables(numVars, root){
-	if(numVars) {
-		if(typeof numVars !== 'number') {
-			throw new Error('numVars must be a number');
-		}
-	}
+	if (arguments.length === 0) numVars = 0; // This is only for expressions that use no variables (even ROOT).
+	if (typeof numVars !== "number") throw new Error("numVars must be a Number");
+
 	this._root = root || {};
-	this._rest = numVars ? [] : undefined; //An array of `Value`s
+	this._rest = numVars === 0 ? null : new Array(numVars);
 	this._numVars = numVars;
-}, klass = Variables,
-	base = Object,
-	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = Variables, proto = klass.prototype;
 
+klass.uassertValidNameForUserWrite = function uassertValidNameForUserWrite(varName) {
+	// System variables users allowed to write to (currently just one)
+	if (varName === "CURRENT") {
+		return;
+	}
 
-klass.ROOT_ID = -1;
+	if (!varName)
+		throw new Error("empty variable names are not allowed; uassert code 16866");
 
-// PROTOTYPE MEMBERS
+	var firstCharIsValid = (varName[0] >= "a" && varName[0] <= "z") ||
+		(varName[0] & "\x80"); // non-ascii
 
-/**
- * Sets the root variable
- * @method setRoot
- * @parameter root {Document} The root variable
- **/
-proto.setRoot = function setRoot(root){
-	if(!(root instanceof Object && root.constructor.name === 'Object')) { //NOTE: Type checking cause c++ does this for you
-		throw new Error('root must be an Object');
+	if (!firstCharIsValid)
+		throw new Error("'" + varName + "' starts with an invalid character for a user variable name; uassert code 16867");
+
+	for (var i = 1, l = varName.length; i < l; i++) {
+		var charIsValid = (varName[i] >= 'a' && varName[i] <= 'z') ||
+			(varName[i] >= 'A' && varName[i] <= 'Z') ||
+			(varName[i] >= '0' && varName[i] <= '9') ||
+			(varName[i] == '_') ||
+			(varName[i] & '\x80'); // non-ascii
+
+		if (!charIsValid)
+			throw new Error("'" + varName + "' contains an invalid character " +
+				"for a variable name: '" + varName[i] + "'; uassert code 16868");
 	}
-	this._root = root;
 };
 
-/**
- * Clears the root variable
- * @method clearRoot
- **/
-proto.clearRoot = function clearRoot(){
-	this._root = {};
-};
+klass.uassertValidNameForUserRead = function uassertValidNameForUserRead(varName) {
+	if (!varName)
+		throw new Error("empty variable names are not allowed; uassert code 16869");
 
-/**
- * Gets the root variable
- * @method getRoot
- * @return {Document} the root variable
- **/
-proto.getRoot = function getRoot(){
-	return this._root;
+	var firstCharIsValid = (varName[0] >= "a" && varName[0] <= "z") ||
+		(varName[0] >= "A" && varName[0] <= "Z") ||
+		(varName[0] & "\x80"); // non-ascii
+
+	if (!firstCharIsValid)
+		throw new Error("'" + varName + "' starts with an invalid character for a variable name; uassert code 16870");
+
+	for (var i = 1, l = varName.length; i < l; i++) {
+		var charIsValid = (varName[i] >= "a" && varName[i] <= "z") ||
+			(varName[i] >= "A" && varName[i] <= "Z") ||
+			(varName[i] >= "0" && varName[i] <= "9") ||
+			(varName[i] == "_") ||
+			(varName[i] & "\x80"); // non-ascii
+
+		if (!charIsValid)
+			throw new Error("'" + varName + "' contains an invalid character " +
+				"for a variable name: '" + varName[i] + "'; uassert code 16871");
+	}
 };
 
 /**
@@ -59,20 +74,11 @@ proto.getRoot = function getRoot(){
  * @method setValue
  * @param id {Number} The index where the value is stored in the _rest Array
  * @param value {Value} The value to store
- **/
+ */
 proto.setValue = function setValue(id, value) {
-	//NOTE: Some added type enforcement cause c++ does this for you
-	if(typeof id !== 'number') {
-		throw new Error('id must be a Number');
-	}
-
-	if(id === klass.ROOT_ID) {
-		throw new Error("mError 17199: can't use Variables#setValue to set ROOT");
-	}
-	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
-		throw new Error("You have more variables than _numVars");
-	}
-
+	if (typeof id !== "number") throw new Error("id must be a Number");
+	if (id === klass.ROOT_ID) throw new Error("can't use Variables#setValue to set ROOT; massert code 17199");
+	if (id >= this._numVars) throw new Error("Assertion error");
 	this._rest[id] = value;
 };
 
@@ -81,46 +87,60 @@ proto.setValue = function setValue(id, value) {
  * @method getValue
  * @param id {Number} The index where the value was stored
  * @return {Value} The value
- **/
+ */
 proto.getValue = function getValue(id) {
-	//NOTE: Some added type enforcement cause c++ does this for you
-	if(typeof id !== 'number') {
-		throw new Error('id must be a Number');
-	}
-
-	if(id === klass.ROOT_ID) {
+	if (typeof id !== "number") throw new Error("id must be a Number");
+	if (id === klass.ROOT_ID)
 		return this._root;
-	}
-	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
-		throw new Error("Cannot get value; id was greater than _numVars");
-	}
-
+	if (id >= this._numVars) throw new Error("Assertion error");
 	return this._rest[id];
 };
 
-
 /**
  * Get the value for id if it's a document
  * @method getDocument
  * @param id {Number} The index where the document was stored
  * @return {Object} The document
- **/
+ */
 proto.getDocument = function getDocument(id) {
-	//NOTE: Some added type enforcement cause c++ does this for you
-	if(typeof id !== 'number') {
-		throw new Error('id must be a Number');
-	}
+	if (typeof id !== "number") throw new Error("id must be a Number");
 
-	if(id === klass.ROOT_ID) {
+	if (id === klass.ROOT_ID)
 		return this._root;
-	}
-	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
-		throw new Error("Cannot get value; id was greater than _numVars");
-	}
 
+	if (id >= this._numVars) throw new Error("Assertion error");
 	var value = this._rest[id];
-	if(typeof value === 'object' && value.constructor.name === 'Object') {
+	if (value instanceof Object && value.constructor === Object)
 		return value;
-	}
+
 	return {};
 };
+
+klass.ROOT_ID = -1;
+
+/**
+ * Use this instead of setValue for setting ROOT
+ * @method setRoot
+ * @parameter root {Document} The root variable
+ */
+proto.setRoot = function setRoot(root){
+	if (!(root instanceof Object && root.constructor === Object)) throw new Error("Assertion failure");
+	this._root = root;
+};
+
+/**
+ * Clears the root variable
+ * @method clearRoot
+ */
+proto.clearRoot = function clearRoot(){
+	this._root = {};
+};
+
+/**
+ * Gets the root variable
+ * @method getRoot
+ * @return {Document} the root variable
+ */
+proto.getRoot = function getRoot(){
+	return this._root;
+};

+ 8 - 8
lib/pipeline/expressions/VariablesIdGenerator.js

@@ -1,31 +1,31 @@
 "use strict";
 
-/** 
- * Class generates unused ids
+/**
+ * Generates Variables::Ids and keeps track of the number of Ids handed out.
  * @class VariablesIdGenerator
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var VariablesIdGenerator = module.exports = function VariablesIdGenerator(){
 	this._nextId = 0;
-}, klass = VariablesIdGenerator, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = VariablesIdGenerator, proto = klass.prototype;
 
 /**
  * Gets the next unused id
  * @method generateId
  * @return {Number} The unused id
- **/
+ */
 proto.generateId = function generateId() {
 	return this._nextId++;
 };
 
 /**
- * Gets the number of used ids
+ * Returns the number of Ids handed out by this Generator.
+ * Return value is intended to be passed to Variables constructor.
  * @method getIdCount
  * @return {Number} The number of used ids
- **/
+ */
 proto.getIdCount = function getIdCount() {
 	return this._nextId;
 };
-

+ 26 - 23
lib/pipeline/expressions/VariablesParseState.js

@@ -1,22 +1,25 @@
 "use strict";
 
-/** 
- * Class generates unused ids
+/**
+ * This class represents the Variables that are defined in an Expression tree.
+ *
+ * All copies from a given instance share enough information to ensure unique Ids are assigned
+ * and to propagate back to the original instance enough information to correctly construct a
+ * Variables instance.
+ *
  * @class VariablesParseState
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var Variables = require('./Variables'),
-	VariablesIdGenerator = require('./VariablesIdGenerator');
+ */
+var Variables = require("./Variables"),
+	VariablesIdGenerator = require("./VariablesIdGenerator");
 
 var VariablesParseState = module.exports = function VariablesParseState(idGenerator){
-	if(!idGenerator || idGenerator.constructor !== VariablesIdGenerator) {
-		throw new Error("idGenerator is required and must be of type VariablesIdGenerator");
-	}
+	if (!(idGenerator instanceof VariablesIdGenerator)) throw new Error("idGenerator is required and must be of type VariablesIdGenerator");
 	this._idGenerator = idGenerator;
-	this._variables = {}; //Note: The c++ type was StringMap<Variables::Id>
-}, klass = VariablesParseState, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	this._variables = {};
+}, klass = VariablesParseState, proto = klass.prototype;
 
 /**
  * Assigns a named variable a unique Id. This differs from all other variables, even
@@ -27,12 +30,12 @@ var VariablesParseState = module.exports = function VariablesParseState(idGenera
  * breaks that equivalence.
  *
  * NOTE: Name validation is responsibility of caller.
- **/
-proto.defineVariable = function generateId(name) {
+ */
+proto.defineVariable = function defineVariable(name) {
 	// caller should have validated before hand by using Variables::uassertValidNameForUserWrite
-	if(name === 'ROOT') {
-		throw new Error("mError 17275: Can't redefine ROOT");
-	}
+	if (name === "ROOT")
+		throw new Error("Can't redefine ROOT; massert code 17275");
+
 	var id = this._idGenerator.generateId();
 	this._variables[name] = id;
 	return id;
@@ -42,14 +45,14 @@ proto.defineVariable = function generateId(name) {
  * Returns the current Id for a variable. uasserts if the variable isn't defined.
  * @method getVariable
  * @param name {String} The name of the variable
- **/
-proto.getVariable = function getIdCount(name) {
-	var found = this._variables[name];
-	if(typeof found === 'number') return found;
-	if(name !== "ROOT" && name !== "CURRENT") {
-		throw new Error("uError 17276: Use of undefined variable " + name);
-	}
+ */
+proto.getVariable = function getVariable(name) {
+	var it = this._variables[name];
+	if (typeof it === "number")
+		return it;
+
+	if (name !== "ROOT" && name !== "CURRENT")
+		throw new Error("Use of undefined variable " + name + "; uassert code 17276");
 
 	return Variables.ROOT_ID;
 };
-

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

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

+ 14 - 31
lib/pipeline/expressions/WeekExpression.js

@@ -2,49 +2,32 @@
 
 /**
  * A $week pipeline expression.
- * @see evaluateInternal
  * @class WeekExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var WeekExpression = module.exports = function WeekExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = WeekExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = WeekExpression, base = require("./FixedArityExpressionT")(WeekExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	DayOfYearExpression = require("./DayOfYearExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$week";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate),
+		//NOTE: DEVIATION FROM MONGO: our calculations are a little different but are equivalent
+		y11 = new Date(date.getUTCFullYear(), 0, 1), // same year, first month, first day; time omitted
+		ymd = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1), // same y,m,d; time omitted, add 1 because days start at 1
+		yday = Math.ceil((ymd - y11) / 86400000); // count days
+	return (yday / 7) | 0;
 };
 
-/**
- * Takes a date and returns the week of the year as a number between 0 and 53.
- * Weeks begin on Sundays, and week 1 begins with the first Sunday of the year.
- * Days preceding the first Sunday of the year are in week 0.
- * This behavior is the same as the “%U” operator to the strftime standard library function.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars),
-		dayOfWeek = date.getUTCDay(),
-		dayOfYear = DayOfYearExpression.getDateDayOfYear(date),
-		prevSundayDayOfYear = dayOfYear - dayOfWeek, // may be negative
-		nextSundayDayOfYear = prevSundayDayOfYear + 7; // must be positive
-	// Return the zero based index of the week of the next sunday, equal to the one based index of the week of the previous sunday, which is to be returned.
-	return (nextSundayDayOfYear / 7) | 0; // also, the `| 0` here truncates this so that we return an integer
+proto.getOpName = function getOpName() {
+	return "$week";
 };
 
-/** Register Expression */
-Expression.registerExpression("$week", base.parse(WeekExpression));
+Expression.registerExpression("$week", base.parse);

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

@@ -2,42 +2,27 @@
 
 /**
  * A $year pipeline expression.
- * @see evaluateInternal
  * @class YearExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var YearExpression = module.exports = function YearExpression() {
-	this.nargs = 1;
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = YearExpression,
-	base = require("./NaryExpression"),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = YearExpression, base = require("./FixedArityExpressionT")(YearExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	DayOfYearExpression = require("./DayOfYearExpression"),
 	Expression = require("./Expression");
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCFullYear();
+};
 
-// PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
 	return "$year";
 };
 
-/**
- * Takes a date and returns the full year.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCFullYear();
-};
-
-/** 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());
+		},
+
+	},
+
+};

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

@@ -0,0 +1,167 @@
+"use strict";
+var assert = require("assert"),
+	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 = {
+
+	//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));
+				}
+			}
+		},
+
+		"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);
+		},
+
+		"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() {
+			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() {
+			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() {
+			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() {
+			var lDocument = {prop1: 1},
+				rDocument = {prop1: 0},
+				result = Document.compare(lDocument, rDocument);
+			assert.equal(result, 1);
+		},
+
+	},
+
+	".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);
+
+			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);
+		},
+
+	},
+
+	//SKIPPED: FieldIteratorEmpty
+
+	//SKIPPED: FieldIteratorSingle
+
+	//SKIPPED: FieldIteratorMultiple
+
+	".toJson()": {
+
+		"should convert to JSON Object": function() {
+			var doc = {prop1:0};
+			assert.deepEqual(Document.toJson(doc), {prop1:0});
+		},
+
+	},
+
+	"serialize and deserialize for sorter": {
+
+		"should return a string": function serializeDocument() {
+			var doc = {prop1:1},
+				res = Document.serializeForSorter(doc);
+			assert.equal(res, "{\"prop1\":1}");
+		},
+
+		"should return a Document": function deserializeToDocument() {
+			var str = "{\"prop1\":1}",
+				doc = {prop1:1},
+				res = Document.deserializeForSorter(str);
+			assert.deepEqual(res, doc);
+		},
+
+	},
+
+};

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

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

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

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

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

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

+ 148 - 49
test/lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -1,71 +1,170 @@
 "use strict";
 var assert = require("assert"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	AllElementsTrueExpression = require("../../../../lib/pipeline/expressions/AllElementsTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-var allElementsTrueExpression = new AllElementsTrueExpression();
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var ExpectedResultBase = (function() {
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({}),
+					errMsg = "for expression " + fieldFirst +
+							" with argument " + JSON.stringify(args) +
+							" full tree " + JSON.stringify(expr.serialize(false)) +
+							" expected: " + JSON.stringify(expected) +
+							" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+                // var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();
+
+exports.AllElementsTrueExpression = {
 
-module.exports = {
+	"constructor()": {
 
-	"AllElementsTrueExpression": {
+		"should construct instance": function() {
+			assert(new AllElementsTrueExpression() instanceof AllElementsTrueExpression);
+			assert(new AllElementsTrueExpression() instanceof Expression);
+		},
 
-		"constructor()": {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AllElementsTrueExpression("bad stuff");
+			});
+		},
 
-			"should not throw Error when constructing without args": function testConstructor() {
-				assert.doesNotThrow(function() {
-					new AllElementsTrueExpression();
-				});
-			}
+	},
 
+	"#getOpName()": {
+
+		"should return the correct op name; $allElementsTrue": function() {
+			assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $allElements": function testOpName() {
-				assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
-			}
+	"#evaluate()": {
 
+		"should return false if just false": function JustFalse() {
+            new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
 		},
 
-		"#evaluateInternal()": {
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is empty:": function testEmpty() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("asdf");
-				});
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is not an array": function testNonArray() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("This is not an array");
-				});
-			},
-
-			"should return false if first element is false; [false, true, true true]": function testFirstFalse() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [false, true, true, true]
-					}).evaluate()), false);
-			},
-
-			"should return false if last element is false; [true, true, true, false]": function testLastFalse() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [true, true, true, false]
-					}).evaluate()), false);
-			},
-
-			"should return true if all elements are true; [true,true,true,true]": function testAllTrue() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [true, true, true, true]
-					}).evaluate()), true);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-	}
+		"should return false if falsy int": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[0]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-};
+		"should error if given null instead of array": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null],
+					error: [
+						"$allElementsTrue",
+						// "$anyElementTrue",
+					]
+				}
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	},
+
+};

+ 0 - 135
test/lib/pipeline/expressions/AndExpression.js

@@ -1,135 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"AndExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AndExpression();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $and": function testOpName(){
-				assert.equal(new AndExpression().getOpName(), "$and");
-			}
-
-		},
-
-
-		"#evaluate()": {
-
-			"should return true if no operands were given; {$and:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$and:[]}).evaluate(), true);
-			},
-
-			"should return true if operands is one true; {$and:[true]}": function testTrue(){
-				assert.equal(Expression.parseOperand({$and:[true]}).evaluate(), true);
-			},
-
-			"should return false if operands is one false; {$and:[false]}": function testFalse(){
-				assert.equal(Expression.parseOperand({$and:[false]}).evaluate(), false);
-			},
-
-			"should return true if operands are true and true; {$and:[true,true]}": function testTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true]}).evaluate(), true);
-			},
-
-			"should return false if operands are true and false; {$and:[true,false]}": function testTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,false]}).evaluate(), false);
-			},
-
-			"should return false if operands are false and true; {$and:[false,true]}": function testFalseTrue(){
-				assert.equal(Expression.parseOperand({$and:[false,true]}).evaluate(), false);
-			},
-
-			"should return false if operands are false and false; {$and:[false,false]}": function testFalseFalse(){
-				assert.equal(Expression.parseOperand({$and:[false,false]}).evaluate(), false);
-			},
-
-			"should return true if operands are true, true, and true; {$and:[true,true,true]}": function testTrueTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true,true]}).evaluate(), true);
-			},
-
-			"should return false if operands are true, true, and false; {$and:[true,true,false]}": function testTrueTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,true,false]}).evaluate(), false);
-			},
-
-			"should return false if operands are 0 and 1; {$and:[0,1]}": function testZeroOne(){
-				assert.equal(Expression.parseOperand({$and:[0,1]}).evaluate(), false);
-			},
-
-			"should return false if operands are 1 and 2; {$and:[1,2]}": function testOneTwo(){
-				assert.equal(Expression.parseOperand({$and:[1,2]}).evaluate(), true);
-			},
-
-			"should return true if operand is a path String to a truthy value; {$and:['$a']}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$and:['$a']}).evaluate({a:1}), true);
-			}
-
-		},
-
-		"#optimize()": {
-
-			"should optimize a constant expression to a constant; {$and:[1]} == true": function testOptimizeConstantExpression(){
-				assert.deepEqual(Expression.parseOperand({$and:[1]}).optimize().toJSON(true), {$const:true});
-			},
-
-			"should not optimize a non-constant expression; {$and:['$a']}": function testNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a']}).optimize().toJSON(), {$and:['$a']});
-			},
-
-			"optimize an expression beginning with a constant; {$and:[1,'$a']};": function testConstantNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,'$a']}).optimize().toJSON(), {$and:[1,'$a']});
-				assert.notEqual(Expression.parseOperand({$and:[1,'$a']}).optimize().toJSON(), {$and:[0,'$a']});
-			},
-
-			"should optimize an expression with a path and a '1' (is entirely constant); {$and:['$a',1]}": function testNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a',1]}).optimize().toJSON(), {$and:['$a']});
-			},
-
-			"should optimize an expression with a field path and a '0'; {$and:['$a',0]}": function testNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a',0]}).optimize().toJSON(true), {$const:false});
-			},
-
-			"should optimize an expression with two field paths and '1'; {$and:['$a','$b',1]}": function testNonConstantNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a','$b',1]}).optimize().toJSON(), {$and:['$a','$b']});
-			},
-
-			"should optimize an expression with two field paths and '0'; {$and:['$a','$b',0]}": function testNonConstantNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a','$b',0]}).optimize().toJSON(true), {$const:false});
-			},
-
-			"should optimize an expression with '0', '1', and a field path; {$and:[0,1,'$a']}": function testZeroOneNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:[0,1,'$a']}).optimize().toJSON(true), {$const:false});
-			},
-
-			"should optimize an expression with '1', '1', and a field path; {$and:[1,1,'$a']}": function testOneOneNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,1,'$a']}).optimize().toJSON(), {$and:['$a']});
-			},
-
-			"should optimize nested $and expressions properly and optimize out values evaluating to true; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[1]},'$a','$b']}).optimize().toJSON(), {$and:['$a','$b']});
-			},
-
-			"should optimize nested $and expressions containing a nested value evaluating to false; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}).optimize().toJSON(true), {$const:false});
-			}
-
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 213 - 0
test/lib/pipeline/expressions/AndExpression_test.js

@@ -0,0 +1,213 @@
+"use strict";
+var assert = require("assert"),
+	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+
+module.exports = {
+
+	"AndExpression": {
+
+		beforeEach: function() {
+			this.vps = new VariablesParseState(new VariablesIdGenerator());
+		},
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
+					new AndExpression();
+				});
+			},
+
+			"should throw Error when constructing with args": function testConstructor(){
+				assert.throws(function(){
+					new AndExpression(1);
+				});
+			}
+
+		},
+
+		"#getOpName()": {
+
+			"should return the correct op name; $and": function testOpName(){
+				assert.equal(new AndExpression().getOpName(), "$and");
+			}
+
+		},
+
+
+		"#evaluate()": {
+
+			"should return true if no operands were given; {$and:[]}": function testEmpty(){
+				assert.equal(Expression.parseOperand({$and:[]},this.vps).evaluate(), true);
+			},
+
+			"should return true if operands is one true; {$and:[true]}": function testTrue(){
+				assert.equal(Expression.parseOperand({$and:[true]},this.vps).evaluate(), true);
+			},
+
+			"should return false if operands is one false; {$and:[false]}": function testFalse(){
+				assert.equal(Expression.parseOperand({$and:[false]},this.vps).evaluate(), false);
+			},
+
+			"should return true if operands are true and true; {$and:[true,true]}": function testTrueTrue(){
+				assert.equal(Expression.parseOperand({$and:[true,true]},this.vps).evaluate(), true);
+			},
+
+			"should return false if operands are true and false; {$and:[true,false]}": function testTrueFalse(){
+				assert.equal(Expression.parseOperand({$and:[true,false]},this.vps).evaluate(), false);
+			},
+
+			"should return false if operands are false and true; {$and:[false,true]}": function testFalseTrue(){
+				assert.equal(Expression.parseOperand({$and:[false,true]},this.vps).evaluate(), false);
+			},
+
+			"should return false if operands are false and false; {$and:[false,false]}": function testFalseFalse(){
+				assert.equal(Expression.parseOperand({$and:[false,false]},this.vps).evaluate(), false);
+			},
+
+			"should return true if operands are true, true, and true; {$and:[true,true,true]}": function testTrueTrueTrue(){
+				assert.equal(Expression.parseOperand({$and:[true,true,true]},this.vps).evaluate(), true);
+			},
+
+			"should return false if operands are true, true, and false; {$and:[true,true,false]}": function testTrueTrueFalse(){
+				assert.equal(Expression.parseOperand({$and:[true,true,false]},this.vps).evaluate(), false);
+			},
+
+			"should return false if operands are 0 and 1; {$and:[0,1]}": function testZeroOne(){
+				assert.equal(Expression.parseOperand({$and:[0,1]},this.vps).evaluate(), false);
+			},
+
+			"should return false if operands are 1 and 2; {$and:[1,2]}": function testOneTwo(){
+				assert.equal(Expression.parseOperand({$and:[1,2]},this.vps).evaluate(), true);
+			},
+
+			"should return true if operand is a path String to a truthy value; {$and:['$a']}": function testFieldPath(){
+				assert.equal(Expression.parseOperand({$and:['$a']},this.vps).evaluate({a:1}), true);
+			}
+
+		},
+
+		"#optimize()": {
+
+			"should optimize a constant expression to a constant; {$and:[1]} == true": function testOptimizeConstantExpression(){
+				var a = Expression.parseOperand({$and:[1]}, this.vps).optimize();
+				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
+				assert.equal(a.evaluateInternal(), true);
+			},
+
+			"should not optimize a non-constant expression; {$and:['$a']}": function testNonConstant(){
+				var a = Expression.parseOperand({$and:['$a']}, this.vps).optimize();
+				assert.equal(a.operands[0]._fieldPath.fieldNames.length, 2);
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
+			},
+
+			"should not optimize an expression ending with a non-constant. {$and:[1,'$a']};": function testConstantNonConstant(){
+				var a = Expression.parseOperand({$and:[1,'$a']}, this.vps).optimize();
+				assert(a instanceof CoerceToBoolExpression);
+				assert(a.expression instanceof FieldPathExpression);
+
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
+			},
+
+			"should optimize an expression with a path and a '1'; {$and:['$a',1]}": function testNonConstantOne(){
+				var a = Expression.parseOperand({$and:['$a', 1]}, this.vps).optimize();
+				// The 1 should be removed as it is redundant.
+				assert(a instanceof CoerceToBoolExpression, "The result should be forced to a boolean");
+
+				// This is the '$a' which cannot be optimized.
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
+			},
+
+			"should optimize an expression with a field path and a '0'; {$and:['$a',0]}": function testNonConstantZero(){
+				var a = Expression.parseOperand({$and:['$a',0]}, this.vps).optimize();
+				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
+				assert.equal(a.evaluateInternal(), false, "The 0 operand should have been converted to false");
+			},
+
+			"should optimize an expression with two field paths and '1'; {$and:['$a','$b',1]}": function testNonConstantNonConstantOne(){
+				var a = Expression.parseOperand({$and:['$a', '$b', 1]}, this.vps).optimize();
+				assert.equal(a.operands.length, 2, "Two operands should remain.");
+
+				// This is the '$a' which cannot be optimized.
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames.length, 2);
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
+
+				// This is the '$b' which cannot be optimized.
+				assert.deepEqual(a.operands[1]._fieldPath.fieldNames.length, 2);
+				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[0], "CURRENT");
+				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[1], "b");
+			},
+
+			"should optimize an expression with two field paths and '0'; {$and:['$a','$b',0]}": function testNonConstantNonConstantZero(){
+				var a = Expression.parseOperand({$and:['$a', '$b', 0]}, this.vps).optimize();
+				assert(a instanceof ConstantExpression, "With that trailing false, we know the result...");
+				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
+				assert.equal(a.evaluateInternal(), false);
+			},
+
+			"should optimize an expression with '0', '1', and a field path; {$and:[0,1,'$a']}": function testZeroOneNonConstant(){
+				var a = Expression.parseOperand({$and:[0,1,'$a']}, this.vps).optimize();
+				assert(a instanceof ConstantExpression);
+				assert.equal(a.evaluateInternal(), false);
+			},
+
+			"should optimize an expression with '1', '1', and a field path; {$and:[1,1,'$a']}": function testOneOneNonConstant(){
+				var a = Expression.parseOperand({$and:[1,1,'$a']}, this.vps).optimize();
+				assert(a instanceof CoerceToBoolExpression);
+				assert(a.expression instanceof FieldPathExpression);
+
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
+			},
+
+			"should optimize nested $and expressions properly and optimize out values evaluating to true; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
+				var a = Expression.parseOperand({$and:[1,{$and:[1]},'$a','$b']}, this.vps).optimize();
+				assert.equal(a.operands.length, 2)
+				assert(a.operands[0] instanceof FieldPathExpression);
+				assert(a.operands[1] instanceof FieldPathExpression);
+			},
+
+			"should optimize nested $and expressions containing a nested value evaluating to false; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
+				//assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize().toJSON(true), {$const:false});
+				var a = Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize();
+				assert(a instanceof ConstantExpression);
+				assert.equal(a.evaluateInternal(), false);
+			},
+
+			"should optimize when the constants are on the right of the operand list. The rightmost is true": function(){
+				// 1, "x", and 1 are all true.  They should be optimized away.
+				var a = Expression.parseOperand({$and:['$a', 1, "x", 1]}, this.vps).optimize();
+				assert(a instanceof CoerceToBoolExpression);
+				assert(a.expression instanceof FieldPathExpression);
+
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
+			},
+			"should optimize when the constants are on the right of the operand list. The rightmost is false": function(){
+				// 1, "x", and 1 are all true.  They should be optimized away.
+				var a = Expression.parseOperand({$and:['$a', 1, "x", 0]}, this.vps).optimize();
+				assert(a instanceof ConstantExpression, "The rightmost false kills it all");
+				assert.equal(a.evaluateInternal(), false);
+			}
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 146 - 31
test/lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -1,55 +1,170 @@
 "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();
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var ExpectedResultBase = (function() {
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({}),
+					errMsg = "for expression " + fieldFirst +
+							" with argument " + JSON.stringify(args) +
+							" full tree " + JSON.stringify(expr.serialize(false)) +
+							" expected: " + JSON.stringify(expected) +
+							" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+				// var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();
 
-module.exports = {
+exports.AnyElementTrueExpression = {
 
-	"AnyElementTrueExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should construct instance": function() {
+			assert(new AnyElementTrueExpression() instanceof AnyElementTrueExpression);
+			assert(new AnyElementTrueExpression() instanceof Expression);
+		},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AnyElementTrueExpression();
-				});
-			}
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AnyElementTrueExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
 
+		"should return the correct op name; $anyElementTrue": function() {
+			assert.equal(new AnyElementTrueExpression().getOpName(), "$anyElementTrue");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $anyElement": function testOpName(){
-				assert.equal(new AnyElementTrueExpression().getOpName(), "$anyElementTrue");
-			}
+	"#evaluate()": {
 
+		"should return false if just false": function JustFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
 		},
 
-		"#evaluateInternal()": {
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is not an array": function testEmpty(){
-				assert.throws(function(){
-					anyElementTrueExpression.evaluateInternal("TEST");});
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return true if only true was given a; {true}": function testEmpty(){
-				assert.equal(anyElementTrueExpression.evaluateInternal({$anyElementTrue:[1,2,3,4]}), false);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"should return false if no element is true": function testEmpty(){
-				assert.equal(anyElementTrueExpression.evaluateInternal({$anyElementTrue:[1,2,3,4]}), false);
-			},
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return true if any element is true": function testEmpty(){
-				assert.equal(anyElementTrueExpression.evaluateInternal({$anyElementTrue:[1,true,2,3,4]}), true);
-			},
+		"should return false if falsy int": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[0]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should error if given null instead of array": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null],
+					error: [
+						// "$allElementsTrue",
+						"$anyElementTrue",
+					]
+				}
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 56 - 34
test/lib/pipeline/expressions/CoerceToBoolExpression.js

@@ -1,59 +1,81 @@
 "use strict";
 var assert = require("assert"),
 	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker");
 
+exports.CoerceToBoolExpression = {
 
-module.exports = {
+	"constructor()": {
 
-	"CoerceToBoolExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function construct(){
-				assert.throws(function(){
-					new CoerceToBoolExpression();
-				});
-			}
+		"should create instance": function() {
+			var nested = ConstantExpression.create(5);
+			assert(new CoerceToBoolExpression(nested) instanceof Expression);
+		},
 
+		"should throw Error unless one arg": function() {
+			assert.throws(function() {
+				new CoerceToBoolExpression();
+			});
+			assert.throws(function() {
+				new CoerceToBoolExpression("foo", "bar");
+			});
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return true if nested expression is coerced to true; {$const:5}": function testEvaluateTrue(){
-				var expr = new CoerceToBoolExpression(new ConstantExpression(5));
-				assert.equal(expr.evaluateInternal({}), true);
-			},
+	"#evaluate()": {
 
-			"should return false if nested expression is coerced to false; {$const:0}": function testEvaluateFalse(){
-				var expr = new CoerceToBoolExpression(new ConstantExpression(0));
-				assert.equal(expr.evaluateInternal({}), false);
-			}
+		"should return true if nested expression is coerced to true; {$const:5}": function testEvaluateTrue() {
+			/** Nested expression coerced to true. */
+			var nested = ConstantExpression.create(5),
+				expr = CoerceToBoolExpression.create(nested);
+			assert.strictEqual(expr.evaluate({}), true);
+		},
 
+		"should return false if nested expression is coerced to false; {$const:0}": function testEvaluateFalse() {
+			/** Nested expression coerced to false. */
+			var expr = CoerceToBoolExpression.create(ConstantExpression.create(0));
+			assert.strictEqual(expr.evaluate({}), false);
 		},
 
-		"#addDependencies()": {
+	},
 
-			"should forward dependencies of nested expression": function testDependencies(){
-				var expr = new CoerceToBoolExpression(new FieldPathExpression('a.b')),
-					deps = expr.addDependencies({});
-				assert.equal(Object.keys(deps).length, 1);
-				assert.ok(deps['a.b']);
-			}
+	"#addDependencies()": {
 
+		"should forward dependencies of nested expression": function testDependencies() {
+			/** Dependencies forwarded from nested expression. */
+			var nested = FieldPathExpression.create("a.b"),
+				expr = CoerceToBoolExpression.create(nested),
+				deps = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.strictEqual( Object.keys(deps.fields).length, 1 );
+			assert.strictEqual("a.b" in deps.fields, true);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
 		},
 
-		"#toJSON()": {
+	},
 
-			"should serialize as $and which will coerceToBool; '$foo'": function(){
-				var expr = new CoerceToBoolExpression(new FieldPathExpression('foo'));
-				assert.deepEqual(expr.toJSON(), {$and:['$foo']});
-			}
+	"#serialize": {
 
-		}
+		"should be able to output in to JSON Object": function testAddToBsonObj() {
+			/** Output to BSONObj. */
+			var expr = CoerceToBoolExpression.create(FieldPathExpression.create("foo"));
+            // serialized as $and because CoerceToBool isn't an ExpressionNary
+			assert.deepEqual({field:{$and:["$foo"]}}, {field:expr.serialize(false)});
+		},
+
+		"should be able to output in to JSON Array": function testAddToBsonArray() {
+			/** Output to BSONArray. */
+			var expr = CoerceToBoolExpression.create(FieldPathExpression.create("foo"));
+			// serialized as $and because CoerceToBool isn't an ExpressionNary
+			assert.deepEqual([{$and:["$foo"]}], [expr.serialize(false)]);
+		},
 
-	}
+	},
 
 };
 

+ 409 - 308
test/lib/pipeline/expressions/CompareExpression.js

@@ -1,367 +1,468 @@
 "use strict";
 var assert = require("assert"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	pipeline = require("../../../../lib/pipeline"),
+	expressions = pipeline.expressions,
+	Expression = expressions.Expression,
 	CompareExpression = require("../../../../lib/pipeline/expressions/CompareExpression"),
-	FieldRangeExpression = require("../../../../lib/pipeline/expressions/FieldRangeExpression"),
 	VariablesParseState = require("../../../../Lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../Lib/pipeline/expressions/VariablesIdGenerator"),
-	ConstantExpression = require("../../../../Lib/pipeline/expressions/ConstantExpression");
-
-module.exports = {
-
-	"CompareExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function testConstructor() {
-				assert.throws(function() {
-					new CompareExpression();
-				});
-			}
-
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+				base.apply(this, arguments);
+			},
+			base = TestBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps),
+				optimized = expression.optimize();
+			assert.deepEqual(constify(this.expectedOptimized()), expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	FieldRangeOptimize = (function() {
+		var klass = function FieldRangeOptimize() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function(){
+			return this.spec;
+		};
+		return klass;
+	})(),
+	NoOptimize = (function() {
+		var klass = function NoOptimize() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function(){
+			return this.spec;
+		};
+		return klass;
+	})(),
+	ExpectedResultBase = (function() {
+		/** Check expected result for expressions depending on constants. */
+		var klass = function ExpectedResultBase() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			base.prototype.run.call(this);
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps);
+			// Check expression spec round trip.
+			assert.deepEqual(expressionToJson(expression), constify(specElement));
+			// Check evaluation result.
+			assert.strictEqual(expression.evaluate({}), this.expectedResult);
+			// Check that the result is the same after optimizing.
+			var optimized = expression.optimize();
+			assert.strictEqual(optimized.evaluate({}), this.expectedResult);
+		};
+		proto.expectedOptimized = function() {
+			return {$const:this.expectedResult};
+		};
+		return klass;
+	})(),
+	ExpectedTrue = (function(){
+		var klass = function ExpectedTrue() {
+				base.apply(this, arguments);
+			},
+			base = ExpectedResultBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedResult = true;
+		return klass;
+	})(),
+	ExpectedFalse = (function(){
+		var klass = function ExpectedFalse() {
+				base.apply(this, arguments);
+			},
+			base = ExpectedResultBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedResult = false;
+		return klass;
+	})(),
+	ParseError = (function(){
+		var klass = function ParseError() {
+				base.apply(this, arguments);
+			},
+			base = TestBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator);
+			assert.throws(function() {
+				Expression.parseOperand(specElement, vps);
+			});
+		};
+		return klass;
+	})();
+
+exports.CompareExpression = {
+
+	"constructor()": {
+
+		"should throw Error if no args": function() {
+			assert.throws(function() {
+				new CompareExpression();
+			});
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function testOpName() {
-				assert.equal((new CompareExpression(CompareExpression.EQ)).getOpName(), "$eq");
-				assert.equal((new CompareExpression(CompareExpression.NE)).getOpName(), "$ne");
-				assert.equal((new CompareExpression(CompareExpression.GT)).getOpName(), "$gt");
-				assert.equal((new CompareExpression(CompareExpression.GTE)).getOpName(), "$gte");
-				assert.equal((new CompareExpression(CompareExpression.LT)).getOpName(), "$lt");
-				assert.equal((new CompareExpression(CompareExpression.LTE)).getOpName(), "$lte");
-				assert.equal((new CompareExpression(CompareExpression.CMP)).getOpName(), "$cmp");
-			}
-
+		"should throw if more than 1 args": function() {
+			assert.throws(function() {
+				new CompareExpression(1,2);
+			});
 		},
 
-		"#evaluateInternal()": {
-
-			"$eq": {
-
-				"should return false if first < second; {$eq:[1,2]}": function testEqLt() {
-					//debugger;
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					var parseOp = Expression.parseOperand({
-						$eq: [{
-							$const: 1
-						}, {
-							$const: 2
-						}]
-					}, vps);
-					var result = parseOp.evaluateInternal({});
-
-					//assert.equal(new CompareExpression( CompareExpression.EQ).evaluateInternal({"$eq":[1,2]}), false);
-					assert.equal(result, false);
-
-				},
-
-				"should return true if first == second; {$eq:[1,1]}": function testEqEq() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-
-					assert.equal(Expression.parseOperand({
-						$eq: [1, 1]
-					}, vps).evaluateInternal({}), true);
-				},
-
-				"should return false if first > second {$eq:[1,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [1, 0]
-					}).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different types {$eq:[null,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [null, 0]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different types {$eq:[undefined,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [undefined, 0]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different arrays {$eq:[[1],[null]]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[null]
-						]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different arrays {$eq:[[1],[]]}": function testEqGt() {
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[]
-						]
-					}, vps).evaluateInternal({}), false);
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-				},
-
-				"should return true if first and second are the same arrays {$eq:[[1],[1]]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[1]
-						]
-					}, vps).evaluateInternal({}), true);
-				}
-			},
-
-			//      "$ne": {
-
-			//              "should return true if first < second; {$ne:[1,2]}": function testNeLt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,2]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return false if first == second; {$ne:[1,1]}": function testNeLt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first > second; {$ne:[1,0]}": function testNeGt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$gt": {
-
-			//              "should return false if first < second; {$gt:[1,2]}": function testGtLt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,2]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return false if first == second; {$gt:[1,1]}": function testGtLt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first > second; {$gt:[1,0]}": function testGtGt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$gte": {
-
-			//              "should return false if first < second; {$gte:[1,2]}": function testGteLt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,2]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first == second; {$gte:[1,1]}": function testGteLt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,1]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return true if first > second; {$gte:[1,0]}": function testGteGt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$lt": {
-
-			//              "should return true if first < second; {$lt:[1,2]}": function testLtLt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,2]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return false if first == second; {$lt:[1,1]}": function testLtLt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return false if first > second; {$lt:[1,0]}": function testLtGt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,0]}).evaluateInternal({}), false);
-			//              }
-
-			//      },
+		"should not throw if 1 arg and arg is string": function() {
+			assert.doesNotThrow(function() {
+				new CompareExpression("a");
+			});
+		},
 
-			//      "$lte": {
+	},
 
-			//              "should return true if first < second; {$lte:[1,2]}": function testLteLt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,2]}).evaluateInternal({}), true);
-			//              },
+	"#getOpName()": {
 
-			//              "should return true if first == second; {$lte:[1,1]}": function testLteLt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,1]}).evaluateInternal({}), true);
-			//              },
+		"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function() {
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.EQ).getOpName(), "$eq");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.NE).getOpName(), "$ne");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.GT).getOpName(), "$gt");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.GTE).getOpName(), "$gte");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.LT).getOpName(), "$lt");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.LTE).getOpName(), "$lte");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.CMP).getOpName(), "$cmp");
+		},
 
-			//              "should return false if first > second; {$lte:[1,0]}": function testLteGt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,0]}).evaluateInternal({}), false);
-			//              }
+	},
 
-			//      },
+	"#evaluate()": {
 
-			//      "$cmp": {
+		/** $eq with first < second. */
+		"EqLt": function EqLt() {
+			new ExpectedFalse({
+				spec: {$eq:[1,2]},
+			}).run();
+		},
 
-			//              "should return -1 if first < second; {$cmp:[1,2]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,2]}).evaluateInternal({}), -1);
-			//              },
+		/** $eq with first == second. */
+		"EqEq": function EqEq() {
+			new ExpectedTrue({
+				spec: {$eq:[1,1]},
+			}).run();
+		},
 
-			//              "should return 0 if first < second; {$cmp:[1,1]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,1]}).evaluateInternal({}), 0);
-			//              },
+		/** $eq with first > second. */
+		"EqGt": function EqEq() {
+			new ExpectedFalse({
+				spec: {$eq:[1,0]},
+			}).run();
+		},
 
-			//              "should return 1 if first < second; {$cmp:[1,0]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,0]}).evaluateInternal({}), 1);
-			//              },
+		/** $ne with first < second. */
+		"NeLt": function NeLt() {
+			new ExpectedTrue({
+				spec: {$ne:[1,2]},
+			}).run();
+		},
 
-			//              "should return 1 even if comparison is larger; {$cmp:['z','a']}": function testCmpBracketed(){
-			//	      assert.equal(Expression.parseOperand({$cmp:['z','a']}).evaluateInternal({}), 1);
-			//              }
+		/** $ne with first == second. */
+		"NeEq": function NeEq() {
+			new ExpectedFalse({
+				spec: {$ne:[1,1]},
+			}).run();
+		},
 
-			//      },
+		/** $ne with first > second. */
+		"NeGt": function NeGt() {
+			new ExpectedTrue({
+				spec: {$ne:[1,0]},
+			}).run();
+		},
 
-			//      "should throw Error": {
+		/** $gt with first < second. */
+		"GtLt": function GtLt() {
+			new ExpectedFalse({
+				spec: {$gt:[1,2]},
+			}).run();
+		},
 
-			//              "if zero operands are provided; {$ne:[]}": function testZeroOperands(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$ne:[]}).evaluateInternal({});
-			//	      });
-			//              },
+		/** $gt with first == second. */
+		"GtEq": function GtEq() {
+			new ExpectedFalse({
+				spec: {$gt:[1,1]},
+			}).run();
+		},
 
-			//              "if one operand is provided; {$eq:[1]}": function testOneOperand(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$eq:[1]}).evaluateInternal({});
-			//	      });
-			//              },
+		/** $gt with first > second. */
+		"GtGt": function GtGt() {
+			new ExpectedTrue({
+				spec: {$gt:[1,0]},
+			}).run();
+		},
 
-			//              "if three operands are provided; {$gt:[2,3,4]}": function testThreeOperands(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$gt:[2,3,4]}).evaluateInternal({});
-			//	      });
-			//              }
-			//      }
+		/** $gte with first < second. */
+		"GteLt": function GteLt() {
+			new ExpectedFalse({
+				spec: {$gte:[1,2]},
+			}).run();
+		},
 
-			// },
+		/** $gte with first == second. */
+		"GteEq": function GteEq() {
+			new ExpectedTrue({
+				spec: {$gte:[1,1]},
+			}).run();
+		},
 
-			// "#optimize()": {
+		/** $gte with first > second. */
+		"GteGt": function GteGt() {
+			new ExpectedTrue({
+				spec: {$gte:[1,0]},
+			}).run();
+		},
 
-			//      "should optimize constants; {$eq:[1,1]}": function testOptimizeConstants(){
-			//              assert.deepEqual(Expression.parseOperand({$eq:[1,1]}).optimize().toJSON(true), {$const:true});
-			//      },
+		/** $gte with first > second. */
+		"LtLt": function LtLt() {
+			new ExpectedTrue({
+				spec: {$lt:[1,2]},
+			}).run();
+		},
 
-			//      "should not optimize if $cmp op; {$cmp:[1,'$a']}": function testNoOptimizeCmp(){
-			//              assert.deepEqual(Expression.parseOperand({$cmp:[1,'$a']}).optimize().toJSON(), {$cmp:[1,'$a']});
-			//      },
+		/** $lt with first == second. */
+		"LtEq": function LtEq() {
+			new ExpectedFalse({
+				spec: {$lt:[1,1]},
+			}).run();
+		},
 
-			//      "should not optimize if $ne op; {$ne:[1,'$a']}": function testNoOptimizeNe(){
-			//              assert.deepEqual(Expression.parseOperand({$ne:[1,'$a']}).optimize().toJSON(), {$ne:[1,'$a']});
-			//      },
+		/** $lt with first > second. */
+		"LtGt": function LtGt() {
+			new ExpectedFalse({
+				spec: {$lt:[1,0]},
+			}).run();
+		},
 
-			//      "should not optimize if no constants; {$ne:['$a','$b']}": function testNoOptimizeNoConstant(){
-			//              assert.deepEqual(Expression.parseOperand({$ne:['$a','$b']}).optimize().toJSON(), {$ne:['$a','$b']});
-			//      },
+		/** $lte with first < second. */
+		"LteLt": function LteLt() {
+			new ExpectedTrue({
+				spec: {$lte:[1,2]},
+			}).run();
+		},
 
-			//      "should not optimize without an immediate field path;": {
+		/** $lte with first == second. */
+		"LteEq": function LteEq() {
+			new ExpectedTrue({
+				spec: {$lte:[1,1]},
+			}).run();
+		},
 
-			//              "{$eq:[{$and:['$a']},1]}": function testNoOptimizeWithoutFieldPath(){
-			//	      assert.deepEqual(Expression.parseOperand({$eq:[{$and:['$a']},1]}).optimize().toJSON(), {$eq:[{$and:['$a']},1]});
-			//              },
+		/** $lte with first > second. */
+		"LteGt": function LteGt() {
+			new ExpectedFalse({
+				spec: {$lte:[1,0]},
+			}).run();
+		},
 
-			//              "(reversed); {$eq:[1,{$and:['$a']}]}": function testNoOptimizeWithoutFieldPathReverse(){
-			//	      assert.deepEqual(Expression.parseOperand({$eq:[1,{$and:['$a']}]}).optimize().toJSON(), {$eq:[1,{$and:['$a']}]});
-			//              }
+		/** $cmp with first < second. */
+		"CmpLt": function CmpLt() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,2]},
+				expectedResult: -1,
+			}).run();
+		},
 
-			//      },
+		/** $cmp with first == second. */
+		"CmpEq": function CmpEq() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,1]},
+				expectedResult: 0,
+			}).run();
+		},
 
-			//      "should optimize $eq expressions;": {
+		/** $cmp with first > second. */
+		"CmpGt": function CmpGt() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,0]},
+				expectedResult: 1,
+			}).run();
+		},
 
-			//              "{$eq:['$a',1]}": function testOptimizeEq(){
-			//	      var expr = Expression.parseOperand({$eq:['$a',1]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$eq:['$a',1]});
-			//              },
+		/** $cmp results are bracketed to an absolute value of 1. */
+		"CmpBracketed": function CmpBracketed() {
+			var test = new ExpectedResultBase({
+				spec: {$cmp:["z","a"]},
+				expectedResult: 1,
+			}).run();
+		},
 
-			//              "{$eq:[1,'$a']} (reversed)": function testOptimizeEqReverse(){
-			//	      var expr = Expression.parseOperand({$eq:[1,'$a']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$eq:['$a',1]});
-			//              }
+		/** Zero operands provided. */
+		"ZeroOperands": function ZeroOperands() {
+			new ParseError({
+				spec: {$ne:[]},
+			}).run();
+		},
 
-			//      },
+		/** One operands provided. */
+		"OneOperand": function OneOperand() {
+			new ParseError({
+				spec: {$eq:[1]},
+			}).run();
+		},
 
-			//      "should optimize $lt expressions;": {
+        /** Incompatible types can be compared. */
+		"IncompatibleTypes": function IncompatibleTypes() {
+			var specElement = {$ne:["a",1]},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(expr.evaluate({}), true);
+		},
 
-			//              "{$lt:['$a',1]}": function testOptimizeLt(){
-			//	      var expr = Expression.parseOperand({$lt:['$a',1]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lt:['$a',1]});
-			//              },
+		/** Three operands provided. */
+		"ThreeOperands": function ThreeOperands() {
+			new ParseError({
+				spec: {$gt:[2,3,4]},
+			}).run();
+		},
 
-			//              "{$lt:[1,'$a']} (reversed)": function testOptimizeLtReverse(){
-			//	      var expr = Expression.parseOperand({$lt:[1,'$a']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gt:['$a',1]});
-			//              }
+		/**
+		 * An expression depending on constants is optimized to a constant via
+		 * ExpressionNary::optimize().
+		 */
+		"OptimizeConstants": function OptimizeConstants() {
+			new OptimizeBase({
+				spec: {$eq:[1,1]},
+				expectedOptimized: function() {
+					return {$const: true};
+				},
+			}).run();
+		},
 
-			//      },
+		/** $cmp is not optimized. */
+		"NoOptimizeCmp": function NoOptimizeCmp() {
+			new NoOptimize({
+				spec: {$cmp:[1,"$a"]},
+			}).run();
+		},
 
-			//      "should optimize $lte expressions;": {
+		/** $ne is not optimized. */
+		"NoOptimizeNe": function NoOptimizeNe() {
+			new NoOptimize({
+				spec: {$ne:[1,"$a"]},
+			}).run();
+		},
 
-			//              "{$lte:['$b',2]}": function testOptimizeLte(){
-			//	      var expr = Expression.parseOperand({$lte:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lte:['$b',2]});
-			//              },
+		/** No optimization is performend without a constant. */
+		"NoOptimizeNoConstant": function NoOptimizeNoConstant() {
+			new NoOptimize({
+				spec: {$ne:["$a", "$b"]},
+			}).run();
+		},
 
-			//              "{$lte:[2,'$b']} (reversed)": function testOptimizeLteReverse(){
-			//	      var expr = Expression.parseOperand({$lte:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gte:['$b',2]});
-			//              }
+		/** No optimization is performend without an immediate field path. */
+		"NoOptimizeWithoutFieldPath": function NoOptimizeWithoutFieldPath() {
+			new NoOptimize({
+				spec: {$eq:[{$and:["$a"]},1]},
+			}).run();
+		},
 
-			//      },
+		/** No optimization is performend without an immediate field path. */
+		"NoOptimizeWithoutFieldPathReverse": function NoOptimizeWithoutFieldPathReverse() {
+			new NoOptimize({
+				spec: {$eq:[1,{$and:["$a"]}]},
+			}).run();
+		},
 
-			//      "should optimize $gt expressions;": {
+		/** An equality expression is optimized. */
+		"OptimizeEq": function OptimizeEq() {
+			new FieldRangeOptimize({
+				spec: {$eq:["$a",1]},
+			}).run();
+		},
 
-			//              "{$gt:['$b',2]}": function testOptimizeGt(){
-			//	      var expr = Expression.parseOperand({$gt:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gt:['$b',2]});
-			//              },
+		/** A reverse sense equality expression is optimized. */
+		"OptimizeEqReverse": function OptimizeEqReverse() {
+			new FieldRangeOptimize({
+				spec: {$eq:[1,"$a"]},
+			}).run();
+		},
 
-			//              "{$gt:[2,'$b']} (reversed)": function testOptimizeGtReverse(){
-			//	      var expr = Expression.parseOperand({$gt:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lt:['$b',2]});
-			//              }
+		/** A $lt expression is optimized. */
+		"OptimizeLt": function OptimizeLt() {
+			new FieldRangeOptimize({
+				spec: {$lt:["$a",1]},
+			}).run();
+		},
 
-			//      },
+		/** A reverse sense $lt expression is optimized. */
+		"OptimizeLtReverse": function OptimizeLtReverse() {
+			new FieldRangeOptimize({
+				spec: {$lt:[1,"$a"]},
+			}).run();
+		},
 
-			//      "should optimize $gte expressions;": {
+		/** A $lte expression is optimized. */
+		"OptimizeLte": function OptimizeLte() {
+			new FieldRangeOptimize({
+				spec: {$lte:["$b",2]},
+			}).run();
+		},
 
-			//              "{$gte:['$b',2]}": function testOptimizeGte(){
-			//	      var expr = Expression.parseOperand({$gte:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gte:['$b',2]});
-			//              },
+		/** A reverse sense $lte expression is optimized. */
+		"OptimizeLteReverse": function OptimizeLteReverse() {
+			new FieldRangeOptimize({
+				spec: {$lte:[2,"$b"]},
+			}).run();
+		},
 
-			//              "{$gte:[2,'$b']} (reversed)": function testOptimizeGteReverse(){
-			//	      var expr = Expression.parseOperand({$gte:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lte:['$b',2]});
-			//              }
+		/** A $gt expression is optimized. */
+		"OptimizeGt": function OptimizeGt() {
+			new FieldRangeOptimize({
+				spec: {$gt:["$b",2]},
+			}).run();
+		},
 
-			//      },
+		/** A reverse sense $gt expression is optimized. */
+		"OptimizeGtReverse": function OptimizeGtReverse() {
+			new FieldRangeOptimize({
+				spec: {$gt:["$b",2]},
+			}).run();
+		},
 
+		/** A $gte expression is optimized. */
+		"OptimizeGte": function OptimizeGte() {
+			new FieldRangeOptimize({
+				spec: {$gte:["$b",2]},
+			}).run();
+		},
 
-		}
+		/** A reverse sense $gte expression is optimized. */
+		"OptimizeGteReverse": function OptimizeGteReverse() {
+			new FieldRangeOptimize({
+				spec: {$gte:[2,"$b"]},
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 61
test/lib/pipeline/expressions/ConcatExpression.js

@@ -1,61 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	ConcatExpression = require("../../../../lib/pipeline/expressions/ConcatExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"ConcatExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new ConcatExpression();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $concat": function testOpName(){
-				assert.equal(new ConcatExpression().getOpName(), "$concat");
-			}
-
-		},
-
-		"#getFactory()": {
-
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.equal(new ConcatExpression().getFactory(), ConcatExpression);
-			}
-
-		},
-
-		"#evaluate()": {
-
-			"should return empty string if no operands were given; {$concat:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$concat:[]}).evaluate(), "");
-			},
-
-			"should return mystring if operands are my string; {$concat:[my, string]}": function testConcat(){
-				assert.equal(Expression.parseOperand({$concat:["my", "string"]}).evaluate(), "mystring");
-			},
-
-			"should return mystring if operands are my and $a; {$concat:[my,$a]}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:"string"}), "mystring");
-			},
-
-			"should return null if an operand evaluates to null; {$concat:[my,$a]}": function testNull(){
-				assert.equal(Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:null}), null);
-			}
-
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 91 - 0
test/lib/pipeline/expressions/ConcatExpression_test.js

@@ -0,0 +1,91 @@
+"use strict";
+var assert = require("assert"),
+	ConcatExpression = require("../../../../lib/pipeline/expressions/ConcatExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.ConcatExpression = {
+
+	beforeEach: function() {
+		this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new ConcatExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new ConcatExpression("should die");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $concat": function() {
+			assert.equal(new ConcatExpression().getOpName(), "$concat");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should return empty string if no operands were given; {$concat:[]}": function() {
+			var expr = Expression.parseOperand({$concat:[]}, this.vps);
+			assert.equal(expr.evaluate(), "");
+		},
+
+		"should return mystring if operands are my string; {$concat:[my, string]}": function() {
+			var expr = Expression.parseOperand({$concat:["my", "string"]}, this.vps);
+			assert.equal(expr.evaluate(), "mystring");
+		},
+
+		"should return mystring if operands are my and $a; {$concat:[my,$a]}": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.equal(expr.evaluate({a:"string"}), "mystring");
+		},
+
+		"should return null if an operand evaluates to null; {$concat:[my,$a]}": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.equal(expr.evaluate({a:null}), null);
+		},
+
+		"should return null if an operand evaluates to undefined; {$concat:[my,$a]}": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.equal(expr.evaluate({a:undefined}), null);
+		},
+
+		"should throw if an operand is a number": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.throws(function() {
+				expr.evaluate({a:100});
+			});
+		},
+
+		"should throw if an operand is a date": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.throws(function() {
+				expr.evaluate({a:new Date()});
+			});
+		},
+
+		"should throw if an operand is a boolean": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps)
+			assert.throws(function() {
+				expr.evaluate({a:true});
+			});
+		},
+
+	},
+
+};

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

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

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

@@ -0,0 +1,117 @@
+"use strict";
+var assert = require("assert"),
+	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+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");
+		},
+
+	},
+
+	"#evaluate()": {
+		"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 ]}, {}).evaluate({}), 1);
+			},
+
+			"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function () {
+				assert.strictEqual(Expression.parseOperand({$cond: [ false, 1, 0 ]}, {}).evaluate({}), 0);
+			},
+
+		},
+
+		"object style": {
+
+			beforeEach: function(){
+				this.shouldFail = function(expr) {
+					assert.throws(function(){
+						Expression.parseOperand(expr, {});
+					});
+				};
+				this.vps = new VariablesParseState(new VariablesIdGenerator());
+			},
+
+			"should fail because of missing if": function(){
+				this.shouldFail({$cond:{ then:2, else:3}});
+			},
+
+			"should fail because of missing then": function(){
+				this.shouldFail({$cond:{if:1,  else:3}});
+			},
+
+			"should fail because of missing else": function(){
+				this.shouldFail({$cond:{if:1, then:2 }});
+			},
+
+			"should fail because of mystery args": function(){
+				this.shouldFail({$cond:{if:1, then:2, else:3, zoot:4}});
+			},
+
+			"should evaluate true": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
+					1);
+			},
+
+			"should evaluate true even with mixed up args": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, this.vps).evaluate({a: 1}),
+					1);
+			},
+
+			"should evaluate false": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, this.vps).evaluate({a: 0}),
+					1);
+			},
+
+			"should evaluate false even with mixed up args": function() {
+				assert.strictEqual(
+					Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, this.vps).evaluate({a: 0}),
+					1);
+			},
+
+		},
+
+	},
+
+};

+ 0 - 55
test/lib/pipeline/expressions/ConstantExpression.js

@@ -1,55 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
-
-
-module.exports = {
-
-	"ConstantExpression": {
-
-		"constructor() / #evaluate": {
-
-			"should be able to construct from a value type": function testCreate(){
-				assert.strictEqual(new ConstantExpression(5).evaluateInternal({}), 5);
-			}
-
-			//TODO: CreateFromBsonElement ? ?? ???
-
-		},
-
-// TODO: the constructor() tests this so not really needed here
-//		"#evaluate()": {
-//		},
-
-		"#optimize()": {
-
-			"should not optimize anything": function testOptimize(){
-				var expr = new ConstantExpression(5);
-				assert.strictEqual(expr, expr.optimize());
-			}
-
-		},
-
-		"#addDependencies()": {
-
-			"should return nothing": function testDependencies(){
-				assert.strictEqual(new ConstantExpression(5).addDependencies(), undefined);
-			}
-
-		},
-
-		"#toJSON()": {
-
-			"should output proper JSON": function testJson(){
-				var expr = new ConstantExpression(5);
-				assert.strictEqual(expr.serialize(), 5);
-				assert.deepEqual(expr.serialize(true), {$const:5});
-			}
-
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 100 - 0
test/lib/pipeline/expressions/ConstantExpression_test.js

@@ -0,0 +1,100 @@
+"use strict";
+var assert = require("assert"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	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));
+
+exports.ConstantExpression = {
+
+	".constructor()": {
+
+		"should accept one argument": function () {
+			new ConstantExpression(5);
+		},
+
+		"should not accept 0 arguments": function () {
+			assert.throws(function () {
+				new ConstantExpression();
+			});
+		},
+
+		"should not accept 2 arguments": function () {
+			assert.throws(function () {
+				new ConstantExpression(1, 2);
+			});
+		}
+	},
+
+	".parse()": {
+
+		"should create an expression from a json element": function testCreateFromBsonElement() {
+			var idGenerator = new VariablesIdGenerator(),
+				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()": {
+
+		"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 = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.deepEqual(deps.fields, {});
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
+		}
+	},
+
+	//TODO: AddToBsonObj
+
+	//TODO: AddToBsonArray
+
+	"#evaluate()": {
+
+		"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 () {
+			var c = 567.123;
+			var expr = new ConstantExpression(c);
+			assert.deepEqual(expr.evaluate(), c);
+		},
+
+		"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 () {
+			var c = new Date();
+			var expr = new ConstantExpression(c);
+			assert.deepEqual(expr.evaluate(), c);
+		}
+	}
+};

+ 29 - 59
test/lib/pipeline/expressions/DayOfMonthExpression.js

@@ -1,74 +1,44 @@
 "use strict";
 var assert = require("assert"),
-		DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression"),
-		VariablesParseState = require("../../../../Lib/pipeline/expressions/VariablesParseState"),
-		VariablesIdGenerator = require("../../../../Lib/pipeline/expressions/VariablesIdGenerator");
+	DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.DayOfMonthExpression = {
 
-		"DayOfMonthExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should create instance": function() {
+			assert(new DayOfMonthExpression() instanceof DayOfMonthExpression);
+			assert(new DayOfMonthExpression() instanceof Expression);
+		},
 
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new DayOfMonthExpression();
-								});
-						},
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfMonthExpression("bad stuff");
+			});
+		},
 
-						"should not throw Error when constructing with an arg": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new DayOfMonthExpression("1/1/2014");
-								});
-						}
+	},
 
-				},
+	"#getOpName()": {
 
-				"#getOpName()": {
+		"should return the correct op name; $dayOfMonth": function() {
+			assert.equal(new DayOfMonthExpression().getOpName(), "$dayOfMonth");
+		},
 
-						"should return the correct op name; $dayOfMonth": function testOpName() {
-								assert.equal(new DayOfMonthExpression("1/1/2014").getOpName(), "$dayOfMonth");
-						}
+	},
 
-				},
+	"#evaluate()": {
 
-				"#evaulateInternal1()": {
+		"should return day of month; 10 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfMonth", operands);
+			assert.strictEqual(expr.evaluate({}), 1);
+		},
 
-						"should return day of month; 10 for 2013-03-10": function testOpName() {
-								assert.equal(new DayOfMonthExpression("2013-03-10T00:00:00.000Z").evaluateInternal(), "10");
-						}
+	},
 
-				},
-
-				"#evaluateInternal2()": {
-
-						"should return day of month; 18 for 2013-02-18": function testStuff() {
-
-								var idGenerator = new VariablesIdGenerator();
-								var vps = new VariablesParseState(idGenerator);
-								var parseOp = Expression.parseOperand({
-										$dayOfMonth: "$someDate"
-								}, vps);
-
-								var result = parseOp.evaluateInternal({
-										$someDate: new Date("2013-02-18T00:00:00.000Z")
-								});
-
-								assert.strictEqual(result, "2");
-
-										// assert.strictEqual(Expression.parseOperand({
-										// $dayOfMonth: "$someDate"
-										// }, vps).evaluate({
-										// someDate: new Date("2013-02-18T00:00:00.000Z")
-										// }), 18);
-								}
-
-						}
-
-				}
-
-		};
-
-		if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 23 - 28
test/lib/pipeline/expressions/DayOfWeekExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	DayOfWeekExpression = require("../../../../lib/pipeline/expressions/DayOfWeekExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.DayOfWeekExpression = {
 
-	"DayOfWeekExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new DayOfWeekExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new DayOfWeekExpression() instanceof DayOfWeekExpression);
+			assert(new DayOfWeekExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $dayOfWeek": function testOpName(){
-				assert.equal(new DayOfWeekExpression().getOpName(), "$dayOfWeek");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfWeekExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new DayOfWeekExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $dayOfWeek": function() {
+			assert.equal(new DayOfWeekExpression().getOpName(), "$dayOfWeek");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return day of week; 2 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$dayOfWeek:"$someDate"}).evaluateInternal({someDate:new Date("2013-02-18T00:00:00.000Z")}), 2);
-			}
+	"#evaluate()": {
 
-		}
+		"should return day of week; 7 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfWeek", operands);
+			assert.strictEqual(expr.evaluate({}), 7);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/DayOfYearExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	DayOfYearExpression = require("../../../../lib/pipeline/expressions/DayOfYearExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.DayOfYearExpression = {
 
-	"DayOfYearExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new DayOfYearExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new DayOfYearExpression() instanceof DayOfYearExpression);
+			assert(new DayOfYearExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $dayOfYear": function testOpName(){
-				assert.equal(new DayOfYearExpression().getOpName(), "$dayOfYear");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfYearExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new DayOfYearExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $dayOfYear": function() {
+			assert.equal(new DayOfYearExpression().getOpName(), "$dayOfYear");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return day of year; 49 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$dayOfYear:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 49);
-			}
+	"#evaluate()": {
 
-		}
+		"should return day of year; 305 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfYear", operands);
+			assert.strictEqual(expr.evaluate({}), 305);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	DivideExpression = require("../../../../lib/pipeline/expressions/DivideExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.DivideExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new DivideExpression() instanceof DivideExpression);
+			assert(new DivideExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new DivideExpression("bad stuff");
+			});
+		}
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $size": function() {
+			assert.equal(new DivideExpression().getOpName(), "$divide");
+		}
+
+	},
+
+	"#evaluate()": {
+
+		"should divide two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$divide: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 3);
+		}
+
+	}
+
+};

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

+ 23 - 28
test/lib/pipeline/expressions/HourExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	HourExpression = require("../../../../lib/pipeline/expressions/HourExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.HourExpression = {
 
-	"HourExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new HourExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new HourExpression() instanceof HourExpression);
+			assert(new HourExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $hour": function testOpName(){
-				assert.equal(new HourExpression().getOpName(), "$hour");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new HourExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new HourExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $hour": function() {
+			assert.equal(new HourExpression().getOpName(), "$hour");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return hour; 15 for 2013-02-18 3:00pm": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$hour:"$someDate"}).evaluate({someDate:new Date("2013-02-18T15:00:00.000Z")}), 15);
-			}
+	"#evaluate()": {
 
-		}
+		"should return hour; 19 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$hour", operands);
+			assert.strictEqual(expr.evaluate({}), 19);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

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

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

@@ -0,0 +1,59 @@
+"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");
+
+// 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.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");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		beforeEach: function () {
+			this.vps = new VariablesParseState(new VariablesIdGenerator());
+			this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
+		},
+
+		"should return the left hand side if the left hand side is not null or undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({a: 1, b: 2}), 1);
+		},
+
+		"should return the right hand side if the left hand side is null": function() {
+			assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
+		},
+
+		"should return the right hand side if the left hand side is undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
+		},
+
+	},
+
+};

+ 115 - 0
test/lib/pipeline/expressions/MapExpression_test.js

@@ -0,0 +1,115 @@
+"use strict";
+var assert = require("assert"),
+	MapExpression = require("../../../../lib/pipeline/expressions/MapExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"), // jshint ignore:line
+	IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"), // jshint ignore:line
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if (!module.parent)return(require.cache[__filename] = null, (new (require("mocha"))({ui: "exports", reporter: "spec", grep: process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.MapExpression = {
+
+	"constructor()": {
+
+		"should accept 4 arguments": function () {
+			new MapExpression(1, 2, 3, 4);
+		},
+
+		"should accept only 4 arguments": function () {
+			assert.throws(function () { new MapExpression(); });
+			assert.throws(function () { new MapExpression(1); });
+			assert.throws(function () { new MapExpression(1, 2); });
+			assert.throws(function () { new MapExpression(1, 2, 3); });
+			assert.throws(function () { new MapExpression(1, 2, 3, 4, 5); });
+		},
+
+	},
+
+	"#optimize()": {
+
+		"should optimize both $map.input and $map.in": function() {
+			var spec = {$map:{
+					input: {$ifNull:[null, {$const:[1,2,3]}]},
+					as: "i",
+					in: {$add:["$$i","$$i",1,2]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps),
+				optimized = expr.optimize();
+			assert.strictEqual(optimized, expr, "should be same reference");
+			assert.deepEqual(expressionToJson(optimized._input), {$const:[1,2,3]});
+			assert.deepEqual(expressionToJson(optimized._each), constify({$add:["$$i","$$i",1,2]}));
+		},
+
+	},
+
+	"#serialize()": {
+
+		"should serialize to consistent order": function() {
+			var spec = {$map:{
+					as: "i",
+					in: {$add:["$$i","$$i"]},
+					input: {$const:[1,2,3]},
+				}},
+				expected = {$map:{
+					input: {$const:[1,2,3]},
+					as: "i",
+					in: {$add:["$$i","$$i"]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps);
+			assert.deepEqual(expressionToJson(expr), expected);
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should be able to map over a simple array": function() {
+			var spec = {$map:{
+					input: {$const:[1,2,3]},
+					as: "i",
+					in: {$add:["$$i","$$i"]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps),
+				vars = new Variables(1, {}); // must set numVars (usually handled by doc src)
+			assert.deepEqual(expr.evaluate(vars), [2, 4, 6]);
+		},
+
+	},
+
+	"#addDependencies()": {
+
+		"should add dependencies to both $map.input and $map.in": function () {
+			var spec = {$map:{
+					input: "$inputArray",
+					as: "i",
+					in: {$add:["$$i","$someConst"]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps),
+				deps = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.strictEqual(Object.keys(deps.fields).length, 2);
+			assert.strictEqual("inputArray" in deps.fields, true);
+			assert.strictEqual("someConst" in deps.fields, true);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
+		},
+
+	},
+
+};

+ 28 - 43
test/lib/pipeline/expressions/MillisecondExpression.js

@@ -1,59 +1,44 @@
 "use strict";
 var assert = require("assert"),
-		MillisecondExpression = require("../../../../lib/pipeline/expressions/MillisecondExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+	MillisecondExpression = require("../../../../lib/pipeline/expressions/MillisecondExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.MillisecondExpression = {
 
-		"MillisecondExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should create instance": function() {
+			assert(new MillisecondExpression() instanceof MillisecondExpression);
+			assert(new MillisecondExpression() instanceof Expression);
+		},
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new MillisecondExpression();
-								});
-						}
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MillisecondExpression("bad stuff");
+			});
+		},
 
-				},
+	},
 
-				"#getOpName()": {
+	"#getOpName()": {
 
-						"should return the correct op name; $millisecond": function testOpName() {
-								assert.equal(new MillisecondExpression().getOpName(), "$millisecond");
-						}
+		"should return the correct op name; $millisecond": function() {
+			assert.equal(new MillisecondExpression().getOpName(), "$millisecond");
+		},
 
-				},
+	},
 
-				"#getFactory()": {
+	"#evaluate()": {
 
-						"should return the constructor for this class": function factoryIsConstructor() {
-								assert.strictEqual(new MillisecondExpression().getFactory(), undefined);
-						}
+		"should return millisecond; 819 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$millisecond", operands);
+			assert.strictEqual(expr.evaluate({}), 819);
+		},
 
-				},
-
-				"#evaluate()": {
-
-						"should return the current millisecond in the date; 19 for 2013-02-18 11:24:19 EST": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$millisecond: "$someDate"
-								}).evaluate({
-										someDate: new Date("2013-02-18T11:24:19.456Z")
-								}), 456);
-						}
-
-						/*
-			"should return the leap millisecond in the date; 60 for June 30, 2012 at 23:59:60 UTC": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$millisecond:"$someDate"}).evaluate({someDate:new Date("June 30, 2012 at 23:59:60 UTC")}), 60);
-			}
-
-				*/
-				}
-
-		}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/MinuteExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	MinuteExpression = require("../../../../lib/pipeline/expressions/MinuteExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.MinuteExpression = {
 
-	"MinuteExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MinuteExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new MinuteExpression() instanceof MinuteExpression);
+			assert(new MinuteExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $minute": function testOpName(){
-				assert.equal(new MinuteExpression().getOpName(), "$minute");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MinuteExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new MinuteExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $minute": function() {
+			assert.equal(new MinuteExpression().getOpName(), "$minute");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return minute; 47 for 2013-02-18 3:47 pm": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$minute:"$someDate"}).evaluateInternal({someDate:new Date("2013-02-18T15:47:00.000Z")}), 47);
-			}
+	"#evaluate()": {
 
-		}
+		"should return minute; 31 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$minute", operands);
+			assert.strictEqual(expr.evaluate({}), 31);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 62 - 35
test/lib/pipeline/expressions/ModExpression.js

@@ -1,53 +1,80 @@
 "use strict";
 var assert = require("assert"),
 	ModExpression = require("../../../../lib/pipeline/expressions/ModExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/Expression");
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.ModExpression = {
 
-	"ModExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should construct instance": function() {
+			assert(new ModExpression() instanceof ModExpression);
+			assert(new ModExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ModExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $mod": function() {
+			assert.equal(new ModExpression().getOpName(), "$mod");
+		},
+
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new ModExpression();
-				});
-			}
+	"#evaluate()": {
 
+		"should return modulus of two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 0);
 		},
 
-		"#getOpName()": {
-			"should return the correct op name; $mod": function testOpName(){
-				assert.equal(new ModExpression().getOpName(), "$mod");
-			}
+		"should return null if first is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: null, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
+		"should return null if first is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: undefined, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
 		},
 
-		"#evaluateInternal()": {
-			"should return rhs if rhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}, new VariablesParseState()).evaluate({lhs:20.453, rhs:null}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453}), undefined);
-			},
-			"should return lhs if lhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:null, rhs:20.453}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({rhs:20.453}), undefined);
-			},
-			"should return undefined if rhs is 0": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453, rhs:0}), undefined);
-			},
-			"should return proper mod of rhs and lhs if both are numbers": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:234.4234, rhs:45}), 234.4234 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:0, rhs:45}), 0 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:-6, rhs:-0.5}), -6 % -0.5);
-			}
+		"should return null if second is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 11, b: null};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-		}
+		"should return null if second is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 42, b: undefined};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/MonthExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	MonthExpression = require("../../../../lib/pipeline/expressions/MonthExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.MonthExpression = {
 
-	"MonthExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MonthExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new MonthExpression() instanceof MonthExpression);
+			assert(new MonthExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $month": function testOpName(){
-				assert.equal(new MonthExpression().getOpName(), "$month");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MonthExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new MonthExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $month": function() {
+			assert.equal(new MonthExpression().getOpName(), "$month");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return month; 2 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$month:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 2);
-			}
+	"#evaluate()": {
 
-		}
+		"should return month; 11 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$month", operands);
+			assert.strictEqual(expr.evaluate({}), 11);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 45
test/lib/pipeline/expressions/MultiplyExpression.js

@@ -1,45 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	MultiplyExpression = require("../../../../lib/pipeline/expressions/MultiplyExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"MultiplyExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MultiplyExpression();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $multiply": function testOpName(){
-				assert.equal(new MultiplyExpression().getOpName(), "$multiply");
-			}
-
-		},
-
-		"#evaluate()": {
-
-			"should return result of multiplying numbers": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$multiply:["$a", "$b"]}).evaluateInternal({a:1, b:2}), 1*2);
-				assert.strictEqual(Expression.parseOperand({$multiply:["$a", "$b", "$c"]}).evaluateInternal({a:1.345, b:2e45, c:0}), 1.345*2e45*0);
-				assert.strictEqual(Expression.parseOperand({$multiply:["$a"]}).evaluateInternal({a:1}), 1);
-			},
-			"should throw an exception if the result is not a number": function testStuff(){
-				assert.throws(Expression.parseOperand({$multiply:["$a", "$b"]}).evaluateInternal({a:1e199, b:1e199}));
-			}
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 112 - 0
test/lib/pipeline/expressions/MultiplyExpression_test.js

@@ -0,0 +1,112 @@
+"use strict";
+var assert = require("assert"),
+	MultiplyExpression = require("../../../../lib/pipeline/expressions/MultiplyExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.MultiplyExpression = {
+
+	beforeEach: function(){
+		this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function(){
+				new MultiplyExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function(){
+				new MultiplyExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $multiply": function() {
+			assert.equal(new MultiplyExpression().getOpName(), "$multiply");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should multiply constants": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: [2, 3, 4]}, this.vps).evaluate(), 2 * 3 * 4);
+		},
+
+		"should 'splode if an operand is a string": function () {
+			assert.throws(function () {
+				Expression.parseOperand({$multiply: [2, "x", 4]}, this.vps).evaluate();
+			});
+		},
+
+		"should 'splode if an operand is a boolean": function () {
+			assert.throws(function () {
+				Expression.parseOperand({$multiply: [2, "x", 4]}, this.vps).evaluate();
+			});
+		},
+
+		"should 'splode if an operand is a date": function () {
+			assert.throws(function () {
+				Expression.parseOperand({$multiply: [2, "x", 4]}, this.vps).evaluate();
+			});
+		},
+
+		"should handle a null operand": function(){
+			assert.strictEqual(Expression.parseOperand({$multiply: [2, null]}, this.vps).evaluate(), null);
+		},
+
+		"should handle an undefined operand": function(){
+			assert.strictEqual(Expression.parseOperand({$multiply: [2, undefined]}, this.vps).evaluate(), null);
+		},
+
+		"should multiply mixed numbers": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: [2.1, 3, 4.4]}, this.vps).evaluate(), 2.1 * 3 * 4.4);
+		},
+
+		"should return result of multiplying simple variables": function () {
+			assert.equal(Expression.parseOperand({$multiply: ["$a", "$b"]}, this.vps).evaluate({a: 1, b: 2}), 1 * 2);
+		},
+
+		"should return result of multiplying large variables": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: ["$a", "$b", "$c"]}, this.vps).evaluate({a: 1.345, b: 2e45, c: 0}), 1.345 * 2e45 * 0);
+		},
+
+		"should return result of multiplying one number": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: ["$a"]}, this.vps).evaluate({a: 1}), 1);
+		},
+
+		"should throw an exception if the result is not a number": function () {
+			assert.throws(function() {
+				Expression.parseOperand({$multiply: ["$a", "$b"]}, this.vps).evaluate({a: 1e199, b: 1e199});
+			});
+		},
+
+	},
+
+	"optimize": {
+
+		"should optimize out constants separated by a variable": function () {
+			var a = Expression.parseOperand({$multiply: [2, 3, 4, 5, '$a', 6, 7, 8]}, this.vps).optimize();
+			assert(a instanceof MultiplyExpression);
+			assert.equal(a.operands.length, 2);
+			assert(a.operands[0] instanceof FieldPathExpression);
+			assert(a.operands[1] instanceof ConstantExpression);
+			assert.equal(a.operands[1].evaluateInternal(), 2*3*4*5*6*7*8);
+		},
+
+	},
+
+};

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

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

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

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.NotExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function(){
+				new NotExpression();
+			});
+		},
+
+		"should throw when constructing with args": function() {
+			assert.throws(function(){
+				new NotExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $not": function() {
+			assert.equal(new NotExpression().getOpName(), "$not");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should return false for a true input; false for true": function() {
+			assert.strictEqual(Expression.parseOperand({$not:true}, {}).evaluateInternal({}), false);
+		},
+
+		"should return true for a false input; true for false": function() {
+			assert.strictEqual(Expression.parseOperand({$not:false}, {}).evaluateInternal({}), true);
+		},
+
+	},
+
+};

+ 637 - 631
test/lib/pipeline/expressions/ObjectExpression.js

@@ -1,20 +1,35 @@
 "use strict";
 var assert = require("assert"),
 	ObjectExpression = require("../../../../lib/pipeline/expressions/ObjectExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
-	Variables = require("../../../../lib/pipeline/expressions/Variables");
-
-
-function assertEqualJson(actual, expected, message){
-	if(actual.sort) {
-		actual.sort();
-		if(expected.sort) {
-			expected.sort();
-		}
-	}
-	assert.strictEqual(message + ":  " + JSON.stringify(actual), message + ":  " + JSON.stringify(expected));
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	utils = require("./utils");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var constify = utils.constify;
+//SKIPPED: assertBinaryEqual
+//SKIPPED: toJson
+function expressionToJson(expr) {
+	return expr.serialize(false);
+}
+//SKIPPED: fromJson
+//SKIPEPD: valueFromBson
+
+function assertDependencies(expectedDependencies, expression, includePath) {
+	if (includePath === undefined) includePath = true;
+	var path = [],
+		dependencies = new DepsTracker();
+	expression.addDependencies(dependencies, includePath ? path : undefined);
+	var bab = Object.keys(dependencies.fields);
+	assert.deepEqual(bab.sort(), expectedDependencies.sort());
+	assert.strictEqual(dependencies.needWholeDocument, false);
+	assert.strictEqual(dependencies.needTextScore, false);
 }
 
 /// An assertion for `ObjectExpression` instances based on Mongo's `ExpectedResultBase` class
@@ -24,637 +39,628 @@ function assertExpectedResult(args) {
 		if (!("expected" in args)) throw new Error("missing arg: `args.expected` is required");
 		if (!("expectedDependencies" in args)) throw new Error("missing arg: `args.expectedDependencies` is required");
 		if (!("expectedJsonRepresentation" in args)) throw new Error("missing arg: `args.expectedJsonRepresentation` is required");
-	}// check for required args
+	}
 	{// base args if none provided
 		if (args.source === undefined) args.source = {_id:0, a:1, b:2};
-		if (args.expectedIsSimple === undefined) args.expectedIsSimple = false;
+		if (args.expectedIsSimple === undefined) args.expectedIsSimple = true;
 		if (args.expression === undefined) args.expression = ObjectExpression.createRoot(); //NOTE: replaces prepareExpression + _expression assignment
-	}// base args if none provided
+	}
 	// run implementation
-	var result = {},
-		variable = new Variables(1, args.source);
-
-	args.expression.addToDocument(result, args.source, variable);
+	var doc = args.source,
+		result = {},
+		vars = new Variables(0, doc);
+	args.expression.addToDocument(result, doc, vars);
 	assert.deepEqual(result, args.expected);
-	var dependencies = {};
-	args.expression.addDependencies(dependencies, [/*FAKING: includePath=true*/]);
-	//dependencies.sort(), args.expectedDependencies.sort();	// NOTE: this is a minor hack added for munge because I'm pretty sure order doesn't matter for this anyhow
-	assert.deepEqual(Object.keys(dependencies).sort(), Object.keys(args.expectedDependencies).sort());
-	assert.deepEqual(args.expression.serialize(true), args.expectedJsonRepresentation);
-	assert.deepEqual(args.expression.getIsSimple(), args.expectedIsSimple);
+	assertDependencies(args.expectedDependencies, args.expression);
+	assert.deepEqual(expressionToJson(args.expression), args.expectedJsonRepresentation);
+	assert.deepEqual(args.expression.isSimple(), args.expectedIsSimple);
 }
 
+exports.ObjectExpression = {
 
-module.exports = {
-
-	"ObjectExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					ObjectExpression.create();
-				});
-			}
-
-		},
-
-		"#addDependencies":{
-
-			"should be able to get dependencies for non-inclusion expressions": function testNonInclusionDependencies(){
-				/** Dependencies for non inclusion expressions. */
-				var expr = ObjectExpression.create();
-				expr.addField("a", new ConstantExpression(5));
-				assertEqualJson(expr.addDependencies({}, [/*FAKING: includePath=true*/]), {"_id":1});
-				expr.excludeId = true;
-				assertEqualJson(expr.addDependencies({}, []), {});
-				expr.addField("b", FieldPathExpression.create("c.d"));
-				var deps = {};
-				expr.addDependencies(deps, []);
-				assert.deepEqual(deps, {"c.d":1});
-				expr.excludeId = false;
-				deps = {};
-				expr.addDependencies(deps, []);
-				assert.deepEqual(deps, {"_id":1, "c.d":1});
-			},
-
-			"should be able to get dependencies for inclusion expressions": function testInclusionDependencies(){
-				/** Dependencies for inclusion expressions. */
-				var expr = ObjectExpression.create();
-				expr.includePath( "a" );
-				assertEqualJson(expr.addDependencies({}, [/*FAKING: includePath=true*/]), {"_id":1, "a":1});
-				assert.throws(function(){
-					expr.addDependencies({});
-				}, Error);
-			}
-
-		},
-
-		"#toJSON": {
-
-			"should be able to convert to JSON representation and have constants represented by expressions": function testJson(){
-				/** Serialize to a BSONObj, with constants represented by expressions. */
-				var expr = ObjectExpression.create(true);
-				expr.addField("foo.a", new ConstantExpression(5));
-				assertEqualJson({foo:{a:{$const:5}}}, expr.serialize(true));
-			}
-
-		},
-
-		"#optimize": {
-
-			"should be able to optimize expression and sub-expressions": function testOptimize(){
-				/** Optimizing an object expression optimizes its sub expressions. */
-				var expr = ObjectExpression.createRoot();
-				// Add inclusion.
-				expr.includePath( "a" );
-				// Add non inclusion.
-				expr.addField( "b", new AndExpression());
-				expr.optimize();
-				// Optimizing 'expression' optimizes its non inclusion sub expressions, while inclusion sub expressions are passed through.
-				assertEqualJson({a:{$const:null}, b:{$const:true}}, expr.serialize(true));
-			}
-
-		},
-
-		"#evaluate()": {
-
-			"should be able to provide an empty object": function testEmpty(){
-				/** Empty object spec. */
-				var expr = ObjectExpression.createRoot();
-				assertExpectedResult({
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {}
-
-				});
-			},
-
-			"should be able to include 'a' field only": function testInclude(){
-				/** Include 'a' field only. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a" );
-				assertExpectedResult({
-					expression: expr,
-					expected: {"_id":0, "a":1},
-					expectedDependencies: {"_id":1, "a":1},
-					expectedJsonRepresentation: {"a":{$const:null}}
-				});
-			},
-
-			"should NOT be able to include missing 'a' field": function testMissingInclude(){
-				/** Cannot include missing 'a' field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a" );
-				assertExpectedResult({
-					source: {"_id":0, "b":2},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1, "a":1},
-					expectedJsonRepresentation: {"a":{$const:null}}
-				});
-			},
-
-			"should be able to include '_id' field only": function testIncludeId(){
-				/** Include '_id' field only. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "_id" );
-				assertExpectedResult({
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"_id":{$const:null}}
-				});
-			},
-
-			"should be able to exclude '_id' field": function testExcludeId(){
-				/** Exclude '_id' field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "b" );
-				expr.excludeId = true;
-				assertExpectedResult({
-					expression: expr,
-					expected: {"b":2},
-					expectedDependencies: {"b":1},
-					expectedJsonRepresentation: {"b":{$const:null}}
-				});
-			},
-
-			"should be able to include fields in source document order regardless of inclusion order": function testSourceOrder(){
-				/** Result order based on source document field order, not inclusion spec field order. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "b" );
-				expr.includePath( "a" );
-				assertExpectedResult({
-					expression: expr,
-					get expected() { return this.source; },
-					expectedDependencies: {"_id":1, "a":1, "b":1},
-					expectedJsonRepresentation: {"b":{$const:null}, "a":{$const:null}}
-				});
-			},
-
-			"should be able to include a nested field": function testIncludeNested(){
-				/** Include a nested field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "b":5, "c":6}, "z":2 },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":5} },
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should be able to include two nested fields": function testIncludeTwoNested(){
-				/** Include two nested fields. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				expr.includePath( "a.c" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "b":5, "c":6}, "z":2 },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":5, "c":6} },
-					expectedDependencies: {"_id":1, "a.b":1, "a.c":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}, "c":{$const:null}} }
-				});
-			},
-
-			"should be able to include two fields nested within different parents": function testIncludeTwoParentNested(){
-				/** Include two fields nested within different parents. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				expr.includePath( "c.d" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "b":5 }, "c":{"d":6} },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":5}, "c":{"d":6} },
-					expectedDependencies: {"_id":1, "a.b":1, "c.d":1},
-					expectedJsonRepresentation: {"a":{"b":{$const:null}}, "c":{"d":{$const:null}} }
-				});
-			},
-
-			"should be able to attempt to include a missing nested field": function testIncludeMissingNested(){
-				/** Attempt to include a missing nested field. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "c":6}, "z":2 },
-					expression: expr,
-					expected: {"_id":0, "a":{} },
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should be able to attempt to include a nested field within a non object": function testIncludeNestedWithinNonObject(){
-				/** Attempt to include a nested field within a non object. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":2, "z":2},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should be able to include a nested field within an array": function testIncludeArrayNested(){
-				/** Include a nested field within an array. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {_id:0,a:[{b:5,c:6},{b:2,c:9},{c:7},[],2],z:1},
-					expression: expr,
-					expected: {_id:0,a:[{b:5},{b:2},{}]},
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}} }
-				});
-			},
-
-			"should NOT include non-root '_id' field implicitly": function testExcludeNonRootId(){
-				/** Don't include not root '_id' field implicitly. */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath( "a.b" );
-				assertExpectedResult({
-					source: {"_id":0, "a":{ "_id":1, "b":1} },
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":1} },
-					expectedDependencies: {"_id":1, "a.b":1},
-					expectedJsonRepresentation: {"a":{ "b":{$const:null}}}
-				});
-			},
-
-			"should be able to project a computed expression": function testComputed(){
-				/** Project a computed expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":5},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{ "$const":5} },
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a computed expression replacing an existing field": function testComputedReplacement(){
-				/** Project a computed expression replacing an existing field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0, "a":99},
-					expression: expr,
-					expected: {"_id": 0, "a": 5},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a": {"$const": 5}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should NOT be able to project an undefined value": function testComputedUndefined(){
-				/** An undefined value is not projected.. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(undefined));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{$const:undefined}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a computed expression replacing an existing field with Undefined": function testComputedUndefinedReplacement(){
-				/** Project a computed expression replacing an existing field with Undefined. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0, "a":99},
-					expression: expr,
-					expected: {"_id":0, "a":5},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{"$const":5}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a null value": function testComputedNull(){
-				/** A null value is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(null));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":null},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{"$const":null}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a nested value": function testComputedNested(){
-				/** A nested value is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(5));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{"b":5}},
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {"a":{"b":{"$const":5}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a field path": function testComputedFieldPath(){
-				/** A field path is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", FieldPathExpression.create("x"));
-				assertExpectedResult({
-					source: {"_id":0, "x":4},
-					expression: expr,
-					expected: {"_id":0, "a":4},
-					expectedDependencies: {"_id":1, "x":1},
-					expectedJsonRepresentation: {"a":"$x"},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a nested field path": function testComputedNestedFieldPath(){
-				/** A nested field path is projected. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", FieldPathExpression.create("x.y"));
-				assertExpectedResult({
-					source: {"_id":0, "x":{"y":4}},
-					expression: expr,
-					expected: {"_id":0, "a":{"b":4}},
-					expectedDependencies: {"_id":1, "x.y":1},
-					expectedJsonRepresentation: {"a":{"b":"$x.y"}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should NOT project an empty subobject expression for a missing field": function testEmptyNewSubobject(){
-				/** An empty subobject expression for a missing field is not projected. */
-				var expr = ObjectExpression.createRoot();
-				// Create a sub expression returning an empty object.
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", FieldPathExpression.create("a.b"));
-				expr.addField( "a", subExpr );
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0},
-					expectedDependencies: {"_id":1, 'a.b':1},
-					expectedJsonRepresentation: {a:{b:"$a.b"}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project a non-empty new subobject": function testNonEmptyNewSubobject(){
-				/** A non empty subobject expression for a missing field is projected. */
-				var expr = ObjectExpression.createRoot();
-				// Create a sub expression returning an empty object.
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(6));
-				expr.addField( "a", subExpr );
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent": function testAdjacentDottedComputedFields(){
-				/** Two computed fields within a common parent. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				expr.addField("a.c", new ConstantExpression(7));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent (w/ one case dotted)": function testAdjacentDottedAndNestedComputedFields(){
-				/** Two computed fields within a common parent, in one case dotted. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("c", new ConstantExpression( 7 ) );
-				expr.addField("a", subExpr);
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent (in another case dotted)": function testAdjacentNestedAndDottedComputedFields(){
-				/** Two computed fields within a common parent, in another case dotted. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(6));
-				expr.addField("a", subExpr );
-				expr.addField("a.c", new ConstantExpression(7));
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project two computed fields within a common parent (nested rather than dotted)": function testAdjacentNestedComputedFields(){
-				/** Two computed fields within a common parent, nested rather than dotted. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr1 = ObjectExpression.create();
-				subExpr1.addField("b", new ConstantExpression(6));
-				expr.addField("a", subExpr1);
-				var subExpr2 = ObjectExpression.create();
-				subExpr2.addField("c", new ConstantExpression(7));
-				expr.addField("a", subExpr2);
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "c":7} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project multiple nested fields out of order without affecting output order": function testAdjacentNestedOrdering(){
-				/** Field ordering is preserved when nested fields are merged. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				var subExpr = ObjectExpression.create();
-				// Add field 'd' then 'c'.  Expect the same field ordering in the result doc.
-				subExpr.addField("d", new ConstantExpression(7));
-				subExpr.addField("c", new ConstantExpression(8));
-				expr.addField("a", subExpr);
-				assertExpectedResult({
-					source: {"_id":0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":6, "d":7, "c":8} },
-					expectedDependencies: {"_id":1},
-					expectedJsonRepresentation: {a:{b:{$const:6},d:{$const:7},c:{$const:8}}},
-					expectedIsSimple: false
-				});
-			},
-
-			"should be able to project adjacent fields two levels deep": function testMultipleNestedFields(){
-				/** Adjacent fields two levels deep. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b.c", new ConstantExpression(6));
-				var bSubExpression = ObjectExpression.create();
-				bSubExpression.addField("d", new ConstantExpression(7));
-				var aSubExpression = ObjectExpression.create();
-				aSubExpression.addField("b", bSubExpression);
-				expr.addField("a", aSubExpression);
-				assertExpectedResult({
-					source:{_id:0},
-					expression: expr,
-					expected: {"_id":0, "a":{ "b":{ "c":6, "d":7}}},
-					expectedDependencies:{_id:1},
-					expectedJsonRepresentation:{"a":{"b":{"c":{$const:6},"d":{$const:7}}}},
-					expectedIsSimple:false
-				});
-				var res = expr.evaluateDocument(new Variables(1, {_id:1}));
-			},
-
-			"should throw an Error if two expressions generate the same field": function testConflictingExpressionFields(){
-				/** Two expressions cannot generate the same field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(6)); // Duplicate field.
-				}, Error);
-			},
-
-			"should throw an Error if an expression field conflicts with an inclusion field": function testConflictingInclusionExpressionFields(){
-				/** An expression field conflicts with an inclusion field. */
-				var expr = ObjectExpression.createRoot();
+	"constructor()": {
+
+		"should return instance if given arg": function() {
+			assert(new ObjectExpression(false) instanceof Expression);
+			assert(new ObjectExpression(true) instanceof Expression);
+		},
+
+		"should throw Error when constructing without args": function() {
+			assert.throws(function() {
+				new ObjectExpression();
+			});
+		},
+
+	},
+
+	"#addDependencies": {
+
+		"should be able to get dependencies for non-inclusion expressions": function testNonInclusionDependencies() {
+			/** Dependencies for non inclusion expressions. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assertDependencies(["_id"], expr, true);
+			assertDependencies([], expr, false);
+			expr.addField("b", FieldPathExpression.create("c.d"));
+			assertDependencies(["_id", "c.d"], expr, true);
+			assertDependencies(["c.d"], expr, false);
+		},
+
+		"should be able to get dependencies for inclusion expressions": function testInclusionDependencies() {
+			/** Dependencies for inclusion expressions. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assertDependencies(["_id", "a"], expr, true);
+			var unused = new DepsTracker();
+			assert.throws(function() {
+				expr.addDependencies(unused);
+			}, Error);
+		},
+
+	},
+
+	"#serialize": {
+
+		"should be able to convert to JSON representation and have constants represented by expressions": function testJson() {
+			/** Serialize to a BSONObj, with constants represented by expressions. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("foo.a", ConstantExpression.create(5));
+			assert.deepEqual({foo:{a:{$const:5}}}, expr.serialize());
+		},
+
+	},
+
+	"#optimize": {
+
+		"should be able to optimize expression and sub-expressions": function testOptimize() {
+			/** Optimizing an object expression optimizes its sub expressions. */
+			var expr = ObjectExpression.createRoot();
+			// Add inclusion.
+			expr.includePath("a");
+			// Add non inclusion.
+			var andExpr = new AndExpression();
+			expr.addField("b", andExpr);
+			expr.optimize();
+			// Optimizing 'expression' optimizes its non inclusion sub expressions, while
+			// inclusion sub expressions are passed through.
+			assert.deepEqual({a:true, b:{$const:true}}, expressionToJson(expr));
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should be able to provide an empty object": function testEmpty() {
+			/** Empty object spec. */
+			var expr = ObjectExpression.createRoot();
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {}
+			});
+		},
+
+		"should be able to include 'a' field only": function testInclude() {
+			/** Include 'a' field only. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:1},
+				expectedDependencies: ["_id", "a"],
+				expectedJsonRepresentation: {a:true}
+			});
+		},
+
+		"should NOT be able to include missing 'a' field": function testMissingInclude() {
+			/** Cannot include missing 'a' field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assertExpectedResult({
+				source: {_id:0, b:2},
+				expression: expr,
+				expected: {_id:0},
+				expectedDependencies: ["_id", "a"],
+				expectedJsonRepresentation: {a:true}
+			});
+		},
+
+		"should be able to include '_id' field only": function testIncludeId() {
+			/** Include '_id' field only. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("_id");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {_id:true}
+			});
+		},
+
+		"should be able to exclude '_id' field": function testExcludeId() {
+			/** Exclude '_id' field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("b");
+			expr.excludeId = true;
+			assertExpectedResult({
+				expression: expr,
+				expected: {b:2},
+				expectedDependencies: ["b"],
+				expectedJsonRepresentation: {_id:false, b:true}
+			});
+		},
+
+		"should be able to include fields in source document order regardless of inclusion order": function testSourceOrder() {
+			/** Result order based on source document field order, not inclusion spec field order. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("b");
+			expr.includePath("a");
+			assertExpectedResult({
+				expression: expr,
+				get expected() { return this.source; },
+				expectedDependencies: ["_id", "a", "b"],
+				expectedJsonRepresentation: {b:true, a:true}
+			});
+		},
+
+		"should be able to include a nested field": function testIncludeNested() {
+			/** Include a nested field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{b:5}},
+				source: {_id:0, a:{b:5, c:6}, z:2},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to include two nested fields": function testIncludeTwoNested() {
+			/** Include two nested fields. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			expr.includePath("a.c");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{b:5, c:6}},
+				source: {_id:0, a:{b:5,c:6}, z:2},
+				expectedDependencies: ["_id", "a.b", "a.c"],
+				expectedJsonRepresentation: {a:{b:true, c:true}}
+			});
+		},
+
+		"should be able to include two fields nested within different parents": function testIncludeTwoParentNested() {
+			/** Include two fields nested within different parents. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			expr.includePath("c.d");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{b:5}, c:{d:6}},
+				source: {_id:0, a:{b:5}, c:{d:6}, z:2},
+				expectedDependencies: ["_id", "a.b", "c.d"],
+				expectedJsonRepresentation: {a:{b:true}, c:{d:true}}
+			});
+		},
+
+		"should be able to attempt to include a missing nested field": function testIncludeMissingNested() {
+			/** Attempt to include a missing nested field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0, a:{}},
+				source: {_id:0, a:{c:6}, z:2},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to attempt to include a nested field within a non object": function testIncludeNestedWithinNonObject() {
+			/** Attempt to include a nested field within a non object. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0},
+				source: {_id:0, a:2, z:2},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to include a nested field within an array": function testIncludeArrayNested() {
+			/** Include a nested field within an array. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				expected: {_id:0,a:[{b:5},{b:2},{}]},
+				source: {_id:0,a:[{b:5,c:6},{b:2,c:9},{c:7},[],2],z:1},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should NOT include non-root '_id' field implicitly": function testExcludeNonRootId() {
+			/** Don't include not root '_id' field implicitly. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a.b");
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, a:{_id:1, b:1}},
+				expected: {_id:0, a:{b:1}},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:true}}
+			});
+		},
+
+		"should be able to project a computed expression": function testComputed() {
+			/** Project a computed expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:5},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:5}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a computed expression replacing an existing field": function testComputedReplacement() {
+			/** Project a computed expression replacing an existing field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, a:99},
+				expected: {_id:0, a:5},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:5}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should NOT be able to project an undefined value": function testComputedUndefined() {
+			/** An undefined value is passed through */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(undefined));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:undefined},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:undefined}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a computed expression replacing an existing field with Undefined": function testComputedUndefinedReplacement() {
+			/** Project a computed expression replacing an existing field with Undefined. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(undefined));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, a:99},
+				expected: {_id:0, a:undefined},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:undefined}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a null value": function testComputedNull() {
+			/** A null value is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(null));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:null},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{$const:null}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a nested value": function testComputedNested() {
+			/** A nested value is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(5));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:5}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:5}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a field path": function testComputedFieldPath() {
+			/** A field path is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", FieldPathExpression.create("x"));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, x:4},
+				expected: {_id:0, a:4},
+				expectedDependencies: ["_id", "x"],
+				expectedJsonRepresentation: {a:"$x"},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a nested field path": function testComputedNestedFieldPath() {
+			/** A nested field path is projected. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", FieldPathExpression.create("x.y"));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0, x:{y:4}},
+				expected: {_id:0, a:{b:4}},
+				expectedDependencies: ["_id", "x.y"],
+				expectedJsonRepresentation: {a:{b:"$x.y"}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should NOT project an empty subobject expression for a missing field": function testEmptyNewSubobject() {
+			/** An empty subobject expression for a missing field is not projected. */
+			var expr = ObjectExpression.createRoot();
+			// Create a sub expression returning an empty object.
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", FieldPathExpression.create("a.b"));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0},
+				expectedDependencies: ["_id", "a.b"],
+				expectedJsonRepresentation: {a:{b:"$a.b"}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project a non-empty new subobject": function testNonEmptyNewSubobject() {
+			/** A non empty subobject expression for a missing field is projected. */
+			var expr = ObjectExpression.createRoot();
+			// Create a sub expression returning an empty object.
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(6));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent": function testAdjacentDottedComputedFields() {
+			/** Two computed fields within a common parent. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			expr.addField("a.c", ConstantExpression.create(7));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent (w/ one case dotted)": function testAdjacentDottedAndNestedComputedFields() {
+			/** Two computed fields within a common parent, in one case dotted. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("c", ConstantExpression.create(7));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent (in another case dotted)": function testAdjacentNestedAndDottedComputedFields() {
+			/** Two computed fields within a common parent, in another case dotted. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(6));
+			expr.addField("a", subExpr);
+			expr.addField("a.c", ConstantExpression.create(7));
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project two computed fields within a common parent (nested rather than dotted)": function testAdjacentNestedComputedFields() {
+			/** Two computed fields within a common parent, nested rather than dotted. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr1 = ObjectExpression.create();
+			subExpr1.addField("b", ConstantExpression.create(6));
+			expr.addField("a", subExpr1);
+			var subExpr2 = ObjectExpression.create();
+			subExpr2.addField("c", ConstantExpression.create(7));
+			expr.addField("a", subExpr2);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, c:7}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},c:{$const:7}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project multiple nested fields out of order without affecting output order": function testAdjacentNestedOrdering() {
+			/** Field ordering is preserved when nested fields are merged. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			var subExpr = ObjectExpression.create();
+			// Add field 'd' then 'c'.  Expect the same field ordering in the result doc.
+			subExpr.addField("d", ConstantExpression.create(7));
+			subExpr.addField("c", ConstantExpression.create(8));
+			expr.addField("a", subExpr);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:6, d:7, c:8}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{$const:6},d:{$const:7},c:{$const:8}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should be able to project adjacent fields two levels deep": function testMultipleNestedFields() {
+			/** Adjacent fields two levels deep. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b.c", ConstantExpression.create(6));
+			var bSubExpression = ObjectExpression.create();
+			bSubExpression.addField("d", ConstantExpression.create(7));
+			var aSubExpression = ObjectExpression.create();
+			aSubExpression.addField("b", bSubExpression);
+			expr.addField("a", aSubExpression);
+			assertExpectedResult({
+				expression: expr,
+				source: {_id:0},
+				expected: {_id:0, a:{b:{c:6, d:7}}},
+				expectedDependencies: ["_id"],
+				expectedJsonRepresentation: {a:{b:{c:{$const:6},d:{$const:7}}}},
+				expectedIsSimple: false
+			});
+		},
+
+		"should throw an Error if two expressions generate the same field": function testConflictingExpressionFields() {
+			/** Two expressions cannot generate the same field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(6)); // Duplicate field.
+			}, Error);
+		},
+
+		"should throw an Error if an expression field conflicts with an inclusion field": function testConflictingInclusionExpressionFields() {
+			/** An expression field conflicts with an inclusion field. */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(6));
+			}, Error);
+		},
+
+		"should throw an Error if an inclusion field conflicts with an expression field": function testConflictingExpressionInclusionFields() {
+			/** An inclusion field conflicts with an expression field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assert.throws(function() {
 				expr.includePath("a");
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(6));
-				}, Error);
-			},
-
-			"should throw an Error if an inclusion field conflicts with an expression field": function testConflictingExpressionInclusionFields(){
-				/** An inclusion field conflicts with an expression field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.includePath("a");
-				}, Error);
-			},
-
-			"should throw an Error if an object expression conflicts with a constant expression": function testConflictingObjectConstantExpressionFields(){
-				/** An object expression conflicts with a constant expression. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr = ObjectExpression.create();
-				subExpr.includePath("b");
+			}, Error);
+		},
+
+		"should throw an Error if an object expression conflicts with a constant expression": function testConflictingObjectConstantExpressionFields() {
+			/** An object expression conflicts with a constant expression. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr = ObjectExpression.create();
+			subExpr.includePath("b");
+			expr.addField("a", subExpr);
+			assert.throws(function() {
+				expr.addField("a.b", ConstantExpression.create(6));
+			}, Error);
+		},
+
+		"should throw an Error if a constant expression conflicts with an object expression": function testConflictingConstantObjectExpressionFields() {
+			/** A constant expression conflicts with an object expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(6));
+			var subExpr = ObjectExpression.create();
+			subExpr.includePath("b");
+			assert.throws(function() {
 				expr.addField("a", subExpr);
-				assert.throws(function(){
-					expr.addField("a.b", new ConstantExpression(6));
-				}, Error);
-			},
-
-			"should throw an Error if a constant expression conflicts with an object expression": function testConflictingConstantObjectExpressionFields(){
-				/** A constant expression conflicts with an object expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(6));
-				var subExpr = ObjectExpression.create();
-				subExpr.includePath("b");
-				assert.throws(function(){
-					expr.addField("a", subExpr);
-				}, Error);
-			},
-
-			"should throw an Error if two nested expressions cannot generate the same field": function testConflictingNestedFields(){
-				/** Two nested expressions cannot generate the same field. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a.b", new ConstantExpression(6));	// Duplicate field.
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a subfield of another expression": function testConflictingFieldAndSubfield(){
-				/** An expression cannot be created for a subfield of another expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a.b", new ConstantExpression(5));
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a nested field of another expression.": function testConflictingFieldAndNestedField(){
-				/** An expression cannot be created for a nested field of another expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a", new ConstantExpression(5));
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a", subExpr);
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a parent field of another expression": function testConflictingSubfieldAndField(){
-				/** An expression cannot be created for a parent field of another expression. */
-				var expr = ObjectExpression.createRoot();
-				expr.addField("a.b", new ConstantExpression(5));
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(5));
-				}, Error);
-			},
-
-			"should throw an Error if an expression is created for a parent of a nested field": function testConflictingNestedFieldAndField(){
-				/** An expression cannot be created for a parent of a nested field. */
-				var expr = ObjectExpression.createRoot();
-				var subExpr = ObjectExpression.create();
-				subExpr.addField("b", new ConstantExpression(5));
+			}, Error);
+		},
+
+		"should throw an Error if two nested expressions cannot generate the same field": function testConflictingNestedFields() {
+			/** Two nested expressions cannot generate the same field. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a.b", ConstantExpression.create(6));	// Duplicate field.
+			}, Error);
+		},
+
+		"should throw an Error if an expression is created for a subfield of another expression": function testConflictingFieldAndSubfield() {
+			/** An expression cannot be created for a subfield of another expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a.b", ConstantExpression.create(5));
+			}, Error);
+		},
+
+		"should throw an Error if an expression is created for a nested field of another expression.": function testConflictingFieldAndNestedField() {
+			/** An expression cannot be created for a nested field of another expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a", ConstantExpression.create(5));
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(5));
+			assert.throws(function() {
 				expr.addField("a", subExpr);
-				assert.throws(function(){
-					expr.addField("a", new ConstantExpression(5));
-				}, Error);
-			},
-
-			"should be able to evaluate expressions in general": function testEvaluate(){
-				/**
-				 * evaluate() does not supply an inclusion document.
-				 * Inclusion spec'd fields are not included.
-				 * (Inclusion specs are not generally expected/allowed in cases where evaluate is called instead of addToDocument.)
-				 */
-				var expr = ObjectExpression.createRoot();
-				expr.includePath("a");
-				expr.addField("b", new ConstantExpression(5));
-				expr.addField("c", FieldPathExpression.create("a"));
-				var res = expr.evaluateInternal(new Variables(1, {_id:0, a:1}));
-				assert.deepEqual({"b":5, "c":1}, res);
-			}
-		}
+			}, Error);
+		},
 
-	}
+		"should throw an Error if an expression is created for a parent field of another expression": function testConflictingSubfieldAndField() {
+			/** An expression cannot be created for a parent field of another expression. */
+			var expr = ObjectExpression.createRoot();
+			expr.addField("a.b", ConstantExpression.create(5));
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(5));
+			}, Error);
+		},
 
-};
+		"should throw an Error if an expression is created for a parent of a nested field": function testConflictingNestedFieldAndField() {
+			/** An expression cannot be created for a parent of a nested field. */
+			var expr = ObjectExpression.createRoot();
+			var subExpr = ObjectExpression.create();
+			subExpr.addField("b", ConstantExpression.create(5));
+			expr.addField("a", subExpr);
+			assert.throws(function() {
+				expr.addField("a", ConstantExpression.create(5));
+			}, Error);
+		},
+
+		"should be able to evaluate expressions in general": function testEvaluate() {
+			/**
+			 * evaluate() does not supply an inclusion document.
+			 * Inclusion spec'd fields are not included.
+			 * (Inclusion specs are not generally expected/allowed in cases where evaluate is called instead of addToDocument.)
+			 */
+			var expr = ObjectExpression.createRoot();
+			expr.includePath("a");
+			expr.addField("b", ConstantExpression.create(5));
+			expr.addField("c", FieldPathExpression.create("a"));
+			var res = expr.evaluateInternal(new Variables(1, {_id:0, a:1}));
+			assert.deepEqual({"b":5, "c":1}, res);
+		},
+
+	},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

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

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

+ 23 - 34
test/lib/pipeline/expressions/SecondExpression.js

@@ -3,53 +3,42 @@ var assert = require("assert"),
 	SecondExpression = require("../../../../lib/pipeline/expressions/SecondExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.SecondExpression = {
 
-	"SecondExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SecondExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new SecondExpression() instanceof SecondExpression);
+			assert(new SecondExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $second": function testOpName(){
-				assert.equal(new SecondExpression().getOpName(), "$second");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new SecondExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new SecondExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $second": function() {
+			assert.equal(new SecondExpression().getOpName(), "$second");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return the current second in the date; 19 for 2013-02-18 11:24:19 EST": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$second:"$someDate"}).evaluate({someDate:new Date("2013-02-18T11:24:19.000Z")}), 19);
-			}
+	"#evaluate()": {
 
-				/*
-			"should return the leap second in the date; 60 for June 30, 2012 at 23:59:60 UTC": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$second:"$someDate"}).evaluate({someDate:new Date("June 30, 2012 at 23:59:60 UTC")}), 60);
-			}
-
-				*/
-		}
+		"should return second; 53 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$second", operands);
+			assert.strictEqual(expr.evaluate({}), 53);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 321 - 83
test/lib/pipeline/expressions/SetDifferenceExpression.js

@@ -1,87 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetDifferenceExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetDifferenceExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setdifference": function testOpName() {
-								assert.equal(new SetDifferenceExpression([1, 2, 3], [4, 5, 6]).getOpName(), "$setdifference");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setdifference: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setdifference: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setdifference: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and return a difference between the arrays": function testBasicAssignment() {
-								var array1 = [1, 9, 2, 3, 4, 5],
-										array2 = [5, 6, 7, 2, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setdifference: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [1, 3, 4, 6, 7, 8]);
-						},
-
-				}
-
-		}
+	SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
-};
+// 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.SetDifferenceExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetDifferenceExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetDifferenceExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setDifference": function() {
+			assert.equal(new SetDifferenceExpression().getOpName(), "$setDifference");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						$setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						$setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						$setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						$setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						"$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						// $setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						"$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 321 - 83
test/lib/pipeline/expressions/SetEqualsExpression.js

@@ -1,87 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetEqualsExpression = require("../../../../lib/pipeline/expressions/SetEqualsExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetEqualsExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetEqualsExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setequals": function testOpName() {
-								assert.equal(new SetEqualsExpression([1,2,3],[4,5,6]).getOpName(), "$setequals");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setequals: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setequals: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setequals: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and array1 should equal array2": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4],
-										array2 = [6, 7, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setequals: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [6, 7, 8, 9]);
-						},
-
-				}
-
-		}
+	SetEqualsExpression = require("../../../../lib/pipeline/expressions/SetEqualsExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
-};
+// 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.SetEqualsExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetEqualsExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetEqualsExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setEquals": function() {
+			assert.equal(new SetEqualsExpression().getOpName(), "$setEquals");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						$setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						$setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 59 - 0
test/lib/pipeline/expressions/SetExpectedResultBase.js

@@ -0,0 +1,59 @@
+"use strict";
+var assert = require("assert"),
+	SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"), //jshint ignore:line
+	SetIsSubsetExpression = require("../../../../lib/pipeline/expressions/SetIsSubsetExpression"), //jshint ignore:line
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var ExpectedResultBase = module.exports = (function() { //jshint ignore:line
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({});
+				if (result instanceof Array){
+					result.sort();
+				}
+				var errMsg = "for expression " + fieldFirst +
+					" with argument " + JSON.stringify(args) +
+					" full tree: " + JSON.stringify(expr.serialize(false)) +
+					" expected: " + JSON.stringify(expected) +
+					" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+				// var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác