Browse Source

Merge branch 'feature/mongo_2.6.5_documentSource' into feature/mongo_2.6.5_documentSource_Cursor

Phil Murray 11 năm trước cách đây
mục cha
commit
f11097eddd
32 tập tin đã thay đổi với 1650 bổ sung986 xóa
  1. 1 1
      lib/pipeline/Document.js
  2. 2 2
      lib/pipeline/documentSources/RedactDocumentSource.js
  3. 127 187
      lib/pipeline/documentSources/UnwindDocumentSource.js
  4. 40 23
      lib/pipeline/expressions/AddExpression.js
  5. 60 44
      lib/pipeline/expressions/AndExpression.js
  6. 3 4
      lib/pipeline/expressions/ConstantExpression.js
  7. 1 1
      lib/pipeline/expressions/Expression.js
  8. 6 7
      lib/pipeline/expressions/FixedArityExpressionT.js
  9. 3 3
      lib/pipeline/expressions/MultiplyExpression.js
  10. 5 4
      lib/pipeline/expressions/NaryBaseExpressionT.js
  11. 1 1
      lib/pipeline/expressions/NaryExpression.js
  12. 53 36
      lib/pipeline/expressions/OrExpression.js
  13. 6 5
      lib/pipeline/expressions/VariadicExpressionT.js
  14. 2 0
      lib/pipeline/expressions/index.js
  15. 20 21
      lib/pipeline/matcher/ComparisonMatchExpression.js
  16. 15 13
      lib/pipeline/matcher/GTEMatchExpression.js
  17. 14 13
      lib/pipeline/matcher/GTMatchExpression.js
  18. 14 12
      lib/pipeline/matcher/LTEMatchExpression.js
  19. 14 12
      lib/pipeline/matcher/LTMatchExpression.js
  20. 2 1
      package.json
  21. 2 2
      test/lib/pipeline/documentSources/MatchDocumentSource.js
  22. 137 16
      test/lib/pipeline/documentSources/RedactDocumentSource.js
  23. 206 90
      test/lib/pipeline/expressions/AddExpression_test.js
  24. 245 172
      test/lib/pipeline/expressions/AndExpression_test.js
  25. 1 1
      test/lib/pipeline/expressions/ConcatExpression_test.js
  26. 245 101
      test/lib/pipeline/expressions/OrExpression_test.js
  27. 0 50
      test/lib/pipeline/matcher/ComparisonMatchExpression.js
  28. 110 0
      test/lib/pipeline/matcher/ComparisonMatchExpression_test.js
  29. 67 32
      test/lib/pipeline/matcher/GTEMatchExpression.js
  30. 70 38
      test/lib/pipeline/matcher/GTMatchExpression.js
  31. 85 42
      test/lib/pipeline/matcher/LTEMatchExpression.js
  32. 93 52
      test/lib/pipeline/matcher/LTMatchExpression.js

+ 1 - 1
lib/pipeline/Document.js

@@ -151,7 +151,7 @@ klass.cloneDeep = function cloneDeep(doc) {	//there are casese this is actually
 	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;
+			obj[key] = val instanceof Object && val.constructor === Object ? Document.cloneDeep(val) : val;
 		}
 	}
 	return obj;

+ 2 - 2
lib/pipeline/documentSources/RedactDocumentSource.js

@@ -64,7 +64,7 @@ proto.getNext = function getNext(callback) {
 proto.redactValue = function redactValue(input) {
 	// reorder to make JS happy with types
 	if (input instanceof Array) {
-		var newArr,
+		var newArr = [],
 			arr = input;
 		for (var i = 0; i < arr.length; i++) {
 			if ((arr[i] instanceof Object && arr[i].constructor === Object) || arr[i] instanceof Array) {
@@ -99,7 +99,7 @@ proto.redactObject = function redactObject() {
 		return DocumentSource.EOF;
 	} else if (expressionResult === DESCEND_VAL) {
 		var input = this._variables.getDocument(this._currentId);
-		var out;
+		var out = {};
 
 		var inputKeys = Object.keys(input);
 		for (var i = 0; i < inputKeys.length; i++) {

+ 127 - 187
lib/pipeline/documentSources/UnwindDocumentSource.js

@@ -1,6 +1,11 @@
 "use strict";
 
-var async = require("async");
+var async = require('async'),
+	DocumentSource = require('./DocumentSource'),
+	Expression = require('../expressions/Expression'),
+	FieldPath = require('../FieldPath'),
+	Value = require('../Value'),
+	Document = require('../Document');
 
 /**
  * A document source unwinder
@@ -11,61 +16,51 @@ var async = require("async");
  * @param [ctx] {ExpressionContext}
  **/
 var UnwindDocumentSource = module.exports = function UnwindDocumentSource(ctx){
-	if (arguments.length > 1) throw new Error("up to one arg expected");
-	base.call(this, ctx);
-
-	// Configuration state.
-	this._unwindPath = null;
+	if (arguments.length > 1) {
+		throw new Error('Up to one argument expected.');
+	}
 
-	// Iteration state.
-	this._unwinder = null;
+	base.call(this, ctx);
 
+	this._unwindPath = null; // Configuration state.
+	this._unwinder = null; // Iteration state.
 }, klass = UnwindDocumentSource, base = require('./DocumentSource'), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-var DocumentSource = base,
-	FieldPath = require('../FieldPath'),
-	Document = require('../Document'),
-	Expression = require('../expressions/Expression');
+klass.unwindName = '$unwind';
 
-klass.Unwinder = (function(){
+klass.Unwinder = (function() {
 	/**
-	 * Helper class to unwind arrays within a series of documents.
-	 * @param	{String}	unwindPath is the field path to the array to unwind.
-	 **/
-	var klass = function Unwinder(unwindPath){
-		// Path to the array to unwind.
-		this._unwindPath = unwindPath;
-		// The souce document to unwind.
-		this._document = null;
-		// Document indexes of the field path components.
-		this._unwindPathFieldIndexes = [];
-		// Iterator over the array within _document to unwind.
-		this._unwindArrayIterator = null;
-		// The last value returned from _unwindArrayIterator.
-		//this._unwindArrayIteratorCurrent = undefined; //dont define this yet
-	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	 * Construct a new Unwinder instance. Used as a parent class for UnwindDocumentSource.
+	 *
+	 * @param unwindPath
+	 * @constructor
+	 */
+	var klass = function Unwinder(unwindPath) {
+		this._unwindPath = new FieldPath(unwindPath);
 
-	/**
-	 * Reset the unwinder to unwind a new document.
-	 * @param	{Object}	document
-	 **/
-	proto.resetDocument = function resetDocument(document){
-		if (!document) throw new Error("document is required!");
+		this._inputArray = undefined;
+		this._document = undefined;
+		this._index = undefined;
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-		// Reset document specific attributes.
+	proto.resetDocument = function resetDocument(document) {
+		if (!document) throw new Error('Document is required!');
+
+		this._inputArray = [];
 		this._document = document;
-		this._unwindPathFieldIndexes.length = 0;
-		this._unwindArrayIterator = null;
-		delete this._unwindArrayIteratorCurrent;
+		this._index = 0;
 
-		var pathValue = this.extractUnwindValue(); // sets _unwindPathFieldIndexes
-		if (!pathValue || pathValue.length === 0) return;  // The path does not exist.
+		var pathValue = Document.getNestedField(this._document, this._unwindPath);
 
-		if (!(pathValue instanceof Array)) throw new Error(UnwindDocumentSource.unwindName + ":  value at end of field path must be an array; code 15978");
+		if (!pathValue || pathValue.length === 0) {
+			return;
+		}
 
-		// Start the iterator used to unwind the array.
-		this._unwindArrayIterator = pathValue.slice(0);
-		this._unwindArrayIteratorCurrent = this._unwindArrayIterator.splice(0,1)[0];
+		if (!(pathValue instanceof Array)) {
+			throw new Error(UnwindDocumentSource.unwindName + ':  value at end of field path must be an array; code 15978');
+		}
+
+		this._inputArray = pathValue;
 	};
 
 	/**
@@ -75,199 +70,144 @@ klass.Unwinder = (function(){
 	 * than the original mongo implementation, but should get updated to follow the current API.
 	 **/
 	proto.getNext = function getNext() {
-		if (this.eof())
+		if (this._inputArray === undefined || this._index === this._inputArray.length) {
 			return DocumentSource.EOF;
-
-		var output = this.getCurrent();
-		this.advance();
-		return output;
-	};
-
-	/**
-	 * eof
-	 * @returns	{Boolean}	true if done unwinding the last document passed to resetDocument().
-	 **/
-	proto.eof = function eof(){
-		return !this.hasOwnProperty("_unwindArrayIteratorCurrent");
-	};
-
-	/**
-	 * Try to advance to the next document unwound from the document passed to resetDocument().
-	 * @returns	{Boolean} true if advanced to a new unwound document, but false if done advancing.
-	 **/
-	proto.advance = function advance(){
-		if (!this._unwindArrayIterator) {
-			// resetDocument() has not been called or the supplied document had no results to
-			// unwind.
-			delete this._unwindArrayIteratorCurrent;
-		} else if (!this._unwindArrayIterator.length) {
-			// There are no more results to unwind.
-			delete this._unwindArrayIteratorCurrent;
-		} else {
-			this._unwindArrayIteratorCurrent = this._unwindArrayIterator.splice(0, 1)[0];
 		}
-	};
 
-	/**
-	 * Get the current document unwound from the document provided to resetDocument(), using
-	 * the current value in the array located at the provided unwindPath.  But return
-	 * intrusive_ptr<Document>() if resetDocument() has not been called or the results to unwind
-	 * have been exhausted.
-	 *
-	 * @returns	{Object}
-	 **/
-	proto.getCurrent = function getCurrent(){
-		if (!this.hasOwnProperty("_unwindArrayIteratorCurrent")) {
-			return null;
-		}
-
-		// Clone all the documents along the field path so that the end values are not shared across
-		// documents that have come out of this pipeline operator.  This is a partial deep clone.
-		// Because the value at the end will be replaced, everything along the path leading to that
-		// will be replaced in order not to share that change with any other clones (or the
-		// original).
-
-		var clone = Document.clone(this._document);
-		var current = clone;
-		var n = this._unwindPathFieldIndexes.length;
-		if (!n) throw new Error("unwindFieldPathIndexes are empty");
-		for (var i = 0; i < n; ++i) {
-			var fi = this._unwindPathFieldIndexes[i];
-			var fp = current[fi];
-			if (i + 1 < n) {
-				// For every object in the path but the last, clone it and continue on down.
-				var next = Document.clone(fp);
-				current[fi] = next;
-				current = next;
-			} else {
-				// In the last nested document, subsitute the current unwound value.
-				current[fi] = this._unwindArrayIteratorCurrent;
-			}
-		}
-
-		return clone;
-	};
-
-	/**
-	 * Get the value at the unwind path, otherwise an empty pointer if no such value
-	 * exists.  The _unwindPathFieldIndexes attribute will be set as the field path is traversed
-	 * to find the value to unwind.
-	 *
-	 * @returns	{Object}
-	 **/
-	proto.extractUnwindValue = function extractUnwindValue() {
-		var current = this._document;
-		var pathValue;
-		var pathLength = this._unwindPath.getPathLength();
-		for (var i = 0; i < pathLength; ++i) {
-
-			var idx = this._unwindPath.getFieldName(i);
-
-			if (!current.hasOwnProperty(idx)) return null; // The target field is missing.
-
-			// Record the indexes of the fields down the field path in order to quickly replace them
-			// as the documents along the field path are cloned.
-			this._unwindPathFieldIndexes.push(idx);
-
-			pathValue = current[idx];
+		this._document = Document.cloneDeep(this._document);
+		Document.setNestedField(this._document, this._unwindPath, this._inputArray[this._index++]);
 
-			if (i < pathLength - 1) {
-				if (typeof pathValue !== 'object') return null; // The next field in the path cannot exist (inside a non object).
-				current = pathValue; // Move down the object tree.
-			}
-		}
-
-		return pathValue;
+		return this._document;
 	};
 
 	return klass;
 })();
 
 /**
- * Specify the field to unwind.
-**/
-proto.unwindPath = function unwindPath(fieldPath){
-	// Can't set more than one unwind path.
-	if (this._unwindPath) throw new Error(this.getSourceName() + " can't unwind more than one path; code 15979");
-
-	// Record the unwind path.
-	this._unwindPath = new FieldPath(fieldPath);
-	this._unwinder = new klass.Unwinder(this._unwindPath);
-};
-
-klass.unwindName = "$unwind";
-
-proto.getSourceName = function getSourceName(){
+ * Get the document source name.
+ *
+ * @returns {string}
+ */
+proto.getSourceName = function getSourceName() {
 	return klass.unwindName;
 };
 
 /**
- * Get the fields this operation needs to do its job.
- * Deps should be in "a.b.c" notation
+ * Get the next source.
  *
- * @method	getDependencies
- * @param	{Object} deps	set (unique array) of strings
- * @returns	DocumentSource.GetDepsReturn
-**/
-proto.getDependencies = function getDependencies(deps) {
-	if (!this._unwindPath) throw new Error("unwind path does not exist!");
-	deps[this._unwindPath.getPath(false)] = 1;
-	return DocumentSource.GetDepsReturn.SEE_NEXT;
-};
-
+ * @param callback
+ * @returns {*}
+ */
 proto.getNext = function getNext(callback) {
-	if (!callback) throw new Error(this.getSourceName() + ' #getNext() requires callback');
+	if (!callback) {
+		throw new Error(this.getSourceName() + ' #getNext() requires callback.');
+	}
+
+	if (this.expCtx.checkForInterrupt && this.expCtx.checkForInterrupt() === false) {
+		return callback(new Error('Interrupted'));
+	}
 
 	var self = this,
 		out = this._unwinder.getNext(),
 		exhausted = false;
 
 	async.until(
-		function() {
-			if(out === DocumentSource.EOF && exhausted) return true;	// Really is EOF, not just an empty unwinder
-			else if(out !== DocumentSource.EOF) return true; // Return whatever we got that wasn't EOF
+		function () {
+			if (out !== DocumentSource.EOF || exhausted) {
+				return true;
+			}
+
 			return false;
 		},
-		function(cb) {
-			self.source.getNext(function(err, doc) {
-				if(err) return cb(err);
-				out = doc;
-				if(out === DocumentSource.EOF) { // Our source is out of documents, we're done
+		function (cb) {
+			self.source.getNext(function (err, doc) {
+				if (err) {
+					return cb(err);
+				}
+
+				if (doc === DocumentSource.EOF) {
 					exhausted = true;
-					return cb();
 				} else {
 					self._unwinder.resetDocument(doc);
 					out = self._unwinder.getNext();
-					return cb();
 				}
+
+				return cb();
 			});
 		},
 		function(err) {
-			if(err) return callback(err);
+			if (err) {
+				return callback(err);
+			}
+
 			return callback(null, out);
 		}
 	);
 
-	return out; //For sync mode
+	return out;
 };
 
+/**
+ * Serialize the data.
+ *
+ * @param explain
+ * @returns {{}}
+ */
 proto.serialize = function serialize(explain) {
-	if (!this._unwindPath) throw new Error("unwind path does not exist!");
+	if (!this._unwindPath) {
+		throw new Error('unwind path does not exist!');
+	}
+
 	var doc = {};
+
 	doc[this.getSourceName()] = this._unwindPath.getPath(true);
+
 	return doc;
 };
 
+/**
+ * Get the fields this operation needs to do its job.
+ *
+ * @param deps
+ * @returns {DocumentSource.GetDepsReturn.SEE_NEXT|*}
+ */
+proto.getDependencies = function getDependencies(deps) {
+	if (!this._unwindPath) {
+		throw new Error('unwind path does not exist!');
+	}
+
+	deps[this._unwindPath.getPath(false)] = 1;
+
+	return DocumentSource.GetDepsReturn.SEE_NEXT;
+};
+
+/**
+ * Unwind path.
+ *
+ * @param fieldPath
+ */
+proto.unwindPath = function unwindPath(fieldPath) {
+	if (this._unwindPath) {
+		throw new Error(this.getSourceName() + ' can\'t unwind more than one path; code 15979');
+	}
+
+	// Record the unwind path.
+	this._unwindPath = new FieldPath(fieldPath);
+	this._unwinder = new klass.Unwinder(fieldPath);
+};
+
 /**
  * Creates a new UnwindDocumentSource with the input path as the path to unwind
  * @param {String} JsonElement this thing is *called* Json, but it expects a string
 **/
 klass.createFromJson = function createFromJson(jsonElement, ctx) {
-	// The value of $unwind should just be a field path.
-	if (jsonElement.constructor !== String) throw new Error("the " + klass.unwindName + " field path must be specified as a string; code 15981");
+	if (jsonElement.constructor !== String) {
+		throw new Error('the ' + klass.unwindName + ' field path must be specified as a string; code 15981');
+	}
+
+	var pathString = Expression.removeFieldPrefix(jsonElement),
+		unwind = new UnwindDocumentSource(ctx);
 
-	var pathString = Expression.removeFieldPrefix(jsonElement);
-	var unwind = new UnwindDocumentSource(ctx);
 	unwind.unwindPath(pathString);
 
 	return unwind;

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

@@ -3,41 +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, base = require("./VariadicExpressionT")(AddExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$add";
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
-
-/**
- * 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(klass.opName,base.parse);
+Expression.registerExpression("$add", base.parse);
+
+proto.getOpName = function getOpName(){
+	return "$add";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

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

@@ -7,71 +7,87 @@
  * returns false on the first operand that evaluates to false.
  *
  * @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("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+}, klass = AndExpression, base = require("./VariadicExpressionT")(AndExpression), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	ConstantExpression = require("./ConstantExpression"),
 	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$and";
-proto.getOpName = function getOpName() {
-	return klass.opName;
-};
-
-/**
- * Takes an array one or more values and returns true if all of the values in the array are true. Otherwise $and returns false.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var value = this.operands[i].evaluateInternal(vars);
-		if (!Value.coerceToBool(value)) return false;
-	}
-	return true;
-};
-
-proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() { return true; };
-
 proto.optimize = function optimize() {
-	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.evaluateInternal());
-	if (!last) return new ConstantExpression(false);
+	/*
+	 * Evaluate and coerce the last argument to a boolean.  If it's false,
+	 * then we can replace this entire expression.
+	 */
+	var last = Value.coerceToBool(constExpr.getValue());
+	if (!last)
+		return ConstantExpression.create(false);
 
-	// If we got here, the final operand was true, so we don't need it anymore.
-	// If there was only one other operand, we don't need the conjunction either.
-	// Note we still need to keep the promise that the result will be a boolean.
-	if (n == 2) return new CoerceToBoolExpression(andExpr.operands[0]);
+	/*
+	 * If we got here, the final operand was true, so we don't need it
+	 * anymore.  If there was only one other operand, we don't need the
+	 * conjunction either.  Note we still need to keep the promise that
+	 * the result will be a boolean.
+	 */
+	if (n === 2)
+		return CoerceToBoolExpression.create(andExpr.operands[0]);
 
-	//Remove the final "true" value, and return the new expression.
-	//CW TODO: Note that because of any implicit conversions, we may need to apply an implicit boolean conversion.
-	andExpr.operands.length = n - 1; //truncate the array
+	/*
+	 * Remove the final "true" value, and return the new expression.
+	 *
+	 * CW TODO:
+	 * Note that because of any implicit conversions, we may need to
+	 * apply an implicit boolean conversion.
+	 */
+	andExpr.operands.length = n - 1;
 	return expr;
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var n = this.operands.length;
+	for (var i = 0; i < n; ++i) {
+		var value = this.operands[i].evaluateInternal(vars);
+		if (!Value.coerceToBool(value))
+			return false;
+	}
+	return true;
+};
+
+Expression.registerExpression("$and", base.parse);
 
-//TODO: proto.toMatcherBson
+proto.getOpName = function getOpName() {
+	return "$and";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

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

@@ -1,8 +1,5 @@
 "use strict";
 
-var Value = require("../Value"),
-    Expression = require("./Expression");
-
 /**
  * Internal expression for constant values
  * @class ConstantExpression
@@ -14,7 +11,9 @@ var ConstantExpression = module.exports = function ConstantExpression(value){
     if (arguments.length !== 1) throw new Error(klass.name + ": args expected: value");
     this.value = value;
     base.call(this);
-}, klass = ConstantExpression, base = require("./FixedArityExpressionT")(klass,1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = ConstantExpression, base = require("./FixedArityExpressionT")(ConstantExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+var Expression = require("./Expression");
 
 klass.parse = function parse(exprElement, vps) {
 	return new ConstantExpression(exprElement);

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

@@ -15,7 +15,7 @@
  */
 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"),

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

@@ -6,8 +6,7 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-
+ */
 var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubClass, nArgs) {
 
 	var FixedArityExpression = function FixedArityExpression() {
@@ -15,12 +14,16 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		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 " +
@@ -28,9 +31,5 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		}
 	};
 
-	klass.parse = base.parse; 						// NOTE: Need to explicitly
-	klass.parseArguments = base.parseArguments;		// bubble static members in
-													// our inheritance chain
 	return FixedArityExpression;
 };
-

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

@@ -2,14 +2,14 @@
 
 /**
  * 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(klass.name + ": no args expected");
+var MultiplyExpression = module.exports = function MultiplyExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
 }, klass = MultiplyExpression, base = require("./VariadicExpressionT")(MultiplyExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 

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

@@ -3,9 +3,9 @@
 /**
  * Inherit from ExpressionVariadic or ExpressionFixedArity instead of directly from this class.
  * @class NaryBaseExpressionT
+ * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
- * @extends mungedb-aggregate.pipeline.expressions.NaryExpression
  * @constructor
  */
 var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass) {
@@ -15,6 +15,10 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		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);
@@ -23,8 +27,5 @@ var NaryBaseExpressionT = module.exports = function NaryBaseExpressionT(SubClass
 		return expr;
 	};
 
-	klass.parseArguments = base.parseArguments;		// NOTE: Need to explicitly
-													// bubble static members in
-													// our inheritance chain
 	return NaryBaseExpression;
 };

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

@@ -3,9 +3,9 @@
 /**
  * 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 NaryExpression = module.exports = function NaryExpression() {

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

@@ -2,67 +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");
+	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
 }, 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
-klass.opName = "$or";
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
-
-/**
- * Takes an array of one or more values and returns true if any of the values in the array are true. Otherwise $or returns false.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars){
-	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(klass.opName, base.parse);
+Expression.registerExpression("$or", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$or";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 6 - 5
lib/pipeline/expressions/VariadicExpressionT.js

@@ -3,11 +3,11 @@
 /**
  * A factory and base class for all expressions that are variadic (AKA they accept any number of arguments)
  * @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() {
@@ -15,8 +15,9 @@ var VariadicExpressionT = module.exports = function VariadicExpressionT(SubClass
 		base.call(this);
 	}, klass = VariadicExpression, base = require("./NaryBaseExpressionT")(SubClass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-	klass.parse = base.parse; 						// NOTE: Need to explicitly
-	klass.parseArguments = base.parseArguments;		// bubble static members in
-													// our inheritance chain
+	//NOTE: attach statics to emulate the C++ behavior
+	for (var propName in base)
+		klass[propName] = base[propName];
+
 	return VariadicExpression;
 };

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

@@ -24,6 +24,8 @@ module.exports = {
 	ObjectExpression: require("./ObjectExpression.js"),
 	OrExpression: require("./OrExpression.js"),
 	SecondExpression: require("./SecondExpression.js"),
+	SetIntersectionExpression: require("./SetIntersectionExpression.js"),
+	SizeExpression: require("./SizeExpression.js"),
 	StrcasecmpExpression: require("./StrcasecmpExpression.js"),
 	SubstrExpression: require("./SubstrExpression.js"),
 	SubtractExpression: require("./SubtractExpression.js"),

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

@@ -1,6 +1,6 @@
 "use strict";
-var LeafMatchExpression = require('./LeafMatchExpression.js');
-var Value = require('../Value');
+var LeafMatchExpression = require("./LeafMatchExpression.js");
+var Value = require("../Value");
 
 /**
  * ComparisonMatchExpression
@@ -27,31 +27,31 @@ proto._rhs = undefined;
 proto.debugString = function debugString(level) {
 	var retStr = this._debugAddSpace(level) + this.path() + " ";
 	switch (this._matchType) {
-		case 'LT':
-			retStr += '$lt';
+		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() : '?');
+	retStr += (this._rhs ? this._rhs.toString() : "?");
 	if (this.getTag()) {
 		retStr += this.getTag().debugString();
 	}
-	return retStr + '\n';
+	return retStr + "\n";
 };
 
 /**
@@ -96,11 +96,11 @@ proto.getRHS = function getRHS() {
  */
 proto.init = function init(path,rhs) {
 	this._rhs = rhs;
-	if ((rhs instanceof Object && Object.keys(rhs).length === 0)) return {'code':'BAD_VALUE', 'description':'Need a real operand'};
+	if ((rhs instanceof Object && Object.keys(rhs).length === 0)) return {"code":"BAD_VALUE", "description":"Need a real operand"};
 
-	if (rhs === undefined) return {'code':'BAD_VALUE', 'desc':'Cannot compare to undefined'};
+	if (rhs === undefined) return {"code":"BAD_VALUE", "desc":"Cannot compare to undefined"};
 	if (!(this._matchType in {"LT":1, "LTE":1, "EQ":1, "GT":1, "GTE":1})) {
-		return {'code':'BAD_VALUE', 'description':'Bad match type for ComparisonMatchExpression'};
+		return {"code":"BAD_VALUE", "description":"Bad match type for ComparisonMatchExpression"};
 	}
 	return this.initPath(path);
 };
@@ -118,7 +118,7 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 			return ["EQ","LTE","GTE"].indexOf(this._matchType) != -1;
 		}
 
-		if (['MaxKey','MinKey'].indexOf(Value.getType(this._rhs)) != -1) {
+		if (["MaxKey","MinKey"].indexOf(Value.getType(this._rhs)) != -1) {
 			return this._matchType !== "EQ";
 		}
 		return false;
@@ -128,17 +128,16 @@ proto.matchesSingleElement = function matchesSingleElement(e) {
 
 	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;
 };

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

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

+ 2 - 1
package.json

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

+ 2 - 2
test/lib/pipeline/documentSources/MatchDocumentSource.js

@@ -364,7 +364,7 @@ module.exports = {
 		"#isTextQuery()": {
 
 			"should return true when $text operator is first stage in pipeline": function () {
-				var query = {$text:'textQuery'}
+				var query = {$text:'textQuery'};
 				assert.ok(MatchDocumentSource.isTextQuery(query)); // true
 			},
 
@@ -374,7 +374,7 @@ module.exports = {
 			},
 
 			"should return false when $text operator is not in pipeline": function () {
-				var query = {$notText:'textQuery'}
+				var query = {$notText:'textQuery'};
 				assert.ok(!MatchDocumentSource.isTextQuery(query)); // false
 			}
 

+ 137 - 16
test/lib/pipeline/documentSources/RedactDocumentSource.js

@@ -4,23 +4,28 @@ var assert = require("assert"),
 	DocumentSource = require("../../../../lib/pipeline/documentSources/DocumentSource"),
 	RedactDocumentSource = require("../../../../lib/pipeline/documentSources/RedactDocumentSource"),
 	CursorDocumentSource = require("../../../../lib/pipeline/documentSources/CursorDocumentSource"),
-	Cursor = require("../../../../lib/Cursor");
-
-var exampleRedact = {$cond: [
-	{$gt:[3, 0]},
-	"$$DESCEND",
-	"$$PRUNE"]
+	Cursor = require("../../../../lib/Cursor"),
+	Expressions = require("../../../../lib/pipeline/expressions");
+
+var exampleRedact = {$cond:{
+	if:{$gt:[0,4]},
+	then:"$$DESCEND",
+	else:"$$PRUNE"
+}};
+
+var createCursorDocumentSource = function createCursorDocumentSource (input) {
+	if (!input || input.constructor !== Array) throw new Error('invalid');
+	var cwc = new CursorDocumentSource.CursorWithContext();
+	cwc._cursor = new Cursor(input);
+	return new CursorDocumentSource(cwc);
 };
 
-////////////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
-//////////////////////////////////// BUSTED ////////////////////////////////////
-//           This DocumentSource is busted without new Expressions            //
-////////////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
+var createRedactDocumentSource = function createRedactDocumentSource (src, expression) {
+	var rds = RedactDocumentSource.createFromJson(expression);
+	rds.setSource(src);
+	return rds;
+};
 
-//TESTS
 module.exports = {
 
 	"RedactDocumentSource": {
@@ -83,6 +88,7 @@ module.exports = {
 				var rds = new RedactDocumentSource();
 				assert.throws(rds.getNext.bind(rds));
 			},
+
 		},
 
 		"#optimize()": {
@@ -109,9 +115,124 @@ module.exports = {
 			}
 
 		},
+
+		"#redact()": {
+
+			"should redact subsection where tag does not match": function (done) {
+				var cds = createCursorDocumentSource([{
+					_id: 1,
+					title: "123 Department Report",
+					tags: ["G", "STLW"],
+					year: 2014,
+					subsections: [
+						{
+							subtitle: "Section 1: Overview",
+							tags: ["SI", "G"],
+							content: "Section 1: This is the content of section 1."
+						},
+						{
+							subtitle: "Section 2: Analysis",
+							tags: ["STLW"],
+							content: "Section 2: This is the content of section 2."
+						},
+						{
+							subtitle: "Section 3: Budgeting",
+							tags: ["TK"],
+							content: {
+								text: "Section 3: This is the content of section3.",
+								tags: ["HCS"]
+							}
+						}
+					]
+				}]);
+
+				var expression = {$cond:{
+					if:{$gt: [{$size: {$setIntersection: ["$tags", [ "STLW", "G" ]]}},0]},
+					then:"$$DESCEND",
+					else:"$$PRUNE"
+				}};
+
+				var rds = createRedactDocumentSource(cds, expression);
+
+				var result = {
+					"_id": 1,
+					"title": "123 Department Report",
+					"tags": ["G", "STLW"],
+					"year": 2014,
+					"subsections": [{
+						"subtitle": "Section 1: Overview",
+						"tags": ["SI", "G"],
+						"content": "Section 1: This is the content of section 1."
+					}, {
+						"subtitle": "Section 2: Analysis",
+						"tags": ["STLW"],
+						"content": "Section 2: This is the content of section 2."
+					}]
+				};
+
+				rds.getNext(function (err, actual) {
+					assert.deepEqual(actual, result);
+					done();
+				});
+
+			},
+
+			"should redact an entire subsection based on a defined access level": function (done) {
+				var cds = createCursorDocumentSource([{
+					_id: 1,
+					level: 1,
+					acct_id: "xyz123",
+					cc: {
+						level: 5,
+						type: "yy",
+						exp_date: new Date("2015-11-01"),
+						billing_addr: {
+							level: 5,
+							addr1: "123 ABC Street",
+							city: "Some City"
+						},
+						shipping_addr: [
+							{
+								level: 3,
+								addr1: "987 XYZ Ave",
+								city: "Some City"
+							},
+							{
+								level: 3,
+								addr1: "PO Box 0123",
+								city: "Some City"
+							}
+						]
+					},
+					status: "A"
+				}]);
+
+				var expression = {$cond:{
+					if:{$eq:["$level",5]},
+					then:"$$PRUNE",
+					else:"$$DESCEND"
+				}};
+
+				var rds = createRedactDocumentSource(cds, expression);
+
+				var result = {
+					_id:1,
+					level:1,
+					acct_id:"xyz123",
+					status:"A"
+				};
+
+				rds.getNext(function (err, actual) {
+					assert.deepEqual(actual, result);
+					done();
+				});
+
+			}
+
+		}
+
 	}
 
 };
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).grep(process.env.MOCHA_GREP || '').run(process.exit);
-
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).grep(process.env.MOCHA_GREP || '').run(process.exit);

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

@@ -1,132 +1,248 @@
 "use strict";
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var expr = new AddExpression();
+			this.populateOperands(expr);
+			var expectedResult = this.expectedResult instanceof Function ? this.expectedResult() : this.expectedResult;
+			if (expectedResult instanceof Date) //NOTE: DEVIATION FROM MONGO: special case for Date
+				return assert.strictEqual(Date(expectedResult), Date(expr.evaluate({})));
+			assert.strictEqual(expectedResult, expr.evaluate({}));
+		};
+		return klass;
+	})(),
+	SingleOperandBase = (function() {
+		var klass = function SingleOperandBase() {
+			base.apply(this, arguments);
+		}, base = ExpectedResultBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.populateOperands = function(expr) {
+			var operand = this.operand instanceof Function ? this.operand() : this.operand;
+			expr.addOperand(ConstantExpression.create(operand));
+		};
+		proto.expectedResult = function() {
+			var operand = this.operand instanceof Function ? this.operand() : this.operand;
+			return operand;
+		};
+		return klass;
+	})(),
+	TwoOperandBase = (function() {
+		var klass = function TwoOperandBase() {
+			base.apply(this, arguments);
+		}, base = ExpectedResultBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			base.prototype.run.call(this);
+            // Now add the operands in the reverse direction.
+            this._reverse = true;
+			base.prototype.run.call(this);
+		};
+		proto.populateOperands = function(expr) {
+			var operand1 = this.operand1 instanceof Function ? this.operand1() : this.operand1,
+				operand2 = this.operand1 instanceof Function ? this.operand2() : this.operand2;
+			expr.addOperand(ConstantExpression.create(this._reverse ? operand2 : operand1));
+			expr.addOperand(ConstantExpression.create(this._reverse ? operand1 : operand2));
+		};
+		proto._reverse = false;
+		return klass;
+	})();
+
+exports.AddExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new AddExpression() instanceof AddExpression);
+			assert(new AddExpression() instanceof Expression);
+		},
 
-//TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
-module.exports = {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AddExpression("bad stuff");
+			});
+		},
 
-	"AddExpression": {
+	},
 
-		"constructor()": {
+	"#getOpName()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AddExpression();
-				});
-			},
+		"should return the correct op name; $add": function() {
+			assert.equal(new AddExpression().getOpName(), "$add");
+		}
+	},
+
+	"#evaluate()": {
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new AddExpression(1);
-				});
-			}
+		"should return the operand if null document is given": function testNullDocument() {
+			/** $add with a NULL Document pointer, as called by ExpressionNary::optimize(). */
+			var expr = new AddExpression();
+			expr.addOperand(ConstantExpression.create(2));
+			assert.strictEqual(expr.evaluate({}), 2);
 		},
 
-		"#getOpName()": {
+		"should return 0 if no operands were given": function testNoOperands() {
+			/** $add without operands. */
+			var expr = new AddExpression();
+			assert.strictEqual(expr.evaluate({}), 0);
+		},
 
-			"should return the correct op name; $add": function testOpName(){
-				assert.equal(new AddExpression().getOpName(), "$add");
-			}
+		"should throw Error if a String operand was given": function testString() {
+			/** String type unsupported. */
+			var expr = new AddExpression();
+			expr.addOperand(ConstantExpression.create("a"));
+			assert.throws(function () {
+				expr.evaluate({});
+			});
+		},
 
+		"should throw Error if a Boolean operand was given": function testBool() {
+			var expr = new AddExpression();
+			expr.addOperand(ConstantExpression.create(true));
+			assert.throws(function () {
+				expr.evaluate({});
+			});
 		},
 
-		"#evaluateInternal()": {
+		"w/ 1 operand": {
 
-			"should return the operand if null document is given": function nullDocument(){
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(2));
-				assert.equal(expr.evaluateInternal(null), 2);
+			"should pass through a single int": function testInt() {
+        		/** Single int argument. */
+				new SingleOperandBase({
+					operand: 1,
+				}).run();
 			},
 
-			"should return 0 if no operands were given": function noOperands(){
-				var expr = new AddExpression();
-				assert.equal(expr.evaluateInternal({}), 0);
-			},
+			//SKIPPED: Long -- would be same as Int above
 
-			"should throw Error if a Date operand was given": function date(){
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(new Date()));
-				assert.throws(function(){
-					expr.evaluateInternal({});
-				});
+			"should pass through a single float": function testDouble() {
+				/** Single double argument. */
+				new SingleOperandBase({
+					operand: 99.99,
+				}).run();
 			},
 
-			"should throw Error if a String operand was given": function string(){
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(""));
-				assert.throws(function(){
-					expr.evaluateInternal({});
-				});
+			"should pass through a single date": function testDate() {
+				/** Single Date argument. */
+				new SingleOperandBase({
+					operand: new Date(12345),
+				}).run();
 			},
 
-			"should throw Error if a Boolean operand was given": function bool() {
-				var expr = new AddExpression();
-				expr.addOperand(new ConstantExpression(true));
-				assert.throws(function() {
-					expr.evaluateInternal({});
-				});
+			"should pass through a single null": function testNull() {
+				/** Single null argument. */
+				new SingleOperandBase({
+					operand: null,
+				}).run();
 			},
 
-			"should pass thru a single number": function number() {
-				var expr = new AddExpression(),
-					input = 123,
-					expected = 123;
-				expr.addOperand(new ConstantExpression(input));
-				assert.equal(expr.evaluateInternal({}), expected);
+			"should pass through a single undefined": function testUndefined() {
+				/** Single undefined argument. */
+				new SingleOperandBase({
+					operand: undefined,
+					expectedResult: null,
+				}).run();
 			},
 
-			"should pass thru a single null": function nullSupport() {
-				var expr = new AddExpression(),
-					input = null,
-					expected = 0;
-				expr.addOperand(new ConstantExpression(input));
-				assert.equal(expr.evaluateInternal({}), expected);
+		},
+
+		"w/ 2 operands": {
+
+			"should add two ints": function testIntInt() {
+				/** Add two ints. */
+				new TwoOperandBase({
+					operand1: 1,
+					operand2: 5,
+					expectedResult: 6,
+				}).run();
 			},
 
-			"should pass thru a single undefined": function undefinedSupport() {
-				var expr = new AddExpression(),
-					input,
-					expected = 0;
-				expr.addOperand(new ConstantExpression(input));
-				assert.equal(expr.evaluateInternal({}), expected);
+			//SKIPPED: IntIntNoOverflow
+
+			//SKIPPED: IntLong
+
+			//SKIPPED: IntLongOverflow
+
+			"should add int and double": function testIntDouble() {
+				/** Adding an int and a double produces a double. */
+				new TwoOperandBase({
+					operand1: 9,
+					operand2: 1.1,
+					expectedResult: 10.1,
+				}).run();
 			},
 
-			"should add two numbers": function numbers() {
-				var expr = new AddExpression(),
-					inputs = [1, 5],
-					expected = 6;
-				inputs.forEach(function(input) {
-					expr.addOperand(new ConstantExpression(input));
-				});
-				assert.equal(expr.evaluateInternal({}), expected);
+			"should add int and date": function testIntDate() {
+				/** Adding an int and a Date produces a Date. */
+				new TwoOperandBase({
+					operand1: 6,
+					operand2: new Date(123450),
+					expectedResult: new Date(123456),
+				}).run();
 			},
 
-			"should add a number and a null": function numberAndNull() {
-				var expr = new AddExpression(),
-					inputs = [1, null],
-					expected = 1;
-				inputs.forEach(function(input) {
-					expr.addOperand(new ConstantExpression(input));
-				});
-				assert.equal(expr.evaluateInternal({}), expected);
+			//SKIPPED: LongDouble
+
+			//SKIPPED: LongDoubleNoOverflow
+
+			"should add int and null": function testIntNull() {
+				/** Adding an int and null. */
+				new TwoOperandBase({
+					operand1: 1,
+					operand2: null,
+					expectedResult: null,
+				}).run();
 			},
 
-			"should add a number and an undefined": function numberAndUndefined() {
-				var expr = new AddExpression(),
-					inputs = [1, undefined],
-					expected = 1;
-				inputs.forEach(function(input) {
-					expr.addOperand(new ConstantExpression(input));
-				});
-				assert.equal(expr.evaluateInternal({}), expected);
-			}
+			"should add long and undefined": function testLongUndefined() {
+				/** Adding a long and undefined. */
+				new TwoOperandBase({
+					operand1: 5e11,
+					operand2: undefined,
+					expectedResult: null,
+				}).run();
+			},
 
 		}
 
-	}
+	},
+
+	"optimize": {
+
+		"should understand a single number": function() {
+			var vps = new VariablesParseState(new VariablesIdGenerator()),
+				expr = Expression.parseOperand({$add:[123]}, vps).optimize();
+			assert.strictEqual(expr.operands.length, 0, "should optimize operands away");
+			assert(expr instanceof ConstantExpression);
+			assert.strictEqual(expr.evaluate(), 123);
+		},
+
+		"should optimize strings of numbers without regard to their order": function() {
+			var vps = new VariablesParseState(new VariablesIdGenerator()),
+				expr = Expression.parseOperand({$add:[1,2,3,'$a',4,5,6]}, vps).optimize();
+			assert.strictEqual(expr.operands.length, 2, "should optimize operands away");
+			assert(expr.operands[0] instanceof FieldPathExpression);
+			assert(expr.operands[1] instanceof ConstantExpression);
+			assert.strictEqual(expr.operands[1].evaluate(), 1 + 2 + 3 + 4 + 5 + 6);
+		},
+
+	},
 
 };
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

@@ -1,213 +1,286 @@
 "use strict";
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
-	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
-	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var expectedResult = this.expectedResult instanceof Function ? this.expectedResult() : this.expectedResult;
+			assert.strictEqual(expectedResult, expr.evaluate({a:1}));
+			var optimized = expr.optimize();
+			assert.strictEqual(expectedResult, optimized.evaluate({a:1}));
+		};
+		return klass;
+	})(),
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var optimized = expr.optimize(),
+				expectedOptimized = this.expectedOptimized instanceof Function ? this.expectedOptimized() : this.expectedOptimized;
+			assert.deepEqual(expectedOptimized, expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	NoOptimizeBase = (function() {
+		var klass = function NoOptimizeBase() {
+			base.apply(this, arguments);
+		}, base = OptimizeBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function() {
+			return constify(this.spec instanceof Function ? this.spec() : this.spec);
+		};
+		return klass;
+	})();
+
+exports.AndExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new AndExpression() instanceof AndExpression);
+			assert(new AndExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AndExpression("bad stuff");
+			});
+		},
 
+	},
 
-module.exports = {
+	"#getOpName()": {
 
-	"AndExpression": {
+		"should return the correct op name; $and": function() {
+			assert.equal(new AndExpression().getOpName(), "$and");
+		}
 
-		beforeEach: function() {
-			this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
+
+	"#evaluate()": {
+
+		"should return true if no operands": function testNoOperands() {
+			/** $and without operands. */
+			new ExpectedResultBase({
+				spec: {$and:[]},
+				expectedResult: true,
+			}).run();
 		},
 
-		"constructor()": {
+		"should return true if given true": function testTrue() {
+			/** $and passed 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AndExpression();
-				});
-			},
+		"should return false if given false": function testFalse() {
+			/** $and passed 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new AndExpression(1);
-				});
-			}
+		"should return true if given true and true": function testTrueTrue() {
+			/** $and passed 'true', 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, true]},
+				expectedResult: true,
+			}).run();
+		},
 
+		"should return false if given true and false": function testTrueFalse() {
+			/** $and passed 'true', 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, false]},
+				expectedResult: false,
+			}).run();
 		},
 
-		"#getOpName()": {
+		"should return false if given false and true": function testFalseTrue() {
+			/** $and passed 'false', 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[false, true]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return the correct op name; $and": function testOpName(){
-				assert.equal(new AndExpression().getOpName(), "$and");
-			}
+		"should return false if given false and false": function testFalseFalse() {
+			/** $and passed 'false', 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[false, false]},
+				expectedResult: false,
+			}).run();
+		},
 
+		"should return true if given true and true and true": function testTrueTrueTrue() {
+			/** $and passed 'true', 'true', 'true'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, true, true]},
+				expectedResult: true,
+			}).run();
 		},
 
+		"should return false if given true and true and false": function testTrueTrueFalse() {
+			/** $and passed 'true', 'true', 'false'. */
+			new ExpectedResultBase({
+				spec: {$and:[true, true, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-		"#evaluate()": {
+		"should return false if given 0 and 1": function testZeroOne() {
+			/** $and passed '0', '1'. */
+			new ExpectedResultBase({
+				spec: {$and:[0, 1]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return true if no operands were given; {$and:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$and:[]},this.vps).evaluate(), true);
-			},
+		"should return true if given 1 and 2": function testOneTwo() {
+			/** $and passed '1', '2'. */
+			new ExpectedResultBase({
+				spec: {$and:[1, 2]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return true if operands is one true; {$and:[true]}": function testTrue(){
-				assert.equal(Expression.parseOperand({$and:[true]},this.vps).evaluate(), true);
-			},
+		"should return true if given a field path to a truthy value": function testFieldPath() {
+			/** $and passed a field path. */
+			new ExpectedResultBase({
+				spec: {$and:["$a"]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operands is one false; {$and:[false]}": function testFalse(){
-				assert.equal(Expression.parseOperand({$and:[false]},this.vps).evaluate(), false);
-			},
+	},
 
-			"should return true if operands are true and true; {$and:[true,true]}": function testTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true]},this.vps).evaluate(), true);
-			},
+	"#optimize()": {
 
-			"should return false if operands are true and false; {$and:[true,false]}": function testTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,false]},this.vps).evaluate(), false);
-			},
+		"should optimize a constant expression": function testOptimizeConstantExpression() {
+			/** A constant expression is optimized to a constant. */
+			new OptimizeBase({
+				spec: {$and:[1]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should return false if operands are false and true; {$and:[false,true]}": function testFalseTrue(){
-				assert.equal(Expression.parseOperand({$and:[false,true]},this.vps).evaluate(), false);
-			},
+		"should not optimize a non constant": function testNonConstant() {
+			/** A non constant expression is not optimized. */
+			new NoOptimizeBase({
+				spec: {$and:["$a"]},
+			}).run();
+		},
 
-			"should return false if operands are false and false; {$and:[false,false]}": function testFalseFalse(){
-				assert.equal(Expression.parseOperand({$and:[false,false]},this.vps).evaluate(), false);
-			},
+		"should optimize truthy constant and truthy expression": function testConstantNonConstantTrue() {
+			/** An expression beginning with a single constant is optimized. */
+			new OptimizeBase({
+				spec: {$and:[1,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+			// note: using $and as serialization of ExpressionCoerceToBool rather than ExpressionAnd
+		},
 
-			"should return true if operands are true, true, and true; {$and:[true,true,true]}": function testTrueTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true,true]},this.vps).evaluate(), true);
-			},
+		"should optimize falsy constant and truthy expression": function testConstantNonConstantFalse() {
+			new OptimizeBase({
+				spec: {$and:[0,"$a"]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
-			"should return false if operands are true, true, and false; {$and:[true,true,false]}": function testTrueTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,true,false]},this.vps).evaluate(), false);
-			},
+		"should optimize truthy expression and truthy constant": function testNonConstantOne() {
+			/** An expression with a field path and '1'. */
+			new OptimizeBase({
+				spec: {$and:["$a",1]},
+				expectedOptimized: {$and:["$a"]}
+			}).run();
+		},
 
-			"should return false if operands are 0 and 1; {$and:[0,1]}": function testZeroOne(){
-				assert.equal(Expression.parseOperand({$and:[0,1]},this.vps).evaluate(), false);
-			},
+		"should optimize truthy expression and falsy constant": function testNonConstantZero() {
+			/** An expression with a field path and '0'. */
+			new OptimizeBase({
+				spec: {$and:["$a",0]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
-			"should return false if operands are 1 and 2; {$and:[1,2]}": function testOneTwo(){
-				assert.equal(Expression.parseOperand({$and:[1,2]},this.vps).evaluate(), true);
-			},
+		"should optimize truthy expression, falsy expression, and truthy constant": function testNonConstantNonConstantOne() {
+			/** An expression with two field paths and '1'. */
+			new OptimizeBase({
+				spec: {$and:["$a","$b",1]},
+				expectedOptimized: {$and:["$a","$b"]}
+			}).run();
+		},
 
-			"should return true if operand is a path String to a truthy value; {$and:['$a']}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$and:['$a']},this.vps).evaluate({a:1}), true);
-			}
+		"should optimize truthy expression, falsy expression, and falsy constant": function testNonConstantNonConstantZero() {
+			/** An expression with two field paths and '0'. */
+			new OptimizeBase({
+				spec: {$and:["$a","$b",0]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
+		"should optimize to false if [0,1,'$a']": function testZeroOneNonConstant() {
+			/** An expression with '0', '1', and a field path. */
+			new OptimizeBase({
+				spec: {$and:[0,1,"$a"]},
+				expectedOptimized: {$const:false},
+			}).run();
 		},
 
-		"#optimize()": {
+		"should optimize to {$and:'$a'} if [1,1,'$a']": function testOneOneNonConstant() {
+			/** An expression with '1', '1', and a field path. */
+			new OptimizeBase({
+				spec: {$and:[1,1,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+		},
 
-			"should optimize a constant expression to a constant; {$and:[1]} == true": function testOptimizeConstantExpression(){
-				var a = Expression.parseOperand({$and:[1]}, this.vps).optimize();
-				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
-				assert.equal(a.evaluateInternal(), true);
-			},
+		"should optimize away nested truthy $and expressions": function testNested() {
+			/** Nested $and expressions. */
+			new OptimizeBase({
+				spec: {$and:[1, {$and:[1]}, "$a", "$b"]},
+				expectedOptimized: {$and:["$a","$b"]},
+			}).run();
+		},
 
-			"should not optimize a non-constant expression; {$and:['$a']}": function testNonConstant(){
-				var a = Expression.parseOperand({$and:['$a']}, this.vps).optimize();
-				assert.equal(a.operands[0]._fieldPath.fieldNames.length, 2);
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
-			},
-
-			"should not optimize an expression ending with a non-constant. {$and:[1,'$a']};": function testConstantNonConstant(){
-				var a = Expression.parseOperand({$and:[1,'$a']}, this.vps).optimize();
-				assert(a instanceof CoerceToBoolExpression);
-				assert(a.expression instanceof FieldPathExpression);
-
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-
-			"should optimize an expression with a path and a '1'; {$and:['$a',1]}": function testNonConstantOne(){
-				var a = Expression.parseOperand({$and:['$a', 1]}, this.vps).optimize();
-				// The 1 should be removed as it is redundant.
-				assert(a instanceof CoerceToBoolExpression, "The result should be forced to a boolean");
-
-				// This is the '$a' which cannot be optimized.
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-
-			"should optimize an expression with a field path and a '0'; {$and:['$a',0]}": function testNonConstantZero(){
-				var a = Expression.parseOperand({$and:['$a',0]}, this.vps).optimize();
-				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
-				assert.equal(a.evaluateInternal(), false, "The 0 operand should have been converted to false");
-			},
-
-			"should optimize an expression with two field paths and '1'; {$and:['$a','$b',1]}": function testNonConstantNonConstantOne(){
-				var a = Expression.parseOperand({$and:['$a', '$b', 1]}, this.vps).optimize();
-				assert.equal(a.operands.length, 2, "Two operands should remain.");
-
-				// This is the '$a' which cannot be optimized.
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames.length, 2);
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
-				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
-
-				// This is the '$b' which cannot be optimized.
-				assert.deepEqual(a.operands[1]._fieldPath.fieldNames.length, 2);
-				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[0], "CURRENT");
-				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[1], "b");
-			},
-
-			"should optimize an expression with two field paths and '0'; {$and:['$a','$b',0]}": function testNonConstantNonConstantZero(){
-				var a = Expression.parseOperand({$and:['$a', '$b', 0]}, this.vps).optimize();
-				assert(a instanceof ConstantExpression, "With that trailing false, we know the result...");
-				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
-				assert.equal(a.evaluateInternal(), false);
-			},
-
-			"should optimize an expression with '0', '1', and a field path; {$and:[0,1,'$a']}": function testZeroOneNonConstant(){
-				var a = Expression.parseOperand({$and:[0,1,'$a']}, this.vps).optimize();
-				assert(a instanceof ConstantExpression);
-				assert.equal(a.evaluateInternal(), false);
-			},
-
-			"should optimize an expression with '1', '1', and a field path; {$and:[1,1,'$a']}": function testOneOneNonConstant(){
-				var a = Expression.parseOperand({$and:[1,1,'$a']}, this.vps).optimize();
-				assert(a instanceof CoerceToBoolExpression);
-				assert(a.expression instanceof FieldPathExpression);
-
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-
-			"should optimize nested $and expressions properly and optimize out values evaluating to true; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				var a = Expression.parseOperand({$and:[1,{$and:[1]},'$a','$b']}, this.vps).optimize();
-				assert.equal(a.operands.length, 2)
-				assert(a.operands[0] instanceof FieldPathExpression);
-				assert(a.operands[1] instanceof FieldPathExpression);
-			},
-
-			"should optimize nested $and expressions containing a nested value evaluating to false; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				//assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize().toJSON(true), {$const:false});
-				var a = Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize();
-				assert(a instanceof ConstantExpression);
-				assert.equal(a.evaluateInternal(), false);
-			},
-
-			"should optimize when the constants are on the right of the operand list. The rightmost is true": function(){
-				// 1, "x", and 1 are all true.  They should be optimized away.
-				var a = Expression.parseOperand({$and:['$a', 1, "x", 1]}, this.vps).optimize();
-				assert(a instanceof CoerceToBoolExpression);
-				assert(a.expression instanceof FieldPathExpression);
-
-				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
-				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
-				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
-			},
-			"should optimize when the constants are on the right of the operand list. The rightmost is false": function(){
-				// 1, "x", and 1 are all true.  They should be optimized away.
-				var a = Expression.parseOperand({$and:['$a', 1, "x", 0]}, this.vps).optimize();
-				assert(a instanceof ConstantExpression, "The rightmost false kills it all");
-				assert.equal(a.evaluateInternal(), false);
-			}
-		}
+		"should optimize to false if nested falsey $and expressions": function testNestedZero() {
+			/** Nested $and expressions containing a nested value evaluating to false. */
+			new OptimizeBase({
+				spec: {$and:[1, {$and:[ {$and:[0]} ]}, "$a", "$b"]},
+				expectedOptimized: {$const:false},
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 1 - 1
test/lib/pipeline/expressions/ConcatExpression_test.js

@@ -80,7 +80,7 @@ exports.ConcatExpression = {
 		},
 
 		"should throw if an operand is a boolean": function() {
-			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps)
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
 			assert.throws(function() {
 				expr.evaluate({a:true});
 			});

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

@@ -1,143 +1,287 @@
 "use strict";
 var assert = require("assert"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	OrExpression = require("../../../../lib/pipeline/expressions/OrExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var expectedResult = this.expectedResult instanceof Function ? this.expectedResult() : this.expectedResult;
+			assert.strictEqual(expectedResult, expr.evaluate({a:1}));
+			var optimized = expr.optimize();
+			assert.strictEqual(expectedResult, optimized.evaluate({a:1}));
+		};
+		return klass;
+	})(),
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			var specElement = this.spec instanceof Function ? this.spec() : this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			var optimized = expr.optimize(),
+				expectedOptimized = this.expectedOptimized instanceof Function ? this.expectedOptimized() : this.expectedOptimized;
+			assert.deepEqual(expectedOptimized, expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	NoOptimizeBase = (function() {
+		var klass = function NoOptimizeBase() {
+			base.apply(this, arguments);
+		}, base = OptimizeBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function() {
+			return constify(this.spec instanceof Function ? this.spec() : this.spec);
+		};
+		return klass;
+	})();
+
+exports.OrExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new OrExpression() instanceof OrExpression);
+			assert(new OrExpression() instanceof Expression);
+		},
 
+		"should error if given args": function() {
+			assert.throws(function() {
+				new OrExpression("bad stuff");
+			});
+		},
 
-module.exports = {
+	},
 
-	"OrExpression": {
+	"#getOpName()": {
 
-		"constructor()": {
+		"should return the correct op name; $or": function(){
+			assert.equal(new OrExpression().getOpName(), "$or");
+		}
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new OrExpression();
-				});
-			},
+	},
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new OrExpression(1);
-				});
-			}
+	"#evaluate()": {
 
+		"should return false if no operands": function testNoOperands(){
+			/** $or without operands. */
+			new ExpectedResultBase({
+				spec: {$or:[]},
+				expectedResult: false,
+			}).run();
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $or": function testOpName(){
-				assert.equal(new OrExpression().getOpName(), "$or");
-			}
-
+		"should return true if given true": function testTrue(){
+			/** $or passed 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[true]},
+				expectedResult: true,
+			}).run();
 		},
 
-		"#getFactory()": {
-
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.equal(new OrExpression().getFactory(), OrExpression);
-			}
-
+		"should return false if given false": function testFalse(){
+			/** $or passed 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[false]},
+				expectedResult: false,
+			}).run();
 		},
 
-		"#evaluateInternalInternal()": {
-
-			"should return false if no operors were given; {$or:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$or:[]}).evaluateInternal(), false);
-			},
-
-			"should return true if operors is one true; {$or:[true]}": function testTrue(){
-				assert.equal(Expression.parseOperand({$or:[true]}).evaluateInternal(), true);
-			},
+		"should return true if given true and true": function testTrueTrue(){
+			/** $or passed 'true', 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[true, true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors is one false; {$or:[false]}": function testFalse(){
-				assert.equal(Expression.parseOperand({$or:[false]}).evaluateInternal(), false);
-			},
+		"should return true if given true and false": function testTrueFalse(){
+			/** $or passed 'true', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[true, false]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return true if operors are true or true; {$or:[true,true]}": function testTrueTrue(){
-				assert.equal(Expression.parseOperand({$or:[true,true]}).evaluateInternal(), true);
-			},
+		"should return true if given false and true": function testFalseTrue(){
+			/** $or passed 'false', 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return true if operors are true or false; {$or:[true,false]}": function testTrueFalse(){
-				assert.equal(Expression.parseOperand({$or:[true,false]}).evaluateInternal(), true);
-			},
+		"should return false if given false and false": function testFalseFalse(){
+			/** $or passed 'false', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return true if operors are false or true; {$or:[false,true]}": function testFalseTrue(){
-				assert.equal(Expression.parseOperand({$or:[false,true]}).evaluateInternal(), true);
-			},
+		"should return false if given false and false and false": function testFalseFalseFalse(){
+			/** $or passed 'false', 'false', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, false, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return false if operors are false or false; {$or:[false,false]}": function testFalseFalse(){
-				assert.equal(Expression.parseOperand({$or:[false,false]}).evaluateInternal(), false);
-			},
+		"should return true if given false and false and true": function testFalseFalseTrue(){
+			/** $or passed 'false', 'false', 'true'. */
+			new ExpectedResultBase({
+				spec: {$or:[false, false, true]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors are false, false, or false; {$or:[false,false,false]}": function testFalseFalseFalse(){
-				assert.equal(Expression.parseOperand({$or:[false,false,false]}).evaluateInternal(), false);
-			},
+		"should return true if given 0 and 1": function testZeroOne(){
+			/** $or passed '0', '1'. */
+			new ExpectedResultBase({
+				spec: {$or:[0, 1]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors are false, false, or false; {$or:[false,false,true]}": function testFalseFalseTrue(){
-				assert.equal(Expression.parseOperand({$or:[false,false,true]}).evaluateInternal(), true);
-			},
+		"should return false if given 0 and false": function testZeroFalse(){
+			/** $or passed '0', 'false'. */
+			new ExpectedResultBase({
+				spec: {$or:[0, false]},
+				expectedResult: false,
+			}).run();
+		},
 
-			"should return true if operors are 0 or 1; {$or:[0,1]}": function testZeroOne(){
-				assert.equal(Expression.parseOperand({$or:[0,1]}).evaluateInternal(), true);
-			},
+		"should return true if given a field path to a truthy value": function testFieldPath(){
+			/** $or passed a field path. */
+			new ExpectedResultBase({
+				spec: {$or:["$a"]},
+				expectedResult: true,
+			}).run();
+		},
 
-			"should return false if operors are 0 or false; {$or:[0,false]}": function testZeroFalse(){
-				assert.equal(Expression.parseOperand({$or:[0,false]}).evaluateInternal(), false);
-			},
+	},
 
-			"should return true if operor is a path String to a truthy value; {$or:['$a']}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$or:['$a']}).evaluateInternal({a:1}), true);
-			}
+	"#optimize()": {
 
+		"should optimize a constant expression": function testOptimizeConstantExpression() {
+			/** A constant expression is optimized to a constant. */
+			new OptimizeBase({
+				spec: {$or:[1]},
+				expectedOptimized: {$const:true},
+			}).run();
 		},
 
-		"#optimize()": {
-
-			"should optimize a constant expression to a constant; {$or:[1]} == true": function testOptimizeConstantExpression(){
-				assert.deepEqual(Expression.parseOperand({$or:[1]}).optimize().toJSON(true), {$const:true});
-			},
+		"should not optimize a non constant": function testNonConstant() {
+			/** A non constant expression is not optimized. */
+			new NoOptimizeBase({
+				spec: {$or:["$a"]},
+			}).run();
+		},
 
-			"should not optimize a non-constant expression; {$or:['$a']}; SERVER-6192": function testNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a']}).optimize().toJSON(), {$or:['$a']});
-			},
+		"should optimize truthy constant and truthy expression": function testConstantNonConstantTrue() {
+			/** An expression beginning with a single constant is optimized. */
+			new OptimizeBase({
+				spec: {$or:[1,"$a"]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize an expression with a path or a '1' (is entirely constant); {$or:['$a',1]}": function testNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a',1]}).optimize().toJSON(true), {$const:true});
-			},
+		"should optimize falsy constant and truthy expression": function testConstantNonConstantFalse() {
+			/** An expression beginning with a single constant is optimized. */
+			new OptimizeBase({
+				spec: {$or:[0,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+			// note: using $and as serialization of ExpressionCoerceToBool rather than ExpressionAnd
+		},
 
-			"should optimize an expression with a field path or a '0'; {$or:['$a',0]}": function testNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a',0]}).optimize().toJSON(), {$and:['$a']});
-			},
+		"should optimize truthy expression and truthy constant": function testNonConstantOne() {
+			/** An expression with a field path and '1'. */
+			new OptimizeBase({
+				spec: {$or:["$a", 1]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize an expression with two field paths or '1' (is entirely constant); {$or:['$a','$b',1]}": function testNonConstantNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a','$b',1]}).optimize().toJSON(true), {$const:true});
-			},
+		"should optimize truthy expression and falsy constant": function testNonConstantZero() {
+			/** An expression with a field path and '0'. */
+			new OptimizeBase({
+				spec: {$or:["$a", 0]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+		},
 
-			"should optimize an expression with two field paths or '0'; {$or:['$a','$b',0]}": function testNonConstantNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$or:['$a','$b',0]}).optimize().toJSON(), {$or:['$a','$b']});
-			},
+		"should optimize truthy expression, falsy expression, and truthy constant": function testNonConstantNonConstantOne() {
+			/** An expression with two field paths and '1'. */
+			new OptimizeBase({
+				spec: {$or:["$a","$b",1]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize an expression with '0', '1', or a field path; {$or:[0,1,'$a']}": function testZeroOneNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,1,'$a']}).optimize().toJSON(true), {$const:true});
-			},
+		"should optimize truthy expression, falsy expression, and falsy constant": function testNonConstantNonConstantZero() {
+			/** An expression with two field paths and '0'. */
+			new OptimizeBase({
+				spec: {$or:["$a","$b",0]},
+				expectedOptimized: {$or:["$a", "$b"]},
+			}).run();
+		},
 
-			"should optimize an expression with '0', '0', or a field path; {$or:[0,0,'$a']}": function testZeroZeroNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,0,'$a']}).optimize().toJSON(), {$and:['$a']});
-			},
+		"should optimize to true if [0,1,'$a']": function testZeroOneNonConstant() {
+			/** An expression with '0', '1', and a field path. */
+			new OptimizeBase({
+				spec: {$or:[0,1,"$a"]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-			"should optimize nested $or expressions properly or optimize out values evaluating to false; {$or:[0,{$or:[0]},'$a','$b']}": function testNested(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,{$or:[0]},'$a','$b']}).optimize().toJSON(), {$or:['$a','$b']});
-			},
+		"should optimize to {$and:'$a'} if [0,0,'$a']": function testZeroZeroNonConstant() {
+			/** An expression with '0', '0', and a field path. */
+			new OptimizeBase({
+				spec: {$or:[0,0,"$a"]},
+				expectedOptimized: {$and:["$a"]},
+			}).run();
+		},
 
-			"should optimize nested $or expressions containing a nested value evaluating to false; {$or:[0,{$or:[{$or:[1]}]},'$a','$b']}": function testNestedOne(){
-				assert.deepEqual(Expression.parseOperand({$or:[0,{$or:[{$or:[1]}]},'$a','$b']}).optimize().toJSON(true), {$const:true});
-			}
+		"should optimize away nested falsey $or expressions": function testNested() {
+			/** Nested $or expressions. */
+			new OptimizeBase({
+				spec: {$or:[0, {$or:[0]}, "$a", "$b"]},
+				expectedOptimized: {$or: ["$a", "$b"]},
+			}).run();
+		},
 
-		}
+		"should optimize to tru if nested truthy $or expressions": function testNestedOne() {
+			/** Nested $or expressions containing a nested value evaluating to false. */
+			new OptimizeBase({
+				spec: {$or:[0, {$or:[ {$or:[1]} ]}, "$a", "$b"]},
+				expectedOptimized: {$const:true},
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 50
test/lib/pipeline/matcher/ComparisonMatchExpression.js

@@ -1,50 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	ComparisonMatchExpression = require("../../../../lib/pipeline/matcher/ComparisonMatchExpression");
-
-
-module.exports = {
-	"ComparisonMatchExpression": {
-
-		"Should properly initialize with an empty path and a number": function (){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'LT';
-			assert.strictEqual(e.init('', 5 ).code,'OK');
-		},
-		"Should not initialize when given an undefined rhs": function() {
-			var e = new ComparisonMatchExpression();
-			assert.strictEqual(e.init('',5).code,'BAD_VALUE');
-			e._matchType = 'LT';
-			assert.strictEqual(e.init('',{}).code,'BAD_VALUE');	
-			assert.strictEqual(e.init('',undefined).code,'BAD_VALUE');
-			assert.strictEqual(e.init('',{}).code,'BAD_VALUE');
-		},
-		"Should match numbers with GTE": function (){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'GTE';
-			assert.strictEqual(e.init('',5).code,'OK');
-			assert.ok(e.matchesSingleElement(6), "6 ≥ 5");
-			assert.ok(e.matchesSingleElement(5), "5 ≥ 5");
-			assert.ok(!e.matchesSingleElement(4), "4 ≥ 5");
-			assert.ok(!e.matchesSingleElement('foo'), "5 ≥ 'foo'");
-		},
-		"Should match with simple paths and GTE": function(){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'GTE';
-			assert.strictEqual(e.init('a', 5).code,'OK');
-			assert.ok(e.matches({'a':6}));
-		},
-		"Should match arrays with GTE": function (){
-			var e = new ComparisonMatchExpression();
-			e._matchType = 'GTE';
-			assert.strictEqual(e.init('a',5).code,'OK');
-			assert.ok(e.matches({'a':[6,10]}),'[6,10] ≥ 5');
-			assert.ok(e.matches({'a':[4,5.5]}), '[4,5.5] ≥ 5');
-			assert.ok(!e.matches({'a':[1,2]}),'[1,2] ≥ 5');
-			assert.ok(e.matches({'a':[1,10]}),'[1,10] ≥ 5');
-		}
-	}
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
-

+ 110 - 0
test/lib/pipeline/matcher/ComparisonMatchExpression_test.js

@@ -0,0 +1,110 @@
+"use strict";
+var assert = require("assert"),
+	bson = require("bson"),
+	MinKey = bson.BSONPure.MinKey,
+	MaxKey = bson.BSONPure.MaxKey,
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
+	ComparisonMatchExpression = require("../../../../lib/pipeline/matcher/ComparisonMatchExpression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.ComparisonMatchExpression = {
+
+	"should properly initialize with an empty path and a number": function () {
+		var e = new ComparisonMatchExpression('LT');
+		assert.strictEqual(e.init('',5).code,'OK');
+	},
+	"should not initialize when given an invalid operand": function() {
+		var e = new ComparisonMatchExpression('');
+		assert.strictEqual(e.init('',5).code, 'BAD_VALUE');
+	},
+	"should not initialize when given an undefined rhs": function() {
+		var e = new ComparisonMatchExpression();
+		assert.strictEqual(e.init('',5).code,'BAD_VALUE');
+		e._matchType = 'LT';
+		assert.strictEqual(e.init('',{}).code,'BAD_VALUE');
+		assert.strictEqual(e.init('',undefined).code,'BAD_VALUE');
+		assert.strictEqual(e.init('',{}).code,'BAD_VALUE');
+	},
+	"should match numbers with GTE": function () {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('',5).code,'OK');
+		assert.ok(e.matchesSingleElement(6),'6 ≥ 5');
+		assert.ok(e.matchesSingleElement(5),'5 ≥ 5');
+		assert.ok(!e.matchesSingleElement(4),'4 !≥ 5');
+		assert.ok(!e.matchesSingleElement('foo'),"'foo' !≥ 5");
+	},
+	"should match with simple paths and GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',5).code,'OK');
+		assert.ok(e.matches({'a':6}));
+	},
+	"should match array values with GTE": function () {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',5).code,'OK');
+		assert.ok(e.matches({'a':[6,10]}),'[6,10] ≥ 5');
+		assert.ok(e.matches({'a':[4,5.5]}),'[4,5.5] ≥ 5');
+		assert.ok(!e.matches({'a':[1,2]}),'[1,2] !≥ 5');
+		assert.ok(e.matches({'a':[1,10]}),'[1,10] ≥ 5');
+	},
+	"should match entire arrays with GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',[5]).code,'OK');
+		assert.ok(!e.matches({'a':[4]}),'[4] !≥ [5]');
+		assert.ok(e.matches({'a':[5]}),'[5] !≥ [5]');
+		assert.ok(e.matches({'a':[6]}),'[6] !≥ [5]');
+		// documents current behavior
+		assert.ok(e.matches({'a':[[6]]}),'[[4]] ≥ [5]');
+		assert.ok(e.matches({'a':[[6]]}),'[[5]] ≥ [5]');
+		assert.ok(e.matches({'a':[[6]]}),'[[6]] ≥ [5]');
+	},
+	"should match null with GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		e._matchType = 'GTE';
+		assert.strictEqual(e.init('a',null).code,'OK');
+		assert.ok(e.matches({}),'{} ≥ null');
+		assert.ok(e.matches({'a':null}),'null ≥ null');
+		assert.ok(!e.matches({'a':4}),'4 !≥ null');
+		assert.ok(e.matches({'b':null}),'non-existent field ≥ null');
+	},
+	"should match null in dotted paths with GTE": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a.b',null).code,'OK');
+		assert.ok(e.matches({}),'{} ≥ null');
+		assert.ok(e.matches({'a':null}),'{a:null} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':4}),'{a:4} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':{}}),'{a:{}} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':[{'b':null}]}),'{a:[{b:null}]} ≥ {a.b:null}');
+		assert.ok(e.matches({'a':[{'a':4},{'b':4}]}),'{a:[{a:4},{b:4}]} ≥ {a.b:null}');
+		assert.ok(!e.matches({'a':[4]}),'{a:[4]} !≥ {a.b:null}');
+		assert.ok(!e.matches({'a':[{'b':4}]}),'{a:[{b:4}]} !≥ {a.b:null}');
+	},
+	"should match MinKeys": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',new MinKey()).code,'OK');
+		assert.ok(e.matches({'a':new MinKey()}),'minKey ≥ minKey');
+		assert.ok(e.matches({'a':new MaxKey()}),'maxKey ≥ minKey');
+		assert.ok(e.matches({'a':4}),'4 ≥ minKey');
+	},
+	"should match MaxKeys": function() {
+		var e = new ComparisonMatchExpression('GTE');
+		assert.strictEqual(e.init('a',new MaxKey()).code,'OK');
+		assert.ok(e.matches({'a':new MaxKey()}),'maxKey ≥ maxKey');
+		assert.ok(!e.matches({'a':new MinKey()}),'minKey !≥ maxKey');
+		assert.ok(!e.matches({'a':4},null),'4 !≥ maxKey');
+	},
+	"should properly set match keys": function() {
+		var e = new ComparisonMatchExpression('GTE'),
+			d = new MatchDetails();
+		d.requestElemMatchKey();
+		assert.strictEqual(e.init('a',5).code,'OK');
+		assert.ok(!e.matchesJSON({'a':4},d),'4 !≥ 5');
+		assert(!d.hasElemMatchKey());
+		assert.ok(e.matchesJSON({'a':6},d),'6 ≥ 5');
+		assert(!d.hasElemMatchKey());
+		assert.ok(e.matchesJSON({'a':[2,6,5]},d),'[2,6,5] ≥ 5');
+		assert(d.hasElemMatchKey());
+		assert.strictEqual('1',d.elemMatchKey());
+	}
+};

+ 67 - 32
test/lib/pipeline/matcher/GTEMatchExpression.js

@@ -1,6 +1,7 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	GTEMatchExpression = require("../../../../lib/pipeline/matcher/GTEMatchExpression");
 
 
@@ -8,68 +9,102 @@ module.exports = {
 	"GTEMatchExpression": {
 		"should match scalars and strings properly": function (){
 			var e = new GTEMatchExpression();
-			var s = e.init('x',5);
+			var s = e.init("",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'x':5}) );
-			assert.ok( ! e.matches({'x':4}) );
-			assert.ok( e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesSingleElement(5.5) );
+			assert.ok( e.matchesSingleElement(5) );
+			assert.ok( ! e.matchesSingleElement(4) );
+			assert.ok( ! e.matchesSingleElement( "foo" ) );
 		},
 		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
 			var e = new GTEMatchExpression();
-			var s = e.init('',{});
+			var s = e.init("",{});
 
-			assert.strictEqual(s.code, 'BAD_VALUE');
+			assert.strictEqual(s.code, "BAD_VALUE");
 		},
 		"should match a pathed number":function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':5.5}) );
-			assert.ok( ! e.matches({'a':4}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":5.5}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
 		},
 		"should match stuff in an array": function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[4,5.5]}) );
-			assert.ok( ! e.matches({'a':[1,2]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[4,5.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[1,2]}) );
 		},
 		"should not match full array" : function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( e.matchesJSON({"a":[5]}) );
+			assert.ok( e.matchesJSON({"a":[6]}) );
 		},
 		"should not match null" : function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({}) );
-			assert.ok( e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+			var s = e.init("a",null);
+
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({}) );
+			assert.ok( e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			assert.ok( e.matchesJSON({"b":4}) );
+		},
+		"should match dot notation nulls": function() {
+			var e = new GTEMatchExpression();
+			var s = e.init("a.b",null);
+
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({}));
+			assert.ok(e.matchesJSON({a:null}));
+			assert.ok(e.matchesJSON({a:{}}));
+			assert.ok(e.matchesJSON({a:[{b: null}]}));
+			assert.ok(e.matchesJSON({a:[{a:4}, {b:4}]}));
+			assert.ok(!e.matchesJSON({a:[4]}));
+			assert.ok(!e.matchesJSON({a:[{b:4}]}));
+		},
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new GTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new GTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new GTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':4}, m) );
+			assert.ok( ! e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':6}, m) );
+			assert.ok( e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[2,6,5]}, m));
+			assert.ok( e.matchesJSON({"a":[2,6,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 	}
 };

+ 70 - 38
test/lib/pipeline/matcher/GTMatchExpression.js

@@ -1,75 +1,107 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	GTMatchExpression = require("../../../../lib/pipeline/matcher/GTMatchExpression");
 
 
 module.exports = {
 	"GTMatchExpression": {
-		"should match scalars and strings properly": function (){
+		"should handle invalid End of Object Operand": function (){
 			var e = new GTMatchExpression();
-			var s = e.init('x',5);
+			var s = e.init("",{});
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( ! e.matches({'x':5}) );
-			assert.ok( ! e.matches({'x':4}) );
-			assert.ok( e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+			assert.strictEqual(s.code, "BAD_VALUE");
 		},
-		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
+		"should match scalars":function() {
 			var e = new GTMatchExpression();
-			var s = e.init('',{});
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'BAD_VALUE');
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":5.5}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
 		},
-		"should match a pathed number":function() {
+		"should match array value": function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':5.5}) );
-			assert.ok( ! e.matches({'a':4}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[3,5.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[2,4]}) );
 		},
-		"should match stuff in an array": function() {
+		"should match whole array": function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[3,5.5]}) );
-			assert.ok( ! e.matches({'a':[2,4]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[5]}) );
+			assert.ok( e.matchesJSON({"a":[6]}) );
+			// Nested array.
+			// XXX: The following assertion documents current behavior.
+			assert.ok( e.matchesJSON({"a":[[4]]}) );
+			assert.ok( e.matchesJSON({"a":[[5]]}) );
+			assert.ok( e.matchesJSON({"a":[[6]]}) );
 		},
-		"should not match full array" : function() {
+		"should match null" : function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( !e.matchesJSON({}) );
+			assert.ok( !e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			// A non-existent field is treated same way as an empty bson object
+			assert.ok( ! e.matchesJSON({"b":4}) );
 		},
-		"should not match null" : function() {
+		"should match dot notation null" : function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( !e.matches({}) );
-			assert.ok( !e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+			var s = e.init("a.b",null);
+
+			assert.strictEqual(s.code, "OK");
+			assert.ok( !e.matchesJSON({}) );
+			assert.ok( !e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			assert.ok( ! e.matchesJSON({"a":{}}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:null}]}) );
+			assert.ok( ! e.matchesJSON({"a":[{a:4},{b:4}]}) );
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:4}]}) );
+		},
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new GTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new GTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new GTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':4}, m) );
+			assert.ok( ! e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':6}, m) );
+			assert.ok( e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[2,6,5]}, m));
+			assert.ok( e.matchesJSON({"a":[2,6,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 	}
 };

+ 85 - 42
test/lib/pipeline/matcher/LTEMatchExpression.js

@@ -1,75 +1,118 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	LTEMatchExpression = require("../../../../lib/pipeline/matcher/LTEMatchExpression");
 
 
 module.exports = {
 	"LTEMatchExpression": {
-		"should match scalars and strings properly": function (){
-			var e = new LTEMatchExpression();
-			var s = e.init('x',5);
-			
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'x':5}) );
-			assert.ok( e.matches({'x':4}) );
-			assert.ok( ! e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+		"should match element": function (){
+			var operand = {$lte:5},
+				match = {a:4.5},
+				equalMatch = {a:5},
+				notMatch = {a:6},
+				notMatchWrongType = {a:"foo"},
+				lte = new LTEMatchExpression();
+			var s = lte.init("",operand.$lte);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lte.matchesSingleElement(match.a));
+			assert.ok(lte.matchesSingleElement(equalMatch.a));
+			assert.ok(!lte.matchesSingleElement(notMatch.a));
+			assert.ok(!lte.matchesSingleElement(notMatchWrongType.a));
+		},
+		"should not work for invalid eoo operand": function(){
+			var operand = {},
+				lte = new LTEMatchExpression();
+			assert.ok(lte.init("", operand).code !== "OK");
+		},
+		"should match scalars properly": function (){
+			var operand = {$lte:5},
+				lte = new LTEMatchExpression();
+			var s = lte.init("a",operand.$lte);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lte.matchesJSON({"a":4.5}, null));
+			assert.ok(!lte.matchesJSON({"a":6}), null);
 		},
-		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
+		"should match array value": function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('',{});
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'BAD_VALUE');
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[6,4.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[6,7]}) );
 		},
-		"should match a pathed number":function() {
-			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+		"should match whole array" : function() {
+			var e = new LTEMatchExpression(),
+				s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':4.5}) );
-			assert.ok( ! e.matches({'a':6}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":[4]}));
+			assert.ok(e.matchesJSON({"a":[5]}));
+			assert.ok(!e.matchesJSON({"a":[6]}));
+			assert.ok(e.matchesJSON({"a":[[4]]}));
+			assert.ok(e.matchesJSON({"a":[[5]]}));
+			assert.ok(!e.matchesJSON({"a":[[6]]}));
 		},
-		"should match stuff in an array": function() {
+		"should match null" : function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6,4.5]}) );
-			assert.ok( ! e.matches({'a':[6,7]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({}) );
+			assert.ok( e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			// A non-existent field is treated same way as an empty bson object
+			assert.ok( e.matchesJSON({"b":4}) );
 		},
-		"should not match full array" : function() {
+		"should match dot notation null" : function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a.b",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok(e.matches({'a':[4]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({}) );
+			assert.ok( e.matchesJSON({"a":null}) );
+			assert.ok( e.matchesJSON({"a":4}) );
+			assert.ok( e.matchesJSON({"a":{}}) );
+			assert.ok( e.matchesJSON({"a":[{b:null}]}) );
+			assert.ok( e.matchesJSON({"a":[{a:4},{b:4}]}) );
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:4}]}) );
 		},
-		"should not match null" : function() {
-			var e = new LTEMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({}) );
-			assert.ok( e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new LTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new LTEMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new LTEMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':6}, m) );
+			assert.ok( ! e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':4}, m) );
+			assert.ok( e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[6,2,5]}, m));
+			assert.ok( e.matchesJSON({"a":[6,2,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
 
 	}

+ 93 - 52
test/lib/pipeline/matcher/LTMatchExpression.js

@@ -1,87 +1,128 @@
 "use strict";
 var assert = require("assert"),
-	MatchDetails = require('../../../../lib/pipeline/matcher/MatchDetails'),
+	BSON = require("bson"),
+	MatchDetails = require("../../../../lib/pipeline/matcher/MatchDetails"),
 	LTMatchExpression = require("../../../../lib/pipeline/matcher/LTMatchExpression");
 
 
 module.exports = {
 	"LTMatchExpression": {
-		"should match scalars and strings properly": function (){
-			var e = new LTMatchExpression();
-			var s = e.init('x',5);
-			
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( ! e.matches({'x':5}) );
-			assert.ok( e.matches({'x':4}) );
-			assert.ok( ! e.matches({'x':6}) );
-			assert.ok( ! e.matches({'x': 'eliot'}) );
+		"should match element": function (){
+			var operand = {$lt:5},
+				match = {a:4.5},
+				notMatch = {a:6},
+				notMatchEqual = {a:5},
+				notMatchWrongType = {a:"foo"},
+				lt = new LTMatchExpression();
+			var s = lt.init("",operand.$lt);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lt.matchesSingleElement(match.a));
+			assert.ok(!lt.matchesSingleElement(notMatch.a));
+			assert.ok(!lt.matchesSingleElement(notMatchEqual.a));
+			assert.ok(!lt.matchesSingleElement(notMatchWrongType.a));
 		},
-		"should handle invalid End of Object Operand": function testInvalidEooOperand(){
-			var e = new LTMatchExpression();
-			var s = e.init('',{});
-
-			assert.strictEqual(s.code, 'BAD_VALUE');
+		"should not work for invalid eoo operand": function(){
+			var operand = {},
+				lt = new LTMatchExpression();
+			assert.ok(lt.init("", operand).code !== "OK");
 		},
-		"should match a pathed number":function() {
+		"should match scalars properly": function (){
+			var operand = {$lt:5},
+				lt = new LTMatchExpression();
+			var s = lt.init("a",operand.$lt);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lt.matchesJSON({"a":4.5}, null));
+			assert.ok(!lt.matchesJSON({"a":6}), null);
+		},
+		"should match scalars with empty keys properly": function (){
+			var operand = {$lt:5},
+				lt = new LTMatchExpression();
+			var s = lt.init("",operand.$lt);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(lt.matchesJSON({"":4.5}, null));
+			assert.ok(!lt.matchesJSON({"":6}), null);
+		},
+		"should match array value": function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':4.5}) );
-			assert.ok( ! e.matches({'a':6}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( e.matchesJSON({"a":[6,4.5]}) );
+			assert.ok( ! e.matchesJSON({"a":[6,7]}) );
 		},
-		"should match an empty pathed number":function() {
-			var e = new LTMatchExpression();
-			var s = e.init('',5);
+		"should match whole array" : function() {
+			var e = new LTMatchExpression(),
+				s = e.init("a",[5]);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'':4.5}) );
-			assert.ok( ! e.matches({'':6}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok(e.matchesJSON({"a":[4]}));
+			assert.ok(!e.matchesJSON({"a":[5]}));
+			assert.ok(!e.matchesJSON({"a":[6]}));
+			// Nested array.
+			assert.ok(e.matchesJSON({"a":[[4]]}));
+			assert.ok(!e.matchesJSON({"a":[[5]]}));
+			assert.ok(!e.matchesJSON({"a":[[6]]}));
 		},
-		"should match stuff in an array": function() {
+		"should match null" : function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[6,4.5]}) );
-			assert.ok( ! e.matches({'a':[6,7]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({}) );
+			assert.ok( ! e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			// A non-existent field is treated same way as an empty bson object
+			assert.ok( ! e.matchesJSON({"b":4}) );
 		},
-		"should not match full array" : function() {
+		"should match dot notation null" : function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',[5]);
+			var s = e.init("a.b",null);
 
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( e.matches({'a':[4]}) );
+			assert.strictEqual(s.code, "OK");
+			assert.ok( ! e.matchesJSON({}) );
+			assert.ok( ! e.matchesJSON({"a":null}) );
+			assert.ok( ! e.matchesJSON({"a":4}) );
+			assert.ok( ! e.matchesJSON({"a":{}}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:null}]}) );
+			assert.ok( ! e.matchesJSON({"a":[{a:4},{b:4}]}) );
+			assert.ok( ! e.matchesJSON({"a":[4]}) );
+			assert.ok( ! e.matchesJSON({"a":[{b:4}]}) );
 		},
-		"should not match null" : function() {
-			var e = new LTMatchExpression();
-			var s = e.init('a',null);
-		
-			assert.strictEqual(s.code, 'OK');
-			assert.ok( ! e.matches({}) );
-			assert.ok( ! e.matches({'a':null}) );
-			assert.ok( ! e.matches({'a':4}) );
+		"should match MinKey": function (){
+			var operand = {a:new BSON.MinKey()},
+				e = new LTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(!e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(!e.matchesJSON({"a":4}), null);
+		},
+		"should match MaxKey": function (){
+			var operand = {a:new BSON.MaxKey()},
+				e = new LTMatchExpression();
+			var s = e.init("a",operand.a);
+			assert.strictEqual(s.code, "OK");
+			assert.ok(!e.matchesJSON({"a":new BSON.MaxKey()}, null));
+			assert.ok(e.matchesJSON({"a":new BSON.MinKey()}, null));
+			assert.ok(e.matchesJSON({"a":4}), null);
 		},
 		"should handle elemMatchKey":function() {
 			var e = new LTMatchExpression();
-			var s = e.init('a',5);
+			var s = e.init("a",5);
 			var m = new MatchDetails();
 			m.requestElemMatchKey();
-			assert.strictEqual( s.code, 'OK' );
+			assert.strictEqual( s.code, "OK" );
 
-			assert.ok( ! e.matches({'a':6}, m) );
+			assert.ok( ! e.matchesJSON({"a":6}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':4}, m) );
+			assert.ok( e.matchesJSON({"a":4}, m) );
 			assert.ok( ! m.hasElemMatchKey() );
 
-			assert.ok( e.matches({'a':[6,2,5]}, m));
+			assert.ok( e.matchesJSON({"a":[6,2,5]}, m));
 			assert.ok( m.hasElemMatchKey());
-			assert.strictEqual('1', m.elemMatchKey());
+			assert.strictEqual("1", m.elemMatchKey());
 		}
-
-
-
 	}
 };