Browse Source

Merge pull request #118 from RiveraGroup/feature/mongo_2.6.5_expressions

Feature/mongo 2.6.5 expressions
Kyle P Davis 11 năm trước cách đây
mục cha
commit
9cb55333b0
100 tập tin đã thay đổi với 3698 bổ sung3862 xóa
  1. 17 1
      .gitignore
  2. 107 0
      lib/pipeline/DepsTracker.js
  3. 110 33
      lib/pipeline/Document.js
  4. 30 27
      lib/pipeline/FieldPath.js
  5. 85 0
      lib/pipeline/ParsedDeps.js
  6. 255 130
      lib/pipeline/Value.js
  7. 88 0
      lib/pipeline/ValueSet.js
  8. 33 41
      lib/pipeline/accumulators/Accumulator.js
  9. 27 36
      lib/pipeline/accumulators/AddToSetAccumulator.js
  10. 29 33
      lib/pipeline/accumulators/AvgAccumulator.js
  11. 10 18
      lib/pipeline/accumulators/FirstAccumulator.js
  12. 16 13
      lib/pipeline/accumulators/LastAccumulator.js
  13. 25 31
      lib/pipeline/accumulators/MinMaxAccumulator.js
  14. 20 25
      lib/pipeline/accumulators/PushAccumulator.js
  15. 25 18
      lib/pipeline/accumulators/SumAccumulator.js
  16. 0 114
      lib/pipeline/documentSources/DocumentSource.js
  17. 41 23
      lib/pipeline/expressions/AddExpression.js
  18. 14 30
      lib/pipeline/expressions/AllElementsTrueExpression.js
  19. 60 47
      lib/pipeline/expressions/AndExpression.js
  20. 14 22
      lib/pipeline/expressions/AnyElementTrueExpression.js
  21. 20 24
      lib/pipeline/expressions/CoerceToBoolExpression.js
  22. 85 80
      lib/pipeline/expressions/CompareExpression.js
  23. 23 24
      lib/pipeline/expressions/ConcatExpression.js
  24. 38 58
      lib/pipeline/expressions/CondExpression.js
  25. 33 34
      lib/pipeline/expressions/ConstantExpression.js
  26. 13 25
      lib/pipeline/expressions/DayOfMonthExpression.js
  27. 13 18
      lib/pipeline/expressions/DayOfWeekExpression.js
  28. 17 26
      lib/pipeline/expressions/DayOfYearExpression.js
  29. 23 18
      lib/pipeline/expressions/DivideExpression.js
  30. 190 158
      lib/pipeline/expressions/Expression.js
  31. 121 167
      lib/pipeline/expressions/FieldPathExpression.js
  32. 0 207
      lib/pipeline/expressions/FieldRangeExpression.js
  33. 35 0
      lib/pipeline/expressions/FixedArityExpressionT.js
  34. 13 20
      lib/pipeline/expressions/HourExpression.js
  35. 10 25
      lib/pipeline/expressions/IfNullExpression.js
  36. 75 54
      lib/pipeline/expressions/LetExpression.js
  37. 62 59
      lib/pipeline/expressions/MapExpression.js
  38. 12 26
      lib/pipeline/expressions/MillisecondExpression.js
  39. 12 24
      lib/pipeline/expressions/MinuteExpression.js
  40. 25 36
      lib/pipeline/expressions/ModExpression.js
  41. 12 24
      lib/pipeline/expressions/MonthExpression.js
  42. 32 24
      lib/pipeline/expressions/MultiplyExpression.js
  43. 31 0
      lib/pipeline/expressions/NaryBaseExpressionT.js
  44. 108 90
      lib/pipeline/expressions/NaryExpression.js
  45. 11 23
      lib/pipeline/expressions/NotExpression.js
  46. 170 165
      lib/pipeline/expressions/ObjectExpression.js
  47. 53 35
      lib/pipeline/expressions/OrExpression.js
  48. 12 26
      lib/pipeline/expressions/SecondExpression.js
  49. 33 33
      lib/pipeline/expressions/SetDifferenceExpression.js
  50. 33 27
      lib/pipeline/expressions/SetEqualsExpression.js
  51. 43 29
      lib/pipeline/expressions/SetIntersectionExpression.js
  52. 69 61
      lib/pipeline/expressions/SetIsSubsetExpression.js
  53. 25 34
      lib/pipeline/expressions/SetUnionExpression.js
  54. 10 21
      lib/pipeline/expressions/SizeExpression.js
  55. 22 28
      lib/pipeline/expressions/StrcasecmpExpression.js
  56. 24 31
      lib/pipeline/expressions/SubstrExpression.js
  57. 36 25
      lib/pipeline/expressions/SubtractExpression.js
  58. 11 24
      lib/pipeline/expressions/ToLowerExpression.js
  59. 10 23
      lib/pipeline/expressions/ToUpperExpression.js
  60. 94 74
      lib/pipeline/expressions/Variables.js
  61. 8 8
      lib/pipeline/expressions/VariablesIdGenerator.js
  62. 26 23
      lib/pipeline/expressions/VariablesParseState.js
  63. 23 0
      lib/pipeline/expressions/VariadicExpressionT.js
  64. 14 31
      lib/pipeline/expressions/WeekExpression.js
  65. 9 24
      lib/pipeline/expressions/YearExpression.js
  66. 2 2
      lib/pipeline/expressions/index.js
  67. 28 56
      lib/pipeline/matcher/AllElemMatchOp.js
  68. 6 0
      lib/pipeline/matcher/AndMatchExpression.js
  69. 26 30
      lib/pipeline/matcher/ArrayFilterEntries.js
  70. 30 37
      lib/pipeline/matcher/ArrayMatchingMatchExpression.js
  71. 2 6
      lib/pipeline/matcher/AtomicMatchExpression.js
  72. 40 58
      lib/pipeline/matcher/ComparisonMatchExpression.js
  73. 0 73
      lib/pipeline/matcher/Context.js
  74. 18 24
      lib/pipeline/matcher/ElemMatchObjectMatchExpression.js
  75. 6 13
      lib/pipeline/matcher/ElemMatchValueMatchExpression.js
  76. 26 35
      lib/pipeline/matcher/ElementPath.js
  77. 5 4
      lib/pipeline/matcher/EqualityMatchExpression.js
  78. 6 9
      lib/pipeline/matcher/ExistsMatchExpression.js
  79. 19 9
      lib/pipeline/matcher/FalseMatchExpression.js
  80. 15 13
      lib/pipeline/matcher/GTEMatchExpression.js
  81. 14 13
      lib/pipeline/matcher/GTMatchExpression.js
  82. 55 74
      lib/pipeline/matcher/InMatchExpression.js
  83. 0 58
      lib/pipeline/matcher/IndexKeyMatchableDocument.js
  84. 14 12
      lib/pipeline/matcher/LTEMatchExpression.js
  85. 14 12
      lib/pipeline/matcher/LTMatchExpression.js
  86. 39 76
      lib/pipeline/matcher/LeafMatchExpression.js
  87. 53 43
      lib/pipeline/matcher/ListOfMatchExpression.js
  88. 42 49
      lib/pipeline/matcher/MatchDetails.js
  89. 139 88
      lib/pipeline/matcher/MatchExpression.js
  90. 117 52
      lib/pipeline/matcher/MatchExpressionParser.js
  91. 14 279
      lib/pipeline/matcher/Matcher2.js
  92. 10 16
      lib/pipeline/matcher/ModMatchExpression.js
  93. 5 0
      lib/pipeline/matcher/NorMatchExpression.js
  94. 9 16
      lib/pipeline/matcher/NotMatchExpression.js
  95. 6 0
      lib/pipeline/matcher/OrMatchExpression.js
  96. 42 25
      lib/pipeline/matcher/RegexMatchExpression.js
  97. 14 17
      lib/pipeline/matcher/SizeMatchExpression.js
  98. 0 55
      lib/pipeline/matcher/TagData.js
  99. 112 0
      lib/pipeline/matcher/TextMatchExpression.js
  100. 25 0
      lib/pipeline/matcher/TextMatchExpressionParser.js

+ 17 - 1
.gitignore

@@ -1,2 +1,18 @@
-/node_modules/
+!.gitkeep
+
+# node
+/node_modules
+npm-debug.log
+
+# IDE files
+/.idea
+/.settings.xml
+/.settings
+/.c9revisions/
+
+# misc files
+*.swp
+.DS_Store
+
+# build files
 /reports/

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

+ 255 - 130
lib/pipeline/Value.js

@@ -8,162 +8,287 @@
  * @constructor
  **/
 var Value = module.exports = function Value(){
-	if(this.constructor == Value) throw new Error("Never create instances of this! Use the static helpers only.");
-}, klass = Value, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-// PRIVATE STUFF
-function getTypeVerifier(type, IClass, isStrict) {
-	return function verifyType(value) {
-		if (typeof(value) != type) throw new Error("typeof value is not: " + type + "; actual: " + typeof(value));
-		if (typeof(IClass) == "function" && !(isStrict ? value.constructor == IClass : value instanceof IClass)) throw new Error("instanceof value is not: " + IClass.name + "; actual: " + value.constructor.name);
-		return value;
-	};
-}
-
-// STATIC MEMBERS
-klass.verifyNumber = getTypeVerifier("number", Number);	//NOTE: replaces #getDouble(), #getInt(), and #getLong()
-klass.verifyString = getTypeVerifier("string", String);
-klass.verifyDocument = getTypeVerifier("object", Object, true);	//TODO: change to verifyObject? since we're not using actual Document instances
-klass.verifyArray = getTypeVerifier("object", Array, true);
-klass.verifyDate = getTypeVerifier("object", Date, true);
-klass.verifyRegExp = getTypeVerifier("object", RegExp, true);	//NOTE: renamed from #getRegex()
-//TODO:	klass.verifyOid = ...?
-//TODO:	klass.VerifyTimestamp = ...?
-klass.verifyBool = getTypeVerifier("boolean", Boolean, true);
+	if(this.constructor === Value) throw new Error("Never create instances of this! Use the static helpers only.");
+}, klass = Value;
 
+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":
-		return 0;
-	case "number":
-		return value;
-	case "object":
-		switch (value.constructor.name) {
-			case "Long":
-				return parseInt(value.toString(), 10);
-			case "Double":
-				return parseFloat(value.value, 10);
-			default:
-				throw new Error("can't convert from BSON type " + value.constructor.name + " to int; codes 16003, 16004, 16005");
-		}
-		return value;
-	default:
-		throw new Error("can't convert from BSON type " + typeof(value) + " to int; codes 16003, 16004, 16005");
+	switch (Value.getType(value)) {
+		case "undefined":
+			return 0;
+		case "number":
+			return value;
+		case "Long":
+			return parseInt(value.toString(), 10);
+		case "Double":
+			return parseFloat(value.value, 10);
+		default:
+			throw new Error("can't convert from BSON type " + Value.getType(value) + " to int; codes 16003, 16004, 16005");
 	}
 };
+klass.coerceToDouble = klass.coerceToNumber;
 klass.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");
+	throw new Error("can't convert from BSON type " + Value.getType(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 = Value.getType(value);
+	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 " + Value.getType(value) + " to String; uassert code 16007");
 	}
 };
-//TODO:	klass.coerceToTimestamp = ...?
+//SKIPPED: coerceToTimestamp
 
 /**
- * Compare two Values.
- *
+ * Helper function for Value.compare
+ * @method cmp
+ * @static
+ */
+var cmp = klass.cmp = function cmp(left, right){
+	// The following is lifted directly from compareElementValues
+	// to ensure identical handling of NaN
+	if (left < right)
+		return -1;
+	if (left === right)
+		return 0;
+	if (isNaN(left))
+		return isNaN(right) ? 0 : -1;
+	return 1;
+};
+
+/** Compare two Values.
  * @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),
-		rt = r === null ? "null" : typeof(r),
+	var lType = Value.getType(l),
+		rType = Value.getType(r),
 		ret;
 
-	// NOTE: deviation from mongo code: javascript types do not work quite the same, so for proper results we always canonicalize, and we don't need the "speed" hack
-	ret = (klass.cmp(klass.canonicalize(l), klass.canonicalize(r)));
+	ret = lType === rType ?
+	 	0 // fast-path common case
+		: cmp(klass.canonicalize(l), klass.canonicalize(r));
 
-	if(ret !== 0) return ret;
+	if(ret !== 0)
+		return ret;
 
-	// Numbers
-	if (lt === "number" && rt === "number"){
-		//NOTE: deviation from Mongo code: they handle NaN a bit differently
-		if (isNaN(l)) return isNaN(r) ? 0 : -1;
-		if (isNaN(r)) return 1;
-		return klass.cmp(l,r);
-	}
-	// Compare MinKey and MaxKey cases
-	if(l.constructor && l.constructor.name in {'MinKey':1,'MaxKey':1} ){
-		if(l.constructor.name == r.constructor.name) { 
-			return 0; 
-		} else if (l.constructor.name === 'MinKey'){
-			return -1;
-		} else {
-			return 1; // Must be MaxKey, which is greater than everything but MaxKey (which r cannot be)
-		}	
-	}
-	// hack: These should really get converted to their BSON type ids and then compared, we use int vs object in queries
-	if (lt === "number" && rt === "object"){
-		return -1;
-	} else if (lt === "object" && rt === "number") {
-		return 1;
-	}
 	// CW TODO for now, only compare like values
-	if (lt !== rt) throw new Error("can't compare values of BSON types [" + lt + " " + l.constructor.name + "] and [" + rt + ":" + r.constructor.name + "]; code 16016");
-	// Compare everything else
-	switch (lt) {
-	case "number":
-		throw new Error("number types should have been handled earlier!");
-	case "string":
-		return klass.cmp(l,r);
-	case "boolean":
-		return l == r ? 0 : l ? 1 : -1;
-	case "undefined": //NOTE: deviation from mongo code: we are comparing null to null or undefined to undefined (otherwise the ret stuff above would have caught it)
-	case "null":
-		return 0;
-	case "object":
-		if (l instanceof Array) {
-			for (var i = 0, ll = l.length, rl = r.length; true ; ++i) {
-				if (i > ll) {
-					if (i > rl) return 0; // arrays are same length
-					return -1; // left array is shorter
-				}
-				if (i > rl) return 1; // right array is shorter
-				var cmp = Value.compare(l[i], r[i]);
-				if (cmp !== 0) return cmp;
+	if (lType !== rType)
+		throw new Error("can't compare values of BSON types [" + lType + "] and [" + rType + "]; code 16016");
+
+	switch (lType) {
+		// Order of types is the same as in compareElementValues() to make it easier to verify
+
+		// These are valueless types
+		//SKIPPED: case "EOO":
+		case "undefined":
+		case "null":
+		//SKIPPED: case "jstNULL":
+		case "MaxKey":
+		case "MinKey":
+			return ret;
+
+		case "boolean":
+			return l - r;
+
+		// WARNING: Timestamp and Date have same canonical type, but compare differently.
+		// Maintaining behavior from normal BSON.
+		//SKIPPED: case "Timestamp": //unsigned-----//TODO: handle case for bson.Timestamp()
+		case "Date": // signed
+			return cmp(l.getTime(), r.getTime());
+
+        // Numbers should compare by equivalence even if different types
+		case "number":
+			return cmp(l, r);
+
+        //SKIPPED: case "jstOID":----//TODO: handle case for bson.ObjectID()
+
+        case "Code":
+        case "Symbol":
+        case "string":
+			l = String(l);
+			r = String(r);
+			return l < r ? -1 : l > r ? 1 : 0;
+
+		case "Object":
+			if (Document === undefined) Document = require("./Document");	//TODO: a dirty hack; need to investigate and clean up
+			return Document.compare(l, r);
+
+		case "Array":
+			var lArr = l,
+				rArr = r;
+
+			var elems = Math.min(lArr.length, rArr.length);
+			for (var i = 0; i < elems; i++) {
+				// compare the two corresponding elements
+				ret = Value.compare(lArr[i], rArr[i]);
+				if (ret !== 0)
+					return ret;
 			}
+			// if we get here we are either equal or one is prefix of the other
+			return cmp(lArr.length, rArr.length);
+
+		//SKIPPED: case "DBRef":-----//TODO: handle case for bson.DBRef()
+		//SKIPPED: case "BinData":-----//TODO: handle case for bson.BinData()
 
-			throw new Error("logic error in Value.compare for Array types!");
-		}
-		if (l instanceof Date) return klass.cmp(l,r);
-		if (l instanceof RegExp) return klass.cmp(l,r);
-		if (Document === undefined) Document = require("./Document");	//TODO: a dirty hack; need to investigate and clean up
-		return Document.compare(l, r);
-	default:
-		throw new Error("unhandled left hand type:" + lt);
+		case "RegExp": // same as String in this impl but keeping order same as compareElementValues
+			l = String(l);
+			r = String(r);
+			return l < r ? -1 : l > r ? 1 : 0;
+
+		//SKIPPED: case "CodeWScope":-----//TODO: handle case for bson.CodeWScope()
 	}
+	throw new Error("Assertion failure");
+};
+
+//SKIPPED: hash_combine
+//SKIPPED: 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);
 };
 
-//TODO:	klass.hashCombine = ...?
-//TODO:	klass.getWidestNumeric = ...?
-//TODO:	klass.getApproximateSize = ...?
-//TODO:	klass.addRef = ...?
-//TODO:	klass.release = ...?
+//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")
+		return t;
+	if (v === null)
+		return "null";
+	return v.constructor.name || 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 = Value.getType(x);
+	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;
+	}
+};

+ 88 - 0
lib/pipeline/ValueSet.js

@@ -0,0 +1,88 @@
+"use strict";
+
+/**
+ * A set of values (i.e., `typedef unordered_set<Value, Value::Hash> ValueSet;`)
+ * @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;
+};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 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

+ 85 - 80
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;
-    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
+	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("./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";
+};

+ 38 - 58
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;
+ */
+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("./FixedArityExpressionT")(CondExpression, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-    if (arguments.length !== 3) throw new Error("three args expected");
-    base.call(this);
-}, klass = CondExpression,
-    base = require("./NaryExpression"),
-    proto = klass.prototype = Object.create(base.prototype, {
-	constructor: {
-	    value: klass
-	}
-    });
-
-// DEPENDENCIES
 var Value = require("../Value"),
-    Expression = require("./Expression");
+	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");
-
-    var ret = new CondExpression();
-
-    var ex = Expression.parseObject(expr);
-    var args = Expression.parseOperand(expr, vps);
-    if (args[0] !== "if")
-		throw new Error("Missing 'if' parameter to $cond");
-    if (args[1] !== "then")
-		throw new Error("Missing 'then' parameter to $cond");
-    if (args[2] !== "else")
-		throw new Error("Missing 'else' parameter to $cond");
+	if (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");
+		}
+	}
 
+	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;
+	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);

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

@@ -6,59 +6,58 @@
  * @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?
-    base.call(this);
-}, klass = ConstantExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	if (arguments.length !== 1) throw new Error(klass.name + ": args expected: value");
+	this.value = value;
+	base.call(this);
+}, klass = ConstantExpression, base = require("./FixedArityExpressionT")(ConstantExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
+var Expression = require("./Expression");
 
-// DEPENDENCIES
-var Value = require("../Value"),
-    Expression = require("./Expression");
-
-// 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
+
+proto.getOpName = function getOpName() {
+	return "$const";
+};
 
-/** Register Expression */
-Expression.registerExpression("$const",klass.parse(ConstantExpression));
-Expression.registerExpression("$literal", klass.parse(ConstantExpression)); // alias
+proto.getValue = function getValue() {
+	return this.value;
+};

+ 13 - 25
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;
-    base.call(this);
-}, klass = DayOfMonthExpression,
-    base = require("./NaryExpression"),
-    proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-    });
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+	base.call(this);
+}, 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);

+ 23 - 18
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;
-    base.call(this);
-}, klass = DivideExpression, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+	base.call(this);
+}, 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 - 158
lib/pipeline/expressions/Expression.js

@@ -2,42 +2,26 @@
 
 /**
  * A base class for all pipeline expressions; Performs common expressions within an Op.
- *
- * NOTE: An object expression can take any of the following forms:
- *
- *      f0: {f1: ..., f2: ..., f3: ...}
- *      f0: {$operator:[operand1, operand2, ...]}
- *
  * @class Expression
  * @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, proto = klass.prototype;
 
 
+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 +37,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 +53,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 +131,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");

+ 121 - 167
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));
-    } else {
-        return new FieldPathExpression("CURRENT." + raw.slice(1), vps.getVariable("CURRENT"));
-    }
+	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.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() {
-    return this;
+	// nothing can be done for these
+	return this;
 };
 
+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;
+		}
+	}
+};
+
+/**
+ * Helper for evaluatePath to handle Array case
+ */
+proto._evaluatePathArray = function _evaluatePathArray(index, input) {
+	if (!(input instanceof Array)) throw new Error("must be array; dassert");
+
+	// 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;
+
+		var nested = this._evaluatePath(index, input[i]);
+		if (nested !== undefined)
+			result.push(nested);
+	}
+	return result;
+};
 
 /**
- * Internal implementation of evaluate(), used recursively.
+ * 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.
+ * 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)
+ * @param input 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;
+ */
+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 (++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;
+	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;
 	}
-	return undefined;
-};
-
-proto.evaluatePathArray = function evaluatePathArray(index, input) {
-
-    if(!(input instanceof Array)) {
-        throw new Error("evaluatePathArray called on non-array");
-    }
-    var result = [];
-
-    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);
-            }
-        }
-    }
-    return result;
 };
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+	if (this._fieldPath.fieldNames.length === 1) // get the whole variable
+		return vars.getValue(this._variable);
 
-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);
-    } else {
-        return undefined;
-    }
-
-};
-
-
-
-proto.optimize = function(){
-        return this;
-};
+	if (this._variable === Variables.ROOT_ID) {
+		// ROOT is always a document so use optimized code path
+		return this._evaluatePath(1, vars.getRoot());
+	}
 
-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;
-		}
+	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;
 	}
 };
 
-// renamed write to get because there are no streams
-proto.getFieldPath = function getFieldPath(usePrefix){
-        return this.path.getPath(usePrefix);
+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._fieldPath.getPath(false);
+	}
 };
 
-proto.serialize = function toJSON(){
-    if(this.path.fields[0] === "CURRENT" && this.path.fields.length > 1) {
-        return "$" + this.path.tail().getPath(false);
-    } else {
-        return "$$" + this.path.getPath(false);
-    }
+proto.getFieldPath = function getFieldPath(){
+	return this._fieldPath;
 };
-
-//TODO: proto.addToBsonObj = ...?
-//TODO: proto.addToBsonArray = ...?
-
-//proto.writeFieldPath = ...?   use #getFieldPath instead

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

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

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

@@ -0,0 +1,35 @@
+"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}});
+
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
+	/**
+	 * Check that the number of args is what we expected
+	 * @method validateArguments
+	 * @param args Array The array of arguments to the expression
+	 * @throws
+	 */
+	proto.validateArguments = function validateArguments(args) {
+		if(args.length !== nArgs) {
+			throw new Error("Expression " + this.getOpName() + " takes exactly " +
+				nArgs + " arguments. " + args.length + " were passed in.");
+		}
+	};
+
+	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";
+};

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

@@ -1,105 +1,126 @@
 "use strict";
 
 var LetExpression = module.exports = function LetExpression(vars, subExpression){
-	if (arguments.length !== 2) throw new Error("Two args expected");
+	if (arguments.length !== 2) throw new Error(klass.name + ": expected args: vars, subExpression");
 	this._variables = vars;
 	this._subExpression = subExpression;
 }, klass = LetExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Variables = require("./Variables"),
-	VariablesParseState = require("./VariablesParseState");
 
-// PROTOTYPE MEMBERS
+var Value = require("../Value"),
+	Variables = require("./Variables");
 
 
-proto.parse = function parse(expr, vpsIn){
-	if(!("$let" in expr)) {
-		throw new Error("Tried to create a $let with something other than let. Looks like your parse map went all funny.");
-	}
+function NameAndExpression(name, expr){
+	this.name = name;
+	this.expression = expr;
+}
 
-	if(typeof(expr.$let) !== 'object' || (expr.$let instanceof Array)) {
-		throw new Error("$let only supports an object as it's argument:16874");
-	}
 
-	var args = expr.$let,
-		varsElem = args.vars,
-		inElem = args['in'];
+klass.parse = function parse(expr, vpsIn){
 
-	if(!varsElem) {
-		throw new Error("Missing 'vars' parameter to $let: 16876");
-	}
-	if(!inElem) {
-		throw new Error("Missing 'in' parameter to $let: 16877");
-	}
+	// if (!(exprFieldName === "$let")) throw new Error("Assertion failure"); //NOTE: DEVIATION FROM MONGO: we do not have exprFieldName here
 
-	if(Object.keys(args).length > 2) {
-		var bogus = Object.keys(args).filter(function(x) {return !(x === 'in' || x === 'vars');});
-		throw new Error("Unrecognized parameter to $let: " + bogus.join(",") + "- 16875");
-	}
+	if (Value.getType(expr) !== "Object")
+		throw new Error("$let only supports an object as it's argument; uassert code 16874");
+	var args = expr;
 
-	var vpsSub = new VariablesParseState(vpsIn),
-		vars = {};
+	// varsElem must be parsed before inElem regardless of BSON order.
+	var varsElem,
+		inElem;
+	for (var argFieldName in args) {
+		var arg = args[argFieldName];
+		if (argFieldName === "vars") {
+			varsElem = arg;
+		} else if (argFieldName === "in") {
+			inElem = arg;
+		} else {
+			throw new Error("Unrecognized parameter to $let: " + argFieldName + "; uasserted code 16875");
+		}
+	}
 
-	for(var varName in varsElem) {
+	if (!varsElem)
+		throw new Error("Missing 'vars' parameter to $let; uassert code 16876");
+	if (!inElem)
+		throw new Error("Missing 'in' parameter to $let; uassert code 16877");
+
+	// parse "vars"
+	var vpsSub = vpsIn, // vpsSub gets our vars, vpsIn doesn't.
+		vars = {}; // using like a VariableMap
+	if (Value.getType(varsElem) !== "Object") //NOTE: emulate varsElem.embeddedObjectUserCheck()
+		throw new Error("invalid parameter: expected an object (vars); uasserted code 10065");
+	for (var varName in varsElem) {
+		var varElem = varsElem[varName];
 		Variables.uassertValidNameForUserWrite(varName);
 		var id = vpsSub.defineVariable(varName);
 
-		vars[id] = {};
-		vars[id][varName] = Expression.parseOperand(varsElem, vpsIn);
+		vars[id] = new NameAndExpression(varName,
+			Expression.parseOperand(varElem, vpsIn)); // only has outer vars
 	}
 
-	var subExpression = Expression.parseOperand(inElem, vpsSub);
+	// parse "in"
+	var subExpression = Expression.parseOperand(inElem, vpsSub); // has our vars
+
 	return new LetExpression(vars, subExpression);
 };
 
+
 proto.optimize = function optimize() {
-	if(this._variables.empty()) {
+	if (Object.keys(this._variables).length === 0) {
+		// we aren't binding any variables so just return the subexpression
 		return this._subExpression.optimize();
 	}
 
-	for(var id in this._variables){
-		for(var name in this._variables[id]) {
-			this._variables[id][name] = this._variables[id][name].optimize();
-		}
+	for (var id in this._variables) {
+		this._variables[id].expression = this._variables[id].expression.optimize();
 	}
 
+	// TODO be smarter with constant "variables"
 	this._subExpression = this._subExpression.optimize();
 
 	return this;
 };
 
-proto.addDependencies = function addDependencies(deps, path){
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			this._variables[id][name].addDependencies(deps);
-		}
+
+proto.serialize = function serialize(explain) {
+	var vars = {};
+	for (var id in this._variables) {
+		vars[this._variables[id].name] = this._variables[id].expression.serialize(explain);
 	}
-	this._subExpression.addDependencies(deps, path);
-	return deps;
 
+	return {
+		$let: {
+			vars: vars,
+			in : this._subExpression.serialize(explain)
+		}
+	};
 };
 
+
 proto.evaluateInternal = function evaluateInternal(vars) {
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			vars.setValue(id, this._variables[id][name]);
-		}
+	for (var id in this._variables) {
+		var itFirst = +id, //NOTE: using the unary + to coerce it to a Number
+			itSecond = this._variables[itFirst];
+		// It is guaranteed at parse-time that these expressions don't use the variable ids we
+		// are setting
+		vars.setValue(itFirst,
+			itSecond.expression.evaluateInternal(vars));
 	}
 
 	return this._subExpression.evaluateInternal(vars);
 };
 
 
-proto.serialize = function serialize(explain) {
-	var vars = {};
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			vars[name] = this._variables[id][name];
-		}
+proto.addDependencies = function addDependencies(deps, path){
+	for (var id in this._variables) {
+		var itFirst = +id, //NOTE: using the unary + to coerce it to a Number
+			itSecond = this._variables[itFirst];
+			itSecond.expression.addDependencies(deps);
 	}
 
-	return {$let: {vars:vars, 'in':this._subExpression.serialize(explain)}};
+	// TODO be smarter when CURRENT is a bound variable
+	this._subExpression.addDependencies(deps);
 };
 
+
 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);

+ 32 - 24
lib/pipeline/expressions/MultiplyExpression.js

@@ -2,40 +2,48 @@
 
 /**
  * A $multiply pipeline expression.
- * @see evaluateInternal
  * @class MultiplyExpression
+ * @extends mungedb-aggregate.pipeline.expressions.VariadicExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var MultiplyExpression = module.exports = function MultiplyExpression(){
-	if (arguments.length !== 0) throw new Error("Zero args expected");
+ */
+var MultiplyExpression = module.exports = function MultiplyExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MultiplyExpression, base = require("./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));

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

@@ -0,0 +1,31 @@
+"use strict";
+
+/**
+ * Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class.
+ * @class NaryBaseExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @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}});
+
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
+	klass.parse = function(objExpr, vps) {
+		var expr = new SubClass(),
+			args = NaryExpression.parseArguments(objExpr, vps);
+		expr.validateArguments(args);
+		expr.operands = args;
+		return expr;
+	};
+
+	return NaryBaseExpression;
+};

+ 108 - 90
lib/pipeline/expressions/NaryExpression.js

@@ -3,132 +3,150 @@
 /**
  * The base class for all n-ary `Expression`s
  * @class NaryExpression
+ * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
- **/
-var 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}});
 
+var Variables = require("./Variables"),
+	ConstantExpression = require("./ConstantExpression");
 
-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");
+proto.optimize = function optimize() {
+	var n = this.operands.length;
 
-// PROTOTYPE MEMBERS
-proto.evaluate = undefined; // evaluate(doc){ ... defined 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.getOpName = function getOpName(doc){
-	throw new Error("NOT IMPLEMENTED BY INHERITOR");
-};
+		// substitute the optimized expression
+		this.operands[i] = optimized;
 
-proto.optimize = function optimize(){
-	var n = this.operands.length,
-		constantCount = 0;
-
-	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.fixedArity = function(nargs) {
-	this.nargs = nargs;
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return false;
 };
 
-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.");
+/**
+ * Get the name of the operator.
+ * @method getOpName
+ * @returns the name of the operator; this string belongs to the class
+ *  implementation, and should not be deleted
+ *  and should not
+ */
+proto.getOpName = function getOpName() {
+	throw new Error("NOT IMPLEMENTED BY INHERITOR");
+};
+
+/**
+ * Allow subclasses the opportunity to validate arguments at parse time.
+ * @method validateArguments
+ * @param {[type]} args [description]
+ */
+proto.validateArguments = function(args) {};
+
+klass.parseArguments = function(exprElement, vps) {
+	var out = [];
+	if (exprElement instanceof Array) {
+		for (var ii = 0; ii < exprElement.length; ii++) {
+			out.push(Expression.parseOperand(exprElement[ii], vps));
+		}
+	} else {
+		out.push(Expression.parseOperand(exprElement, vps));
 	}
+	return out;
 };

+ 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)
+			this._expressions[key] = 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;
-};

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

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

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

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

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

@@ -0,0 +1,23 @@
+"use strict";
+
+/**
+ * A factory and base class for all expressions that are variadic (AKA they accept any number of arguments)
+ * @class VariadicExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryBaseExpressionT
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ */
+var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass) {
+
+	var VariadicExpression = function VariadicExpression() {
+		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}});
+
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
+	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);

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

@@ -12,14 +12,13 @@ module.exports = {
 	DivideExpression: require("./DivideExpression.js"),
 	Expression: require("./Expression.js"),
 	FieldPathExpression: require("./FieldPathExpression.js"),
-	FieldRangeExpression: require("./FieldRangeExpression.js"),
 	HourExpression: require("./HourExpression.js"),
 	IfNullExpression: require("./IfNullExpression.js"),
 	MinuteExpression: require("./MinuteExpression.js"),
 	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"),
@@ -29,6 +28,7 @@ module.exports = {
 	SubtractExpression: require("./SubtractExpression.js"),
 	ToLowerExpression: require("./ToLowerExpression.js"),
 	ToUpperExpression: require("./ToUpperExpression.js"),
+	VariadicExpressionT: require("./VariadicExpressionT.js"),
 	WeekExpression: require("./WeekExpression.js"),
 	YearExpression: require("./YearExpression.js")
 };

+ 28 - 56
lib/pipeline/matcher/AllElemMatchOp.js

@@ -2,8 +2,7 @@
 
 var MatchExpression = require('./MatchExpression');
 
-
-// Autogenerated by cport.py on 2013-09-17 14:37
+// From expression_array.h
 var AllElemMatchOp = module.exports = function AllElemMatchOp(){
 	base.call(this);
 	this._matchType = 'ALL';
@@ -11,26 +10,15 @@ var AllElemMatchOp = module.exports = function AllElemMatchOp(){
 	this._list = [];
 }, klass = AllElemMatchOp, base =  MatchExpression , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
 	ElementPath = require('./ElementPath.js');
 
-// File: expression_array.h lines: 175-175
-//         ElementPath _elementPath;
-
+// ElementPath _elementPath
 proto._elementPath = undefined;
-
-
-// File: expression_array.h lines: 176-176
-//         std::vector< const ArrayMatchingMatchExpression* > _list;
-
+// std::vector< MatchExpression* > _list;
 proto._list = undefined;
-
-
-// File: expression_array.h lines: 174-174
-//         StringData _path;
-
+// StringData _path;
 proto._path = undefined;
 
 /**
@@ -40,18 +28,16 @@ proto._path = undefined;
  * @param anArray
  *
  */
-proto._allMatch = function _allMatch( anArray ){ //  const BSONObj& anArray
-	// File: expression_array.cpp lines: 208-215
-	if(this._list.length === 0) { return false; }
+proto._allMatch = function _allMatch(anArray) {
+	if (this._list.length === 0) return false;
 
 	for (var i = 0; i < this._list.length; i++) {
-		if( ! this._list[i].matchesArray( anArray, null ) ) { return false; }
+		if (!this._list[i].matchesArray(anArray, null)) return false;
 	}
 
 	return true;
 };
 
-
 /**
  *
  * This method adds a new expression to the internal array of expression
@@ -59,13 +45,11 @@ proto._allMatch = function _allMatch( anArray ){ //  const BSONObj& anArray
  * @param expr
  *
  */
-proto.add = function add( expr ){//  const ArrayMatchingMatchExpression* expr
-	// File: expression_array.cpp lines: 184-186
+proto.add = function add(expr) {
 	if (!expr) throw new Error("AllElemMatchOp:add#68 failed to verify expr");
 	this._list.push(expr);
 };
 
-
 /**
  *
  * Writes a debug string for this object
@@ -73,15 +57,13 @@ proto.add = function add( expr ){//  const ArrayMatchingMatchExpression* expr
  * @param level
  *
  */
-proto.debugString = function debugString( level ){ //   StringBuilder& debug, int level
-	// File: expression_array.cpp lines: 219-224
+proto.debugString = function debugString(level) {
 	console.debug(this._debugAddSpace(level) + this._path + " AllElemMatchOp: " + this._path + '\n');
 	for (var i = 0; i < this._list.length; i++) {
 		this._list[i].debugString(level +1);
 	}
 };
 
-
 /**
  *
  * checks if this expression is == to the other
@@ -89,29 +71,27 @@ proto.debugString = function debugString( level ){ //   StringBuilder& debug, in
  * @param other
  *
  */
-proto.equivalent = function equivalent( other ){//  const MatchExpression* other
-// File: expression_array.cpp lines: 227-242
+proto.equivalent = function equivalent(other) {
 	if (this.matchType() != other.matchType()) {
 		return false;
 	}
 
-	if( this._path != other._path ) {
+	if (this._path != other._path) {
 		return false;
 	}
 
-	if( this._list.length != other._list.length ) {
+	if (this._list.length != other._list.length) {
 		return false;
 	}
+
 	for (var i = 0; i < this._list.length; i++) {
-		if ( !this._list[i].equivalent( other._list[i] ) ) {
+		if (!this._list[i].equivalent(other._list[i])) {
 			return false;
 		}
 	}
 	return true;
 };
 
-
-
 /**
  *
  * gets the specified item from the list
@@ -119,12 +99,10 @@ proto.equivalent = function equivalent( other ){//  const MatchExpression* other
  * @param i
  *
  */
-proto.getChild = function getChild( i ){ //  size_t i
-// File: expression_array.h lines: 167-166
+proto.getChild = function getChild(i) {
 	return this._list[i];
 };
 
-
 /**
  *
  * Initialize the necessary items
@@ -132,11 +110,10 @@ proto.getChild = function getChild( i ){ //  size_t i
  * @param path
  *
  */
-proto.init = function init( path ){ //  const StringData& path
-// File: expression_array.cpp lines: 177-181
+proto.init = function init(path) {
 	this._path = path;
-	var s = this._elementPath.init( this._path );
-	this._elementPath.setTraverseLeafArray( false );
+	var s = this._elementPath.init(this._path);
+	this._elementPath.setTraverseLeafArray(false);
 	return s;
 };
 
@@ -148,17 +125,18 @@ proto.init = function init( path ){ //  const StringData& path
  * @param details
  *
  */
-proto.matches = function matches(doc, details){
-	// File: expression_array.cpp lines: 189-198
+proto.matches = function matches(doc, details) {
 	var self = this,
 		checker = function(element) {
-			if (!(element instanceof Array))
+			if (!(element instanceof Array)) {
 				return false;
+			}
 
 			//var amIRoot = (element.length === 0);
 
-			if (self._allMatch(element))
+			if (self._allMatch(element)) {
 				return true;
+			}
 
 			/*
 			if (!amIRoot && details && details.needRecord() {
@@ -177,15 +155,13 @@ proto.matches = function matches(doc, details){
  * @param e
  *
  */
-proto.matchesSingleElement = function matchesSingleElement( e ){ //  const BSONElement& e
-	// File: expression_array.cpp lines: 201-205
+proto.matchesSingleElement = function matchesSingleElement(e) {
 	if (!(e instanceof Array)) {
 		return false;
 	}
 	return this._allMatch(e);
 };
 
-
 /**
  *
  * return the length of the internal array
@@ -193,8 +169,7 @@ proto.matchesSingleElement = function matchesSingleElement( e ){ //  const BSONE
  * @param
  *
  */
-proto.numChildren = function numChildren( /*  */ ){
-// File: expression_array.h lines: 166-165
+proto.numChildren = function numChildren() {
 	return this._list.length;
 };
 
@@ -206,8 +181,7 @@ proto.numChildren = function numChildren( /*  */ ){
  * @param
  *
  */
-proto.path = function path( /*  */ ){
-// File: expression_array.h lines: 169-168
+proto.path = function path() {
 	return this._path;
 };
 
@@ -219,11 +193,9 @@ proto.path = function path( /*  */ ){
  * @param
  *
  */
-proto.shallowClone = function shallowClone( /*  */ ){
-// File: expression_array.h lines: 145-152
+proto.shallowClone = function shallowClone() {
 	var e = new AllElemMatchOp();
-	e.init( this._path );
+	e.init(this._path);
 	e._list = this._list.slice(0);
 	return e;
 };
-

+ 6 - 0
lib/pipeline/matcher/AndMatchExpression.js

@@ -78,5 +78,11 @@ proto.shallowClone = function shallowClone( /*  */ ){
 	for (var i = 0; i < this.numChildren(); i++) {
 		e.add(this.getChild(i).shallowClone());
 	}
+
+	if (this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
+
+	return e; // Return the shallow copy.
 };
 

+ 26 - 30
lib/pipeline/matcher/ArrayFilterEntries.js

@@ -1,7 +1,7 @@
 "use strict";
-var Value = require('../Value');
+var Value = require('../Value'),
+	ErrorCodes = require('../../Errors').ErrorCodes;
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var ArrayFilterEntries = module.exports = function ArrayFilterEntries(){
 	this._hasNull = false;
 	this._hasEmptyArray = false;
@@ -10,19 +10,12 @@ var ArrayFilterEntries = module.exports = function ArrayFilterEntries(){
 }, klass = ArrayFilterEntries, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// File: expression_leaf.h lines: 266-266
 proto._equalities = undefined;
 
-
-// File: expression_leaf.h lines: 265-265
 proto._hasEmptyArray = undefined;
 
-
-// File: expression_leaf.h lines: 264-264
 proto._hasNull = undefined;
 
-
-// File: expression_leaf.h lines: 267-267
 proto._regexes = undefined;
 
 
@@ -34,14 +27,12 @@ proto._regexes = undefined;
  *
  */
 proto.addEquality = function addEquality(e) {
-	//File expression_leaf.cpp lines 369-387
-
-	if(e instanceof Object && Object.keys(e)[0][0] === '$'){
-		return {'code':'BAD_VALUE', 'desc':'cannot next $ under $in'};
-	}
-
 	if( e instanceof RegExp ) {
-		return {'code': 'BAD_VALUE', 'desc':'ArrayFilterEntries equality cannot be a regex'};
+		return {'code': ErrorCodes.BAD_VALUE, 'desc':'ArrayFilterEntries equality cannot be a regex'};
+	}
+	
+	if (e === undefined) {
+		return {'code': ErrorCodes.BAD_VALUE, 'desc':'ArrayFilterEntries equality cannot be undefined'};
 	}
 
 	if( e === null ) {
@@ -53,7 +44,7 @@ proto.addEquality = function addEquality(e) {
 	}
 
 	this._equalities.push( e );
-	return {'code':'OK'};
+	return {'code':ErrorCodes.OK};	
 };
 
 /**
@@ -64,9 +55,8 @@ proto.addEquality = function addEquality(e) {
  *
  */
 proto.addRegex = function addRegex(expr) {
-	// File: expression_leaf.cpp lines: 389-391
 	this._regexes.push( expr );
-	return {'code':'OK'};
+	return {'code':ErrorCodes.OK};
 };
 
 /**
@@ -77,7 +67,6 @@ proto.addRegex = function addRegex(expr) {
  *
  */
 proto.contains = function contains(elem) {
-	// File: expression_leaf.h lines: 249-248
 	for (var i = 0; i < this._equalities.length; i++) {
 		if(typeof(elem) == typeof(this._equalities[i])){
 			if(Value.compare(elem, this._equalities[i]) === 0) {
@@ -100,9 +89,13 @@ proto.copyTo = function copyTo(toFillIn) {
 	toFillIn._hasNull = this._hasNull;
 	toFillIn._hasEmptyArray = this._hasEmptyArray;
 	toFillIn._equalities = this._equalities.slice(0); // Copy array
+	
 	toFillIn._regexes = this._regexes.slice(0); // Copy array
+	for (var i = 0; i < this._regexes.length; i++){
+		toFillIn._regexes.push(this._regexes[i].shallowClone());
+	}
 };
-
+    
 /**
  *
  * Return the _equalities property
@@ -110,7 +103,6 @@ proto.copyTo = function copyTo(toFillIn) {
  *
  */
 proto.equalities = function equalities(){
-	// File: expression_leaf.h lines: 248-247
 	return this._equalities;
 };
 
@@ -122,9 +114,8 @@ proto.equalities = function equalities(){
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 394-404
 	if (this._hasNull != other._hasNull) {return false;}
-	if (this._regexes.length != other._regexes.length) {return false;}
+	if (this.size() != other.size()) {return false;}
 
 	for (var i = 0; i < this._regexes.length; i++) {
 		if ( !this._regexes[i].equivalent( other._regexes[i] ) ) {
@@ -141,7 +132,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.hasEmptyArray = function hasEmptyArray(){
-	// File: expression_leaf.h lines: 256-255
 	return this._hasEmptyArray;
 };
 
@@ -152,7 +142,6 @@ proto.hasEmptyArray = function hasEmptyArray(){
  *
  */
 proto.hasNull = function hasNull(){
-	// File: expression_leaf.h lines: 254-253
 	return this._hasNull;
 };
 
@@ -163,7 +152,6 @@ proto.hasNull = function hasNull(){
  *
  */
 proto.numRegexes = function numRegexes(){
-	// File: expression_leaf.h lines: 251-250
 	return this._regexes.length;
 };
 
@@ -175,7 +163,6 @@ proto.numRegexes = function numRegexes(){
  *
  */
 proto.regex = function regex(idx) {
-	// File: expression_leaf.h lines: 252-251
 	return this._regexes[idx];
 };
 
@@ -186,7 +173,6 @@ proto.regex = function regex(idx) {
  *
  */
 proto.singleNull = function singleNull(){
-	// File: expression_leaf.h lines: 255-254
 	return this.size() == 1 && this._hasNull;
 };
 
@@ -197,7 +183,17 @@ proto.singleNull = function singleNull(){
  *
  */
 proto.size = function size(){
-	// File: expression_leaf.h lines: 257-256
 	return this._equalities.length + this._regexes.length;
 };
 
+proto.debugString = function debugString(){
+	var debug = "[ ";
+	for (var i = 0; i < this._equalities.length; i++){
+		debug += this._equalities[i].toString() + " ";
+	}
+	for (var j = 0; j < this._regexes.length; j++){
+		debug += this._regexes[j].shortDebugString() + " ";
+	}
+	debug += "]";
+	return debug;
+};

+ 30 - 37
lib/pipeline/matcher/ArrayMatchingMatchExpression.js

@@ -2,11 +2,9 @@
 
 var MatchExpression = require('./MatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var ArrayMatchingMatchExpression = module.exports = function ArrayMatchingMatchExpression(matchType){
 	base.call(this);
 	this._matchType = matchType;
-	// File: expression_array.h lines: 55-55
 	this._elementPath = new ElementPath();
 }, klass = ArrayMatchingMatchExpression, base = MatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
@@ -15,35 +13,8 @@ var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
 	ElementPath = require('./ElementPath.js');
 
-// File: expression_array.h lines: 54-54
 proto._path = undefined;
 
-/**
- *
- * Check if the input element is equivalent to us
- * @method equivalent
- * @param
- *
- */
-proto.equivalent = function equivalent(other){
-	// File: expression_array.cpp lines: 63-79
-	if ( this._matchType != other._matchType)
-		return false;
-
-	var realOther = new ArrayMatchingMatchExpression(other);
-
-	if (this._path != realOther._path)
-		return false;
-
-	if (this.numChildren() != realOther.numChildren())
-		return false;
-
-	for (var i = 0; i < this.numChildren(); i++)
-		if (!this.getChild(i).equivalent(realOther.getChild(i)))
-			return false;
-	return true;
-};
-
 /**
  *
  * Initialize the input path as our element path
@@ -52,24 +23,21 @@ proto.equivalent = function equivalent(other){
  *
  */
 proto.initPath = function initPath(path){
-	// File: expression_array.cpp lines: 27-31
 	this._path = path;
 	var status = this._elementPath.init(this._path);
 	this._elementPath.setTraverseLeafArray(false);
 	return status;
 };
 
-
-
 /**
- *
+ * Deviation from mongo:
  * matches checks the input doc against the internal path to see if it is a match
  * @method matches
  * @param doc
  * @param details
  *
  */
-proto.matches = function matches(doc, details){
+ proto.matches = function matches(doc, details){
 	var self = this,
 		checker = function(element) {
 			// we got the whole path, now check it
@@ -99,12 +67,38 @@ proto.matches = function matches(doc, details){
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(element){
-	// File: expression_array.cpp lines: 56-59
-	if (!(element instanceof Array))
+	if (!(element instanceof Array)){
 		return false;
+	}
+
 	return this.matchesArray(element, null);
 };
 
+/**
+ *
+ * Check if the input element is equivalent to us
+ * @method equivalent
+ * @param
+ *
+ */
+proto.equivalent = function equivalent(other){
+	if ( this._matchType != other._matchType)
+		return false;
+
+	var realOther = new ArrayMatchingMatchExpression(other);
+
+	if (this._path != realOther._path)
+		return false;
+
+	if (this.numChildren() != realOther.numChildren())
+		return false;
+
+	for (var i = 0; i < this.numChildren(); i++)
+		if (! (this.getChild(i).equivalent(realOther.getChild(i)) ) )
+			return false;
+	return true;
+};
+
 /**
  *
  * return the internal path
@@ -113,7 +107,6 @@ proto.matchesSingleElement = function matchesSingleElement(element){
  *
  */
 proto.path = function path(){
-	// File: expression_array.h lines: 52-51
 	return this._path;
 };
 

+ 2 - 6
lib/pipeline/matcher/AtomicMatchExpression.js

@@ -2,6 +2,7 @@
 var MatchExpression = require('./MatchExpression');
 
 // Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression.h
 var AtomicMatchExpression = module.exports = function AtomicMatchExpression(){
 	base.call(this);
 	this._matchType = 'ATOMIC';
@@ -15,7 +16,6 @@ var AtomicMatchExpression = module.exports = function AtomicMatchExpression(){
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression.cpp lines: 48-50
 	return this._debugAddSpace( level ) + "$atomic\n";
 };
 
@@ -27,8 +27,7 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression.h lines: 198-199
-	return other._matchType == 'ATOMIC';
+	return other._matchType === this._matchType;
 };
 
 /**
@@ -39,7 +38,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.matches = function matches(doc) {
-	// File: expression.h lines: 184-185
 	return true;
 };
 
@@ -51,7 +49,6 @@ proto.matches = function matches(doc) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression.h lines: 188-189
 	return true;
 };
 
@@ -62,6 +59,5 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression.h lines: 192-193
 	return new AtomicMatchExpression();
 };

+ 40 - 58
lib/pipeline/matcher/ComparisonMatchExpression.js

@@ -1,16 +1,20 @@
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression.js');
-var Value = require('../Value');
+var LeafMatchExpression = require("./LeafMatchExpression.js");
+var Value = require("../Value");
 
-
-// Autogenerated by cport.py on 2013-09-17 14:37
-var ComparisonMatchExpression = module.exports = function ComparisonMatchExpression( type ){
+/**
+ * ComparisonMatchExpression
+ * @class ComparisonMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
+var ComparisonMatchExpression = module.exports = function ComparisonMatchExpression(type){
 	base.call(this);
 	this._matchType = type;
 }, klass = ComparisonMatchExpression, base =  LeafMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// File: expression_leaf.h lines: 88-88
 proto._rhs = undefined;
 
 /**
@@ -21,34 +25,33 @@ proto._rhs = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 135-154
-	var retStr = this._debugAddSpace( level ) + this.path() + " ";
-	switch (this._matchType){
-		case 'LT':
-			retStr += '$lt';
+	var retStr = this._debugAddSpace(level) + this.path() + " ";
+	switch (this._matchType) {
+		case "LT":
+			retStr += "$lt";
 			break;
-		case 'LTE':
-			retStr += '$lte';
+		case "LTE":
+			retStr += "$lte";
 			break;
-		case 'EQ':
-			retStr += '==';
+		case "EQ":
+			retStr += "==";
 			break;
-		case 'GT':
-			retStr += '$gt';
+		case "GT":
+			retStr += "$gt";
 			break;
-		case 'GTE':
-			retStr += '$gte';
+		case "GTE":
+			retStr += "$gte";
 			break;
 		default:
 			retStr += "Unknown comparison!";
 			break;
 	}
 
-	retStr += (this._rhs ? this._rhs.toString() : '?');
-	if ( this.getTag() ) {
+	retStr += (this._rhs ? this._rhs.toString() : "?");
+	if (this.getTag()) {
 		retStr += this.getTag().debugString();
 	}
-	return retStr + '\n';
+	return retStr + "\n";
 };
 
 /**
@@ -59,7 +62,6 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 53-61
 	if (other._matchType != this._matchType)  return false;
 	return this.path() === other.path() && Value.compare(this._rhs,other._rhs) === 0;
 };
@@ -70,8 +72,7 @@ proto.equivalent = function equivalent(other) {
  * @method getData
  *
  */
-proto.getData = function getData(){
-	// File: expression_leaf.h lines: 85-84
+proto.getData = function getData() {
 	return this._rhs;
 };
 
@@ -81,8 +82,7 @@ proto.getData = function getData(){
  * @method getRHS
  *
  */
-proto.getRHS = function getRHS(){
-	// File: expression_leaf.h lines: 79-78
+proto.getRHS = function getRHS() {
 	return this._rhs;
 };
 
@@ -95,15 +95,14 @@ proto.getRHS = function getRHS(){
  *
  */
 proto.init = function init(path,rhs) {
-	// File: expression_leaf.cpp lines: 65-87
 	this._rhs = rhs;
-	if ( (rhs instanceof Object && Object.keys(rhs).length === 0)) { return {'code':'BAD_VALUE', 'description':'Need a real operand'};}
+	if ((rhs instanceof Object && Object.keys(rhs).length === 0)) return {"code":"BAD_VALUE", "description":"Need a real operand"};
 
-	if ( rhs === undefined ) { return {'code':'BAD_VALUE', 'desc':'Cannot compare to undefined'};}
+	if (rhs === undefined) return {"code":"BAD_VALUE", "desc":"Cannot compare to undefined"};
 	if (!(this._matchType in {"LT":1, "LTE":1, "EQ":1, "GT":1, "GTE":1})) {
-		return {'code':'BAD_VALUE', 'description':'Bad match type for ComparisonMatchExpression'};
+		return {"code":"BAD_VALUE", "description":"Bad match type for ComparisonMatchExpression"};
 	}
-	return this.initPath( path );
+	return this.initPath(path);
 };
 
 /**
@@ -113,49 +112,32 @@ proto.init = function init(path,rhs) {
  * @param e
  *
  */
-proto.matchesSingleElement = function matchesSingleElement(e){
-	// File: expression_leaf.cpp lines: 91-132
-	if( typeof(e) != typeof(this._rhs) ){
-		if( this._rhs === null) {
-			if(this._matchType in {'EQ':1, 'LTE':1, 'GTE':1}){
-				if(e === undefined || e === null || (e instanceof Object && Object.keys(e).length === 0)) {
-					return true;
-				}
-			}
-			return false;
-		}
-		if((e === null || e === undefined) && (this._rhs ===null || this._rhs === undefined)) {
+proto.matchesSingleElement = function matchesSingleElement(e) {
+	if (Value.canonicalize(e) !== Value.canonicalize(this._rhs)) {
+		if (Value.canonicalize(e) + Value.canonicalize(this._rhs) === 5) {
 			return ["EQ","LTE","GTE"].indexOf(this._matchType) != -1;
 		}
 
-		if (this._rhs.constructor.name in {'MaxKey':1,'MinKey':1} ) {
-			return this._matchType != "EQ";
+		if (["MaxKey","MinKey"].indexOf(Value.getType(this._rhs)) != -1) {
+			return this._matchType !== "EQ";
 		}
 		return false;
 	}
 
-	if( this._rhs instanceof Array) {
-		if( this._matchType != 'EQ') {
-			return false;
-		}
-	}
-
-	var x = Value.compare( e, this._rhs );
+	var x = Value.compare(e, this._rhs);
 
-	switch( this._matchType ) {
+	switch(this._matchType) {
 		case "LT":
-			return x == -1;
+			return x < 0;
 		case "LTE":
 			return x <= 0;
 		case "EQ":
 			return x === 0;
 		case "GT":
-			return x === 1;
+			return x > 0;
 		case "GTE":
 			return x >= 0;
 		default:
 			throw new Error("Invalid comparison type evaluated.");
 	}
-	return false;
 };
-

+ 0 - 73
lib/pipeline/matcher/Context.js

@@ -1,73 +0,0 @@
-"use strict";
-
-// Autogenerated by cport.py on 2013-09-17 14:37
-var Context = module.exports = function (){
-
-}, klass = Context, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-// File: path.h lines: 62-62
-proto._arrayOffset = undefined;
-
-// File: path.h lines: 61-61
-proto._element = undefined;
-
-// File: path.h lines: 63-63
-proto._outerArray = undefined;
-
-/**
- *
- * Return the _arrayOffset property
- * @method arrayOffset
- *
- */
-proto.arrayOffset = function arrayOffset( ){
-	// File: path.h lines: 57-56
-	return this._arrayOffset;
-};
-
-/**
- *
- * Return the _element property
- * @method element
- *
- */
-proto.element = function element( ){
-	// File: path.h lines: 56-55
-	return this._element;
-};
-
-/**
- *
- * Return the _outerArray property
- * @method outerArray
- *
- */
-proto.outerArray = function outerArray( ){
-	// File: path.h lines: 58-57
-	return this._outerArray;
-};
-
-/**
- *
- * Set _element to a new empty object
- * @method reset
- *
- */
-proto.reset = function reset(){
-	// File: path.cpp lines: 37-38
-	this._element = {};
-};
-
-/**
- *
- * Set _arrayOffset property to the input element
- * @method setArrayOffset
- * @param e
- *
- */
-proto.setArrayOffset = function setArrayOffset(e){
-	// File: path.h lines: 54-53
-	this._arrayOffset = e;
-};
-
-

+ 18 - 24
lib/pipeline/matcher/ElemMatchObjectMatchExpression.js

@@ -1,12 +1,10 @@
 "use strict";
 var ArrayMatchingMatchExpression = require('./ArrayMatchingMatchExpression.js');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var ElemMatchObjectMatchExpression = module.exports = function ElemMatchObjectMatchExpression(){
 	base.call(this);
 }, klass = ElemMatchObjectMatchExpression, base = ArrayMatchingMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: expression_array.h lines: 77-77
 proto._sub = undefined;
 
 /**
@@ -17,21 +15,28 @@ proto._sub = undefined;
  *
  */
 proto.debugString = function debugString(level){
-	// File: expression_array.cpp lines: 106-109
-	var debug = this._debugAddSpace(level);
-	debug = debug + this.path() + " $elemMatch\n";
-	return debug + this._sub.debugString(level + 1);
+	return this._debugAddSpace( level ) + this.path() + " $elemMatch (obj) " + (this.getTag() ? this.getTag().debugString() : '') + this._exp._debugString( level + 1 );
+};
+
+/**
+ *
+ * Return 1 since we have a single "sub"
+ * @method numChildren
+ *
+ */
+proto.numChildren = function numChildren(){
+	return 1;
 };
 
 /**
  *
  * Return the _sub property
  * @method getChild
+ * @param i
  *
  */
-proto.getChild = function getChild(){
-	// File: expression_array.h lines: 74-73
-	return this._sub;
+proto.getChild = function getChild(i){
+	return this._sub.get();
 };
 
 /**
@@ -43,7 +48,6 @@ proto.getChild = function getChild(){
  *
  */
 proto.init = function init(path, sub){
-	// File: expression_array.cpp lines: 85-87
 	this._sub = sub;
 	return this.initPath(path);
 };
@@ -57,12 +61,11 @@ proto.init = function init(path, sub){
  *
  */
 proto.matchesArray = function matchesArray(anArray, details){
-	// File: expression_array.cpp lines: 90-103
 	for (var i in anArray) {
 		var inner = anArray[i];
 		if (!(inner instanceof Object))
 			continue;
-		if (this._sub.matchesBSON(inner, null)) {
+		if (this._sub.matchesJSON(inner, null)) {
 			if (details && details.needRecord()) {
 				details.setElemMatchKey(i);
 			}
@@ -72,17 +75,6 @@ proto.matchesArray = function matchesArray(anArray, details){
 	return false;
 };
 
-/**
- *
- * Return 1 since we have a single "sub"
- * @method numChildren
- *
- */
-proto.numChildren = function numChildren(){
-	// File: expression_array.h lines: 73-72
-	return 1;
-};
-
 /**
  *
  * clone this instance to a new one
@@ -90,8 +82,10 @@ proto.numChildren = function numChildren(){
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_array.h lines: 65-68
 	var element = new ElemMatchObjectMatchExpression();
 	element.init(this.path(), this._sub.shallowClone());
+	if ( this.getTag() ){
+		element.setTag(this.getTag().clone());
+	}
 	return element;
 };

+ 6 - 13
lib/pipeline/matcher/ElemMatchValueMatchExpression.js

@@ -1,7 +1,6 @@
 "use strict";
 var ArrayMatchingMatchExpression = require('./ArrayMatchingMatchExpression.js');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var ElemMatchValueMatchExpression = module.exports = function ElemMatchValueMatchExpression(){
 	base.call(this);
 	this._matchType = 'ELEM_MATCH_VALUE';
@@ -12,7 +11,6 @@ var ElemMatchValueMatchExpression = module.exports = function ElemMatchValueMatc
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes;
 
-// File: expression_array.h lines: 108-108
 proto._subs = undefined;
 
 /**
@@ -23,7 +21,6 @@ proto._subs = undefined;
  *
  */
 proto._arrayElementMatchesAll = function _arrayElementMatchesAll(element){
-	// File: expression_array.cpp lines: 152-157
 	for (var i = 0; i < this._subs.length; i++ ) {
 		if (!this._subs[i].matchesSingleElement(element))
 			return false;
@@ -39,7 +36,6 @@ proto._arrayElementMatchesAll = function _arrayElementMatchesAll(element){
  *
  */
 proto.add = function add(sub){
-	// File: expression_array.cpp lines: 132-134
 	if (!sub) throw new Error(sub + " ElemMatchValueMatchExpression:36");
 	this._subs.push(sub);
 };
@@ -52,11 +48,9 @@ proto.add = function add(sub){
  *
  */
 proto.debugString = function debugString(level){
-	// File: expression_array.cpp lines: 160-165
-	var debug = this._debugAddSpace(level);
-	debug = debug + this.path() + " $elemMatch\n";
+	var debug = this._debugAddSpace(level) + this.path() + " $elemMatch (value)" + (this.getTag() ? this.getTag().debugString() : '') + "\n";
 	for (var i = 0; i < this._subs.length; i++) {
-		debug = debug + this._subs[i].debugString(level + 1);
+		debug += this._subs[i].debugString(level + 1);
 	}
 	return debug;
 };
@@ -69,7 +63,6 @@ proto.debugString = function debugString(level){
  *
  */
 proto.getChild = function getChild(i){
-	// File: expression_array.h lines: 103-102
 	return this._subs[i];
 };
 
@@ -82,7 +75,6 @@ proto.getChild = function getChild(i){
  *
  */
 proto.init = function init(path, sub){
-	// File: expression_array.cpp lines: 121-124
 	this.initPath(path);
 	if (sub)
 		this.add(sub);
@@ -98,7 +90,6 @@ proto.init = function init(path, sub){
  *
  */
 proto.matchesArray = function matchesArray(anArray, details){
-	// File: expression_array.cpp lines: 137-149
 	for (var i in anArray) {
 		var inner = anArray[i];
 
@@ -119,7 +110,6 @@ proto.matchesArray = function matchesArray(anArray, details){
  *
  */
 proto.numChildren = function numChildren(){
-	// File: expression_array.h lines: 102-101
 	return this._subs.length;
 };
 
@@ -130,11 +120,14 @@ proto.numChildren = function numChildren(){
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_array.h lines: 91-97
 	var element = new ElemMatchValueMatchExpression();
 	element.init(this.path());
 	for (var i = 0; i < this._subs.length; ++i) {
 		element.add(this._subs[i].shallowClone());
 	}
+	var td = this.getTag();
+	if (td) {
+		element.setTag(td.clone());
+	}
 	return element;
 };

+ 26 - 35
lib/pipeline/matcher/ElementPath.js

@@ -1,23 +1,15 @@
 "use strict";
 
-var FieldRef = require('./FieldRef');
+var FieldRef = require('./FieldRef'),
+	ErrorCodes = require('../../Errors.js').ErrorCodes;
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var ElementPath = module.exports = function ElementPath(){
 	this._fieldRef = new FieldRef();
 	this._shouldTraverseLeafArray = false;
 }, klass = ElementPath, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-
-// File: path.h lines: 41-41
-//         FieldRef _fieldRef;
-
 proto._fieldRef = undefined;
 
-
-// File: path.h lines: 42-42
-//         bool _shouldTraverseLeafArray;
-
 proto._shouldTraverseLeafArray = undefined;
 
 /**
@@ -29,7 +21,6 @@ proto._shouldTraverseLeafArray = undefined;
  * @param idxPathObj This is an object with a pathID element. This allows for pass by ref in calling function.
  * */
 klass.getFieldDottedOrArray = function getFieldDottedOrArray(doc, path, idxPathObj){
-	// File path_internal.cpp lines 31-72
 	if (path.numParts() === 0 ) { return doc; }
 
 	var res,curr = doc,
@@ -37,7 +28,7 @@ klass.getFieldDottedOrArray = function getFieldDottedOrArray(doc, path, idxPathO
 		partNum = 0;
 	while (partNum < path.numParts() && !stop) {
 		res = curr[path.getPart( partNum)];
-		if(res == {}){
+		if(res instanceof Object && Object.keys(res).length === 0){
 			stop = true;
 		} else if (res instanceof Object) {
 			curr = res;
@@ -53,7 +44,7 @@ klass.getFieldDottedOrArray = function getFieldDottedOrArray(doc, path, idxPathO
 
 	}
 
-	//idxPathObj.pathID = partNum;
+	idxPathObj.pathID = partNum;
 	return res;
 };
 
@@ -65,7 +56,6 @@ klass.getFieldDottedOrArray = function getFieldDottedOrArray(doc, path, idxPathO
  */
 
 klass.isAllDigits = function isAllDigits ( str ){
-	// File path_internal.cpp lines 23-29
 	var digitCheck = /\D/g;
 	if (digitCheck.exec(str) === null){ return true; }
 	return false;
@@ -82,8 +72,7 @@ klass.isAllDigits = function isAllDigits ( str ){
  * @param
  *
  */
-proto.fieldRef = function fieldRef( /*  */ ){
-// File: path.h lines: 37-36
+proto.fieldRef = function fieldRef(){
 	return this._fieldRef;
 };
 
@@ -95,11 +84,10 @@ proto.fieldRef = function fieldRef( /*  */ ){
  * @param path
  *
  */
-proto.init = function init( path ){  //  const StringData& path
-// File: path.cpp lines: 26-29
+proto.init = function init( path ){
 	this._shouldTraverseLeafArray = true;
 	this._fieldRef.parse( path );
-	return {'code':'OK'};
+	return {'code':ErrorCodes.OK};
 };
 
 
@@ -110,8 +98,7 @@ proto.init = function init( path ){  //  const StringData& path
  * @param
  *
  */
-proto.setTraverseLeafArray = function setTraverseLeafArray( b ){ //  bool b
-// File: path.h lines: 35-34
+proto.setTraverseLeafArray = function setTraverseLeafArray( b ){
 	this._shouldTraverseLeafArray = b;
 };
 
@@ -123,27 +110,27 @@ proto.setTraverseLeafArray = function setTraverseLeafArray( b ){ //  bool b
  * @param
  *
  */
-proto.shouldTraverseLeafArray = function shouldTraverseLeafArray( /*  */ ){
-// File: path.h lines: 38-37
+proto.shouldTraverseLeafArray = function shouldTraverseLeafArray( ){
 	return this._shouldTraverseLeafArray;
 };
 
+
 proto.objAtPath = function objAtPath(doc) {
 	return klass.objAtPath(doc, this._fieldRef._path);
 };
 
 klass.objAtPath = function objAtPath(doc, path) {
-	if(path.length === 0) {
+	if (path.length === 0) {
 		return doc;
 	}
-	if (path.length > 0 && Object.keys(doc).length === 0){
+	if (path.length > 0 && Object.keys(doc).length === 0) {
 		return {};
 	}
 	if (doc === null || doc === undefined) {
 		return doc;
 	}
 	var tpath = path.split('.');
-	return klass.objAtPath(doc[tpath[0]],tpath.slice(1).join('.'));
+	return klass.objAtPath(doc[tpath[0]], tpath.slice(1).join('.'));
 };
 
 
@@ -172,15 +159,14 @@ proto._matches = function _matches(doc, details, checker) {
  *
  */
 klass._matches = function _matches(doc, path, shouldTraverseLeafArray, details, checker){
-	// File: expression_array.cpp lines: 34-53
 	var k, result, ii, il,
 		curr = doc,
 		item = doc;
 	for (k = 0; k < path.length; k++) {
-		if((curr instanceof Object) && (path[k] in curr)) {
+		if ((curr instanceof Object) && (path[k] in curr)) {
 			item = curr[path[k]];
 		}
-		if(path[k].length === 0)
+		if (path[k].length === 0)
 			continue;
 		item = curr[path[k]];
 		if (item instanceof Object && item.constructor === Object) {
@@ -195,7 +181,7 @@ klass._matches = function _matches(doc, path, shouldTraverseLeafArray, details,
 			curr = item;
 			continue;
 		} else if (item instanceof Object && item.constructor === Array) {
-			if (k == path.length-1) {
+			if (k == path.length - 1) {
 				if ((shouldTraverseLeafArray) && (isNaN(parseInt(path[k], 10)))) {
 					for (ii = 0, il = item.length; ii < il; ii++) {
 						result = checker(item[ii]);
@@ -205,20 +191,20 @@ klass._matches = function _matches(doc, path, shouldTraverseLeafArray, details,
 							return result;
 						}
 					}
-					if(item.length === 0)
+					if (item.length === 0)
 						return checker({});
-					
+
 				}
 				curr = item;
 				break; // this is the end of the path, so check this array
-			} else if (!(isNaN(parseInt(path[k+1], 10)))) {
+			} else if (!(isNaN(parseInt(path[k + 1], 10)))) {
 				curr = item;
 				continue; // the *next* path section is an item in the array so we don't check this whole array
 			}
 			// otherwise, check each item in the array against the rest of the path
-			for(ii = 0, il = item.length; ii < il; ii++){
+			for (ii = 0, il = item.length; ii < il; ii++) {
 				var subitem = item[ii];
-				if (subitem.constructor !== Object) continue;	// can't look for a subfield in a non-object value.
+				if (!subitem || subitem.constructor !== Object) continue;	// can't look for a subfield in a non-object value.
 				if (this._matches(subitem, path.slice(k), shouldTraverseLeafArray, null, checker)) { // check the item against the rest of the path
 					if (details && details.needRecord())
 						details.setElemMatchKey(ii.toString());
@@ -226,6 +212,11 @@ klass._matches = function _matches(doc, path, shouldTraverseLeafArray, details,
 				}
 			}
 			return false; // checked all items in the array and found no matches
+		} else {
+
+			if ( details === undefined && item !== null && curr[path[k+1]] !== undefined) {
+				curr = curr[path[k+1]];
+			}
 		}
 	}
 	return checker(item);

+ 5 - 4
lib/pipeline/matcher/EqualityMatchExpression.js

@@ -4,8 +4,7 @@ var ComparisonMatchExpression = require('./ComparisonMatchExpression');
 
 // Autogenerated by cport.py on 2013-09-17 14:37
 var EqualityMatchExpression = module.exports = function EqualityMatchExpression(){
-	base.call(this);
-	this._matchType = 'EQ';
+	base.call(this,'EQ');
 }, klass = EqualityMatchExpression, base =  ComparisonMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 /**
@@ -16,9 +15,11 @@ var EqualityMatchExpression = module.exports = function EqualityMatchExpression(
  *
  */
 proto.shallowClone = function shallowClone( /*  */ ){
-// File: expression_leaf.h lines: 98-101
 	var e = new EqualityMatchExpression();
 	e.init ( this.path(), this._rhs );
+	
+	if ( this.getTag() ) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 6 - 9
lib/pipeline/matcher/ExistsMatchExpression.js

@@ -1,7 +1,7 @@
 "use strict";
 var LeafMatchExpression = require('./LeafMatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression_leaf.cpp
 var ExistsMatchExpression = module.exports = function ExistsMatchExpression(){
 	base.call(this);
 	this._matchType = 'EXISTS';
@@ -15,7 +15,6 @@ var ExistsMatchExpression = module.exports = function ExistsMatchExpression(){
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 286-294
 	return this._debugAddSpace( level ) + this.path() + " exists" + (this.getTag() ? " " + this.getTag().debugString() : "") + "\n";
 };
 
@@ -27,8 +26,7 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 297-302
-	if(this._matchType != other._matchType)	{
+	if(this._matchType !== other._matchType) {
 		return false;
 	}
 	return this.path() == other.path();
@@ -43,7 +41,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.init = function init(path) {
-	// File: expression_leaf.cpp lines: 278-279
 	return this.initPath( path );
 };
 
@@ -55,12 +52,11 @@ proto.init = function init(path) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_leaf.cpp lines: 282-283
-	if(typeof(e) == 'undefined')
+	if(typeof(e) === 'undefined')
 		return false;
 	if(e === null)
 		return true;
-	if(typeof(e) == 'object')
+	if(typeof(e) === 'object')
 		return (Object.keys(e).length > 0);
 	else
 		return true;
@@ -73,9 +69,10 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.h lines: 220-223
 	var e = new ExistsMatchExpression();
 	e.init(this.path());
+	if (this.getTag())
+		e.setTag(this.getTag().clone());
 	return e;
 };
 

+ 19 - 9
lib/pipeline/matcher/FalseMatchExpression.js

@@ -1,9 +1,14 @@
 "use strict";
 var MatchExpression = require('./MatchExpression');
 
-
-// Autogenerated by cport.py on 2013-09-17 14:37
-var FalseMatchExpression = module.exports = function FalseMatchExpression(){
+/**
+ * A match expression that always returns false
+ * @class FalseMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ **/
+ var FalseMatchExpression = module.exports = function FalseMatchExpression(){
 	base.call(this);
 	this._matchType = 'ALWAYS_FALSE';
 }, klass = FalseMatchExpression, base =  MatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
@@ -16,7 +21,6 @@ var FalseMatchExpression = module.exports = function FalseMatchExpression(){
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression.cpp lines: 53-55
 	return this._debugAddSpace( level ) + "$false\n";
 };
 
@@ -28,8 +32,7 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression.h lines: 222-223
-	return other._matchType === 'ALWAYS_FALSE';
+	return other._matchType === this._matchType;
 };
 
 /**
@@ -41,7 +44,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.matches = function matches(doc,details) {
-	// File: expression.h lines: 208-209
 	return false;
 };
 
@@ -53,7 +55,6 @@ proto.matches = function matches(doc,details) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression.h lines: 212-213
 	return false;
 };
 
@@ -64,7 +65,16 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression.h lines: 216-217
 	return new FalseMatchExpression();
 };
 
+/**
+ *
+ * append to JSON object
+ * @method shallowClone
+ *
+ */
+proto.toJson = function toJson(out){
+	out.$false = 1;
+	return out;
+};

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

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

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

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

+ 55 - 74
lib/pipeline/matcher/InMatchExpression.js

@@ -1,7 +1,6 @@
 "use strict";
 var LeafMatchExpression = require('./LeafMatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
 var InMatchExpression = module.exports = function InMatchExpression(){
 	base.call(this);
 	this._matchType = 'MATCH_IN';
@@ -13,9 +12,19 @@ var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
 	ArrayFilterEntries = require("./ArrayFilterEntries.js");
 
-// File: expression_leaf.h lines: 294-294
 proto._arrayEntries = null;
 
+/**
+ *
+ * Initialize the necessary items
+ * @method init
+ * @param path
+ *
+ */
+proto.init = function init(path) {
+	return this.initPath( path );
+};
+
 /**
  *
  * Check if the input element matches a real element
@@ -24,23 +33,13 @@ proto._arrayEntries = null;
  *
  */
 proto._matchesRealElement = function _matchesRealElement(e) {
-	// File: expression_leaf.cpp lines: 422-431
 	if(this._arrayEntries.contains(e)) { // array wrapper.... so no e "in" array
 		return true;
 	}
 
 	for (var i = 0; i < this._arrayEntries.numRegexes(); i++) {
-		if(e.match && e.match(this._arrayEntries.regex(i)._regex)) {
+		if ( this._arrayEntries.regex(i).matchesSingleElement( e ) )
 			return true;
-		} else if (e instanceof RegExp) {
-			if(e.toString() == this._arrayEntries.regex(i)._regex.toString()) {
-				return true;
-			}
-		}
-	}
-
-	if(typeof(e) == 'undefined') {
-		return true; // Every Set contains the Null Set, man.
 	}
 
 	return false;
@@ -48,15 +47,26 @@ proto._matchesRealElement = function _matchesRealElement(e) {
 
 /**
  *
- * Copy our array to the input array
- * @method copyTo
- * @param toFillIn
+ * Check if the input element matches
+ * @method matchesSingleElement
+ * @param e
  *
  */
-proto.copyTo = function copyTo(toFillIn) {
-	// File: expression_leaf.cpp lines: 481-483
-	toFillIn.init(this.path());
-	this._arrayEntries.copyTo( toFillIn._arrayEntries );
+proto.matchesSingleElement = function matchesSingleElement(e) {
+	if( this._arrayEntries.hasNull() && 
+		(	e === null ||
+			e === undefined ||
+			typeof(e) === 'object' && Object.keys(e).length === 0
+		)) 
+	{
+		return true;
+	}
+	
+	if (this._matchesRealElement( e )) {
+		return true;
+	}
+	
+	return false;
 };
 
 /**
@@ -67,8 +77,7 @@ proto.copyTo = function copyTo(toFillIn) {
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 455-463
-	return this._debugAddSpace( level ) + this.path() + ";$in: TODO " + (this.getTag() ? this.getTag().debugString() : '') + "\n";
+	return this._debugAddSpace( level ) + this.path() + " $in " + this._arrayEntries + (this.getTag() ? this.getTag().debugString() : '') + "\n";
 };
 
 /**
@@ -79,83 +88,55 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 466-472
 	if ( other._matchType != 'MATCH_IN' ) {
 		return false;
 	}
-	return this.path() == other.path() && this._arrayEntries.equivalent( other._arrayEntries );
-};
-
-/**
- *
- * Return the _arrayEntries property
- * @method getArrayFilterEntries
- *
- */
-proto.getArrayFilterEntries = function getArrayFilterEntries(){
-	// File: expression_leaf.h lines: 280-279
-	return this._arrayEntries;
+	return this.path() === other.path() && this._arrayEntries.equivalent( other._arrayEntries );
 };
 
 /**
  *
- * Return the _arrayEntries property
- * @method getData
+ * clone this instance to a new one
+ * @method shallowClone
  *
  */
-proto.getData = function getData(){
-	// File: expression_leaf.h lines: 290-289
-	return this._arrayEntries;
+proto.shallowClone = function shallowClone(){
+	var e = new InMatchExpression();
+	this.copyTo( e );
+	if ( this.getTag() ){
+		e.setTag(this.getTag().Clone());
+	}
+	return e;
 };
 
 /**
  *
- * Initialize the necessary items
- * @method init
- * @param path
+ * Copy our array to the input array
+ * @method copyTo
+ * @param toFillIn
  *
  */
-proto.init = function init(path) {
-	// File: expression_leaf.cpp lines: 418-419
-	return this.initPath( path );
+proto.copyTo = function copyTo(toFillIn) {
+	toFillIn.init(this.path());
+	this._arrayEntries.copyTo( toFillIn._arrayEntries );
 };
 
 /**
  *
- * Check if the input element matches
- * @method matchesSingleElement
- * @param e
+ * Return the _arrayEntries property
+ * @method getArrayFilterEntries
  *
  */
-proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_leaf.cpp lines: 434-452
-	if( this._arrayEntries === null && typeof(e) == 'object' && Object.keys(e).length === 0) {
-		return true;
-	}
-	if (this._matchesRealElement( e )) {
-		return true;
-	}
-	/*if (e instanceof Array){
-		for (var i = 0; i < e.length; i++) {
-			if(this._matchesRealElement( e[i] )) {
-				return true;
-			}
-		}
-
-	}*/
-	return false;
+proto.getArrayFilterEntries = function getArrayFilterEntries(){
+	return this._arrayEntries;
 };
 
 /**
  *
- * clone this instance to a new one
- * @method shallowClone
+ * Return the _arrayEntries property
+ * @method getData
  *
  */
-proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.cpp lines: 475-478
-	var e = new InMatchExpression();
-	this.copyTo( e );
-	return e;
+proto.getData = function getData(){
+	return this._arrayEntries;
 };
-

+ 0 - 58
lib/pipeline/matcher/IndexKeyMatchableDocument.js

@@ -1,58 +0,0 @@
-"use strict";
-
-// Autogenerated by cport.py on 2013-09-17 14:37
-var IndexKeyMatchableDocument = module.exports = function IndexKeyMatchableDocument(pattern, doc){
-	this._pattern = pattern;
-	this._doc = doc;
-}, klass = IndexKeyMatchableDocument, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-// File: matcher.cpp lines: 52-52
-proto._doc = undefined;
-
-// File: matcher.cpp lines: 51-51
-proto._pattern = undefined;
-
-// File: matcher.cpp lines: 52-52
-proto._doc = undefined;
-
-// File: matcher.cpp lines: 51-51
-proto._pattern = undefined;
-
-/**
- *
- * get the element at the input path
- * @method _getElement
- * @param path
- *
- */
-proto._getElement = function _getElement(path){
-	// File: matcher.cpp lines: 63-77
-	var patternElement, docElement;
-
-	for (var i in this._pattern) {
-		patternElement = this._pattern[i];
-		//verify( docIterator.more() );
-		if(i >= this._doc.length) throw new Error("Ran out of docs in IndexKeyMatchableDocument:35");
-		docElement = this._doc[i];
-
-		if (path.equalsDottedField(patternElement.fieldName())) {
-			return docElement;
-		}
-	}
-
-	return {};
-};
-
-/**
- *
- * This method returns a JSON representation of the Document
- * @method toBSON
- * @param
- *
- */
-proto.toJSON = function toJSON(){
-	// File: matcher.cpp lines: 39-42
-	// TODO: this isn't quite correct because of dots
-	// don't think it'll ever be called though
-	return this._doc.replaceFieldNames(this._pattern);
-};

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

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

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

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

+ 39 - 76
lib/pipeline/matcher/LeafMatchExpression.js

@@ -1,107 +1,70 @@
 "use strict";
 
-var MatchExpression = require('./MatchExpression');
-var ElementPath = require('./ElementPath');
-
-// Autogenerated by cport.py on 2013-09-17 14:37
-var LeafMatchExpression = module.exports = function LeafMatchExpression( type ){
-	base.call(this);
-	this._matchType = type;
-	this._elementPath = new ElementPath();
-}, klass = LeafMatchExpression, base =  MatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+var MatchExpression = require('./MatchExpression'),
+	ElementPath = require('./ElementPath');
 
+var LeafMatchExpression = module.exports = function LeafMatchExpression(type) {
+	base.call(this, type);
 
-// File: expression_leaf.h lines: 63-63
-//         ElementPath _elementPath;
+	this._elementPath = new ElementPath();
+}, klass = LeafMatchExpression, base =  MatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 proto._elementPath = undefined;
-
-
-// File: expression_leaf.h lines: 62-62
-//         StringData _path;
-
 proto._path = undefined;
 
-
 /**
  *
- * Initialize the ElementPath to the input path
- * @method initPath
- * @param path
- *
- */
-proto.initPath = function initPath( path ) { //  const StringData& path
-// File: expression_leaf.cpp lines: 31-33
-	this._path = path;
-	if(this._elementPath === undefined){
-		this._elementPath = new ElementPath();
-	}
-	return this._elementPath.init( this._path );
-};
-
-
-/**
+ * Checks whether the document matches against what was searched.
  *
- * Check whether the input doc matches
- * @method matches
  * @param doc
- *
+ * @param details
+ * @returns {*}
  */
-proto.matches = function matches( doc, details ) { //  const MatchableDocument* doc, MatchDetails* details
-	// File: expression_leaf.cpp lines: 37-48
+proto.matches = function matches(doc, details) {
 	var self = this,
 		checker = function(element) {
-			/*if (element instanceof Array) {
-				for (var i = 0; i < element.length; i++) {
-					if(self.matchesSingleElement(element[i])) {
-						if(details && details.needRecord()) {
-							details.setElemMatchKey(i.toString());
-						}
-						return true;
-					}
-				}
-				return false;
-			}*/
 			if (!self.matchesSingleElement(element)) {
 				return false;
 			}
-			/*
-			if( details && details.needRecord() && (element instanceof Array)) {
-				details.setElemMatchKey( docKeys[i+1] );
-			}
-			*/
+
 			return true;
 		};
+
 	return this._elementPath._matches(doc, details, checker);
-	/*
-	var tDoc = ElementPath.objAtPath( doc, this._path );
-	if(tDoc instanceof RegExp || typeof(tDoc) != 'object') {
-		return this.matchesSingleElement(tDoc);
-	}
-		if(!this.matchesSingleElement( tDoc[docKeys[i]] ))
-			continue;
-		if( details && details.needRecord() && (tDoc instanceof Array) && i < docKeys.length-1 ) {
-			console.log('test2');
-			details.setElemMatchKey( docKeys[i+1] );
-		}
-		return true;
-	}
-	return false;
-	*/
 };
 
-
-
+/**
+ *
+ * Overridable method for matching against a single element.
+ *
+ * @param e
+ * @returns {boolean}
+ */
+proto.matchesSingleElement = function matchesSingleElement(e) { return false; }; // The child class defines this method.
 
 /**
  *
- * Return the internal path
- * @method path
- * @param
+ * Return the internal path.
  *
+ * @returns {undefined|*|klass._path}
  */
-proto.path = function path( /*  */ ){
-// File: expression_leaf.h lines: 56-55
+proto.path = function path() {
 	return this._path;
 };
 
+/**
+ *
+ * Initialize the ElementPath to the input path.
+ *
+ * @param path
+ * @returns {*}
+ */
+proto.initPath = function initPath(path) {
+	this._path = path;
+
+	if (this._elementPath === undefined) {
+		this._elementPath = new ElementPath();
+	}
+
+	return this._elementPath.init(this._path);
+};

+ 53 - 43
lib/pipeline/matcher/ListOfMatchExpression.js

@@ -2,29 +2,21 @@
 
 var MatchExpression = require('./MatchExpression');
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * Create a match expression to match a list of
+ * @class ListOfMatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var ListOfMatchExpression = module.exports = function ListOfMatchExpression(matchType){
 	base.call(this);
 	this._expressions = [];
 	this._matchType = matchType;
 }, klass = ListOfMatchExpression, base =  MatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: expression_tree.h lines: 56-56
 proto._expressions = undefined;
 
-/**
- *
- * Print the debug info from each expression in the list
- * @method _debugList
- * @param level
- *
- */
-proto._debugList = function _debugList(level){
-	// File: expression_tree.cpp lines: 40-42
-	for (var i = 0; i < this._expressions.length; i++ )
-		this._expressions[i].debugString(level + 1); // debug only takes level now
-};
-
 /**
  *
  * Append a new expression to our list
@@ -33,10 +25,9 @@ proto._debugList = function _debugList(level){
  *
  */
 proto.add = function add( exp ){
-	// File: expression_tree.cpp lines: 34-36
 	// verify(expression)
 	if(!exp)
-		throw new Error(exp + " failed verify on ListOfMatchExpression:34");
+		throw new Error(exp + " failed verify on ListOfMatchExpression:add");
 	if(this._expressions) {
 		this._expressions.push(exp);
 	} else {
@@ -52,10 +43,54 @@ proto.add = function add( exp ){
  *
  */
 proto.clearAndRelease = function clearAndRelease(){
-	// File: expression_tree.h lines: 45-44
 	this._expressions = []; // empty the expressions
 };
 
+/**
+ *
+ * Get the length of the list
+ * @method numChildren
+ * @param
+ *
+ */
+proto.numChildren = function numChildren(){
+	return this._expressions.length;
+};
+
+/**
+ *
+ * Get an item from the expressions
+ * @method getChild
+ * @param i index of the child
+ *
+ */
+proto.getChild = function getChild(i){
+	return this._expressions[i];
+};
+
+/**
+ *
+ * Get the expressions
+ * @method getChildVector
+ * @param
+ *
+ */
+proto.getChildVector = function getChildVector(){
+	return this._expressions;
+};
+
+/**
+ *
+ * Print the debug info from each expression in the list
+ * @method _debugList
+ * @param level
+ *
+ */
+proto._debugList = function _debugList(debug, level){
+	for (var i = 0; i < this._expressions.length; i++ )
+		this._expressions[i].debugString(debug, level + 1);
+};
+
 /**
  *
  * Check if the input list is considered the same as this one
@@ -64,7 +99,6 @@ proto.clearAndRelease = function clearAndRelease(){
  *
  */
 proto.equivalent = function equivalent(other){
-	// File: expression_tree.cpp lines: 45-59
 	if (this._matchType != other._matchType)
 		return false;
 
@@ -80,27 +114,3 @@ proto.equivalent = function equivalent(other){
 
 	return true;
 };
-
-/**
- *
- * Get an item from the expressions
- * @method getChild
- * @param
- *
- */
-proto.getChild = function getChild(i){
-	// File: expression_tree.h lines: 48-47
-	return this._expressions[i];
-};
-
-/**
- *
- * Get the length of the list
- * @method numChildren
- * @param
- *
- */
-proto.numChildren = function numChildren(){
-	// File: expression_tree.h lines: 47-46
-	return this._expressions.length;
-};

+ 42 - 49
lib/pipeline/matcher/MatchDetails.js

@@ -1,42 +1,53 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * MatchDetails
+ * @class MatchDetails
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ **/
 var MatchDetails = module.exports = function (){
-	// File: match_details.cpp lines: 27-29
 	this._elemMatchKeyRequested = false;
 	this.resetOutput();
 }, klass = MatchDetails, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: match_details.h lines: 60-60
 proto._elemMatchKey = undefined;
 
-// File: match_details.h lines: 59-59
 proto._elemMatchKeyRequested = undefined;
 
-// File: match_details.h lines: 58-58
 proto._loadedRecord = undefined;
 
 /**
  *
- * Return the _elemMatchKey property
- * @method elemMatchKey
+ * Set _loadedRecord to false and _elemMatchKey to undefined
+ * @method resetOutput
  *
  */
-proto.elemMatchKey = function elemMatchKey(){
-	// File: match_details.cpp lines: 41-43
-	if (!this.hasElemMatchKey()) throw new Error("no elem match key MatchDetails:29");
-	return this._elemMatchKey;
+proto.resetOutput = function resetOutput(){
+	this._loadedRecord = false;
+	this._elemMatchKey = undefined;
 };
 
 /**
  *
- * Return the _elemMatchKey property so we can check if exists
- * @method hasElemMatchKey
+ * Return a string representation of ourselves
+ * @method toString
  *
  */
-proto.hasElemMatchKey = function hasElemMatchKey(){
-	// File: match_details.cpp lines: 37-38
-	return this._elemMatchKey;
+proto.toString = function toString(){
+	return "loadedRecord: " + this._loadedRecord + " " + "elemMatchKeyRequested: " + this._elemMatchKeyRequested + " " + "elemMatchKey: " + ( this._elemMatchKey ? this._elemMatchKey : "NONE" ) + " ";
+};
+
+/**
+ *
+ * Set the _loadedRecord property
+ * @method setLoadedRecord
+ * @param loadedRecord
+ *
+ */
+proto.setLoadedRecord = function setLoadedRecord(loadedRecord){
+	this._loadedRecord = loadedRecord;
 };
 
 /**
@@ -46,7 +57,6 @@ proto.hasElemMatchKey = function hasElemMatchKey(){
  *
  */
 proto.hasLoadedRecord = function hasLoadedRecord(){
-	// File: match_details.h lines: 41-40
 	return this._loadedRecord;
 };
 
@@ -57,7 +67,6 @@ proto.hasLoadedRecord = function hasLoadedRecord(){
  *
  */
 proto.needRecord = function needRecord(){
-	// File: match_details.h lines: 45-44
 	return this._elemMatchKeyRequested;
 };
 
@@ -68,20 +77,28 @@ proto.needRecord = function needRecord(){
  *
  */
 proto.requestElemMatchKey = function requestElemMatchKey(){
-	// File: match_details.h lines: 50-49
 	this._elemMatchKeyRequested = true;
 };
 
 /**
  *
- * Set _loadedRecord to false and _elemMatchKey to undefined
- * @method resetOutput
+ * Return the _elemMatchKey property so we can check if exists
+ * @method hasElemMatchKey
  *
  */
-proto.resetOutput = function resetOutput(){
-	// File: match_details.cpp lines: 32-34
-	this._loadedRecord = false;
-	this._elemMatchKey = undefined;
+proto.hasElemMatchKey = function hasElemMatchKey(){
+	return (typeof this._elemMatchKey !== 'undefined');
+};
+
+/**
+ *
+ * Return the _elemMatchKey property
+ * @method elemMatchKey
+ *
+ */
+proto.elemMatchKey = function elemMatchKey(){
+	if (!this.hasElemMatchKey()) throw new Error("no elem match key MatchDetails:29");
+	return this._elemMatchKey;
 };
 
 /**
@@ -92,31 +109,7 @@ proto.resetOutput = function resetOutput(){
  *
  */
 proto.setElemMatchKey = function setElemMatchKey(elemMatchKey){
-	// File: match_details.cpp lines: 46-49
 	if ( this._elemMatchKeyRequested ) {
 		this._elemMatchKey = elemMatchKey;
 	}
 };
-
-/**
- *
- * Set the _loadedRecord property
- * @method setLoadedRecord
- * @param loadedRecord
- *
- */
-proto.setLoadedRecord = function setLoadedRecord(loadedRecord){
-	// File: match_details.h lines: 39-38
-	this._loadedRecord = loadedRecord;
-};
-
-/**
- *
- * Return a string representation of ourselves
- * @method toString
- *
- */
-proto.toString = function toString(){
-	// File: match_details.cpp lines: 52-57
-	return "loadedRecord: " + this._loadedRecord + " " + "elemMatchKeyRequested: " + this._elemMatchKeyRequested + " " + "elemMatchKey: " + ( this._elemMatchKey ? this._elemMatchKey : "NONE" ) + " ";
-};

+ 139 - 88
lib/pipeline/matcher/MatchExpression.js

@@ -1,172 +1,223 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
-var MatchExpression = module.exports = function MatchExpression( type ){
+/**
+ * Files: matcher/expression.h/cpp
+ * Function order follows that in the header file
+ * @class MatchExpression
+ * @namespace mungedb-aggregate.pipeline.matcher
+ * @module mungedb-aggregate
+ * @constructor
+ * @param type {String} The type of the match expression
+ */
+var MatchExpression = module.exports = function MatchExpression(type){
 	this._matchType = type;
-}, klass = MatchExpression, base =  Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = MatchExpression, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 // DEPENDENCIES
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes;
 
-	// File: expression.h lines: 172-172
-proto._matchType = undefined;
-
-// File: expression.h lines: 173-173
-proto._tagData = undefined;
+/**
+ * Return the _matchType property
+ * @method matchType
+ */
+proto.matchType = function matchType(){
+	return this._matchType;
+};
 
 /**
- *
- * Writes a debug string for this object
- * @method debugString
- * @param level
- *
+ * Return the number of children we have
+ * @method numChildren
  */
-proto._debugAddSpace = function _debugAddSpace(level){
-	// File: expression.cpp lines: 37-39
-	return new Array( level + 1).join("    ");
+proto.numChildren = function numChildren( ){
+	return 0;
 };
 
+
 /**
- *
- * Get our child elements
+ * Get the i-th child.
  * @method getChild
- *
  */
-proto.getChild = function getChild() {
-	// File: expression.h lines: 78-77
-	throw new Error('Virtual function called.');
+proto.getChild = function getChild(i) {
+	return null;
 };
 
+/**
+ * Get all the children of a node.
+ * @method getChild
+ */
+proto.getChildVector = function getChildVector(i) {
+	return null;
+};
 
 /**
+ * Get the path of the leaf.  Returns StringData() if there is
+ * no path (node is logical).
+ * @method path
+ */
+proto.path = function path( ){
+	return "";
+};
+
+/*
+ * Notes on structure:
+ * isLogical, isArray, and isLeaf define three partitions of all possible operators.
  *
- * Return the _tagData property
- * @method getTag
+ * isLogical can have children and its children can be arbitrary operators.
+ *
+ * isArray can have children and its children are predicates over one field.
  *
+ * isLeaf is a predicate over one field.
  */
-proto.getTag = function getTag(){
-	// File: expression.h lines: 159-158
-	return this._tagData;
+
+/**
+ * Is this node a logical operator?  All of these inherit from ListOfMatchExpression.
+ * AND, OR, NOT, NOR.
+ * @method isLogical
+ */
+proto.isLogical = function isLogical(){
+	switch( this._matchType ){
+		case "AND":
+		case "OR":
+		case "NOT":
+		case "NOR":
+			return true;
+		default:
+			return false;
+	}
+	return false;
 };
 
 /**
+ * Is this node an array operator?  Array operators have multiple clauses but operate on one
+ * field.
  *
- * Return if our _matchType needs an array
+ * ALL (AllElemMatchOp)
+ * ELEM_MATCH_VALUE, ELEM_MATCH_OBJECT, SIZE (ArrayMatchingMatchExpression)
  * @method isArray
- *
  */
 proto.isArray = function isArray(){
-	// File: expression.h lines: 111-113
 	switch (this._matchType){
-		case 'SIZE':
-		case 'ALL':
-		case 'ELEM_MATCH_VALUE':
-		case 'ELEM_MATCH_OBJECT':
+		case "SIZE":
+		case "ALL":
+		case "ELEM_MATCH_VALUE":
+		case "ELEM_MATCH_OBJECT":
 			return true;
 		default:
 			return false;
 	}
-
 	return false;
 };
 
 /**
+ * Not-internal nodes, predicates over one field.  Almost all of these inherit
+ * from LeafMatchExpression.
  *
- * Check if we do not need an array, and we are not a logical element (leaves are very emotional)
+ * Exceptions: WHERE, which doesn't have a field.
+ *             TYPE_OPERATOR, which inherits from MatchExpression due to unique
+ * 							array semantics.
  * @method isLeaf
- *
  */
 proto.isLeaf = function isLeaf(){
-	// File: expression.h lines: 124-125
 	return !this.isArray() && !this.isLogical();
 };
 
 /**
- *
- * Check if we are a vulcan
- * @method isLogical
- *
+ * XXX: document
+ * @method shallowClone
+ * @return {MatchExpression}
+ * @abstract
  */
-proto.isLogical = function isLogical(){
-	// File: expression.h lines: 100-101
-	switch( this._matchType ){
-		case 'AND':
-		case 'OR':
-		case 'NOT':
-		case 'NOR':
-			return true;
-		default:
-			return false;
-	}
-	return false;
+proto.shallowClone = function shallowClone() {
+	throw new Error("NOT IMPLEMENTED");
 };
 
 /**
- *
- * Return the _matchType property
- * @method matchType
- *
+ * XXX document
+ * @method equivalent
+ * @return {Boolean}
+ * @abstract
  */
-proto.matchType = function matchType(){
-	// File: expression.h lines: 67-66
-	return this._matchType;
+proto.equivalent = function equivalent() {
+	throw new Error("NOT IMPLEMENTED");
 };
 
+//
+// Determine if a document satisfies the tree-predicate.
+//
+
+/**
+ * @method matches
+ * @return {Boolean}
+ * @abstract
+ */
+proto.matches = function matches(doc, details/* = 0 */) {
+	throw new Error("NOT IMPLEMENTED");
+};
+
+
 /**
- *
  * Wrapper around matches function
- * @method matchesBSON
- * @param
- *
+ * @method matchesJSON
  */
-proto.matchesBSON = function matchesBSON(doc, details){
-	// File: expression.cpp lines: 42-44
+proto.matchesJSON = function matchesJSON(doc, details/* = 0 */){
 	return this.matches(doc, details);
 };
 
 /**
- *
- * Return the number of children we have
- * @method numChildren
- *
+ * Determines if the element satisfies the tree-predicate.
+ * Not valid for all expressions (e.g. $where); in those cases, returns false.
+ * @method matchesSingleElement
  */
-proto.numChildren = function numChildren( ){
-	// File: expression.h lines: 73-72
-	return 0;
+proto.matchesSingleElement = function matchesSingleElement(doc) {
+	throw new Error("NOT IMPLEMENTED");
 };
 
 /**
- *
- * Return our internal path
- * @method path
- *
+ * Return the _tagData property
+ * @method getTag
  */
-proto.path = function path( ){
-	// File: expression.h lines: 83-82
-	return '';
+proto.getTag = function getTag(){
+	return this._tagData;
 };
 
 /**
- *
  * Set the _tagData property
  * @method setTag
  * @param data
- *
  */
 proto.setTag = function setTag(data){
-	// File: expression.h lines: 158-157
 	this._tagData = data;
 };
 
+proto.resetTag = function resetTag() {
+	this.setTag(null);
+	for(var i=0; i<this.numChildren(); i++) {
+		this.getChild(i).resetTag();
+	}
+};
+
 /**
- *
  * Call the debugString method
  * @method toString
- *
  */
 proto.toString = function toString(){
-	// File: expression.cpp lines: 31-34
-	return this.debugString( 0 );
+	return this.debugString(0);
 };
+/**
+ * Debug information
+ * @method debugString
+ */
+proto.debugString = function debugString(level) {
+	throw new Error("NOT IMPLEMENTED");
+};
+/**
+ * @method _debugAddSpace
+ * @param level
+ */
+proto._debugAddSpace = function _debugAddSpace(level){
+	return new Array(level+1).join("    ");
+};
+
+
 

+ 117 - 52
lib/pipeline/matcher/MatchExpressionParser.js

@@ -1,6 +1,6 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression_parser.cpp
 var MatchExpressionParser = module.exports = function (){
 
 }, klass = MatchExpressionParser, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
@@ -32,6 +32,11 @@ var errors = require("../../Errors.js"),
 	AllElemMatchOp = require("./AllElemMatchOp.js"),
 	AtomicMatchExpression = require("./AtomicMatchExpression.js");
 
+proto.expressionParserTextCallback = require('./TextMatchExpressionParser').expressionParserTextCallbackReal;
+
+// The maximum allowed depth of a query tree. Just to guard against stack overflow.
+var MAXIMUM_TREE_DEPTH = 100;
+
 /**
  *
  * Check if the input element is an expression
@@ -39,8 +44,7 @@ var errors = require("../../Errors.js"),
  * @param element
  *
  */
-proto._isExpressionDocument = function _isExpressionDocument(element){
-	// File: expression_parser.cpp lines: 340-355
+proto._isExpressionDocument = function _isExpressionDocument(element, allowIncompleteDBRef){
 	if (!(element instanceof Object))
 		return false;
 
@@ -51,26 +55,50 @@ proto._isExpressionDocument = function _isExpressionDocument(element){
 	if (name[0] != '$')
 		return false;
 
-	if ("$ref" == name)
+	if (this._isDBRefDocument(element, allowIncompleteDBRef))
 		return false;
 
 	return true;
 };
 
+proto._isDBRefDocument = function _isDBRefDocument(obj, allowIncompleteDBRef) {
+	var hasRef, hasID, hasDB = false;
+
+	var i, fieldName, element = null,
+		keys = Object.keys(obj), length = keys.length;
+	for (i = 0; i < length; i++) {
+		fieldName = keys[i];
+		element = obj[fieldName];
+		if (!hasRef && fieldName === '$ref')
+			hasRef = true;
+		else if (!hasID && fieldName === '$id')
+			hasID = true;
+		else if (!hasDB && fieldName === '$db')
+			hasDB = true;
+	}
+
+	return allowIncompleteDBRef && (hasRef || hasID || hasDB) || (hasRef && hasID);
+};
+
 /**
  *
  * Parse the input object into individual elements
  * @method _parse
  * @param obj
- * @param topLevel
+ * @param level
  *
  */
-proto._parse = function _parse(obj, topLevel){
-	// File: expression_parser.cpp lines: 217-319
+proto._parse = function _parse(obj, level){
+	if (level > MAXIMUM_TREE_DEPTH)
+		return {code:ErrorCodes.BAD_VALUE, description:"exceeded maximum query tree depth of " +
+			MAXIMUM_TREE_DEPTH + " at " + JSON.stringify(obj)};
+
 	var rest, temp, status, element, eq, real;
 	var root = new AndMatchExpression();
 	var objkeys = Object.keys(obj);
 	var currname, currval;
+	var topLevel = level === 0;
+	level++;
 	for (var i = 0; i < objkeys.length; i++) {
 		currname = objkeys[i];
 		currval = obj[currname];
@@ -82,7 +110,7 @@ proto._parse = function _parse(obj, topLevel){
 				if (!(currval instanceof Array))
 					return {code:ErrorCodes.BAD_VALUE, description:"$or needs an array"};
 				temp = new OrMatchExpression();
-				status = this._parseTreeList(currval, temp);
+				status = this._parseTreeList(currval, temp, level);
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(temp);
@@ -91,7 +119,7 @@ proto._parse = function _parse(obj, topLevel){
 				if (!(currval instanceof Array))
 					return {code:ErrorCodes.BAD_VALUE, description:"and needs an array"};
 				temp = new AndMatchExpression();
-				status = this._parseTreeList(currval, temp);
+				status = this._parseTreeList(currval, temp, level);
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(temp);
@@ -100,7 +128,7 @@ proto._parse = function _parse(obj, topLevel){
 				if (!(currval instanceof Array))
 					return {code:ErrorCodes.BAD_VALUE, description:"and needs an array"};
 				temp = new NorMatchExpression();
-				status = this._parseTreeList(currval, temp);
+				status = this._parseTreeList(currval, temp, level);
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(temp);
@@ -123,10 +151,14 @@ proto._parse = function _parse(obj, topLevel){
 				if (status.code != ErrorCodes.OK)
 					return status;
 				root.add(status.result);*/
+			} else if ('text' === rest) {
+				if (typeof currval !== 'object') {
+					return {code: ErrorCodes.BAD_VALUE, description: '$text expects an object'};
+				}
+
+				return this.expressionTextCallback(currval);
 			}
-			else if ("comment" == rest) {
-				1+1;
-			}
+			else if ("comment" == rest) {}
 			else {
 				return {code:ErrorCodes.BAD_VALUE, description:"unknown top level operator: " + currname};
 			}
@@ -135,7 +167,7 @@ proto._parse = function _parse(obj, topLevel){
 		}
 
 		if (this._isExpressionDocument(currval)) {
-			status = this._parseSub(currname, currval, root);
+			status = this._parseSub(currname, currval, root, level);
 			if (status.code != ErrorCodes.OK)
 				return status;
 			continue;
@@ -172,8 +204,7 @@ proto._parse = function _parse(obj, topLevel){
  * @param element
  *
  */
-proto._parseAll = function _parseAll(name, element){
-	// File: expression_parser.cpp lines: 512-583
+proto._parseAll = function _parseAll(name, element, level){
 	var status, i;
 
 	if (!(element instanceof Array))
@@ -201,7 +232,7 @@ proto._parseAll = function _parseAll(name, element){
 				return {code:ErrorCodes.BAD_VALUE, description:"$all/$elemMatch has to be consistent"};
 			}
 
-			status = this._parseElemMatch("", hopefullyElemMatchElement.$elemMatch ); // TODO: wrong way to do this?
+			status = this._parseElemMatch("", hopefullyElemMatchElement.$elemMatch, level);
 			if (status.code != ErrorCodes.OK)
 				return status;
 			temp.add(status.result);
@@ -249,11 +280,14 @@ proto._parseAll = function _parseAll(name, element){
  *
  */
 proto._parseArrayFilterEntries = function _parseArrayFilterEntries(entries, theArray){
-	// File: expression_parser.cpp lines: 445-468
 	var status, e, r;
 	for (var i = 0; i < theArray.length; i++) {
 		e = theArray[i];
 
+		if (this._isExpressionDocument(e, false)) {
+			return {code:ErrorCodes.BAD_VALUE, description:"cannot nest $ under $in"};
+		}
+
 		if (e instanceof RegExp ) {
 			r = new RegexMatchExpression();
 			status = r.init("", e);
@@ -282,7 +316,6 @@ proto._parseArrayFilterEntries = function _parseArrayFilterEntries(entries, theA
  *
  */
 proto._parseComparison = function _parseComparison(name, cmp, element){
-	// File: expression_parser.cpp lines: 34-43
 	var temp = new ComparisonMatchExpression(cmp);
 
 	var status = temp.init(name, element);
@@ -300,17 +333,31 @@ proto._parseComparison = function _parseComparison(name, cmp, element){
  * @param element
  *
  */
-proto._parseElemMatch = function _parseElemMatch(name, element){
-	// File: expression_parser.cpp lines: 471-509
+proto._parseElemMatch = function _parseElemMatch(name, element, level){
 	var temp, status;
 	if (!(element instanceof Object))
 		return {code:ErrorCodes.BAD_VALUE, description:"$elemMatch needs an Object"};
 
-	if (this._isExpressionDocument(element)) {
+	// $elemMatch value case applies when the children all
+	// work on the field 'name'.
+	// This is the case when:
+	//     1) the argument is an expression document; and
+	//     2) expression is not a AND/NOR/OR logical operator. Children of
+	//        these logical operators are initialized with field names.
+	//     3) expression is not a WHERE operator. WHERE works on objects instead
+	//        of specific field.
+	var elt = element[Object.keys(element)[0]],
+		isElemMatchValue = this._isExpressionDocument(element, true) &&
+			elt !== '$and' &&
+			elt !== '$nor' &&
+			elt !== '$or' &&
+			elt !== '$where';
+
+	if (isElemMatchValue) {
 		// value case
 
 		var theAnd = new AndMatchExpression();
-		status = this._parseSub("", element, theAnd);
+		status = this._parseSub("", element, theAnd, level);
 		if (status.code != ErrorCodes.OK)
 			return status;
 
@@ -327,9 +374,13 @@ proto._parseElemMatch = function _parseElemMatch(name, element){
 		return {code:ErrorCodes.OK, result:temp};
 	}
 
+	// DBRef value case
+	// A DBRef document under a $elemMatch should be treated as an object case
+	// because it may contain non-DBRef fields in addition to $ref, $id and $db.
+
 	// object case
 
-	status = this._parse(element, false);
+	status = this._parse(element, level);
 	if (status.code != ErrorCodes.OK)
 		return status;
 
@@ -350,7 +401,6 @@ proto._parseElemMatch = function _parseElemMatch(name, element){
  *
  */
 proto._parseMOD = function _parseMOD(name, element){
-	// File: expression_parser.cpp lines: 360-387
 	var d,r;
 	if (!(element instanceof Array))
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, needs to be an array"};
@@ -358,13 +408,13 @@ proto._parseMOD = function _parseMOD(name, element){
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, not enough elements"};
 	if (element.length > 2)
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, too many elements"};
-	if ((typeof(element[0]) != 'number')) {
+	if (typeof element[0] !== 'number') {
 		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, divisor not a number"};
 	} else {
 		d = element[0];
 	}
-	if (( typeof(element[1]) != 'number') ) {
-		r = 0;
+	if (typeof element[1] !== 'number') {
+		return {code:ErrorCodes.BAD_VALUE, result:"malformed mod, remainder not a number"};
 	} else {
 		r = element[1];
 	}
@@ -385,8 +435,7 @@ proto._parseMOD = function _parseMOD(name, element){
  * @param element
  *
  */
-proto._parseNot = function _parseNot(name, element){
-	// File: expression_parser_tree.cpp lines: 55-91
+proto._parseNot = function _parseNot(name, element, level){
 	var status;
 	if (element instanceof RegExp) {
 		status = this._parseRegexElement(name, element);
@@ -406,7 +455,7 @@ proto._parseNot = function _parseNot(name, element){
 		return {code:ErrorCodes.BAD_VALUE, result:"$not cannot be empty"};
 
 	var theAnd = new AndMatchExpression();
-	status = this._parseSub(name, element, theAnd);
+	status = this._parseSub(name, element, theAnd, level);
 	if (status.code != ErrorCodes.OK)
 		return status;
 
@@ -434,7 +483,6 @@ proto._parseNot = function _parseNot(name, element){
  *
  */
 proto._parseRegexDocument = function _parseRegexDocument(name, doc){
-	// File: expression_parser.cpp lines: 402-442
 	var regex = '', regexOptions = '', e;
 
 	if(doc.$regex) {
@@ -450,7 +498,7 @@ proto._parseRegexDocument = function _parseRegexDocument(name, doc){
 			}
 			regex = (flagIndex? str : str.substr(1, flagIndex-1));
 			regexOptions = str.substr(flagIndex, str.length);
-		} else if (typeof(e) == 'string') {
+		} else if (typeof e  === 'string') {
 			regex = e;
 		} else {
 			return {code:ErrorCodes.BAD_VALUE, description:"$regex has to be a string"};
@@ -459,7 +507,7 @@ proto._parseRegexDocument = function _parseRegexDocument(name, doc){
 
 	if(doc.$options) {
 		e = doc.$options;
-		if(typeof(e) == 'string') {
+		if(typeof(e) === 'string') {
 			regexOptions = e;
 		} else {
 			return {code:ErrorCodes.BAD_VALUE, description:"$options has to be a string"};
@@ -484,7 +532,6 @@ proto._parseRegexDocument = function _parseRegexDocument(name, doc){
  *
  */
 proto._parseRegexElement = function _parseRegexElement(name, element){
-	// File: expression_parser.cpp lines: 390-399
 	if (!(element instanceof RegExp))
 		return {code:ErrorCodes.BAD_VALUE, description:"not a regex"};
 
@@ -516,18 +563,26 @@ proto._parseRegexElement = function _parseRegexElement(name, element){
  * @param root
  *
  */
-proto._parseSub = function _parseSub(name, sub, root){
-	// File: expression_parser.cpp lines: 322-337
+proto._parseSub = function _parseSub(name, sub, root, level){
 	var subkeys = Object.keys(sub),
 		currname, currval;
 
+	if (level > MAXIMUM_TREE_DEPTH) {
+		return {code:ErrorCodes.BAD_VALUE, description:"exceeded maximum query tree depth of " +
+			MAXIMUM_TREE_DEPTH + " at " + JSON.stringify(sub)};
+	}
+
+	level++;
+
+	// DERIVATION: We are not implementing Geo functions yet.
+
 	for (var i = 0; i < subkeys.length; i++) {
 		currname = subkeys[i];
 		currval = sub[currname];
 		var deep = {};
 		deep[currname] = currval;
 
-		var status = this._parseSubField(sub, root, name, deep);
+		var status = this._parseSubField(sub, root, name, deep, level);
 		if (status.code != ErrorCodes.OK)
 			return status;
 
@@ -548,20 +603,19 @@ proto._parseSub = function _parseSub(name, sub, root){
  * @param element
  *
  */
-proto._parseSubField = function _parseSubField(context, andSoFar, name, element){
-	// File: expression_parser.cpp lines: 46-214
+proto._parseSubField = function _parseSubField(context, andSoFar, name, element, level){
 	// TODO: these should move to getGtLtOp, or its replacement
 	var currname = Object.keys(element)[0];
 	var currval = element[currname];
 	if ("$eq" == currname)
 		return this._parseComparison(name, 'EQ', currval);
 
-	if ("$not" == currname) {
-		return this._parseNot(name, currval);
-	}
+	if ("$not" == currname)
+		return this._parseNot(name, currval, level);
 
 	var status, temp, temp2;
 	switch (currname) {
+		// TODO: -1 is apparently a value for mongo, but we handle strings so...
 	case '$lt':
 		return this._parseComparison(name, 'LT', currval);
 	case '$lte':
@@ -571,6 +625,11 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 	case '$gte':
 		return this._parseComparison(name, 'GTE', currval);
 	case '$ne':
+		// Just because $ne can be rewritten as the negation of an
+		// equality does not mean that $ne of a regex is allowed. See SERVER-1705.
+		if (currval instanceof RegExp) {
+			return {code:ErrorCodes.BAD_VALUE, description:"Can't have regex as arg to $ne."};
+		}
 		status = this._parseComparison(name, 'EQ', currval);
 		if (status.code != ErrorCodes.OK)
 			return status;
@@ -618,11 +677,19 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 			// matching old odd semantics
 			size = 0;
 		else if (typeof(currval) === 'number')
-			size = currval;
+			// SERVER-11952. Setting 'size' to -1 means that no documents
+			// should match this $size expression.
+			if (currval < 0)
+				size = -1;
+			else
+				size = currval;
 		else {
 			return {code:ErrorCodes.BAD_VALUE, description:"$size needs a number"};
 		}
 
+		// DERIVATION/Potential bug: Mongo checks to see if doube values are exactly equal to
+		// their int converted version. If not, size = -1.
+
 		temp = new SizeMatchExpression();
 		status = temp.init(name, size);
 		if (status.code != ErrorCodes.OK)
@@ -637,7 +704,7 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 		status = temp.init(name);
 		if (status.code != ErrorCodes.OK)
 			return status;
-		if (currval)
+		if (currval) // DERIVATION: This may have to check better than truthy? Need to look at TrueValue
 			return {code:ErrorCodes.OK, result:temp};
 		temp2 = new NotMatchExpression();
 		status = temp2.init(temp);
@@ -674,10 +741,10 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
 		return this._parseRegexDocument(name, context);
 
 	case '$elemMatch':
-		return this._parseElemMatch(name, currval);
+		return this._parseElemMatch(name, currval, level);
 
 	case '$all':
-		return this._parseAll(name, currval);
+		return this._parseAll(name, currval, level);
 
 	case '$geoWithin':
 	case '$geoIntersects':
@@ -701,8 +768,7 @@ proto._parseSubField = function _parseSubField(context, andSoFar, name, element)
  * @param out
  *
  */
-proto._parseTreeList = function _parseTreeList(arr, out){
-	// File: expression_parser_tree.cpp lines: 33-52
+proto._parseTreeList = function _parseTreeList(arr, out, level){
 	if (arr.length === 0)
 		return {code:ErrorCodes.BAD_VALUE, description:"$and/$or/$nor must be a nonempty array"};
 
@@ -713,7 +779,7 @@ proto._parseTreeList = function _parseTreeList(arr, out){
 		if (!(element instanceof Object))
 			return {code:ErrorCodes.BAD_VALUE, description:"$or/$and/$nor entries need to be full objects"};
 
-		status = this._parse(element, false);
+		status = this._parse(element, level);
 		if (status.code != ErrorCodes.OK)
 			return status;
 
@@ -730,6 +796,5 @@ proto._parseTreeList = function _parseTreeList(arr, out){
  *
  */
 proto.parse = function parse(obj){
-	// File: expression_parser.h lines: 40-41
-	return this._parse(obj, true);
+	return this._parse(obj, 0);
 };

+ 14 - 279
lib/pipeline/matcher/Matcher2.js

@@ -1,8 +1,13 @@
 "use strict";
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+/**
+ * Matcher2 is a simple wrapper around a JSONObj and the MatchExpression created from it.
+ * @class Matcher2
+ * @namespace mungedb-aggregate.pipeline.matcher2
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var Matcher2 = module.exports = function Matcher2(pattern, nested){
-	// File: matcher.cpp lines: 83-92
 	this._pattern = pattern;
 	this.parser = new MatchExpressionParser();
 	var result = this.parser.parse(pattern);
@@ -14,243 +19,12 @@ var Matcher2 = module.exports = function Matcher2(pattern, nested){
 // DEPENDENCIES
 var errors = require("../../Errors.js"),
 	ErrorCodes = errors.ErrorCodes,
-	MatchExpression = require("./MatchExpression.js"),
-	MatchExpressionParser = require("./MatchExpressionParser.js"),
-	FalseMatchExpression = require("./FalseMatchExpression.js"),
-	ComparisonMatchExpression = require("./ComparisonMatchExpression.js"),
-	InMatchExpression = require("./InMatchExpression.js"),
-	AndMatchExpression = require("./AndMatchExpression.js"),
-	OrMatchExpression = require("./OrMatchExpression.js"),
-	IndexKeyMatchableDocument = require('./IndexKeyMatchableDocument.js'),
-	ListOfMatchExpression = require('./ListOfMatchExpression.js'),
-	LeafMatchExpression = require("./LeafMatchExpression.js");
+	MatchExpressionParser = require("./MatchExpressionParser.js");
 
-// File: matcher.h lines: 82-82
+//PROTOTYPE MEMBERS
 proto._expression = undefined;
-
-// File: matcher.h lines: 80-80
-proto._indexKey = undefined;
-
-// File: matcher.h lines: 79-79
 proto._pattern = undefined;
 
-// File: matcher.h lines: 84-84
-proto._spliceInfo = undefined;
-
-/**
- *
- * Figure out where our index is
- * @method _spliceForIndex
- * @param keys
- * @param full
- * @param spliceInfo
- *
- */
-proto._spliceForIndex = function _spliceForIndex(keys, full, spliceInfo){
-	// File: matcher.cpp lines: 236-380
-	var dup, i, obj, lme;
-	switch (full) {
-		case MatchExpression.ALWAYS_FALSE:
-			return new FalseMatchExpression();
-
-		case MatchExpression.GEO_NEAR:
-		case MatchExpression.NOT:
-		case MatchExpression.NOR:
-			// maybe?
-			return null;
-
-		case MatchExpression.OR:
-
-		case MatchExpression.AND:
-			dup = new ListOfMatchExpression();
-			for (i = 0; i < full.numChildren(); i++) {
-				var sub = this._spliceForIndex(keys, full.getChild(i), spliceInfo);
-				if (!sub)
-					continue;
-				if (!dup.get()) {
-					if (full.matchType() == MatchExpression.AND)
-						dup.reset(new AndMatchExpression());
-					else
-						dup.reset(new OrMatchExpression());
-				}
-				dup.add(sub);
-			}
-			if (dup.get()) {
-				if (full.matchType() == MatchExpression.OR &&  dup.numChildren() != full.numChildren()) {
-					// TODO: I think this should actuall get a list of all the fields
-					// and make sure that's the same
-					// with an $or, have to make sure its all or nothing
-					return null;
-				}
-				return dup.release();
-			}
-			return null;
-
-		case MatchExpression.EQ:
-			var cmp = new ComparisonMatchExpression(full);
-
-			if (cmp.getRHS().type() == Array) {
-				// need to convert array to an $in
-
-				if (!keys.count(cmp.path().toString()))
-					return null;
-
-				var newIn = new InMatchExpression();
-				newIn.init(cmp.path());
-
-				if (newIn.getArrayFilterEntries().addEquality(cmp.getRHS()).isOK())
-					return null;
-
-				if (cmp.getRHS().Obj().isEmpty())
-					newIn.getArrayFilterEntries().addEquality(undefined);
-
-				obj = cmp.getRHS().Obj();
-				for(i in obj) {
-					var s = newIn.getArrayFilterEntries().addEquality( obj[i].next() );
-					if (s.code != ErrorCodes.OK)
-						return null;
-				}
-
-				return newIn.release();
-			}
-			else if (cmp.getRHS().type() === null) {
-				//spliceInfo.hasNullEquality = true;
-				return null;
-			}
-			break;
-
-		case MatchExpression.LTE:
-		case MatchExpression.LT:
-		case MatchExpression.GT:
-		case MatchExpression.GTE:
-			cmp = new ComparisonMatchExpression(full);
-
-			if ( cmp.getRHS().type() === null) {
-				// null and indexes don't play nice
-				//spliceInfo.hasNullEquality = true;
-				return null;
-			}
-			break;
-
-		case MatchExpression.REGEX:
-		case MatchExpression.MOD:
-			lme = new LeafMatchExpression(full);
-			if (!keys.count(lme.path().toString()))
-				return null;
-			return lme.shallowClone();
-
-		case MatchExpression.MATCH_IN:
-			lme = new LeafMatchExpression(full);
-			if (!keys.count(lme.path().toString()))
-				return null;
-			var cloned = new InMatchExpression(lme.shallowClone());
-			if (cloned.getArrayFilterEntries().hasEmptyArray())
-				cloned.getArrayFilterEntries().addEquality(undefined);
-
-			// since { $in : [[1]] } matches [1], need to explode
-			for (i = cloned.getArrayFilterEntries().equalities().begin(); i != cloned.getArrayFilterEntries().equalities().end(); ++i) {
-				var x = cloned[i];
-				if (x.type() == Array) {
-					for(var j in x) {
-						cloned.getArrayFilterEntries().addEquality(x[j]);
-					}
-				}
-			}
-
-			return cloned;
-
-		case MatchExpression.ALL:
-			// TODO: convert to $in
-			return null;
-
-		case MatchExpression.ELEM_MATCH_OBJECT:
-		case MatchExpression.ELEM_MATCH_VALUE:
-			// future
-			return null;
-
-		case MatchExpression.GEO:
-		case MatchExpression.SIZE:
-		case MatchExpression.EXISTS:
-		case MatchExpression.NIN:
-		case MatchExpression.TYPE_OPERATOR:
-		case MatchExpression.ATOMIC:
-		case MatchExpression.WHERE:
-			// no go
-			return null;
-	}
-
-	return null;
-};
-
-/**
- *
- * return if our _expression property is atomic or not
- * @method atomic
- *
- */
-proto.atomic = function atomic(){
-	// File: matcher.cpp lines: 120-133
-	if (!this._expression)
-		return false;
-
-	if (this._expression.matchType() == MatchExpression.ATOMIC)
-		return true;
-
-	// we only go down one level
-	for (var i = 0; i < this._expression.numChildren(); i++) {
-		if (this._expression.getChild(i).matchType() == MatchExpression.ATOMIC)
-			return true;
-	}
-
-	return false;
-};
-
-/**
- *
- * Return the _pattern property
- * @method getQuery
- *
- */
-proto.getQuery = function getQuery(){
-	// File: matcher.h lines: 65-64
-	return this._pattern;
-};
-
-/**
- *
- * Check if we exist
- * @method hasExistsFalse
- *
- */
-proto.hasExistsFalse = function hasExistsFalse(){
-	// File: matcher.cpp lines: 172-180
-	if (this._spliceInfo.hasNullEquality) {
-		// { a : NULL } is very dangerous as it may not got indexed in some cases
-		// so we just totally ignore
-		return true;
-	}
-
-	return this._isExistsFalse(this._expression.get(), false, this._expression.matchType() == MatchExpression.AND ? -1 : 0);
-};
-
-/**
- *
- * Find if we have a matching key inside us
- * @method keyMatch
- * @param docMatcher
- *
- */
-proto.keyMatch = function keyMatch(docMatcher){
-	// File: matcher.cpp lines: 199-206
-	if (!this._expression)
-		return docMatcher._expression.get() === null;
-	if (!docMatcher._expression)
-		return false;
-	if (this._spliceInfo.hasNullEquality)
-		return false;
-	return this._expression.equivalent(docMatcher._expression.get());
-};
-
 /**
  *
  * matches checks the input doc against the internal element path to see if it is a match
@@ -260,58 +34,20 @@ proto.keyMatch = function keyMatch(docMatcher){
  *
  */
 proto.matches = function matches(doc, details){
-	// File: matcher.cpp lines: 105-116
 	if (!this._expression)
 		return true;
 
-	if (this._indexKey == {})
-		return this._expression.matchesBSON(doc, details);
-
-	if ((doc != {}) && (Object.keys(doc)[0]))
-		return this._expression.matchesBSON(doc, details);
-
-	var mydoc = new IndexKeyMatchableDocument(this._indexKey, doc);
-	return this._expression.matches(mydoc, details);
+	return this._expression.matchesJSON(doc, details);
 };
 
 /**
  *
- * Check if we are a simple match
- * @method singleSimpleCriterion
- *
- */
-proto.singleSimpleCriterion = function singleSimpleCriterion(){
-	// File: matcher.cpp lines: 184-196
-	if (!this._expression)
-		return false;
-
-	if (this._expression.matchType() == MatchExpression.EQ)
-		return true;
-
-	if (this._expression.matchType() == MatchExpression.AND && this._expression.numChildren() == 1 && this._expression.getChild(0).matchType() == MatchExpression.EQ)
-		return true;
-
-	return false;
-};
-
-/**
- *
- * Wrapper around _spliceForIndex
- * @method spliceForIndex
- * @param key
- * @param full
- * @param spliceInfo
+ * Return the _pattern property
+ * @method getQuery
  *
  */
-proto.spliceForIndex = function spliceForIndex(key, full, spliceInfo){
-	// File: matcher.cpp lines: 209-217
-	var keys = [],
-		e, i;
-	for (i in key) {
-		e = key[i];
-		keys.insert(e.fieldName());
-	}
-	return this._spliceForIndex(keys, full, spliceInfo);
+proto.getQuery = function getQuery(){
+	return this._pattern;
 };
 
 /**
@@ -321,6 +57,5 @@ proto.spliceForIndex = function spliceForIndex(key, full, spliceInfo){
  *
  */
 proto.toString = function toString(){
-	// File: matcher.h lines: 66-65
 	return this._pattern.toString();
 };

+ 10 - 16
lib/pipeline/matcher/ModMatchExpression.js

@@ -1,17 +1,16 @@
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression');
+var LeafMatchExpression = require('./LeafMatchExpression'),
+	ErrorCodes = require("../../Errors.js").ErrorCodes;
 
-// Autogenerated by cport.py on 2013-09-17 14:37
+// File: expression_leaf.h
 var ModMatchExpression = module.exports = function ModMatchExpression(){
 	base.call(this);
 	this._matchType = 'MOD';
 }, klass = ModMatchExpression, base =  LeafMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
-// File: expression_leaf.h lines: 210-210
 proto._divisor = undefined;
 
-// File: expression_leaf.h lines: 211-211
 proto._remainder = undefined;
 
 /**
@@ -22,7 +21,6 @@ proto._remainder = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 253-261
 	return this._debugAddSpace( level ) + this.path() + " mod " + this._divisor + " % x == " + this._remainder + (this.getTag() ? " " + this.getTag().debugString() : '') + "\n";
 };
 
@@ -34,10 +32,9 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 264-272
-	if(other._matchType != 'MOD')
+	if(other._matchType !== this._matchType)
 		return false;
-	return this.path() == other.path() && this._divisor == other._divisor && this._remainder == other._remainder;
+	return this.path() === other.path() && this._divisor === other._divisor && this._remainder === other._remainder;
 };
 
 /**
@@ -47,7 +44,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.getDivisor = function getDivisor(){
-	// File: expression_leaf.h lines: 206-205
 	return this._divisor;
 };
 
@@ -58,7 +54,6 @@ proto.getDivisor = function getDivisor(){
  *
  */
 proto.getRemainder = function getRemainder( /*  */ ){
-	// File: expression_leaf.h lines: 207-206
 	return this._remainder;
 };
 
@@ -71,9 +66,8 @@ proto.getRemainder = function getRemainder( /*  */ ){
  *
  */
 proto.init = function init(path,divisor,remainder) {
-	// File: expression_leaf.cpp lines: 239-244
 	if (divisor === 0 ){
-		return {'code':'BAD_VALUE', 'desc':'Divisor cannot be 0'};
+		return {'code':ErrorCodes.BAD_VALUE, 'desc':'Divisor cannot be 0'};
 	}
 
 	this._divisor = divisor;
@@ -89,12 +83,11 @@ proto.init = function init(path,divisor,remainder) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_leaf.cpp lines: 247-250
-	if(typeof(e) != 'number') {
+	if(typeof(e) !== 'number') {
 		return false;
 	}
 
-	return (e % this._divisor) == this._remainder;
+	return (e % this._divisor) === this._remainder;
 };
 
 /**
@@ -104,9 +97,10 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.h lines: 194-197
 	var e = new ModMatchExpression();
 	e.init(this.path(),this._divisor, this._remainder);
+	if (this.getTag())
+		e.setTag(this.getTag().clone());
 	return e;
 };
 

+ 5 - 0
lib/pipeline/matcher/NorMatchExpression.js

@@ -73,6 +73,11 @@ proto.shallowClone = function shallowClone(){
 	for (var i = 0; i < this.numChildren(); i++) {
 		e.add(this.getChild(i).shallowClone());
 	}
+
+	if (this.getTag()) {
+		e.setTag(this.getTag().clone());
+	}
+
 	return e;
 };
 

+ 9 - 16
lib/pipeline/matcher/NotMatchExpression.js

@@ -1,13 +1,10 @@
 "use strict";
 var MatchExpression = require('./MatchExpression');
-
-	// Autogenerated by cport.py on 2013-09-17 14:37
 var NotMatchExpression = module.exports = function NotMatchExpression(){
 	base.call(this);
 	this._matchType = 'NOT';
 }, klass = NotMatchExpression, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-	// File: expression_tree.h lines: 152-152
 proto._exp = undefined;
 
 /**
@@ -18,7 +15,6 @@ proto._exp = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_tree.cpp lines: 146-149
 	return this._debugAddSpace( level ) + "$not\n" + this._exp._debugString( level + 1 );
 };
 
@@ -30,19 +26,17 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_tree.cpp lines: 152-156
 	return other._matchType == 'NOT' && this._exp.equivalent(other.getChild(0));
 };
 
 /**
  *
- * Return the _exp property
- * @method getChild
+ * Return the reset child
+ * @method resetChild
  *
  */
-proto.getChild = function getChild() {
-	// File: expression_tree.h lines: 148-147
-	return this._exp;
+proto.resetChild = function resetChild(newChild) {
+	this._exp.reset(newChild);
 };
 
 /**
@@ -53,7 +47,6 @@ proto.getChild = function getChild() {
  *
  */
 proto.init = function init(exp) {
-	// File: expression_tree.h lines: 123-125
 	this._exp = exp;
 	return {'code':'OK'};
 };
@@ -66,9 +59,8 @@ proto.init = function init(exp) {
  * @param details
  *
  */
-proto.matches = function matches(doc,details) {
-	// File: expression_tree.h lines: 135-136
-	return ! this._exp.matches( doc,null );
+proto.matches = function matches(doc, details) {
+	return ! this._exp.matches(doc, null);
 };
 
 /**
@@ -79,7 +71,6 @@ proto.matches = function matches(doc,details) {
  *
  */
 proto.matchesSingleElement = function matchesSingleElement(e) {
-	// File: expression_tree.h lines: 139-140
 	return ! this._exp.matchesSingleElement( e );
 };
 
@@ -91,7 +82,6 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.numChildren = function numChildren(){
-	// File: expression_tree.h lines: 147-146
 	return 1;
 };
 
@@ -105,6 +95,9 @@ proto.shallowClone = function shallowClone(){
 	// File: expression_tree.h lines: 128-132
 	var e = new NotMatchExpression();
 	e.init(this._exp.shallowClone());
+	if ( this.getTag() ) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
 

+ 6 - 0
lib/pipeline/matcher/OrMatchExpression.js

@@ -64,9 +64,15 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 proto.shallowClone = function shallowClone(){
 	// File: expression_tree.h lines: 86-91
 	var clone = new OrMatchExpression();
+
 	for (var i = 0; i < this.numChildren(); i++) {
 		clone.add(this.getChild(i).shallowClone());
 	}
+
+	if (this.getTag()) {
+		clone.setTag(this.getTag().clone());
+	}
+
 	return clone;
 };
 

+ 42 - 25
lib/pipeline/matcher/RegexMatchExpression.js

@@ -1,23 +1,17 @@
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression');
+var XRegExp = require('xregexp').XRegExp,
+	LeafMatchExpression = require('./LeafMatchExpression'),
+	ErrorCodes = require('../../Errors').ErrorCodes;
 
 
-	// Autogenerated by cport.py on 2013-09-17 14:37
 var RegexMatchExpression = module.exports = function RegexMatchExpression(){
-	base.call(this);
-	this._matchType = 'REGEX';
+	base.call(this, 'REGEX');
 }, klass = RegexMatchExpression, base =  LeafMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-	// File: expression_leaf.h lines: 160-160
 klass.MaxPatternSize = 32764;
 
-	// File: expression_leaf.h lines: 184-184
 proto._flags = undefined;
-
-	// File: expression_leaf.h lines: 185-185
 proto._re = undefined;
-
-	// File: expression_leaf.h lines: 183-183
 proto._regex = undefined;
 
 /**
@@ -28,8 +22,19 @@ proto._regex = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_leaf.cpp lines: 225-234
-	return this._debugAddSpace( level ) + this.path() + " regex /" + this._regex + "/" + this._flags + (this.getTag() ? ' ' + this.getTag().debugString : '') + "\n";
+	var debug = this._debugAddSpace( level );
+	debug += this.path() + " regex /" + this._regex + "/" + this._flags;
+
+	var td = this.getTag();
+	if (td === null) {
+		debug += " " + td.debugString();
+	}
+	debug += "\n";
+	return debug;
+};
+
+proto.shortDebugString = function shortDebugString() {
+	return "/" + this._regex + "/" + this._flags;
 };
 
 /**
@@ -40,8 +45,9 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_leaf.cpp lines: 177-185
-	return other._matchType == 'REGEX' && this.path() == other.path() && this._regex == other._regex && this._flags == other._flags;
+	if (this.matchType() !== other.matchType()) return false;
+	
+	return this.path() === other.path() && this._regex === other._regex && this._flags === other._flags;
 };
 
 /**
@@ -51,7 +57,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.getFlags = function getFlags(){
-	// File: expression_leaf.h lines: 180-179
 	return this._flags;
 };
 
@@ -63,7 +68,6 @@ proto.getFlags = function getFlags(){
  *
  */
 proto.getString = function getString(){
-	// File: expression_leaf.h lines: 179-178
 	return this._regex;
 };
 
@@ -76,14 +80,23 @@ proto.getString = function getString(){
  *
  */
 proto.init = function init(path,regex,flags) {
-	// File: expression_leaf.cpp lines: 196-205
 	if(regex.toString().length > klass.MaxPatternSize){
-		return {'code':'BAD_VALUE', 'desc':'Regular Expression too long.'};
+		return {'code':ErrorCodes.BAD_VALUE, 'desc':'Regular Expression too long.'};
+	}
+
+	if (regex instanceof RegExp){
+		this._regex = regex.source;
+		this._re = regex;
+		this._flags = (this._re.ignoreCase ? 'i' : '') + (this._re.multiline ? 'm' : '');
+	} else if (typeof regex === 'string' && (!flags || typeof flags === 'string' )) {
+		this._regex = regex;
+		//remove invalid flags, sort and uniquify them
+		this._flags = (flags || '').replace( /[^imxs]/g, '').split('').sort().filter(function(el,i,a){return i===a.indexOf(el);}).join('');
+		this._re = new XRegExp(regex,this._flags);
+	} else {
+		return {'code':ErrorCodes.BAD_VALUE, 'desc':'regex not a regex'};
 	}
 
-	this._regex = regex;
-	this._flags = flags;
-	this._re = new RegExp(regex,flags);
 	return this.initPath( path );
 };
 
@@ -96,11 +109,12 @@ proto.init = function init(path,regex,flags) {
  */
 
 proto.matchesSingleElement = function matchesSingleElement(e) {
-// File: expression_leaf.cpp lines: 208-222
 	if(e instanceof RegExp){
 		return e.toString() === this._re.toString();
 	}
-	return e && (e.match) && e.match(this._re);
+	if(typeof e === 'string'){
+		return this._re.test(e);
+	}
 	// No support for SYMBOLS currently
 };
 
@@ -111,9 +125,12 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
  *
  */
 proto.shallowClone = function shallowClone(){
-	// File: expression_leaf.h lines: 167-170
 	var e = new RegexMatchExpression();
 	e.init( this.path(), this._regex, this._flags );
+
+	if ( this.getTag() ) {
+		e.setTag(this.getTag().clone());
+	}
+
 	return e;
 };
-

+ 14 - 17
lib/pipeline/matcher/SizeMatchExpression.js

@@ -1,14 +1,10 @@
 "use strict";
 var ArrayMatchingMatchExpression = require('./ArrayMatchingMatchExpression');
 
-
-// Autogenerated by cport.py on 2013-09-17 14:37
 var SizeMatchExpression = module.exports = function SizeMatchExpression(){
-	base.call(this);
-	this._matchType = 'SIZE';
+	base.call(this, 'SIZE');
 }, klass = SizeMatchExpression, base =  ArrayMatchingMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// File: expression_array.h lines: 131-131
 proto._size = undefined;
 
 /**
@@ -19,8 +15,13 @@ proto._size = undefined;
  *
  */
 proto.debugString = function debugString(level) {
-	// File: expression_array.cpp lines: 259-261
-	return this._debugAddSpace( level ) + this.path() + " $size : " + this._size.toString() + "\n";
+	var debug = this._debugAddSpace( level ) + this.path() + " $size : " + this._size.toString() + "\n";
+	
+	var td = this.tagData();
+	if (td !== null){
+		debug += " " + td.debugString();
+	}
+	return debug;
 };
 
 /**
@@ -31,11 +32,10 @@ proto.debugString = function debugString(level) {
  *
  */
 proto.equivalent = function equivalent(other) {
-	// File: expression_array.cpp lines: 264-269
-	if(other._matchType != 'SIZE') {
+	if(other.matchType() !== this.matchType()) {
 		return false;
 	}
-	return this._size == other._size && this._path == other._path;
+	return this._size === other._size && this.path() === other.path();
 };
 
 /**
@@ -45,7 +45,6 @@ proto.equivalent = function equivalent(other) {
  *
  */
 proto.getData = function getData(){
-	// File: expression_array.h lines: 128-127
 	return this._size;
 };
 
@@ -58,9 +57,6 @@ proto.getData = function getData(){
  *
  */
 proto.init = function init(path,size) {
-	// File: expression_array.cpp lines: 248-250
-	if(size === null)
-		return {code:'BAD_VALUE', 'description':'Cannot assign null to size'};
 	this._size = size;
 	return this.initPath(path);
 };
@@ -74,11 +70,10 @@ proto.init = function init(path,size) {
  *
  */
 proto.matchesArray = function matchesArray(anArray, details) {
-	// File: expression_array.cpp lines: 253-256
 	if(this._size < 0) {
 		return false;
 	}
-	return anArray.length == this._size;
+	return anArray.length === this._size;
 };
 
 /**
@@ -91,6 +86,8 @@ proto.shallowClone = function shallowClone(){
 	// File: expression_array.h lines: 116-119
 	var e = new SizeMatchExpression();
 	e.init(this.path(),this._size);
+	if ( this.getTag() ) {
+		e.setTag(this.getTag().clone());
+	}
 	return e;
 };
-

+ 0 - 55
lib/pipeline/matcher/TagData.js

@@ -1,55 +0,0 @@
-"use strict";
-
-
-
-// Autogenerated by cport.py on 2013-09-17 14:37
-var TagData = module.exports = function TagData (){
-
-}, klass = TagData, base =  Object  , proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-
-
-
-
-/**
- * debugString pure virtual.
- *
- * @method debugString
- *
- */
-proto.debugString = function debugString() {
-	throw new Error('Virtual function called.');
-};
-
-// Below this line is manually generated based on inferred info in the source.
-
-/**
- * resets the tag data.
- *
- *@method reset
- */
-proto.reset = function reset( data ) {
-	this._data = data;
-};
-
-/**
- * Sets the data tag
- * @method set
- * @param data
- */
-proto.set = function set( data ) {
-	this._data = data;
-};
-
-
-
-/**
- * Gets the data tag
- *
- * @method get
- *
- */
-proto.get = function get(){
-	return this._data;
-};
-

+ 112 - 0
lib/pipeline/matcher/TextMatchExpression.js

@@ -0,0 +1,112 @@
+"use strict";
+
+var LeafMatchExpression = require('./LeafMatchExpression.js');
+
+var TextMatchExpression = module.exports = function TextMatchExpression() {
+	base.call(this, 'TEXT');
+}, klass = TextMatchExpression, base = LeafMatchExpression, proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+
+/**
+ *
+ * Initializes the class object.
+ *
+ * @param query
+ * @param language
+ * @returns {*}
+ */
+proto.init = function init(query, language) {
+	this._query = query;
+	this._language = language;
+
+	return this.initPath('_fts');
+};
+
+/**
+ * Gets the query.
+ *
+ * @returns {*}
+ */
+proto.getQuery = function getQuery() {
+	return this._query;
+};
+
+/**
+ * Gets the language.
+ *
+ * @returns {*}
+ */
+proto.getLanguage = function getLanguage() {
+	return this._language;
+};
+
+/**
+ * Check if the input element matches.
+ *
+ * @param e
+ * @returns {boolean}
+ */
+proto.matchesSingleElement = function matchesSingleElement(e) {
+	return true;
+};
+
+/**
+ * Debug a string.
+ *
+ * @param level
+ * @returns {string}
+ */
+proto.debugString = function debugString(level) {
+	var rtn = this._debugAddSpace(level);
+
+	rtn += 'TEXT : query=' + ', language=' + this._language + ', tag=';
+
+	var tagData = this.getTag();
+
+	if (tagData !== null) {
+		tagData.debugString(level);
+	} else {
+		rtn += 'NULL';
+	}
+
+	return rtn + '\n';
+};
+
+/**
+ * Verifies the equivalency of two operands.
+ *
+ * @param other
+ * @returns {boolean}
+ */
+proto.equivalent = function equivalent(other) {
+	if (this.matchType() !== other.matchType()) {
+		return false;
+	}
+
+	if (other.getQuery() !== this._query) {
+		return false;
+	}
+
+	if (other.getLanguage() !== this._language) {
+		return false;
+	}
+
+	return true;
+};
+
+/**
+ * Clone this instance into a new one.
+ *
+ * @returns {TextMatchExpression}
+ */
+proto.shallowClone = function shallowClone() {
+	var next = new TextMatchExpression();
+
+	next.init(this._query, this._language);
+
+	if (this.getTag()) {
+		next.getTag(this.getTag().clone());
+	}
+
+	return next;
+};
+

+ 25 - 0
lib/pipeline/matcher/TextMatchExpressionParser.js

@@ -0,0 +1,25 @@
+/**
+ * Expression parser's text callback function.
+ *
+ * @param queryObj
+ * @returns {*}
+ * @private
+ */
+var _expressionParserTextCallbackReal = function _expressionParserTextCallbackReal(queryObj) {
+	if (queryObj.$search._type !== 'string') {
+		return {code: ErrorCodes.BadValue, description: '$search needs a String'};
+	}
+
+	var e = new TextMatchExpression(),
+		s = e.init(query, language);
+
+	if (s.code !== 'OK') {
+		return s;
+	}
+
+	return e.release();
+};
+
+module.exports = {
+	expressionParserTextCallbackReal: _expressionParserTextCallbackReal
+};

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