Pārlūkot izejas kodu

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

Conflicts:
	lib/pipeline/expressions/CondExpression.js
	lib/pipeline/expressions/NotExpression.js
	test/lib/pipeline/expressions/CondExpression_test.js
	test/lib/pipeline/expressions/IfNullExpression_test.js
Tony Ennis 11 gadi atpakaļ
vecāks
revīzija
f7a8d443f4

+ 37 - 21
lib/pipeline/Document.js

@@ -11,7 +11,8 @@ var Document = module.exports = function Document(){
 	if(this.constructor == Document) throw new Error("Never create instances! Use static helpers only.");
 }, klass = Document, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-var Value = require("./Value");
+var Value = require("./Value"),
+	FieldPath = require("./FieldPath");
 
 /**
  * Shared "_id"
@@ -37,20 +38,28 @@ klass.toJson = function toJson(doc) {
 
 //SKIPPED: most of MutableDocument except for getNestedField and setNestedField, squashed into Document here (because that's how they use it)
 function getNestedFieldHelper(obj, path) {
-	// NOTE: DEVIATION FROM MONGO: from MutableDocument, implemented a bit differently here but should be same basic functionality
-	var paths = Array.isArray(path) ? path : path.split(".");
-	for (var i = 0, l = paths.length, o = obj; i < l && o instanceof Object; i++) {
-		o = o[paths[i]];
+	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+		lastKey = keys[keys.length - 1];
+	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
+		var next = cur[keys[i]];
+		if (!(next instanceof Object)) return undefined;
+		cur = next;
 	}
-	return o;
-};
-klass.getNestedField = klass.getNestedFieldHelper;  // NOTE: due to ours being static these are the same
+	return cur[lastKey];
+}
+klass.getNestedField = getNestedFieldHelper;  // NOTE: ours is static so these are the same
 klass.setNestedField = function setNestedField(obj, path, val) {
-	// NOTE: DEVIATION FROM MONGO: from MutableDocument, implemented a bit differently here but should be same basic functionality
-	var paths = Array.isArray(path) ? path : path.split("."),
-		key = paths.pop(),
-		parent = klass.getNestedField(obj, paths);
-	if (parent) parent[key] = val;
+	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+		lastKey = keys[keys.length - 1];
+	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
+		var next = cur[keys[i]];
+		if (!(next instanceof Object)) cur[keys[i]] = next = {};
+		cur = next;
+	}
+	cur[lastKey] = val;
+	return val;
 };
 //SKIPPED: getApproximateSize -- not implementing mem usage right now
 //SKIPPED: hash_combine
@@ -104,7 +113,7 @@ klass.serializeForSorter = function serializeForSorter(doc) {
 };
 
 klass.deserializeForSorter = function deserializeForSorter(docStr, sorterDeserializeSettings) {
-	JSON.parse(docStr);
+	return JSON.parse(docStr);
 };
 
 //SKIPPED: swap
@@ -120,22 +129,29 @@ klass.empty = function(obj) {
 
 /**
  * Clone a document
+ * This should only be called by MutableDocument and tests
+ * The new document shares all the fields' values with the original.
+ * This is not a deep copy.  Only the fields on the top-level document
+ * are cloned.
  * @static
  * @method clone
  * @param doc
  */
 klass.clone = function clone(doc) {
+	var obj = {};
+	for (var key in doc) {
+		if (doc.hasOwnProperty(key)) {
+			obj[key] = doc[key];
+		}
+	}
+	return obj;
+};
+klass.cloneDeep = function cloneDeep(doc) {	//there are casese this is actually what we want
 	var obj = {};
 	for (var key in doc) {
 		if (doc.hasOwnProperty(key)) {
 			var val = doc[key];
-			if (val === undefined || val === null) { // necessary to handle null values without failing
-				obj[key] = val;
-			} else if (val instanceof Object && val.constructor === Object) {
-				obj[key] = Document.clone(val);
-			} else {
-				obj[key] = val;
-			}
+			obj[key] = val instanceof Object && val.constructor === Object ? Document.clone(val) : val;
 		}
 	}
 	return obj;

+ 30 - 27
lib/pipeline/FieldPath.js

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

+ 8 - 6
lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -33,12 +33,14 @@ proto.getOpName = function getOpName(){
  * @method @evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	if (!vars instanceof Array) throw new Error("$anyElementTrue requires an array");
-
-	var total = 0;
-	for (var i = 0, n = vars.length; i < n; ++i) {
-		var value = vars[i].evaluateInternal([i]);
-		if ( value.coerceToBool() )
+	var arr = this.operands[0].evaluateInternal(vars);
+	if (!(arr instanceof Array)) {
+		throw new Error("uassert 17041: $anyElementTrue's " +
+						"argument must be an array, but is " +
+						typeof arr);
+	}
+	for (var i=0, n=arr.length; i<n; ++i) {
+		if (Value.coerceToBool(arr[i]))
 			return true;
 	}
 	return false;

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

@@ -11,7 +11,7 @@ var CondExpression = module.exports = function CondExpression(vars) {
 		if (arguments.length !== 0) throw new Error("zero args expected");
     base.call(this);
 }, klass = CondExpression,
-	base = require("./FixedArityExpression")(klass, 3),
+	base = require("./FixedArityExpressionT")(klass, 3),
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass
@@ -20,7 +20,8 @@ var CondExpression = module.exports = function CondExpression(vars) {
 
 // DEPENDENCIES
 var Value = require("../Value"),
-    Expression = require("./Expression");
+    Expression = require("./Expression"),
+	FixedArityExpressionT = require("./FixedArityExpressionT");
 
 // PROTOTYPE MEMBERS
 klass.opName = "$cond";
@@ -40,8 +41,8 @@ klass.parse = function parse(expr, vps) {
 
     // if not an object, return;
 	// todo I don't understand why we'd do this.  shouldn't expr be {}, [], or wrong?
-    if (typeof(expr) !== Object)
-		return Expression.parse(expr, vps);
+    if (typeof(expr) !== Object || )
+		return FixedArityExpressionT.parse(expr, vps);
 
 	// ...or expr could be the entirety of $cond:{...} or $cond:[,,,].
 	if(!(klass.opName in expr)) {

+ 91 - 69
lib/pipeline/expressions/Expression.js

@@ -19,7 +19,8 @@ var Expression = module.exports = function Expression() {
 
 
 var Value = require("../Value"),
-	Document = require("../Document");
+	Document = require("../Document"),
+	Variables = require("./Variables");
 
 
 /**
@@ -44,7 +45,7 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 	 *      @param [opts.isInclusionOk]     {Boolean}
 	 */
 	var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/ ) {
-		if (!(opts instanceof Object && opts.constructor == Object)) throw new Error("opts is required and must be an Object containing named args");
+		if (!(opts instanceof Object && opts.constructor === Object)) throw new Error("opts is required and must be an Object containing named args");
 		for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
 			if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
 		}
@@ -64,31 +65,19 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 })();
 
 
-/**
- * Produce a field path string with the field prefix removed.
- * Throws an error if the field prefix is not present.
- *
- * @static
- * @param prefixedField the prefixed field
- * @returns the field path with the prefix removed
- **/
-klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
-	if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
-	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
-	return prefixedField.substr(1);
-};
-
+//
+// Diagram of relationship between parse functions when parsing a $op:
+//
+// { someFieldOrArrayIndex: { $op: [ARGS] } }
+//                             ^ parseExpression on inner $op BSONElement
+//                          ^ parseObject on BSONObject
+//             ^ parseOperand on outer BSONElement wrapping the $op Object
+//
 
 /**
- * Parse an Object.  The object could represent a functional expression or a Document expression.
- *
- * An object expression can take any of the following forms:
- *
- *      f0: {f1: ..., f2: ..., f3: ...}
- *      f0: {$operator:[operand1, operand2, ...]}
- *
- * @static
+ * Parses a JSON Object that could represent a functional expression or a Document expression.
  * @method parseObject
+ * @static
  * @param obj   the element representing the object
  * @param ctx   a MiniCtx representing the options above
  * @param vps	Variables Parse State
@@ -96,6 +85,12 @@ klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  */
 klass.parseObject = function parseObject(obj, ctx, vps) {
 	if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
+	/*
+	  An object expression can take any of the following forms:
+
+	  f0: {f1: ..., f2: ..., f3: ...}
+	  f0: {$operator:[operand1, operand2, ...]}
+	*/
 
 	var expression, // the result
 		expressionObject, // the alt result
@@ -196,6 +191,7 @@ klass.registerExpression = function registerExpression(key, parserFunc) {
 };
 
 
+//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
 /**
  * Parses a BSONElement which has already been determined to be functional expression.
  * @static
@@ -205,12 +201,13 @@ klass.registerExpression = function registerExpression(key, parserFunc) {
  * @param vps the variable parse state
  * @returns the parsed Expression
  */
-//NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3.	//TODO: could easily fix this inconsistency
 klass.parseExpression = function parseExpression(exprElementKey, exprElementValue, vps) {
-	if (!(exprElementKey in Expression.expressionParserMap)) {
-		throw new Error("Invalid operator : " + exprElementKey + "; code 15999");
-	}
-	return Expression.expressionParserMap[exprElementKey](exprElementValue, vps);
+	var opName = exprElementKey,
+		op = Expression.expressionParserMap;
+	if (!op) throw new Error("invalid operator : " + exprElementKey + "; uassert code 15999");
+
+	// make the expression node
+	return op(exprElementValue, vps);
 };
 
 
@@ -230,84 +227,109 @@ klass.parseExpression = function parseExpression(exprElementKey, exprElementValu
  */
 klass.parseOperand = function parseOperand(exprElement, vps) {
 	var t = typeof(exprElement);
-	if (t === "string" && exprElement[0] == "$") { //if we got here, this is a field path expression
-	    return new FieldPathExpression.parse(exprElement, vps);
+	if (t === "string" && exprElement[0] === "$") {
+		//if we got here, this is a field path expression
+	    return FieldPathExpression.parse(exprElement, vps);
 	} else if (t === "object" && exprElement && exprElement.constructor === Object) {
-		return Expression.parseObject(exprElement, new ObjectCtx({
+		var oCtx = new ObjectCtx({
 			isDocumentOk: true
-		}), vps);
+		});
+		return Expression.parseObject(exprElement, oCtx, vps);
 	} else {
 		return ConstantExpression.parse(exprElement, vps);
 	}
 };
 
 
-/**
- * Evaluate the Expression using the given document as input.
- *
- * @method evaluate
- * @returns the computed value
- */
-proto.evaluateInternal = function evaluateInternal(obj) {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
-};
-
-
-/**
- * Evaluate expression with specified inputs and return result.
- *
- * While vars is non-const, if properly constructed, subexpressions modifications to it
- * should not effect outer expressions due to unique variable Ids.
- */
-proto.evaluate = function evaluate(vars) {
-	return this.evaluateInternal(vars);
-};
-
-
 /**
  * Optimize the Expression.
  *
  * This provides an opportunity to do constant folding, or to collapse nested
- *  operators that have the same precedence, such as $add, $and, or $or.
+ * operators that have the same precedence, such as $add, $and, or $or.
  *
  * The Expression should be replaced with the return value, which may or may
- *  not be the same object.  In the case of constant folding, a computed
- *  expression may be replaced by a constant.
+ * not be the same object.  In the case of constant folding, a computed
+ * expression may be replaced by a constant.
  *
  * @method optimize
  * @returns the optimized Expression
  */
 proto.optimize = function optimize() {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+	return this;
 };
 
+
 /**
- * Add this expression's field dependencies to the set Expressions are trees, so this is often recursive.
- *
- * Top-level ExpressionObject gets pointer to empty vector.
- * If any other Expression is an ancestor, or in other cases where {a:1} inclusion objects aren't allowed, they get NULL.
+ * Add this expression's field dependencies to the set.
+ * Expressions are trees, so this is often recursive.
  *
  * @method addDependencies
- * @param deps  output parameter
- * @param path  path to self if all ancestors are ExpressionObjects.
+ * @param deps Fully qualified paths to depended-on fields are added to this set.
+ *             Empty string means need full document.
+ * @param path path to self if all ancestors are ExpressionObjects.
+ *             Top-level ExpressionObject gets pointer to empty vector.
+ *             If any other Expression is an ancestor, or in other cases
+ *             where {a:1} inclusion objects aren't allowed, they get
+ *             NULL.
  */
 proto.addDependencies = function addDependencies(deps, path) {
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
+
 /**
  * simple expressions are just inclusion exclusion as supported by ExpressionObject
- * @method getIsSimple
+ * @method isSimple
  */
-proto.getIsSimple = function getIsSimple() {
+proto.isSimple = function isSimple() {
 	return false;
 };
 
+/**
+ * Serialize the Expression tree recursively.
+ * If explain is false, returns a Value parsable by parseOperand().
+ * @method serialize
+ */
+proto.serialize = function serialize(explain) {
+	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+};
+
+/**
+ * Evaluate expression with specified inputs and return result.
+ *
+ * While vars is non-const, if properly constructed, subexpressions modifications to it
+ * should not effect outer expressions due to unique variable Ids.
+ *
+ * @method evaluate
+ * @param vars
+ */
+proto.evaluate = function evaluate(vars) {
+	if (!(vars instanceof Variables)) vars = new Variables(0, vars); /// Evaluate expression with specified inputs and return result. (only used by tests)
+	return this.evaluateInternal(vars);
+};
 
-proto.toMatcherBson = function toMatcherBson() {
-	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!"); //verify(false && "Expression::toMatcherBson()");
+/**
+ * Produce a field path string with the field prefix removed.
+ * Throws an error if the field prefix is not present.
+ * @method removeFieldPrefix
+ * @static
+ * @param prefixedField the prefixed field
+ * @returns the field path with the prefix removed
+ */
+klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
+	if (prefixedField.indexOf("\0") !== -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
+	if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
+	return prefixedField.substr(1);
 };
 
+/**
+ * Evaluate the subclass Expression using the given Variables as context and return result.
+ * @method evaluate
+ * @returns the computed value
+ */
+proto.evaluateInternal = function evaluateInternal(vars) {
+	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+};
 
 var ObjectExpression = require("./ObjectExpression"),
 	FieldPathExpression = require("./FieldPathExpression"),

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

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

+ 3 - 2
lib/pipeline/expressions/FixedArityExpressionT.js

@@ -28,8 +28,9 @@ var FixedArityExpressionT = module.exports = function FixedArityExpressionT(SubC
 		}
 	};
 
-	klass.parse = base.parse; 	//NOTE: Need to explicitly bubble static members
-								// in our inheritance chain
+	klass.parse = base.parse; 						// NOTE: Need to explicitly
+	klass.parseArguments = base.parseArguments;		// bubble static members in
+													// our inheritance chain
 	return FixedArityExpression;
 };
 

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

@@ -23,5 +23,8 @@ 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/NotExpression.js

@@ -12,7 +12,7 @@ var NotExpression = module.exports = function NotExpression() {
 		if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
 }, klass = NotExpression,
-	base = require("./FixedArityExpression")(klass, 1),
+	base = require("./FixedArityExpressionT")(klass, 1),
 	proto = klass.prototype = Object.create(base.prototype, {
 		constructor: {
 			value: klass

+ 3 - 2
lib/pipeline/expressions/VariadicExpressionT.js

@@ -15,7 +15,8 @@ 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 bubble static members
-								// in our inheritance chain
+	klass.parse = base.parse; 						// NOTE: Need to explicitly
+	klass.parseArguments = base.parseArguments;		// bubble static members in
+													// our inheritance chain
 	return VariadicExpression;
 };

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

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

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

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

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

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

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

@@ -102,22 +102,22 @@ module.exports = {
 				},
 				"should evaluate true": function(){
 					assert.strictEqual(
-						Expression.parseOperand({$cond:{ if: $a, then: 1, else: 0}}, {}).evaluate({$a: 1}),
+						Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
 						1);
 				},
 				"should evaluate true even with mixed up args": function(){
 					assert.strictEqual(
-						Expression.parseOperand({$cond:{ else: 0, then: 1, if: $a }}, {}).evaluate({$a: 1}),
+						Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, {}).evaluate({$a: 1}),
 						1);
 				},
 				"should evaluate false": function(){
 					assert.strictEqual(
-						Expression.parseOperand({$cond:{ if: $a, then: 0, else: 1}}, {}).evaluate({$a: 0}),
+						Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, {}).evaluate({$a: 0}),
 						1);
 				},
 				"should evaluate false even with mixed up args": function() {
 					assert.strictEqual(
-						Expression.parseOperand({$cond: { else: 1, then: 0, if: $a}}, {}).evaluate({$a: 0}),
+						Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, {}).evaluate({$a: 0}),
 						1);
 				}
 			}

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

@@ -2,31 +2,31 @@
 var assert = require("assert"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState");
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker");
 
 // Mocha one-liner to make these tests self-hosted
-if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+if (!module.parent)return(require.cache[__filename] = null, (new (require("mocha"))({ui: "exports", reporter: "spec", grep: process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
 exports.ConstantExpression = {
 
 	".constructor()": {
 
-		"should accept one argument": function() {
+		"should accept one argument": function () {
 			new ConstantExpression(5);
 		},
 
-		"should not accept 0 arguments": function() {
-			assert.throws(function() {
-				 new ConstantExpression();
+		"should not accept 0 arguments": function () {
+			assert.throws(function () {
+				new ConstantExpression();
 			});
 		},
 
-		"should not accept 2 arguments": function() {
-			assert.throws(function() {
+		"should not accept 2 arguments": function () {
+			assert.throws(function () {
 				new ConstantExpression(1, 2);
 			});
-		},
-
+		}
 	},
 
 	".parse()": {
@@ -36,18 +36,15 @@ exports.ConstantExpression = {
 				vps = new VariablesParseState(idGenerator),
 				expression = ConstantExpression.parse("foo", vps);
 			assert.deepEqual("foo", expression.evaluate({}));
-		},
-
+		}
 	},
 
 	".create()": {
 
 		"should create an expression": function testCreate() {
 			assert(ConstantExpression.create() instanceof ConstantExpression);
-		},
-
+		}
 		//SKIPPED: testCreateFronBsonElement
-
 	},
 
 	"#optimize()": {
@@ -55,21 +52,19 @@ exports.ConstantExpression = {
 		"should not optimize anything": function testOptimize() {
 			var expr = new ConstantExpression(5);
 			assert.strictEqual(expr, expr.optimize());
-		},
-
+		}
 	},
 
 	"#addDependencies()": {
 
 		"should return nothing": function testDependencies() {
 			var expr = ConstantExpression.create(5),
-				deps = {}; //TODO: new DepsTracker
+				deps = new DepsTracker();
 			expr.addDependencies(deps);
-			assert.strictEqual(deps.fields.length, 0);
+			assert.deepEqual(deps.fields, {});
 			assert.strictEqual(deps.needWholeDocument, false);
 			assert.strictEqual(deps.needTextScore, false);
-		},
-
+		}
 	},
 
 	//TODO: AddToBsonObj
@@ -78,30 +73,28 @@ exports.ConstantExpression = {
 
 	"#evaluate()": {
 
-		"should do what comes natural with an int": function() {
+		"should do what comes natural with an int": function () {
 			var c = 567;
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
 		},
 
-		"should do what comes natural with a float": function() {
+		"should do what comes natural with a float": function () {
 			var c = 567.123;
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
 		},
 
-		"should do what comes natural with a String": function() {
+		"should do what comes natural with a String": function () {
 			var c = "Quoth the raven";
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
 		},
 
-		"should do what comes natural with a date": function() {
+		"should do what comes natural with a date": function () {
 			var c = new Date();
 			var expr = new ConstantExpression(c);
 			assert.deepEqual(expr.evaluate(), c);
-		},
-
-	},
-
+		}
+	}
 };

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

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

+ 17 - 7
test/lib/pipeline/expressions/IfNullExpression_test.js

@@ -1,6 +1,9 @@
 "use strict";
 var assert = require("assert"),
 	IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
 
@@ -32,20 +35,27 @@ module.exports = {
 
 		"#evaluateInternal()": {
 			beforeEach: function () {
-				debugger;
-				new IfNullExpression();
-				this.expr = {$ifNull:["$a", "$b"]};
-				this.parsed = Expression.parseOperand(this.expr, {});
+				this.vps = new VariablesParseState(new VariablesIdGenerator());
+				this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
+				this.vars = new Variables(2);
+				this.vars.setValue(0, "a");
+				this.vars.setValue(1, "b");
+				this.makeParsed = function(a, b) {
+					return Expression.parseExpression("$ifNull", [a, b], this.vps);
+				}
 			},
 
 			"should return the left hand side if the left hand side is not null or undefined": function() {
-				assert.strictEqual(this.parsed.evaluateInternal({a: 1, b: 2}), 1);
+				//assert.strictEqual(this.parsed.evaluate(this.vars), 1);
+				assert.strictEqual(this.makeParsed(1, 2).evaluate(this.vars), 1);
 			},
 			"should return the right hand side if the left hand side is null": function() {
-				assert.strictEqual(this.parsed.evaluateInternal({a: null, b: 2}), 2);
+				//assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
+				assert.strictEqual(this.makeParsed(null, 2).evaluate(this.vars), 2);
 			},
 			"should return the right hand side if the left hand side is undefined": function() {
-				assert.strictEqual(this.parsed.evaluateInternal({b: 2}), 2);
+				//assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
+				assert.strictEqual(this.makeParsed(undefined, 2).evaluate(this.vars), 2);
 			}
 		}
 	}

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

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