Browse Source

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

* feature/mongo_2.6.5_expressions: (24 commits)
  EAGLESIX-2651: fix space indents in tab indented files
  EAGLESIX-2651: update gitignore
  EAGLESIX-2651: rename accumulator tests to _test.js to avoid conflicts with helper files
  EAGLESIX-2651: Map: fix optimize test case typo
  EAGLESIX-2651: rename tests to _test.js to avoid conflicts with helper files
  EAGLESIX-2651: Map: fix optimize test case
  EAGLESIX-2651: FieldRange: remove unused expression
  EAGLESIX-2651: fix jshint in tests
  EAGLESIX-2651: Let: better sync w/ 2.6.5 code, fix tests to match new code
  EAGLESIX-2651: Let: fix jshint and format in test
  EAGLESIX-2651: Nary: fix failing tests, related bugs
  EAGLESIX-2695 it is easier to fix the typos now...
  EAGLESIX-2695 fixed a typo, put a call-out in the test for Kyle's descerning eye.
  EAGLESIX-2695 The Gauntlet, a reasonably thorough test case, works.
  EAGLESIX-1695 in preparation for another merge with expressions
  EAGLESIX-2695 Fiddled with the test cases.
  EAGLESIX-2695 Completed all test cases expect a full blown test that requires expressions that are not yet available.
  EAGLESIX-2695 Corrected a bug in ObjectExpression that would cause optimizations to be dropped on the floor. Continuing to work through $let test cases.  More To Come.
  EAGLESIX-2695 More improvements, with More To Come.
  EAGLESIX-2695 Improving the tests, correct code bugs. More To Come
  ...
Chris Sexton 11 năm trước cách đây
mục cha
commit
515c398d0a
54 tập tin đã thay đổi với 464 bổ sung593 xóa
  1. 17 1
      .gitignore
  2. 3 3
      lib/pipeline/expressions/CompareExpression.js
  3. 8 8
      lib/pipeline/expressions/CondExpression.js
  4. 5 5
      lib/pipeline/expressions/ConstantExpression.js
  5. 2 2
      lib/pipeline/expressions/DayOfMonthExpression.js
  6. 11 11
      lib/pipeline/expressions/DivideExpression.js
  7. 7 13
      lib/pipeline/expressions/Expression.js
  8. 80 80
      lib/pipeline/expressions/FieldPathExpression.js
  9. 0 207
      lib/pipeline/expressions/FieldRangeExpression.js
  10. 75 68
      lib/pipeline/expressions/LetExpression.js
  11. 1 1
      lib/pipeline/expressions/NaryExpression.js
  12. 1 1
      lib/pipeline/expressions/ObjectExpression.js
  13. 0 1
      lib/pipeline/expressions/index.js
  14. 0 0
      test/lib/pipeline/accumulators/AddToSetAccumulator_test.js
  15. 0 0
      test/lib/pipeline/accumulators/AvgAccumulator_test.js
  16. 0 0
      test/lib/pipeline/accumulators/FirstAccumulator_test.js
  17. 0 0
      test/lib/pipeline/accumulators/LastAccumulator_test.js
  18. 0 0
      test/lib/pipeline/accumulators/MinMaxAccumulator_test.js
  19. 0 0
      test/lib/pipeline/accumulators/PushAccumulator_test.js
  20. 0 0
      test/lib/pipeline/accumulators/SumAccumulator_test.js
  21. 3 3
      test/lib/pipeline/expressions/AddExpression_test.js
  22. 2 2
      test/lib/pipeline/expressions/AllElementsTrueExpression_test.js
  23. 0 0
      test/lib/pipeline/expressions/AnyElementTrueExpression_test.js
  24. 1 1
      test/lib/pipeline/expressions/CoerceToBoolExpression_test.js
  25. 1 1
      test/lib/pipeline/expressions/CompareExpression_test.js
  26. 0 0
      test/lib/pipeline/expressions/DayOfMonthExpression_test.js
  27. 0 0
      test/lib/pipeline/expressions/DayOfWeekExpression_test.js
  28. 0 0
      test/lib/pipeline/expressions/DayOfYearExpression_test.js
  29. 0 0
      test/lib/pipeline/expressions/FieldPathExpression_test.js
  30. 0 139
      test/lib/pipeline/expressions/FieldRangeExpression.js
  31. 0 0
      test/lib/pipeline/expressions/HourExpression_test.js
  32. 197 0
      test/lib/pipeline/expressions/LetExpression_test.js
  33. 9 9
      test/lib/pipeline/expressions/MapExpression_test.js
  34. 0 0
      test/lib/pipeline/expressions/MillisecondExpression_test.js
  35. 0 0
      test/lib/pipeline/expressions/MinuteExpression_test.js
  36. 0 0
      test/lib/pipeline/expressions/ModExpression_test.js
  37. 0 0
      test/lib/pipeline/expressions/MonthExpression_test.js
  38. 37 33
      test/lib/pipeline/expressions/NaryExpression_test.js
  39. 0 0
      test/lib/pipeline/expressions/ObjectExpression_test.js
  40. 0 0
      test/lib/pipeline/expressions/SecondExpression_test.js
  41. 0 0
      test/lib/pipeline/expressions/SetDifferenceExpression_test.js
  42. 0 0
      test/lib/pipeline/expressions/SetEqualsExpression_test.js
  43. 0 0
      test/lib/pipeline/expressions/SetIntersectionExpression_test.js
  44. 0 0
      test/lib/pipeline/expressions/SetIsSubsetExpression_test.js
  45. 0 0
      test/lib/pipeline/expressions/SetUnionExpression_test.js
  46. 0 0
      test/lib/pipeline/expressions/SizeExpression_test.js
  47. 0 0
      test/lib/pipeline/expressions/StrcasecmpExpression_test.js
  48. 0 0
      test/lib/pipeline/expressions/SubtractExpression_test.js
  49. 0 0
      test/lib/pipeline/expressions/VariablesIdGenerator_test.js
  50. 0 0
      test/lib/pipeline/expressions/VariablesParseState_test.js
  51. 0 0
      test/lib/pipeline/expressions/Variables_test.js
  52. 0 0
      test/lib/pipeline/expressions/WeekExpression_test.js
  53. 0 0
      test/lib/pipeline/expressions/YearExpression_test.js
  54. 4 4
      test/lib/pipeline/expressions/utils.js

+ 17 - 1
.gitignore

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

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

@@ -9,8 +9,8 @@
  */
 var CompareExpression = module.exports = function CompareExpression(cmpOp) {
 	if (!(arguments.length === 1 && typeof cmpOp === "string")) throw new Error(klass.name + ": args expected: cmpOp");
-    this.cmpOp = cmpOp;
-    base.call(this);
+	this.cmpOp = cmpOp;
+	base.call(this);
 }, klass = CompareExpression, base = require("./FixedArityExpressionT")(CompareExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 
@@ -82,7 +82,7 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 		right = this.operands[1].evaluateInternal(vars),
 		cmp = Value.compare(left, right);
 
-    // Make cmp one of 1, 0, or -1.
+	// Make cmp one of 1, 0, or -1.
 	if (cmp === 0) {
 		//leave as 0
 	} else if (cmp < 0) {

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

@@ -9,11 +9,11 @@
  */
 var CondExpression = module.exports = function CondExpression() {
 	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
-    base.call(this);
+	base.call(this);
 }, klass = CondExpression, base = require("./FixedArityExpressionT")(CondExpression, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 var Value = require("../Value"),
-    Expression = require("./Expression");
+	Expression = require("./Expression");
 
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var cond = this.operands[0].evaluateInternal(vars);
@@ -22,12 +22,12 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 };
 
 klass.parse = function parse(expr, vps) {
-    if (Value.getType(expr) !== "Object") {
+	if (Value.getType(expr) !== "Object") {
 		return base.parse(expr, vps);
 	}
 	// verify(str::equals(expr.fieldName(), "$cond")); //NOTE: DEVIATION FROM MONGO: we do not have fieldName any more and not sure this is even possible anyway
 
-    var ret = new CondExpression();
+	var ret = new CondExpression();
 	ret.operands.length = 3;
 
 	var args = expr;
@@ -44,11 +44,11 @@ klass.parse = function parse(expr, vps) {
 		}
 	}
 
-    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; uassert code 17080");
-    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; uassert code 17081");
-    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; uassert code 17082");
+	if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; uassert code 17080");
+	if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; uassert code 17081");
+	if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; uassert code 17082");
 
-    return ret;
+	return ret;
 };
 
 Expression.registerExpression("$cond", CondExpression.parse);

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

@@ -8,9 +8,9 @@
  * @constructor
  */
 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);
+	if (arguments.length !== 1) throw new Error(klass.name + ": args expected: value");
+	this.value = value;
+	base.call(this);
 }, klass = ConstantExpression, base = require("./FixedArityExpressionT")(ConstantExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 var Expression = require("./Expression");
@@ -43,7 +43,7 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 
 /// Helper function to easily wrap constants with $const.
 function serializeConstant(val) {
-    return {$const: val};
+	return {$const: val};
 }
 
 proto.serialize = function serialize(explain) {
@@ -59,5 +59,5 @@ proto.getOpName = function getOpName() {
 };
 
 proto.getValue = function getValue() {
-    return this.value;
+	return this.value;
 };

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

@@ -8,8 +8,8 @@
  * @constructor
  */
 var DayOfMonthExpression = module.exports = function DayOfMonthExpression() {
-    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
-    base.call(this);
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+	base.call(this);
 }, klass = DayOfMonthExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 var Expression = require("./Expression"),

+ 11 - 11
lib/pipeline/expressions/DivideExpression.js

@@ -10,8 +10,8 @@
  * @constructor
  **/
 var DivideExpression = module.exports = function DivideExpression(){
-    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
-    base.call(this);
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+	base.call(this);
 }, klass = DivideExpression, base = require("./FixedArityExpressionT")(DivideExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 var Value = require("../Value"),
@@ -26,16 +26,16 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 		rhs = this.operands[1].evaluateInternal(vars);
 
 	if (typeof lhs === "number" && typeof rhs === "number") {
-        var numer = lhs,
-            denom = rhs;
-        if (denom === 0) throw new Error("can't $divide by zero; uassert code 16608");
+		var numer = lhs,
+			denom = rhs;
+		if (denom === 0) throw new Error("can't $divide by zero; uassert code 16608");
 
-        return numer / denom;
-    } else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
-        return null;
-    } else{
-        throw new Error("User assertion: 16609: $divide only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
-    }
+		return numer / denom;
+	} else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	} else{
+		throw new Error("User assertion: 16609: $divide only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+	}
 };
 
 Expression.registerExpression("$divide", base.parse);

+ 7 - 13
lib/pipeline/expressions/Expression.js

@@ -2,12 +2,6 @@
 
 /**
  * A base class for all pipeline expressions; Performs common expressions within an Op.
- *
- * NOTE: An object expression can take any of the following forms:
- *
- *      f0: {f1: ..., f2: ..., f3: ...}
- *      f0: {$operator:[operand1, operand2, ...]}
- *
  * @class Expression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
@@ -80,12 +74,12 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
  */
 klass.parseObject = function parseObject(obj, ctx, vps) {
 	if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
-	/*
-	  An object expression can take any of the following forms:
-
-	  f0: {f1: ..., f2: ..., f3: ...}
-	  f0: {$operator:[operand1, operand2, ...]}
-	*/
+	/**
+	 * An object expression can take any of the following forms:
+	 *
+	 * f0: {f1: ..., f2: ..., f3: ...}
+	 * f0: {$operator:[operand1, operand2, ...]}
+	 */
 
 	var expression, // the result
 		expressionObject, // the alt result
@@ -223,7 +217,7 @@ klass.parseOperand = function parseOperand(exprElement, vps) {
 	var t = typeof(exprElement);
 	if (t === "string" && exprElement[0] === "$") {
 		//if we got here, this is a field path expression
-	    return FieldPathExpression.parse(exprElement, vps);
+		return FieldPathExpression.parse(exprElement, vps);
 	} else if (t === "object" && exprElement && exprElement.constructor === Object) {
 		var oCtx = new ObjectCtx({
 			isDocumentOk: true

+ 80 - 80
lib/pipeline/expressions/FieldPathExpression.js

@@ -1,8 +1,8 @@
 "use strict";
 
 var Expression = require("./Expression"),
-    Variables = require("./Variables"),
-    FieldPath = require("../FieldPath");
+	Variables = require("./Variables"),
+	FieldPath = require("../FieldPath");
 
 /**
  * Create a field path expression.
@@ -18,9 +18,9 @@ var Expression = require("./Expression"),
  * @param {String} theFieldPath the field path string, without any leading document indicator
  */
 var FieldPathExpression = module.exports = function FieldPathExpression(theFieldPath, variable) {
-    if (arguments.length != 2) throw new Error(klass.name + ": expected args: theFieldPath[, variable]");
-    this._fieldPath = new FieldPath(theFieldPath);
-    this._variable = variable;
+	if (arguments.length != 2) throw new Error(klass.name + ": expected args: theFieldPath[, variable]");
+	this._fieldPath = new FieldPath(theFieldPath);
+	this._variable = variable;
 }, klass = FieldPathExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 /**
@@ -35,9 +35,9 @@ var FieldPathExpression = module.exports = function FieldPathExpression(theField
  * @param fieldPath the field path string, without any leading document
  * indicator
  * @returns the newly created field path expression
- **/
+ */
 klass.create = function create(fieldPath) {
-    return new FieldPathExpression("CURRENT." + fieldPath, Variables.ROOT_ID);
+	return new FieldPathExpression("CURRENT." + fieldPath, Variables.ROOT_ID);
 };
 
 // this is the new version that supports every syntax
@@ -48,52 +48,52 @@ klass.create = function create(fieldPath) {
  * @returns a new FieldPathExpression
  */
 klass.parse = function parse(raw, vps) {
-    if (raw[0] !== "$") throw new Error("FieldPath: '" + raw + "' doesn't start with a $; uassert code 16873");
-    if (raw.length < 2) throw new Error("'$' by itself is not a valid FieldPath; uassert code 16872"); // need at least "$" and either "$" or a field name
-    if (raw[1] === "$") {
-        var fieldPath = raw.substr(2), // strip off $$
-            dotIndex = fieldPath.indexOf("."),
-            varName = fieldPath.substr(0, dotIndex !== -1 ? dotIndex : fieldPath.length);
-        Variables.uassertValidNameForUserRead(varName);
-        return new FieldPathExpression(fieldPath, vps.getVariable(varName));
-    } else {
-        return new FieldPathExpression("CURRENT." + raw.substr(1), // strip the "$" prefix
-            vps.getVariable("CURRENT"));
-    }
+	if (raw[0] !== "$") throw new Error("FieldPath: '" + raw + "' doesn't start with a $; uassert code 16873");
+	if (raw.length < 2) throw new Error("'$' by itself is not a valid FieldPath; uassert code 16872"); // need at least "$" and either "$" or a field name
+	if (raw[1] === "$") {
+		var fieldPath = raw.substr(2), // strip off $$
+			dotIndex = fieldPath.indexOf("."),
+			varName = fieldPath.substr(0, dotIndex !== -1 ? dotIndex : fieldPath.length);
+		Variables.uassertValidNameForUserRead(varName);
+		return new FieldPathExpression(fieldPath, vps.getVariable(varName));
+	} else {
+		return new FieldPathExpression("CURRENT." + raw.substr(1), // strip the "$" prefix
+			vps.getVariable("CURRENT"));
+	}
 };
 
 proto.optimize = function optimize() {
-    // nothing can be done for these
-    return this;
+	// nothing can be done for these
+	return this;
 };
 
 proto.addDependencies = function addDependencies(deps) {
-    if (this._variable === Variables.ROOT_ID) {
-        if (this._fieldPath.fieldNames.length === 1) {
-            deps.needWholeDocument = true; // need full doc if just "$$ROOT"
-        } else {
-            deps.fields[this._fieldPath.tail().getPath(false)] = 1;
-        }
-    }
+	if (this._variable === Variables.ROOT_ID) {
+		if (this._fieldPath.fieldNames.length === 1) {
+			deps.needWholeDocument = true; // need full doc if just "$$ROOT"
+		} else {
+			deps.fields[this._fieldPath.tail().getPath(false)] = 1;
+		}
+	}
 };
 
 /**
  * Helper for evaluatePath to handle Array case
  */
 proto._evaluatePathArray = function _evaluatePathArray(index, input) {
-    if (!(input instanceof Array)) throw new Error("must be array; dassert");
-
-    // Check for remaining path in each element of array
-    var result = [];
-    for (var i = 0, l = input.length; i < l; i++) {
-        if (!(input[i] instanceof Object))
-            continue;
-
-        var nested = this._evaluatePath(index, input[i]);
-        if (nested !== undefined)
-            result.push(nested);
-    }
-    return result;
+	if (!(input instanceof Array)) throw new Error("must be array; dassert");
+
+	// Check for remaining path in each element of array
+	var result = [];
+	for (var i = 0, l = input.length; i < l; i++) {
+		if (!(input[i] instanceof Object))
+			continue;
+
+		var nested = this._evaluatePath(index, input[i]);
+		if (nested !== undefined)
+			result.push(nested);
+	}
+	return result;
 };
 
 /**
@@ -110,52 +110,52 @@ proto._evaluatePathArray = function _evaluatePathArray(index, input) {
  * @returns the field found; could be an array
  */
 proto._evaluatePath = function _evaluatePath(index, input) {
-    // Note this function is very hot so it is important that is is well optimized.
-    // In particular, all return paths should support RVO.
-
-    // if we've hit the end of the path, stop
-    if (index == this._fieldPath.fieldNames.length - 1)
-        return input[this._fieldPath.fieldNames[index]];
-
-    // Try to dive deeper
-    var val = input[this._fieldPath.fieldNames[index]];
-    if (val instanceof Object && val.constructor === Object) {
-        return this._evaluatePath(index + 1, val);
-    } else if (val instanceof Array) {
-        return this._evaluatePathArray(index + 1, val);
-    } else {
-        return undefined;
-    }
+	// Note this function is very hot so it is important that is is well optimized.
+	// In particular, all return paths should support RVO.
+
+	// if we've hit the end of the path, stop
+	if (index == this._fieldPath.fieldNames.length - 1)
+		return input[this._fieldPath.fieldNames[index]];
+
+	// Try to dive deeper
+	var val = input[this._fieldPath.fieldNames[index]];
+	if (val instanceof Object && val.constructor === Object) {
+		return this._evaluatePath(index + 1, val);
+	} else if (val instanceof Array) {
+		return this._evaluatePathArray(index + 1, val);
+	} else {
+		return undefined;
+	}
 };
 
 proto.evaluateInternal = function evaluateInternal(vars) {
-    if (this._fieldPath.fieldNames.length === 1) // get the whole variable
-        return vars.getValue(this._variable);
-
-    if (this._variable === Variables.ROOT_ID) {
-        // ROOT is always a document so use optimized code path
-        return this._evaluatePath(1, vars.getRoot());
-    }
-
-    var val = vars.getValue(this._variable);
-    if (val instanceof Object && val.constructor === Object) {
-        return this._evaluatePath(1, val);
-    } else if(val instanceof Array) {
-        return this._evaluatePathArray(1,val);
-    } else {
-        return undefined;
-    }
+	if (this._fieldPath.fieldNames.length === 1) // get the whole variable
+		return vars.getValue(this._variable);
+
+	if (this._variable === Variables.ROOT_ID) {
+		// ROOT is always a document so use optimized code path
+		return this._evaluatePath(1, vars.getRoot());
+	}
+
+	var val = vars.getValue(this._variable);
+	if (val instanceof Object && val.constructor === Object) {
+		return this._evaluatePath(1, val);
+	} else if(val instanceof Array) {
+		return this._evaluatePathArray(1,val);
+	} else {
+		return undefined;
+	}
 };
 
 proto.serialize = function serialize(){
-    if(this._fieldPath.fieldNames[0] === "CURRENT" && this._fieldPath.fieldNames.length > 1) {
-        // use short form for "$$CURRENT.foo" but not just "$$CURRENT"
-        return "$" + this._fieldPath.tail().getPath(false);
-    } else {
-        return "$$" + this._fieldPath.getPath(false);
-    }
+	if(this._fieldPath.fieldNames[0] === "CURRENT" && this._fieldPath.fieldNames.length > 1) {
+		// use short form for "$$CURRENT.foo" but not just "$$CURRENT"
+		return "$" + this._fieldPath.tail().getPath(false);
+	} else {
+		return "$$" + this._fieldPath.getPath(false);
+	}
 };
 
 proto.getFieldPath = function getFieldPath(){
-    return this._fieldPath;
+	return this._fieldPath;
 };

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

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

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

@@ -1,119 +1,126 @@
 "use strict";
 
 var LetExpression = module.exports = function LetExpression(vars, subExpression){
-	//if (arguments.length !== 2) throw new Error("Two args expected");
+	if (arguments.length !== 2) throw new Error(klass.name + ": expected args: vars, subExpression");
 	this._variables = vars;
 	this._subExpression = subExpression;
 }, klass = LetExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-Expression.registerExpression("$let", LetExpression.parse);
 
-// DEPENDENCIES
-var Variables = require("./Variables"),
-	VariablesParseState = require("./VariablesParseState");
+var Value = require("../Value"),
+	Variables = require("./Variables");
 
-// PROTOTYPE MEMBERS
 
+function NameAndExpression(name, expr){
+	this.name = name;
+	this.expression = expr;
+}
 
-proto.parse = function parse(expr, vpsIn){
-	if(!("$let" in expr)) {
-		throw new Error("Tried to create a $let with something other than let. Looks like your parse map went all funny.");
-	}
 
-	if(typeof(expr.$let) !== 'object' || (expr.$let instanceof Array)) {
-		throw new Error("$let only supports an object as its argument: 16874");
-	}
+klass.parse = function parse(expr, vpsIn){
 
-	var args = expr.$let,
-		varsElem = args.vars,
-		inElem = args['in']; // args.in; ??
-
-	//NOTE: DEVIATION FROM MONGO: 1. These if statements are in a loop in the c++ version,
-	// 2. 'vars' and 'in' are each mandatory here. in the c++ code you only need one of the two.
-	// 3. Below, we croak if there are more than 2 arguments.  The original does not have this limitation, specifically.
-	// Upon further review, I think our code is more accurate.  The c++ code will accept if there are multiple 'in'
-	// or 'var' values. The previous ones will be overwritten by newer ones.
-	//
-	// Final note - I think this code is fine.
-	//
-	if(!varsElem) {
-		throw new Error("Missing 'vars' parameter to $let: 16876");
-	}
-	if(!inElem) {
-		throw new Error("Missing 'in' parameter to $let: 16877");
-	}
+	// if (!(exprFieldName === "$let")) throw new Error("Assertion failure"); //NOTE: DEVIATION FROM MONGO: we do not have exprFieldName here
 
-	// Should this be !== 2?  Why would we have fewer than 2 arguments?  Why do we even care what the length of the
-	// array is? It may be an optimization of sorts. But what we're really wanting here is, 'If any keys are not "in"
-	// or "vars" then we need to bugcheck.'
-	if(Object.keys(args).length > 2) {
-		var bogus = Object.keys(args).filter(function(x) {return !(x === 'in' || x === 'vars');});
-		throw new Error("Unrecognized parameter to $let: " + bogus.join(",") + "- 16875");
-	}
+	if (Value.getType(expr) !== "Object")
+		throw new Error("$let only supports an object as it's argument; uassert code 16874");
+	var args = expr;
 
-	var vpsSub = new VariablesParseState(vpsIn),
-		vars = {};
+	// varsElem must be parsed before inElem regardless of BSON order.
+	var varsElem,
+		inElem;
+	for (var argFieldName in args) {
+		var arg = args[argFieldName];
+		if (argFieldName === "vars") {
+			varsElem = arg;
+		} else if (argFieldName === "in") {
+			inElem = arg;
+		} else {
+			throw new Error("Unrecognized parameter to $let: " + argFieldName + "; uasserted code 16875");
+		}
+	}
 
-	for(var varName in varsElem) {
+	if (!varsElem)
+		throw new Error("Missing 'vars' parameter to $let; uassert code 16876");
+	if (!inElem)
+		throw new Error("Missing 'in' parameter to $let; uassert code 16877");
+
+	// parse "vars"
+	var vpsSub = vpsIn, // vpsSub gets our vars, vpsIn doesn't.
+		vars = {}; // using like a VariableMap
+	if (Value.getType(varsElem) !== "Object") //NOTE: emulate varsElem.embeddedObjectUserCheck()
+		throw new Error("invalid parameter: expected an object (vars); uasserted code 10065");
+	for (var varName in varsElem) {
+		var varElem = varsElem[varName];
 		Variables.uassertValidNameForUserWrite(varName);
 		var id = vpsSub.defineVariable(varName);
 
-		vars[id] = {};
-		vars[id][varName] = Expression.parseOperand(varsElem, vpsIn);
+		vars[id] = new NameAndExpression(varName,
+			Expression.parseOperand(varElem, vpsIn)); // only has outer vars
 	}
 
-	var subExpression = Expression.parseOperand(inElem, vpsSub);
+	// parse "in"
+	var subExpression = Expression.parseOperand(inElem, vpsSub); // has our vars
+
 	return new LetExpression(vars, subExpression);
 };
 
+
 proto.optimize = function optimize() {
-	if(this._variables.empty()) {
+	if (Object.keys(this._variables).length === 0) {
+		// we aren't binding any variables so just return the subexpression
 		return this._subExpression.optimize();
 	}
 
-	for(var id in this._variables){
-		for(var name in this._variables[id]) {
-			//NOTE: DEVIATION FROM MONGO: This is actually ok. The c++ code does this with a single map. The js structure
-			// is nested objects.
-			this._variables[id][name] = this._variables[id][name].optimize();
-		}
+	for (var id in this._variables) {
+		this._variables[id].expression = this._variables[id].expression.optimize();
 	}
 
+	// TODO be smarter with constant "variables"
 	this._subExpression = this._subExpression.optimize();
 
 	return this;
 };
 
+
 proto.serialize = function serialize(explain) {
 	var vars = {};
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			vars[name] = this._variables[id][name];
-		}
+	for (var id in this._variables) {
+		vars[this._variables[id].name] = this._variables[id].expression.serialize(explain);
 	}
 
-	return {$let: {vars:vars, 'in':this._subExpression.serialize(explain)}};
+	return {
+		$let: {
+			vars: vars,
+			in : this._subExpression.serialize(explain)
+		}
+	};
 };
 
+
 proto.evaluateInternal = function evaluateInternal(vars) {
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			vars.setValue(id, this._variables[id][name]);
-		}
+	for (var id in this._variables) {
+		var itFirst = +id, //NOTE: using the unary + to coerce it to a Number
+			itSecond = this._variables[itFirst];
+		// It is guaranteed at parse-time that these expressions don't use the variable ids we
+		// are setting
+		vars.setValue(itFirst,
+			itSecond.expression.evaluateInternal(vars));
 	}
 
 	return this._subExpression.evaluateInternal(vars);
 };
 
+
 proto.addDependencies = function addDependencies(deps, path){
-	for(var id in this._variables) {
-		for(var name in this._variables[id]) {
-			this._variables[id][name].addDependencies(deps);
-		}
+	for (var id in this._variables) {
+		var itFirst = +id, //NOTE: using the unary + to coerce it to a Number
+			itSecond = this._variables[itFirst];
+			itSecond.expression.addDependencies(deps);
 	}
-	this._subExpression.addDependencies(deps, path);
-	return deps; //NOTE: DEVIATION FROM MONGO: The c++ version does not return a value. We seem to use the returned value
-					// (or something from a different method named
-					// addDependencies) in many places.
 
+	// TODO be smarter when CURRENT is a bound variable
+	this._subExpression.addDependencies(deps);
 };
+
+
+Expression.registerExpression("$let", LetExpression.parse);

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

@@ -62,7 +62,7 @@ proto.optimize = function optimize() {
 			// this is commutative and associative.  We detect sameness of
 			// the child operator by checking for equality of the opNames
 			var nary = expr instanceof NaryExpression ? expr : undefined;
-			if (!nary || nary.getOpName() !== this.getOpName) {
+			if (!nary || nary.getOpName() !== this.getOpName()) {
 				nonConstExprs.push(expr);
 			} else {
 				// same expression, so flatten by adding to vpOperand which

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

@@ -49,7 +49,7 @@ proto.optimize = function optimize() {
 		if (!this._expressions.hasOwnProperty(key)) continue;
 		var expr = this._expressions[key];
 		if (expr)
-			expr.optimize();
+			this._expressions[key] = expr.optimize();
 	}
 	return this;
 };

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

@@ -12,7 +12,6 @@ module.exports = {
 	DivideExpression: require("./DivideExpression.js"),
 	Expression: require("./Expression.js"),
 	FieldPathExpression: require("./FieldPathExpression.js"),
-	FieldRangeExpression: require("./FieldRangeExpression.js"),
 	HourExpression: require("./HourExpression.js"),
 	IfNullExpression: require("./IfNullExpression.js"),
 	MinuteExpression: require("./MinuteExpression.js"),

+ 0 - 0
test/lib/pipeline/accumulators/AddToSetAccumulator.js → test/lib/pipeline/accumulators/AddToSetAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/AvgAccumulator.js → test/lib/pipeline/accumulators/AvgAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/FirstAccumulator.js → test/lib/pipeline/accumulators/FirstAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/LastAccumulator.js → test/lib/pipeline/accumulators/LastAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/MinMaxAccumulator.js → test/lib/pipeline/accumulators/MinMaxAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/PushAccumulator.js → test/lib/pipeline/accumulators/PushAccumulator_test.js


+ 0 - 0
test/lib/pipeline/accumulators/SumAccumulator.js → test/lib/pipeline/accumulators/SumAccumulator_test.js


+ 3 - 3
test/lib/pipeline/expressions/AddExpression_test.js

@@ -49,8 +49,8 @@ var TestBase = function TestBase(overrides) {
 		}, 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;
+			// Now add the operands in the reverse direction.
+			this._reverse = true;
 			base.prototype.run.call(this);
 		};
 		proto.populateOperands = function(expr) {
@@ -122,7 +122,7 @@ exports.AddExpression = {
 		"w/ 1 operand": {
 
 			"should pass through a single int": function testInt() {
-        		/** Single int argument. */
+				/** Single int argument. */
 				new SingleOperandBase({
 					operand: 1,
 				}).run();

+ 2 - 2
test/lib/pipeline/expressions/AllElementsTrueExpression.js → test/lib/pipeline/expressions/AllElementsTrueExpression_test.js

@@ -40,7 +40,7 @@ var ExpectedResultBase = (function() {
 			var asserters = spec.error,
 				n = asserters.length;
 			for (var i = 0; i < n; ++i) {
-                // var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				// var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
 				var idGenerator2 = new VariablesIdGenerator(),
 					vps2 = new VariablesParseState(idGenerator2);
 				assert.throws(function() {
@@ -82,7 +82,7 @@ exports.AllElementsTrueExpression = {
 	"#evaluate()": {
 
 		"should return false if just false": function JustFalse() {
-            new ExpectedResultBase({
+			new ExpectedResultBase({
 				getSpec: {
 					input: [[false]],
 					expected: {

+ 0 - 0
test/lib/pipeline/expressions/AnyElementTrueExpression.js → test/lib/pipeline/expressions/AnyElementTrueExpression_test.js


+ 1 - 1
test/lib/pipeline/expressions/CoerceToBoolExpression.js → test/lib/pipeline/expressions/CoerceToBoolExpression_test.js

@@ -64,7 +64,7 @@ exports.CoerceToBoolExpression = {
 		"should be able to output in to JSON Object": function testAddToBsonObj() {
 			/** Output to BSONObj. */
 			var expr = CoerceToBoolExpression.create(FieldPathExpression.create("foo"));
-            // serialized as $and because CoerceToBool isn't an ExpressionNary
+			// serialized as $and because CoerceToBool isn't an ExpressionNary
 			assert.deepEqual({field:{$and:["$foo"]}}, {field:expr.serialize(false)});
 		},
 

+ 1 - 1
test/lib/pipeline/expressions/CompareExpression.js → test/lib/pipeline/expressions/CompareExpression_test.js

@@ -329,7 +329,7 @@ exports.CompareExpression = {
 			}).run();
 		},
 
-        /** Incompatible types can be compared. */
+		/** Incompatible types can be compared. */
 		"IncompatibleTypes": function IncompatibleTypes() {
 			var specElement = {$ne:["a",1]},
 				idGenerator = new VariablesIdGenerator(),

+ 0 - 0
test/lib/pipeline/expressions/DayOfMonthExpression.js → test/lib/pipeline/expressions/DayOfMonthExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/DayOfWeekExpression.js → test/lib/pipeline/expressions/DayOfWeekExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/DayOfYearExpression.js → test/lib/pipeline/expressions/DayOfYearExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/FieldPathExpression.js → test/lib/pipeline/expressions/FieldPathExpression_test.js


+ 0 - 139
test/lib/pipeline/expressions/FieldRangeExpression.js

@@ -1,139 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	FieldRangeExpression = require("../../../../lib/pipeline/expressions/FieldRangeExpression");
-
-
-module.exports = {
-
-	"FieldRangeExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function testInvalid(){
-				assert.throws(function() {
-					new FieldRangeExpression();
-				});
-			}
-
-		},
-
-		"#evaluate()": {
-
-
-			"$eq": {
-
-				"should return false if documentValue < rangeValue": function testEqLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 1).evaluate({a:0}), false);
-				},
-
-				"should return true if documentValue == rangeValue": function testEqEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 1).evaluate({a:1}), true);
-				},
-
-				"should return false if documentValue > rangeValue": function testEqGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 1).evaluate({a:2}), false);
-				}
-
-			},
-
-			"$lt": {
-
-				"should return true if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lt", "y").evaluate({a:"x"}), true);
-				},
-
-				"should return false if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lt", "y").evaluate({a:"y"}), false);
-				},
-
-				"should return false if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lt", "y").evaluate({a:"z"}), false);
-				}
-
-			},
-
-			"$lte": {
-
-				"should return true if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lte", 1.1).evaluate({a:1.0}), true);
-				},
-
-				"should return true if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lte", 1.1).evaluate({a:1.1}), true);
-				},
-
-				"should return false if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$lte", 1.1).evaluate({a:1.2}), false);
-				}
-
-			},
-
-			"$gt": {
-
-				"should return false if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gt", 100).evaluate({a:50}), false);
-				},
-
-				"should return false if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gt", 100).evaluate({a:100}), false);
-				},
-
-				"should return true if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gt", 100).evaluate({a:150}), true);
-				}
-
-			},
-
-			"$gte": {
-
-				"should return false if documentValue < rangeValue": function testLtLt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gte", "abc").evaluate({a:"a"}), false);
-				},
-
-				"should return true if documentValue == rangeValue": function testLtEq() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gte", "abc").evaluate({a:"abc"}), true);
-				},
-
-				"should return true if documentValue > rangeValue": function testLtGt() {
-					assert.strictEqual(new FieldRangeExpression(new FieldPathExpression("a"), "$gte", "abc").evaluate({a:"abcd"}), true);
-				}
-
-			},
-
-			"should throw Error if given multikey values": function testMultikey(){
-				assert.throws(function(){
-					new FieldRangeExpression(new FieldPathExpression("a"), "$eq", 0).evaluate({a:[1,0,2]});
-				});
-			}
-
-		},
-
-//		"#optimize()": {
-//			"should optimize if ...": function testOptimize(){
-//			},
-//			"should not optimize if ...": function testNoOptimize(){
-//			}
-//		},
-
-		"#addDependencies()": {
-
-			"should return the range's path as a dependency": function testDependencies(){
-				var deps = new FieldRangeExpression(new FieldPathExpression("a.b.c"), "$eq", 0).addDependencies({});
-				assert.strictEqual(Object.keys(deps).length, 1);
-				assert.ok(deps['a.b.c']);
-			}
-
-		},
-
-//		"#intersect()": {
-//		},
-
-//		"#toJSON()": {
-//		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 0
test/lib/pipeline/expressions/HourExpression.js → test/lib/pipeline/expressions/HourExpression_test.js


+ 197 - 0
test/lib/pipeline/expressions/LetExpression_test.js

@@ -0,0 +1,197 @@
+"use strict";
+var assert = require("assert"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	LetExpression = require("../../../../lib/pipeline/expressions/LetExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	MultiplyExpression = require("../../../../lib/pipeline/expressions/MultiplyExpression"), //jshint ignore:line
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"), //jshint ignore:line
+	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"), //jshint ignore:line
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"), //jshint ignore:line
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.LetExpression = {
+
+	beforeEach: function() {
+		this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
+
+	"constructor()": {
+
+		"should throw an Error when constructing without args": function() {
+			assert.throws(function() {
+				new LetExpression();
+			});
+		},
+
+		"should throw Error when constructing with one arg": function() {
+			assert.throws(function() {
+				new LetExpression(1);
+			});
+		},
+
+		"should not throw when constructing with two args": function() {
+			assert.doesNotThrow(function() {
+				new LetExpression(1, 2);
+			});
+		},
+
+	},
+
+	"#parse()": {
+
+		"should throw if $let isn't in expr": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$xlet: ['$$a', 1]}, self.vps);
+			}, /15999/);
+		},
+
+		"should throw if the $let expression isn't an object": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: "this is not an object"}, self.vps);
+			}, /16874/);
+		},
+
+		"should throw if the $let expression is an array": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: [1, 2, 3]}, self.vps);
+			}, /16874/);
+		},
+
+		"should throw if there is no vars parameter to $let": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: {vars: undefined}}, self.vps);
+			}, /16876/);
+		},
+
+		"should throw if there is no input parameter to $let": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: {vars: 1, in: undefined}}, self.vps);
+			}, /16877/);
+		},
+
+		"should throw if any of the arguments to $let are not 'in' or 'vars'": function() {
+			var self = this;
+			assert.throws(function() {
+				Expression.parseOperand({$let: {vars: 1, in: 2, zoot:3}}, self.vps);
+			}, /16875/);
+		},
+
+		"should return a Let expression": function() {
+			var x = Expression.parseOperand({$let: {vars: {a:{$const:123}}, in: 2}}, this.vps);
+			assert(x instanceof LetExpression);
+			assert(x._subExpression instanceof ConstantExpression);
+			assert.strictEqual(x._subExpression.getValue(), 2);
+			assert(x._variables[0].expression instanceof ConstantExpression);
+			assert.strictEqual(x._variables[0].expression.getValue(), 123);
+		},
+
+		"should show we collect multiple vars": function() {
+			var x = Expression.parseOperand({$let: {vars: {a:{$const:1}, b:{$const:2}, c:{$const:3}}, in: 2}}, this.vps);
+			assert.strictEqual(x._variables[0].expression.getValue(), 1);
+			assert.strictEqual(x._variables[1].expression.getValue(), 2);
+			assert.strictEqual(x._variables[2].expression.getValue(), 3);
+		},
+
+	},
+
+	"#optimize()": {
+
+		beforeEach: function() {
+			this.testInOpt = function(expr, expected) {
+				assert(expr._subExpression instanceof ConstantExpression, "should have $const subexpr");
+				assert.strictEqual(expr._subExpression.operands.length, 0);
+				assert.strictEqual(expr._subExpression.getValue(), expected);
+			};
+			this.testVarOpt = function(expr, expected) {
+				var varExpr = expr._variables[0].expression;
+				assert(varExpr instanceof ConstantExpression, "should have $const first var");
+				assert.strictEqual(varExpr.getValue(), expected);
+			};
+		},
+
+		"should optimize to subexpression if no variables": function() {
+			var x = Expression.parseOperand({$let:{vars:{}, in:{$multiply:[2,3]}}}, this.vps).optimize();
+			assert(x instanceof ConstantExpression, "should become $const");
+			assert.strictEqual(x.getValue(), 6);
+		},
+
+		"should optimize variables": function() {
+			var x = Expression.parseOperand({$let:{vars:{a:{$multiply:[5,4]}}, in:{$const:6}}}, this.vps).optimize();
+			this.testVarOpt(x, 20);
+		},
+
+		"should optimize subexpressions if there are variables": function() {
+			var x = Expression.parseOperand({$let:{vars:{a:{$multiply:[5,4]}}, in: {$multiply:[2,3]}}}, this.vps).optimize();
+			this.testInOpt(x, 6);
+			this.testVarOpt(x, 20);
+		},
+
+	},
+
+	"#serialize()": {
+
+		"should serialize variables and the subexpression": function() {
+			var s = Expression.parseOperand({$let: {vars: {a:{$const:1}, b:{$const:2}}, in: {$multiply: [2,3]}}}, this.vps).optimize().serialize("zoot");
+			var expected = {$let:{vars:{a:{$const:1},b:{$const:2}},in:{$const:6}}};
+			assert.deepEqual(s, expected);
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should perform the evaluation for variables and the subexpression": function() {
+			var x = Expression.parseOperand({$let: {vars: {a: '$in1', b: '$in2'}, in: { $multiply: ["$$a", "$$b"] }}}, this.vps).optimize();
+			var	y = x.evaluate(new Variables(10, {in1: 6, in2: 7}));
+			assert.equal(y, 42);
+		},
+
+	},
+
+	"#addDependencies()": {
+
+		"should add dependencies": function() {
+			var expr = Expression.parseOperand({$let: {vars: {a: {$multiply:['$a','$b']}}, in: {$multiply: ['$c','$d']}}}, this.vps);
+			var deps = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.equal(Object.keys(deps.fields).length, 4);
+			assert('a' in deps.fields);
+			assert('b' in deps.fields);
+			assert('c' in deps.fields);
+			assert('d' in deps.fields);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
+		},
+
+	},
+
+	"The Gauntlet": {
+
+		"example from http://docs.mongodb.org/manual/reference/operator/aggregation/let/": function() {
+			var x = Expression.parseOperand(
+				{$let: { vars: { total: { $add: [ '$price', '$tax' ] },	discounted: { $cond: { if: '$applyDiscount', then: 0.9, else: 1 } }}, in: { $multiply: [ '$$total', '$$discounted' ] }}},
+				this.vps).optimize();
+			var y;
+			y = x.evaluate(new Variables(10, {price: 90, tax: 0.05}));
+			assert.equal(y, 90.05);
+			y = x.evaluate(new Variables(10, {price: 90, tax: 0.05, applyDiscount: 1}));
+			assert.equal(y, 90.05 * 0.9);
+		},
+
+	},
+
+};
+
+
+if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 9 - 9
test/lib/pipeline/expressions/MapExpression_test.js

@@ -19,16 +19,16 @@ exports.MapExpression = {
 
 	"constructor()": {
 
-		"should accept 4 arguments": function () {
+		"should accept 4 arguments": function() {
 			new MapExpression(1, 2, 3, 4);
 		},
 
-		"should accept only 4 arguments": function () {
-			assert.throws(function () { new MapExpression(); });
-			assert.throws(function () { new MapExpression(1); });
-			assert.throws(function () { new MapExpression(1, 2); });
-			assert.throws(function () { new MapExpression(1, 2, 3); });
-			assert.throws(function () { new MapExpression(1, 2, 3, 4, 5); });
+		"should accept only 4 arguments": function() {
+			assert.throws(function() { new MapExpression(); });
+			assert.throws(function() { new MapExpression(1); });
+			assert.throws(function() { new MapExpression(1, 2); });
+			assert.throws(function() { new MapExpression(1, 2, 3); });
+			assert.throws(function() { new MapExpression(1, 2, 3, 4, 5); });
 		},
 
 	},
@@ -47,7 +47,7 @@ exports.MapExpression = {
 				optimized = expr.optimize();
 			assert.strictEqual(optimized, expr, "should be same reference");
 			assert.deepEqual(expressionToJson(optimized._input), {$const:[1,2,3]});
-			assert.deepEqual(expressionToJson(optimized._each), constify({$add:["$$i","$$i",1,2]}));
+			assert.deepEqual(expressionToJson(optimized._each), constify({$add:["$$i","$$i",3]}));
 		},
 
 	},
@@ -92,7 +92,7 @@ exports.MapExpression = {
 
 	"#addDependencies()": {
 
-		"should add dependencies to both $map.input and $map.in": function () {
+		"should add dependencies to both $map.input and $map.in": function() {
 			var spec = {$map:{
 					input: "$inputArray",
 					as: "i",

+ 0 - 0
test/lib/pipeline/expressions/MillisecondExpression.js → test/lib/pipeline/expressions/MillisecondExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/MinuteExpression.js → test/lib/pipeline/expressions/MinuteExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/ModExpression.js → test/lib/pipeline/expressions/ModExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/MonthExpression.js → test/lib/pipeline/expressions/MonthExpression_test.js


+ 37 - 33
test/lib/pipeline/expressions/NaryExpression_test.js

@@ -3,11 +3,16 @@
 var assert = require("assert"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
 	NaryExpression = require("../../../../lib/pipeline/expressions/NaryExpression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
-	utils = require("./utils");
+	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"), //jshint ignore:line
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"), //jshint ignore:line
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
 
 
 // Mocha one-liner to make these tests self-hosted
@@ -16,13 +21,11 @@ if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({
 
 // A dummy child of NaryExpression used for testing
 var Testable = (function(){
-	// CONSTRUCTOR
 	var klass = function Testable(isAssociativeAndCommutative){
 		this._isAssociativeAndCommutative = isAssociativeAndCommutative;
 		base.call(this);
 	}, base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-	// MEMBERS
 	proto.evaluateInternal = function evaluateInternal(vars) {
 		// Just put all the values in a list.  This is not associative/commutative so
 		// the results will change if a factory is provided and operations are reordered.
@@ -54,14 +57,15 @@ var Testable = (function(){
 		var idGenerator = new VariablesIdGenerator(),
 			vps = new VariablesParseState(idGenerator),
 			testable = Testable.create(haveFactory);
-		operands.forEach(function(element) {
+		for (var i = 0, l = operands.length; i < l; i++) {
+			var element = operands[i];
 			testable.addOperand(Expression.parseOperand(element, vps));
-		});
+		}
 		return testable;
 	};
 
 	proto.assertContents = function assertContents(expectedContents) {
-		assert.deepEqual(utils.constify({$testable:expectedContents}), utils.expressionToJson(this));
+		assert.deepEqual(constify({$testable:expectedContents}), expressionToJson(this));
 	};
 
 	return klass;
@@ -103,26 +107,23 @@ exports.NaryExpression = {
 		var testable = Testable.create();
 
 		var assertDependencies = function assertDependencies(expectedDeps, expr) {
-			var deps = {}, //TODO: new DepsTracker
-				depsJson = [];
+			var deps = new DepsTracker();
 			expr.addDependencies(deps);
-			deps.forEach(function(dep) {
-				depsJson.push(dep);
-			});
+			var depsJson = Object.keys(deps.fields).sort();
 			assert.deepEqual(depsJson, expectedDeps);
-			assert.equal(deps.needWholeDocument, false);
-			assert.equal(deps.needTextScore, false);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
 		};
 
 		// No arguments.
 		assertDependencies([], testable);
 
 		// Add a constant argument.
-		testable.addOperand(new ConstantExpression(1));
+		testable.addOperand(ConstantExpression.create(1));
 		assertDependencies([], testable);
 
 		// Add a field path argument.
-		testable.addOperand(new FieldPathExpression("ab.c"));
+		testable.addOperand(FieldPathExpression.create("ab.c"));
 		assertDependencies(["ab.c"], testable);
 
 		// Add an object expression.
@@ -131,7 +132,7 @@ exports.NaryExpression = {
 			ctx = new Expression.ObjectCtx({isDocumentOk:true}),
 			vps = new VariablesParseState(new VariablesIdGenerator());
 		testable.addOperand(Expression.parseObject(specElement, ctx, vps));
-		assertDependencies(["ab.c", "r", "x"]);
+		assertDependencies(["ab.c", "r", "x"], testable);
 	},
 
 	/** Serialize to an object. */
@@ -159,7 +160,7 @@ exports.NaryExpression = {
 		var spec = [{$and:[]}, "$abc"],
 			testable = Testable.createFromOperands(spec);
 		testable.assertContents(spec);
-		assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+		assert.strictEqual(testable, testable.optimize());
 		testable.assertContents([true, "$abc"]);
 	},
 
@@ -169,8 +170,8 @@ exports.NaryExpression = {
 			testable = Testable.createFromOperands(spec);
 		testable.assertContents(spec);
 		var optimized = testable.optimize();
-		assert.notDeepEqual(testable.serialize(), optimized.serialize());
-		assert.deepEqual({$const:[1,2]}, utils.expressionToJson(optimized));
+		assert.notStrictEqual(testable, optimized);
+		assert.deepEqual({$const:[1,2]}, expressionToJson(optimized));
 	},
 
 	"NoFactoryOptimize": {
@@ -201,7 +202,7 @@ exports.NaryExpression = {
 		// The constant expressions are evaluated separately and placed at the end.
 		var testable = Testable.createFromOperands([55, 66, "$path"], true),
 			optimized = testable.optimize();
-		assert.deepEqual(utils.constify({$testable:["$path", [55, 66]]}), utils.expressionToJson(optimized));
+		assert.deepEqual(constify({$testable:["$path", [55, 66]]}), expressionToJson(optimized));
 	},
 
 	/** Factory optimization flattens nested operators of the same type. */
@@ -209,18 +210,23 @@ exports.NaryExpression = {
 		var testable = Testable.createFromOperands(
 				[55, "$path", {$add:[5,6,"$q"]}, 66],
 			true);
+		// Add a nested $testable operand.
 		testable.addOperand(Testable.createFromOperands(
 				[99, 100, "$another_path"],
-			true));
+			true)
+		);
 		var optimized = testable.optimize();
 		assert.deepEqual(
-			utils.constify({$testable:[
-					"$path",
-					{$add:["$q", 11]},
-					"$another_path",
-					[55, 66, [99, 100]]
-				]}),
-			utils.expressionToJson(optimized));
+			constify({$testable:[
+				// non constant parts
+				"$path",
+				{$add:["$q", 11]},
+				"$another_path",
+				// constant part last
+				[55, 66, [99, 100]]
+			]}),
+			expressionToJson(optimized)
+		);
 	},
 
 	/** Three layers of factory optimization are flattened. */
@@ -231,10 +237,8 @@ exports.NaryExpression = {
 		top.addOperand(nested);
 		var optimized = top.optimize();
 		assert.deepEqual(
-			utils.constify({
-				$testable: ["$a", "$b", "$c", [1, 2, [3, 4, [5, 6]]]]
-			}),
-			utils.expressionToJson(optimized)
+			constify({$testable: ["$a", "$b", "$c", [1, 2, [3, 4, [5, 6]]]]}),
+			expressionToJson(optimized)
 		);
 	},
 

+ 0 - 0
test/lib/pipeline/expressions/ObjectExpression.js → test/lib/pipeline/expressions/ObjectExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SecondExpression.js → test/lib/pipeline/expressions/SecondExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetDifferenceExpression.js → test/lib/pipeline/expressions/SetDifferenceExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetEqualsExpression.js → test/lib/pipeline/expressions/SetEqualsExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetIntersectionExpression.js → test/lib/pipeline/expressions/SetIntersectionExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetIsSubsetExpression.js → test/lib/pipeline/expressions/SetIsSubsetExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SetUnionExpression.js → test/lib/pipeline/expressions/SetUnionExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SizeExpression.js → test/lib/pipeline/expressions/SizeExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/StrcasecmpExpression.js → test/lib/pipeline/expressions/StrcasecmpExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/SubtractExpression.js → test/lib/pipeline/expressions/SubtractExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/VariablesIdGenerator.js → test/lib/pipeline/expressions/VariablesIdGenerator_test.js


+ 0 - 0
test/lib/pipeline/expressions/VariablesParseState.js → test/lib/pipeline/expressions/VariablesParseState_test.js


+ 0 - 0
test/lib/pipeline/expressions/Variables.js → test/lib/pipeline/expressions/Variables_test.js


+ 0 - 0
test/lib/pipeline/expressions/WeekExpression.js → test/lib/pipeline/expressions/WeekExpression_test.js


+ 0 - 0
test/lib/pipeline/expressions/YearExpression.js → test/lib/pipeline/expressions/YearExpression_test.js


+ 4 - 4
test/lib/pipeline/expressions/utils.js

@@ -31,10 +31,10 @@ var utils = module.exports = {
 
 	//SKIPPED: toJson
 
-    /**
-     * Convert Expression to BSON.
-     * @method expressionToJson
-     */
+	/**
+	 * Convert Expression to BSON.
+	 * @method expressionToJson
+	 */
 	expressionToJson: function expressionToJson(expr) {
 		return expr.serialize(false);
 	},