Browse Source

Fixes #1612, #963: Renamed `alter` to `munge`. Removed unused `microdb`. Added test for `ExpressionDayOfMonth`. Fixed bug calling base `DayOfMonthExpression#addOperand()`. Fixed off-by-one bug in `DayOfMonthExpression#evaluate()`.

http://source.rd.rcg.local/trac/eagle6/changeset/1267/Eagle6_SVN
Kyle Davis 12 years ago
commit
33a99df088
63 changed files with 4424 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 1 0
      .jscheckstyleignore
  3. 10 0
      .jshintrc
  4. 41 0
      README.md
  5. 1 0
      index.js
  6. 20 0
      lib/Op.js
  7. 60 0
      lib/munge.js
  8. 16 0
      lib/ops/GroupOp.js
  9. 23 0
      lib/ops/LimitOp.js
  10. 20 0
      lib/ops/MatchOp.js
  11. 19 0
      lib/ops/ProjectOp/ProjectOp.js
  12. 1 0
      lib/ops/ProjectOp/index.js
  13. 25 0
      lib/ops/SkipOp.js
  14. 72 0
      lib/ops/SortOp.js
  15. 34 0
      lib/ops/UnwindOp.js
  16. 13 0
      lib/pipeline/Document.js
  17. 56 0
      lib/pipeline/FieldPath.js
  18. 139 0
      lib/pipeline/Value.js
  19. 35 0
      lib/pipeline/expressions/AddExpression.js
  20. 68 0
      lib/pipeline/expressions/AndExpression.js
  21. 48 0
      lib/pipeline/expressions/CoerceToBoolExpression.js
  22. 100 0
      lib/pipeline/expressions/CompareExpression.js
  23. 31 0
      lib/pipeline/expressions/CondExpression.js
  24. 43 0
      lib/pipeline/expressions/ConstantExpression.js
  25. 26 0
      lib/pipeline/expressions/DayOfMonthExpression.js
  26. 26 0
      lib/pipeline/expressions/DayOfWeekExpression.js
  27. 34 0
      lib/pipeline/expressions/DayOfYearExpression.js
  28. 37 0
      lib/pipeline/expressions/DivideExpression.js
  29. 312 0
      lib/pipeline/expressions/Expression.js
  30. 80 0
      lib/pipeline/expressions/FieldPathExpression.js
  31. 189 0
      lib/pipeline/expressions/FieldRangeExpression.js
  32. 27 0
      lib/pipeline/expressions/HourExpression.js
  33. 29 0
      lib/pipeline/expressions/IfNullExpression.js
  34. 27 0
      lib/pipeline/expressions/MinuteExpression.js
  35. 42 0
      lib/pipeline/expressions/ModExpression.js
  36. 27 0
      lib/pipeline/expressions/MonthExpression.js
  37. 39 0
      lib/pipeline/expressions/MultiplyExpression.js
  38. 123 0
      lib/pipeline/expressions/NaryExpression.js
  39. 30 0
      lib/pipeline/expressions/NotExpression.js
  40. 396 0
      lib/pipeline/expressions/ObjectExpression.js
  41. 68 0
      lib/pipeline/expressions/OrExpression.js
  42. 27 0
      lib/pipeline/expressions/SecondExpression.js
  43. 34 0
      lib/pipeline/expressions/StrcasecmpExpression.js
  44. 36 0
      lib/pipeline/expressions/SubstrExpression.js
  45. 32 0
      lib/pipeline/expressions/SubtractExpression.js
  46. 31 0
      lib/pipeline/expressions/ToLowerExpression.js
  47. 31 0
      lib/pipeline/expressions/ToUpperExpression.js
  48. 36 0
      lib/pipeline/expressions/WeekExpression.js
  49. 31 0
      lib/pipeline/expressions/YearExpression.js
  50. 1 0
      munge.js
  51. 298 0
      npm_scripts/test/test.sh
  52. 43 0
      package.json
  53. 146 0
      test/lib/munge.js
  54. 152 0
      test/lib/pipeline/FieldPath.js
  55. 133 0
      test/lib/pipeline/expressions/AddExpression.js
  56. 139 0
      test/lib/pipeline/expressions/AndExpression.js
  57. 58 0
      test/lib/pipeline/expressions/CoerceToBoolExpression.js
  58. 293 0
      test/lib/pipeline/expressions/CompareExpression.js
  59. 53 0
      test/lib/pipeline/expressions/ConstantExpression.js
  60. 47 0
      test/lib/pipeline/expressions/DayOfMonthExpression.js
  61. 129 0
      test/lib/pipeline/expressions/FieldPathExpression.js
  62. 137 0
      test/lib/pipeline/expressions/FieldRangeExpression.js
  63. 147 0
      test/lib/pipeline/expressions/NaryExpression.js

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/node_modules
+/reports

+ 1 - 0
.jscheckstyleignore

@@ -0,0 +1 @@
+node_modules

+ 10 - 0
.jshintrc

@@ -0,0 +1,10 @@
+{
+    "es5": true,
+    "node": true,
+    "browser": true,
+	"jquery": true,
+    "strict": false,
+	"unused": false,
+	"forin": false,
+	"newcap": true
+}

+ 41 - 0
README.md

@@ -0,0 +1,41 @@
+munge
+=====
+A JavaScript data munging pipeline based on the MongoDB aggregation framework.
+
+
+
+exports
+=======
+**TODO:** document the major exports and a little about each here
+
+
+
+Deviations
+===========
+Here is a list of the major items where I have deviated from the MongoDB code and why:
+
+  * Pipeline Expressions
+    * Value class
+      * DESIGN: `Value` now provides static helpers rather than instance helpers since that seems to make more sense here
+      * NAMING: `Value#get{TYPE}` methods have been renamed to `Value.verify{TYPE}` since that seemed to make more sense given what they're really doing for us as statics
+      * DESIGN: `Value.coerceToDate` static returns a JavaScript `Date` object rather than milliseconds since that seems to make more sense where possible
+    * NAMING: The `Expression{FOO}` classes have all been renamed to `{FOO}Expression` to satisfy my naming OCD.
+    * DESIGN: The `{FOO}Expression` classes do not provide `create` statics since calling new is easy enough
+      * DESIGN: To further this, the `CompareExpression` class doesn't provide any of it's additional `create{FOO}` helpers so instead I'm binding the appropriate args to the ctor
+    * TESTING: Most of the expression tests have been written without the expression test base classes
+
+
+
+TODO
+====
+Here is a list of global items that I know about that may need to be done in the future:
+
+  * Go through the TODOs....
+  * `getOpName` should be static!
+  * Need a method by which consumers can provide their own extensions
+  * Move expression name to the ctor? or at least a const prototype property or something
+  * NAMING: need to go back through and make sure that places referencing <Document> in the C++ code are represented here by referencing a var called "doc" or similar
+  * Currently using JS types but may need to support `BSON` types to do everything properly; affects handling of `ObjectId`, `ISODate`, and `Timestamp`
+  * Go through test cases and try to turn `assert.equal()` calls into `assert.strictEqual()` calls
+  * Replace `exports = module.exports =` with `module.exports =` only
+  * Go through uses of `throw` and make them actually use `UserException` vs `SystemException` (or whatever they're called)

+ 1 - 0
index.js

@@ -0,0 +1 @@
+module.exports = require("./alter");

+ 20 - 0
lib/Op.js

@@ -0,0 +1,20 @@
+var su = require("stream-utils");
+
+/** A base class for all pipeline operators; Handles top-level pipeline operator definitions to provide a Stream that transforms Objects **/
+var Op = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = su.ThroughStream, proto, klass = function Op(opts){
+		this.opts = opts;
+		base.call(this, {write:this.write, end:this.end, reset:this.reset});
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	//NOTE: see the stream-utils's through() docs for more info
+	//proto.write = function(obj){ this.queue(obj); }
+	//proto.end = function(){ this.queue("LAST"); }
+	//proto.reset = function(){ this.queue("LAST"); }
+
+	return klass;
+})();
+

+ 60 - 0
lib/munge.js

@@ -0,0 +1,60 @@
+var su = require("stream-utils");
+
+var Alterer = (function(){
+	// CONSTRUCTOR
+	var base = Object, proto, klass = function Alterer(ops){
+		this.ops = typeof(ops) == "object" && typeof(ops.length) === "number" ? ops : Array.prototype.slice.call(arguments, 0);
+		this.opStreams = this.ops.map(function opCompiler(op, i){	//TODO: demote to local only?
+			if(typeof(op) !== "object")
+				throw new Error("pipeline element " + i + " is not an object");
+			for(var opName in op) break;	// get first key
+			if(typeof(op) === "function")
+				return su.through(op);
+			if(!(opName in klass.ops))
+				throw new Error("Unrecognized pipeline op: " + JSON.stringify({opName:opName}));
+			var IOp = klass.ops[opName];
+			return new IOp(op[opName], i);
+		});
+console.log("OPS:", this.ops);
+		this.pipeline = new su.PipelineStream(this.opStreams);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// STATIC MEMBERS
+//	klass.ops = {
+//		$skip: SkipOp,
+//		$limit: LimitOp,
+//		$match: MatchOp,
+//		$project: ProjectOp,
+//		$unwind: UnwindOp,
+//		$group: GroupOp,
+//		$sort: SortOp
+//	};
+
+	// PROTOTYPE MEMBERS
+	proto.execute = function execute(inputs){
+console.debug("\n#execute called with:", inputs);
+		var outputs = [];
+//TODO: why does this break things??
+this.pipeline.reset();
+		this.pipeline.on("data", function(data){
+console.debug("PIPELINE WRITE TO OUTPUTS:", data);
+			outputs.push(data);
+		});
+		inputs.forEach(this.pipeline.write);
+console.debug("PIPELINE ENDING...");
+		this.pipeline.end();
+		this.pipeline.reset();
+		return outputs;
+	};
+
+	return klass;
+})();
+
+
+module.exports = function alter(ops, inputs) {
+	var alterer = new Alterer(ops);
+	if(inputs)
+		return alterer.execute(inputs);
+	return alterer.execute.bind(alterer);
+};

+ 16 - 0
lib/ops/GroupOp.js

@@ -0,0 +1,16 @@
+var Op = require("../Op");
+
+//TODO: ...write this...
+var GroupOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function GroupOp(opts){
+		base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeProjected(obj){
+	};
+
+	return klass;
+})();

+ 23 - 0
lib/ops/LimitOp.js

@@ -0,0 +1,23 @@
+var Op = require("../Op");
+
+/** The $limit operator; opts is the number of Objects to allow before preventing further data to pass through. **/
+var LimitOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function LimitOp(opts){
+		this.n = 0;
+		base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeUnlessLimitted(obj){
+		if(this.n++ < this.opts)
+			this.queue(obj);
+		//else this.end();	//TODO: this should work but we need to hook up the end event to preceeding things in the pipeline for it to function
+	};
+	proto.reset = function resetLimitter(){
+		this.n = 0;
+	};
+
+	return klass;
+})();

+ 20 - 0
lib/ops/MatchOp.js

@@ -0,0 +1,20 @@
+var Op = require("../Op"),
+	sift = require("sift");
+
+/** The $match operator; opts is the expression to be used when matching Objects. **/
+var MatchOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function MatchOp(opts){
+		this.sifter = sift(opts);
+		base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeIfMatches(obj){
+		if(this.sifter.test(obj))
+			this.queue(obj);
+	};
+
+	return klass;
+})();

+ 19 - 0
lib/ops/ProjectOp/ProjectOp.js

@@ -0,0 +1,19 @@
+var Op = require("../../Op");
+
+//TODO: ...write this...
+var ProjectOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function ProjectOp(opts){
+		base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// STATIC MEMBERS
+	klass.expressions = undefined; //TODO: ...
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeProjected(obj){
+	};
+
+	return klass;
+})();

+ 1 - 0
lib/ops/ProjectOp/index.js

@@ -0,0 +1 @@
+module.exports = require("./ProjectOp.js");

+ 25 - 0
lib/ops/SkipOp.js

@@ -0,0 +1,25 @@
+var Op = require("../Op");
+
+/** The $skip operator; opts is the number of Objects to skip. **/
+var SkipOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function SkipOp(opts){
+		this.n = 0;
+		base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeUnlessSkipped(obj){
+//console.debug("$skip write:", {opIndex:this.idx, skip:this.opts, n:this.n, isSkip:(this.n < this.opts), obj:obj});
+		if(this.n++ >= this.opts)
+			this.queue(obj);
+	};
+
+	proto.reset = function resetSkipper(){
+		this.n = 0;
+	};
+
+	return klass;
+})();
+

+ 72 - 0
lib/ops/SortOp.js

@@ -0,0 +1,72 @@
+var Op = require("../Op"),
+	traverse = require("traverse");
+
+//TODO: ...write this...
+var SortOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function SortOp(opts){
+			// Parse sorts from options object
+			if(typeof(opts) !== "object") throw new Error("the $sort key specification must be an object");
+			this.sorts = [];
+			for(var p in opts){
+				if(p[0] === "$") throw new Error("$sort: FieldPath field names may not start with '$'.; code 16410");
+				if(p === "") throw new Error("$sort: FieldPath field names may not be empty strings.; code 15998");
+				this.sorts.push({path:p.split("."), direction:opts[p]});
+			}
+console.log("SORTS FOR $sort OP:", this.sorts);
+			this.objs = [];
+			base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PRIVATE STUFF
+	// Helpers for sorting
+	var types = ["undefined", "null", "NaN", "number", "string", "object", "boolean", "Date"];
+	function getTypeOf(o){
+		if(o === undefined) return "undefined";
+		if(o === null) return "null";
+		if(isNaN(o)) return "NaN";
+		if(o.constructor === Date) return "Date";
+		return typeof(o);
+	}
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeDeferredForSorting(obj){
+console.log("$sort deferring:", obj);
+		this.objs.push(obj);
+	};
+
+	proto.end = function endSort(obj){
+console.log("$sort end event");
+		if(this.objs.length){
+			console.log("OBJS TO BE SORTED:", this.objs);
+			this.objs.sort(function(a, b){
+				for(var i = 0, l = this.sorts.length; i < l; i++){
+					//TODO: this probably needs to compareDeep using traverse(a).forEach(...check b...) or similar
+					var sort = this.sorts[i],
+						aVal = traverse(a).get(sort.path), aType = getTypeOf(aVal),
+						bVal = traverse(b).get(sort.path), bType = getTypeOf(bVal);
+					// null and undefined go first
+					if(aType !== bType){
+						return (types.indexOf(aType) - types.indexOf(bType)) * sort.direction;
+					}else{
+						// don't trust type cohersion
+						if(aType == "number") bVal = parseFloat(bVal);
+						if(isNaN(bVal)) return 1;
+						if(aType == "string") bVal = bVal.toString();
+						// return sort value only if it can be determined at this level
+						if(aVal < bVal) return -1 * sort.direction;
+						if(aVal > bVal) return 1 * sort.direction;
+					}
+				}
+				return 0;
+			});
+console.log("$sort has sorted");
+			for(var i = 0, l = this.objs.length; i < l; i++)
+				this.queue(this.objs[i]);
+		}
+		this.end();
+	};
+
+	return klass;
+})();

+ 34 - 0
lib/ops/UnwindOp.js

@@ -0,0 +1,34 @@
+var Op = require("../Op");
+
+/** The $unwind operator; opts is the $-prefixed path to the Array to be unwound. **/
+var UnwindOp = module.exports = (function(){
+	// CONSTRUCTOR
+	var base = Op, proto, klass = function UnwindOp(opts){
+		if(!opts || opts[0] != "$")
+			throw new Error("$unwind: field path references must be prefixed with a '$' (" + JSON.stringify(opts) + "); code 15982");
+		this.path = opts.substr(1).split(".");
+		base.call(this, opts);
+	};
+	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.write = function writeUnwound(obj){
+		var t = traverse(obj),
+			val = t.get(this.path);
+		if(val !== undefined){
+			if(val.constructor.name !== "Array")
+				throw new Error("$unwind: value at end of field path must be an array; code 15978");
+			else{
+				t.set(this.path, null);	// temporarily set this to null to avoid needlessly cloning it below
+				for(var i = 0, l = val.length; i < l; i++){
+					var o = t.clone();
+					traverse(o).set(this.path, val[i]);
+					this.queue(o);
+				}
+				t.set(this.path, val);	// be nice and put this back on the original just in case somebody cares
+			}
+		}
+	};
+
+	return klass;
+})();

+ 13 - 0
lib/pipeline/Document.js

@@ -0,0 +1,13 @@
+var Document = module.exports = (function(){
+	// CONSTRUCTOR
+	var klass = function Document(){
+		if(this.constructor == Document) throw new Error("Never create instances! Use static helpers only.");
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// STATIC MEMBERS
+	klass.compare = function compare(l, r){
+throw new Error("NOT IMPLEMENTED");
+	};
+
+	return klass;
+})();

+ 56 - 0
lib/pipeline/FieldPath.js

@@ -0,0 +1,56 @@
+var FieldPath = module.exports = FieldPath = (function(){
+	// CONSTRUCTOR
+	/** Constructor for field paths.
+	| @param fieldPath the dotted field path string or non empty pre-split vector.
+	| The constructed object will have getPathLength() > 0.
+	| Uassert if any component field names do not pass validation.
+	**/
+	var klass = 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 Strings or Arrays.; 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");
+		}
+		this.path = path;
+		this.fields = fields;
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// STATIC MEMBERS
+	klass.PREFIX = "$";
+
+	// PROTOTYPE MEMBERS
+	/** Get the full path.
+	| @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(".");
+	};
+
+	/** A FieldPath like this but missing the first element (useful for recursion). Precondition getPathLength() > 1. **/
+	proto.tail = function tail() {
+		return new FieldPath(this.fields.slice(1));
+	};
+
+	/** Get a particular path element from the path.
+	| @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];
+	};
+
+	/** Get the number of path elements in the field path.
+	| @returns the number of path elements
+	**/
+	proto.getPathLength = function getPathLength() {
+		return this.fields.length;
+	};
+
+
+	return klass;
+})();

+ 139 - 0
lib/pipeline/Value.js

@@ -0,0 +1,139 @@
+var Value = module.exports = Value = (function(){
+	// CONSTRUCTOR
+	var klass = function Value(){
+		if(this.constructor == Value) throw new Error("Never create instances of this! Use the static helpers only.");
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PRIVATE STUFF
+	function getTypeVerifier(type, IClass, isStrict) {
+		return function verifyType(value) {
+			if (typeof(value) != type) throw new Error("typeof value is not: " + type + "; actual: " + typeof(value));
+			if (typeof(IClass) == "function" && !(isStrict ? value.constructor == IClass : value instanceof IClass)) throw new Error("instanceof value is not: " + IClass.name + "; actual: " + value.constructor.name);
+			return value;
+		};
+	}
+
+	// STATIC MEMBERS
+	klass.verifyNumber = getTypeVerifier("number", Number);	//NOTE: replaces #getDouble(), #getInt(), and #getLong()
+	klass.verifyString = getTypeVerifier("string", String);
+	klass.verifyDocument = getTypeVerifier("object", Object, true);	//TODO: change to verifyDocument?
+	klass.verifyArray = getTypeVerifier("object", Array, true);
+	klass.verifyDate = getTypeVerifier("object", Date, true);
+	klass.verifyRegExp = getTypeVerifier("object", RegExp, true);	//NOTE: renamed from #getRegex()
+//TODO:	klass.verifyOid = ...?
+//TODO:	klass.VerifyTimestamp = ...?
+	klass.verifyBool = getTypeVerifier("boolean", Boolean, true);
+
+	klass.coerceToBool = function coerceToBool(value) {
+		if (typeof(value) == "string") return true;
+		return !!value;	// including null or undefined
+	};
+	klass.coerceToInt =
+	klass.coerceToLong =
+	klass.coerceToDouble =
+	klass._coerceToNumber = function _coerceToNumber(value) { //NOTE: replaces .coerceToInt(), .coerceToLong(), and .coerceToDouble()
+		if (value === null) return 0;
+		switch (typeof(value)) {
+		case "undefined":
+			return 0;
+		case "number":
+			return value;
+		default:
+			throw new Error("can't convert from BSON type " + typeof(value) + " to int; codes 16003, 16004, 16005");
+		}
+	};
+	klass.coerceToDate = function coerceToDate(value) {
+		//TODO: Support Timestamp BSON type?
+		if (value instanceof Date) return value;
+		throw new Error("can't convert from BSON type " + typeof(value) + " to Date; codes 16006");
+	};
+//TODO: klass.coerceToTimeT = ...?   try to use as Date first rather than having coerceToDate return Date.parse  or dateObj.getTime() or similar
+//TODO:	klass.coerceToTm = ...?
+	klass.coerceToString = function coerceToString(value) {
+		if (value === null) return "";
+		switch (typeof(value)) {
+		case "undefined":
+			return "";
+		case "number":
+			return value.toString();
+		case "string":
+			return value;
+		case "object":
+			if (value instanceof Array) {
+				Value.verifyArray(r);
+				for (var i = 0, ll = l.length, rl = r.length, len = Math.min(ll, rl); i < len; i++) {
+					if (i >= ll) {
+						if (i >= rl) return 0; // arrays are same length
+						return -1; // left array is shorter
+					}
+					if (i >= rl) return 1; // right array is shorter
+					var cmp = Value.compare(l[i], r[i]);
+					if (cmp !== 0) return cmp;
+				}
+				throw new Error("logic error in Value.compare for Array types!");
+			} else if (value instanceof Date) { //TODO: Timestamp ??
+				return value.toISOString();
+			}
+			/* falls through */
+		default:
+			throw new Error("can't convert from BSON type " + typeof(value) + " to String; code 16007");
+		}
+	};
+//TODO:	klass.coerceToTimestamp = ...?
+
+	klass.compare = function compare(l, r) {
+		var lt = typeof(l),
+			rt = typeof(r);
+		// Special handling for Undefined and NULL values ...
+		if (lt === "undefined") {
+			if (rt === "undefined") return 0;
+			return -1;
+		}
+		if (l === null) {
+			if (rt === "undefined") return 1;
+			if (r === null) return 0;
+			return -1;
+		}
+		// We know the left value isn't Undefined, because of the above. Count a NULL value as greater than an undefined one.
+		if (rt === "undefined" || r === null) return 1;
+		// Numbers
+		if (lt === "number" && rt === "number") return l < r ? -1 : l > r ? 1 : 0;
+		// CW TODO for now, only compare like values
+		if (lt !== rt) throw new Error("can't compare values of BSON types [" + lt + " " + l.constructor.name + "] and [" + rt + ":" + r.constructor.name + "]; code 16016");
+		// Compare everything else
+		switch (lt) {
+		case "number":
+			throw new Error("number types should have been handled earlier!");
+		case "string":
+			return l < r ? -1 : l > r ? 1 : 0;
+		case "boolean":
+			return l == r ? 0 : l ? 1 : -1;
+		case "object":
+			if (l instanceof Array) {
+				for (var i = 0, ll = l.length, rl = r.length, len = Math.min(ll, rl); i < len; i++) {
+					if (i >= ll) {
+						if (i >= rl) return 0; // arrays are same length
+						return -1; // left array is shorter
+					}
+					if (i >= rl) return 1; // right array is shorter
+					var cmp = Value.compare(l[i], r[i]);
+					if (cmp !== 0) return cmp;
+				}
+				throw new Error("logic error in Value.compare for Array types!");
+			}
+			if (l instanceof Date) return l < r ? -1 : l > r ? 1 : 0;
+			if (l instanceof RegExp) return l < r ? -1 : l > r ? 1 : 0;
+			return Document.compare(l, r);
+		default:
+			throw new Error("unhandled left hand type:" + lt);
+		}
+	};
+
+//TODO:	klass.hashCombine = ...?
+//TODO:	klass.getWidestNumeric = ...?
+//TODO:	klass.getApproximateSize = ...?
+//TODO:	klass.addRef = ...?
+//TODO:	klass.release = ...?
+
+	return klass;
+})();

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

@@ -0,0 +1,35 @@
+var AddExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** Create an expression that finds the sum of n operands. **/
+	var klass = module.exports = function AddExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, NaryExpression = require("./NaryExpression"), base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$add";
+	};
+
+	proto.getFactory = function getFactory(){
+		return klass;	// using the ctor rather than a separate .create() method
+	};
+
+	/** Takes an array of one or more numbers and adds them together, returning the sum. **/
+	proto.evaluate = function evaluate(doc) {
+		var total = 0;
+		for (var i = 0, n = this.operands.length; i < n; ++i) {
+			var value = this.operands[i].evaluate(doc);
+			if (value instanceof Date) throw new Error("$add does not support dates; code 16415");
+			if (typeof(value) == "string") throw new Error("$add does not support strings; code 16416");
+			total += Value.coerceToDouble(value);
+		}
+		if (typeof(total) != "number") throw new Error("$add resulted in a non-numeric type; code 16417");
+		return total;
+	};
+
+	return klass;
+})();

+ 68 - 0
lib/pipeline/expressions/AndExpression.js

@@ -0,0 +1,68 @@
+var AndExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** Create an expression that finds the conjunction of n operands. The
+	| conjunction uses short-circuit logic; the expressions are evaluated in the
+	| order they were added to the conjunction, and the evaluation stops and
+	| returns false on the first operand that evaluates to false.
+	**/
+	var klass = module.exports = function AndExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value"),
+		ConstantExpression = require("./ConstantExpression"),
+		CoerceToBoolExpression = require("./CoerceToBoolExpression");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$and";
+	};
+
+	proto.getFactory = function getFactory(){
+		return klass;	// using the ctor rather than a separate .create() method
+	};
+
+	/** Takes an array one or more values and returns true if all of the values in the array are true. Otherwise $and returns false. **/
+	proto.evaluate = function evaluate(doc) {
+		for (var i = 0, n = this.operands.length; i < n; ++i) {
+			var value = this.operands[i].evaluate(doc);
+			if (!Value.coerceToBool(value)) return false;
+		}
+		return true;
+	};
+
+	proto.optimize = function optimize() {
+		var expr = base.prototype.optimize.call(this); //optimize the conjunction as much as possible
+
+		// if the result isn't a conjunction, we can't do anything
+		if (!(expr instanceof AndExpression)) return expr;
+		var andExpr = expr;
+
+		// Check the last argument on the result; if it's not constant (as promised by ExpressionNary::optimize(),) then there's nothing we can do.
+		var n = andExpr.operands.length;
+		// ExpressionNary::optimize() generates an ExpressionConstant for {$and:[]}.
+		if (!n) throw new Error("requires operands!");
+		var lastExpr = andExpr.operands[n - 1];
+		if (!(lastExpr instanceof ConstantExpression)) return expr;
+
+		// Evaluate and coerce the last argument to a boolean.  If it's false, then we can replace this entire expression.
+		var last = Value.coerceToBool(lastExpr.evaluate());
+		if (!last) return new ConstantExpression(false);
+
+		// If we got here, the final operand was true, so we don't need it anymore.
+		// If there was only one other operand, we don't need the conjunction either.
+		// Note we still need to keep the promise that the result will be a boolean.
+		if (n == 2) return new CoerceToBoolExpression(andExpr.operands[0]);
+
+		//Remove the final "true" value, and return the new expression.
+		//CW TODO: Note that because of any implicit conversions, we may need to apply an implicit boolean conversion.
+		andExpr.operands.length = n - 1; //truncate the array
+		return expr;
+	};
+
+//TODO:	proto.toMatcherBson
+
+	return klass;
+})();

+ 48 - 0
lib/pipeline/expressions/CoerceToBoolExpression.js

@@ -0,0 +1,48 @@
+var CoerceToBoolExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** internal expression for coercing things to booleans **/
+	var klass = module.exports = function CoerceToBoolExpression(expression){
+		if(arguments.length !== 1) throw new Error("args expected: expression");
+		this.expression = expression;
+		base.call(this);
+	}, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value"),
+		AndExpression = require("./AndExpression"),
+		OrExpression = require("./OrExpression"),
+		NotExpression = require("./NotExpression");
+
+	// PROTOTYPE MEMBERS
+	proto.evaluate = function evaluate(doc){
+		var result = this.expression.evaluate(doc);
+		return Value.coerceToBool(result);
+	};
+
+	proto.optimize = function optimize() {
+        this.expression = this.expression.optimize();	// optimize the operand
+
+		// if the operand already produces a boolean, then we don't need this
+		// LATER - Expression to support a "typeof" query?
+		var expr = this.expression;
+		if(expr instanceof AndExpression ||
+				expr instanceof OrExpression ||
+				expr instanceof NotExpression ||
+				expr instanceof CoerceToBoolExpression)
+			return expr;
+		return this;
+	};
+
+	proto.addDependencies = function addDependencies(deps, path) {
+		return this.expression.addDependencies(deps);
+	};
+
+	proto.toJson = function toJson() {
+		// Serializing as an $and expression which will become a CoerceToBool
+		return {$and:[this.expression.toJson()]};
+	};
+//TODO:	proto.addToBsonObj   --- may be required for $project to work
+//TODO:	proto.addToBsonArray
+
+	return klass;
+})();

+ 100 - 0
lib/pipeline/expressions/CompareExpression.js

@@ -0,0 +1,100 @@
+var CompareExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** Generic comparison expression that gets used for $eq, $ne, $lt, $lte, $gt, $gte, and $cmp. **/
+	var klass = module.exports = CompareExpression = function CompareExpression(cmpOp) {
+		if(arguments.length !== 1) throw new Error("args expected: cmpOp");
+		this.cmpOp = cmpOp;
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value"),
+		Expression = require("./Expression"),
+		ConstantExpression = require("./ConstantExpression"),
+		FieldPathExpression = require("./FieldPathExpression"),
+		FieldRangeExpression = require("./FieldRangeExpression");
+
+	// NESTED CLASSES
+	/**Lookup table for truth value returns
+	| @param truthValues	truth value for -1, 0, 1
+	| @param reverse		reverse comparison operator
+	| @param name			string name
+	**/
+	var CmpLookup = (function(){	// emulating a struct
+		// CONSTRUCTOR
+		var klass = function CmpLookup(truthValues, reverse, name) {
+			if(arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
+			this.truthValues = truthValues;
+			this.reverse = reverse;
+			this.name = name;
+		}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+		return klass;
+	})();
+
+	// PRIVATE STATIC MEMBERS
+	/** a table of cmp type lookups to truth values **/
+	var cmpLookupMap = [	//NOTE: converted from this Array to a Dict/Object below using CmpLookup#name as the key
+		//              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
+		new CmpLookup([false, true, false], Expression.CmpOp.EQ, Expression.CmpOp.EQ),
+		new CmpLookup([true, false, true], Expression.CmpOp.NE, Expression.CmpOp.NE),
+		new CmpLookup([false, false, true], Expression.CmpOp.LT, Expression.CmpOp.GT),
+		new CmpLookup([false, true, true], Expression.CmpOp.LTE, Expression.CmpOp.GTE),
+		new CmpLookup([true, false, false], Expression.CmpOp.GT, Expression.CmpOp.LT),
+		new CmpLookup([true, true, false], Expression.CmpOp.GTE, Expression.CmpOp.LTE),
+		new CmpLookup([false, false, false], Expression.CmpOp.CMP, Expression.CmpOp.CMP)
+	].reduce(function(r,o){r[o.name]=o;return r;},{});
+
+
+	// PROTOTYPE MEMBERS
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(2);
+		base.prototype.addOperand.call(this, expr);
+	};
+
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(2);
+		var left = this.operands[0].evaluate(doc),
+			right = this.operands[1].evaluate(doc),
+			cmp = Expression.signum(Value.compare(left, right));
+		if (this.cmpOp == Expression.CmpOp.CMP) return cmp;
+		return cmpLookupMap[this.cmpOp].truthValues[cmp + 1] || false;
+	};
+
+	proto.optimize = function optimize(){
+		var expr = base.prototype.optimize.call(this); // first optimize the comparison operands
+		if (!(expr instanceof CompareExpression)) return expr; // if no longer a comparison, there's nothing more we can do.
+
+		// check to see if optimizing comparison operator is supported	// CMP and NE cannot use ExpressionFieldRange which is what this optimization uses
+		var newOp = this.cmpOp;
+		if (newOp == Expression.CmpOp.CMP || newOp == Expression.CmpOp.NE) return expr;
+
+		// There's one localized optimization we recognize:  a comparison between a field and a constant.  If we recognize that pattern, replace it with an ExpressionFieldRange.
+        // When looking for this pattern, note that the operands could appear in any order.  If we need to reverse the sense of the comparison to put it into the required canonical form, do so.
+		var leftExpr = this.operands[0],
+			rightExpr = this.operands[1];
+		var fieldPathExpr, constantExpr;
+		if (leftExpr instanceof FieldPathExpression) {
+			fieldPathExpr = leftExpr;
+			if (!(rightExpr instanceof ConstantExpression)) return expr; // there's nothing more we can do
+			constantExpr = rightExpr;
+		} else {
+			// if the first operand wasn't a path, see if it's a constant
+			if (!(leftExpr instanceof ConstantExpression)) return expr; // there's nothing more we can do
+			constantExpr = leftExpr;
+
+			// the left operand was a constant; see if the right is a path
+			if (!(rightExpr instanceof FieldPathExpression)) return expr; // there's nothing more we can do
+			fieldPathExpr = rightExpr;
+
+			// these were not in canonical order, so reverse the sense
+			newOp = cmpLookupMap[newOp].reverse;
+		}
+		return new FieldRangeExpression(fieldPathExpr, newOp, constantExpr.getValue());
+	};
+
+	proto.getOpName = function getOpName(){
+		return this.cmpOp;
+	};
+
+	return klass;
+})();

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

@@ -0,0 +1,31 @@
+var CondExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/* $cond expression; @see evaluate */
+	var klass = module.exports = function CondExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$cond";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(3);
+		base.addOperand(expr);
+	};
+
+	/** Use the $cond operator with the following syntax:  { $cond: [ <boolean-expression>, <true-case>, <false-case> ] } **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(3);
+		var pCond = this.operands[0].evaluate(doc),
+			idx = Value.coerceToBool(pCond) ? 1 : 2;
+		return this.operands[idx].evaluate(doc);
+	};
+
+	return klass;
+})();

+ 43 - 0
lib/pipeline/expressions/ConstantExpression.js

@@ -0,0 +1,43 @@
+var ConstantExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** Internal expression for constant values **/
+	var klass = function ConstantExpression(value){
+		if(arguments.length !== 1) throw new Error("args expected: value");
+		this.value = value;	//TODO: actually make read-only in terms of JS?
+		base.call(this);
+	}, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$const";
+	};
+
+	/**Get the constant value represented by this Expression.
+	| @returns the value
+	**/
+	proto.getValue = function getValue(){	//TODO: convert this to an instance field rather than a property
+		return this.value;
+	};
+
+	proto.addDependencies = function addDependencies(deps, path) {
+		// nothing to do
+	};
+
+	/** Get the constant value represented by this Expression. **/
+	proto.evaluate = function evaluate(doc){
+		return this.value;
+	};
+
+	proto.optimize = function optimize() {
+		return this; // nothing to do
+	};
+
+	proto.toJson = function(isExpressionRequired){
+		return isExpressionRequired ? {$const: this.value} : this.value;
+	};
+//TODO:	proto.addToBsonObj   --- may be required for $project to work -- my hope is that we can implement toJson methods all around and use that instead
+//TODO:	proto.addToBsonArray
+
+
+	return klass;
+})();

+ 26 - 0
lib/pipeline/expressions/DayOfMonthExpression.js

@@ -0,0 +1,26 @@
+var DayOfMonthExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	var klass = function DayOfMonthExpression(){
+		if (arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$dayOfMonth";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.prototype.addOperand.call(this, expr);
+	};
+
+	/** Takes a date and returns the day of the month as a number between 1 and 31. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return date.getDate() + 1;
+	};
+
+	return klass;
+})();

+ 26 - 0
lib/pipeline/expressions/DayOfWeekExpression.js

@@ -0,0 +1,26 @@
+var DayOfWeekExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	var klass = function DayOfWeekExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$dayOfWeek";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the day of the week as a number between 1 (Sunday) and 7 (Saturday.) **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return date.getDay() + 1;
+	};
+
+	return klass;
+})();

+ 34 - 0
lib/pipeline/expressions/DayOfYearExpression.js

@@ -0,0 +1,34 @@
+var DayOfYearExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	var klass = function DayOfYearExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$dayOfYear";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the day of the year as a number between 1 and 366. **/
+	proto.evaluate = function evaluate(doc){
+		//NOTE: the below silliness is to deal with the leap year scenario when we should be returning 366
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return klass.getDateDayOfYear(date);
+	};
+
+	// STATIC METHODS
+	klass.getDateDayOfYear = function getDateDayOfYear(d){
+		var y11 = new Date(d.getFullYear(), 0, 0),	// same year, first month, first year; time omitted
+			ymd = new Date(d.getFullYear(), d.getMonth(), d.getDate());	// same y,m,d; time omitted
+		return Math.ceil((y11 - ymd) / 86400000);	//NOTE: 86400000 ms is 1 day
+	};
+
+	return klass;
+})();

+ 37 - 0
lib/pipeline/expressions/DivideExpression.js

@@ -0,0 +1,37 @@
+var DivideExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $divide pipeline expression. @see evaluate **/
+	var klass = function DivideExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){	//TODO: try to move this to a static and/or instance field instead of a getter function
+		return "$divide";
+	};
+
+	proto.addOperand = function addOperand(expr){
+		this.checkArgLimit(2);
+		base.addOperand.call(this, expr);
+	};
+
+	/** Takes an array that contains a pair of numbers and returns the value of the first number divided by the second number. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(2);
+		var left = this.operands[0].evaluate(doc),
+			right = this.operands[1].evaluate(doc);
+		if (!(left instanceof Date) && (!right instanceof Date)) throw new Error("$divide does not support dates; code 16373");
+		right = Value.coerceToDouble(right);
+		if (right === 0) return undefined;
+		left = Value.coerceToDouble(left);
+		return left / right;
+	};
+
+	return klass;
+})();
+
+

+ 312 - 0
lib/pipeline/expressions/Expression.js

@@ -0,0 +1,312 @@
+var Expression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** 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, ...]}
+	**/
+	var klass = module.exports = Expression = function Expression(opts){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+
+	// NESTED CLASSES
+	/** Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context. **/
+	var ObjectCtx = Expression.ObjectCtx = (function(){
+		// CONSTRUCTOR
+		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");
+			for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
+				if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
+			}
+		}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+		proto.isDocumentOk =
+		proto.isTopLevel =
+		proto.isInclusionOk = undefined;
+		return klass;
+	})();
+
+	/** Decribes how and when to create an Op instance **/
+	var OpDesc = (function(){
+		// CONSTRUCTOR
+		var klass = function OpDesc(name, factory, flags, argCount){
+			if (arguments[0] instanceof Object && arguments[0].constructor == Object) { //TODO: using this?
+				var opts = arguments[0];
+				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];
+				}
+			} else {
+				this.name = name;
+				this.factory = factory;
+				this.flags = flags || 0;
+				this.argCount = argCount || 0;
+			}
+		}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+		// STATIC MEMBERS
+		klass.FIXED_COUNT = 1;
+		klass.OBJECT_ARG = 2;
+
+		// PROTOTYPE MEMBERS
+
+		proto.name =
+		proto.factory =
+		proto.flags =
+		proto.argCount = undefined;
+
+		proto.cmp = function cmp(that) {
+			return this.name < that.name ? -1 : this.name > that.name ? 1 : 0;
+		};
+
+		return klass;
+	})();
+
+	var kinds = {
+		UNKNOWN: "UNKNOWN",
+		OPERATOR: "OPERATOR",
+		NOT_OPERATOR: "NOT_OPERATOR"
+	};
+
+
+	// STATIC MEMBERS
+	/** Enumeration of comparison operators.  These are shared between a few expression implementations, so they are factored out here. **/
+	klass.CmpOp = {
+		EQ: "$eq",		// return true for a == b, false otherwise
+		NE: "$ne",		// return true for a != b, false otherwise
+		GT: "$gt",		// return true for a > b, false otherwise
+		GTE: "$gte",	// return true for a >= b, false otherwise
+		LT: "$lt",		// return true for a < b, false otherwise
+		LTE: "$lte",	// return true for a <= b, false otherwise
+		CMP: "$cmp"		// return -1, 0, 1 for a < b, a == b, a > b
+	};
+
+	// DEPENDENCIES (later in this file as compared to others to ensure that statics are setup first)
+	var FieldPathExpression = require("./FieldPathExpression"),
+		ObjectExpression = require("./ObjectExpression"),
+		ConstantExpression = require("./ConstantExpression"),
+		CompareExpression = require("./CompareExpression");
+
+	// DEFERRED DEPENDENCIES
+	/** Expressions, as exposed to users **/
+	process.nextTick(function(){ // Even though `opMap` is deferred, force it to load early rather than later to prevent even *more* potential silliness
+		Object.defineProperty(klass, "opMap", {value:klass.opMap});
+	});
+	Object.defineProperty(klass, "opMap", {	//NOTE: deferred requires using a getter to allow circular requires (to maintain the ported API)
+		configurable: true,
+		/**
+		* Autogenerated docs! Please modify if you you touch this method
+		*
+		* @method get
+		**/
+		get: function getOpMapOnce() {
+			return Object.defineProperty(klass, "opMap", {
+				value: [	//NOTE: rather than OpTable because it gets converted to a dict via OpDesc#name in the Array#reduce() below
+					new OpDesc("$add", require("./AddExpression"), 0),
+					new OpDesc("$and", require("./AndExpression"), 0),
+					new OpDesc("$cmp", CompareExpression.bind(null, Expression.CmpOp.CMP), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$cond", require("./CondExpression"), OpDesc.FIXED_COUNT, 3),
+			//		$const handled specially in parseExpression
+					new OpDesc("$dayOfMonth", require("./DayOfMonthExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$dayOfWeek", require("./DayOfWeekExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$dayOfYear", require("./DayOfYearExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$divide", require("./DivideExpression"), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$eq", CompareExpression.bind(null, Expression.CmpOp.EQ), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$gt", CompareExpression.bind(null, Expression.CmpOp.GT), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$gte", CompareExpression.bind(null, Expression.CmpOp.GTE), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$hour", require("./HourExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$ifNull", require("./IfNullExpression"), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$lt", CompareExpression.bind(null, Expression.CmpOp.LT), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$lte", CompareExpression.bind(null, Expression.CmpOp.LTE), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$minute", require("./MinuteExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$mod", require("./ModExpression"), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$month", require("./MonthExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$multiply", require("./MultiplyExpression"), 0),
+					new OpDesc("$ne", CompareExpression.bind(null, Expression.CmpOp.NE), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$not", require("./NotExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$or", require("./OrExpression"), 0),
+					new OpDesc("$second", require("./SecondExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$strcasecmp", require("./StrcasecmpExpression"), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$substr", require("./SubstrExpression"), OpDesc.FIXED_COUNT, 3),
+					new OpDesc("$subtract", require("./SubtractExpression"), OpDesc.FIXED_COUNT, 2),
+					new OpDesc("$toLower", require("./ToLowerExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$toUpper", require("./ToUpperExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$week", require("./WeekExpression"), OpDesc.FIXED_COUNT, 1),
+					new OpDesc("$year", require("./YearExpression"), OpDesc.FIXED_COUNT, 1)
+				].reduce(function(r,o){r[o.name]=o; return r;}, {})
+			}).opMap;
+		}
+	});
+
+	/** Parse an Object.  The object could represent a functional expression or a Document expression.
+	| @param obj	the element representing the object
+	| @param ctx	a MiniCtx representing the options above
+	| @returns the parsed Expression
+	| An object expression can take any of the following forms:
+	|	f0: {f1: ..., f2: ..., f3: ...}
+	|	f0: {$operator:[operand1, operand2, ...]}
+	**/
+	klass.parseObject = function parseObject(obj, ctx){
+		if(!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
+		var kind = kinds.UNKNOWN,
+			expr, // the result
+			exprObj; // the alt result
+		if (obj === undefined) return new ObjectExpression();
+		var fieldNames = Object.getOwnPropertyNames(obj);
+		for (var fc = 0, n = fieldNames.length; fc < n; ++fc) {
+			var fn = fieldNames[fc];
+			if (fn[0] === "$") {
+				if (fc !== 0) throw new Error("the operator must be the only field in a pipeline object (at '" + fn + "'.; code 16410");
+				if(ctx.isTopLevel) throw new Error("$expressions are not allowed at the top-level of $project; code 16404");
+				kind = kinds.OPERATOR;	//we've determined this "object" is an operator expression
+				expr = Expression.parseExpression(fn, obj[fn]);
+			} else {
+				if (kind === kinds.OPERATOR) throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + fn + "'.; code 15990");
+				if (!ctx.isTopLevel && fn.indexOf(".") != -1) throw new Error("dotted field names are only allowed at the top level; code 16405");
+				if (expr === undefined) { // if it's our first time, create the document expression
+					if (!ctx.isDocumentOk) throw new Error("document not allowed in this context"); // CW TODO error: document not allowed in this context
+					expr = exprObj = new ObjectExpression();
+					kind = kinds.NOT_OPERATOR;	//this "object" is not an operator expression
+				}
+				var fv = obj[fn];
+				switch (typeof(fv)) {
+				case "object":
+					// it's a nested document
+					var subCtx = new ObjectCtx({
+						isDocumentOk: ctx.isDocumentOk,
+						isInclusionOk: ctx.isInclusionOk
+					});
+					exprObj.addField(fn, Expression.parseObject(fv, subCtx));
+					break;
+				case "string":
+					// it's a renamed field		// CW TODO could also be a constant
+					var pathExpr = new FieldPathExpression(Expression.removeFieldPrefix(fv));
+					exprObj.addField(fn, pathExpr);
+					break;
+				case "boolean":
+				case "number":
+					// it's an inclusion specification
+					if (fv) {
+						if (!ctx.isInclusionOk) throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
+						exprObj.includePath(fn);
+					} else {
+						if (!(ctx.isTopLevel && fn == "_id")) throw new Error("The top-level _id field is the only field currently supported for exclusion; code 16406");
+						exprObj.excludeId(true);
+					}
+					break;
+				default:
+					throw new Error("disallowed field type " + (fv ? fv.constructor.name + ":" : "") + typeof(fv) + " in object expression (at '" + fn + "')");
+				}
+			}
+		}
+		return expr;
+	};
+
+	/** Parse a BSONElement Object which has already been determined to be functional expression.
+	| @param opName	the name of the (prefix) operator
+	| @param obj	the BSONElement to parse
+	| @returns the parsed Expression
+	**/
+	klass.parseExpression = function parseExpression(opName, obj) {
+		// look for the specified operator
+		if (opName === "$const") return new ConstantExpression(obj); //TODO: createFromBsonElement was here, not needed since this isn't BSON?
+		var op = klass.opMap[opName];
+		if (!(op instanceof OpDesc)) throw new Error("invalid operator " + opName + "; code 15999");
+
+		// make the expression node
+		var IExpression = op.factory,	//TODO: should this get renamed from `factory` to `ctor` or something?
+			expr = new IExpression();
+
+		// add the operands to the expression node
+		if (op.flags & OpDesc.FIXED_COUNT && op.argCount > 1 && !(obj instanceof Array)) throw new Error("the " + op.name + " operator requires an array of " + op.argCount + " operands; code 16019");
+		var operand; // used below
+		if (obj.constructor === Object) { // the operator must be unary and accept an object argument
+			if (!(op.flags & OpDesc.OBJECT_ARG)) throw new Error("the " + op.name + " operator does not accept an object as an operand");
+			operand = Expression.parseObject(obj, new ObjectCtx({isDocumentOk: 1}));
+			expr.addOperand(operand);
+		} else if (obj instanceof Array) { // multiple operands - an n-ary operator
+			if (op.flags & OpDesc.FIXED_COUNT && op.argCount !== obj.length) throw new Error("the " + op.name + " operator requires " + op.argCount + " operand(s); code 16020");
+			for (var i = 0, n = obj.length; i < n; ++i) {
+				operand = Expression.parseOperand(obj[i]);
+				expr.addOperand(operand);
+			}
+		} else { //assume it's an atomic operand
+			if (op.flags & OpDesc.FIXED_COUNT && op.argCount != 1) throw new Error("the " + op.name + " operator requires an array of " + op.argCount + " operands; code 16022");
+			operand = Expression.parseOperand(obj);
+			expr.addOperand(operand);
+		}
+
+        return expr;
+	};
+
+	/** Parse a BSONElement which is an operand in an Expression.
+	| @param pBsonElement the expected operand's BSONElement
+	| @returns the parsed operand, as an Expression
+	**/
+	klass.parseOperand = function parseOperand(obj){
+		var t = typeof(obj);
+		if (t === "string" && obj[0] == "$") { //if we got here, this is a field path expression
+			var path = Expression.removeFieldPrefix(obj);
+			return new FieldPathExpression(path);
+		}
+		else if (t === "object" && obj.constructor === Object) return Expression.parseObject(obj, new ObjectCtx({isDocumentOk: true}));
+		else return new ConstantExpression(obj);
+	};
+
+	/** Produce a field path string with the field prefix removed.
+	| @param prefixedField the prefixed field
+	| @returns the field path with the prefix removed
+	| Throws an error if the field prefix is not present.
+	**/
+	klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
+		if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; code 16419");
+		if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); code 15982");
+		return prefixedField.substr(1);
+	};
+
+	/** @returns the sign of a number; -1, 1, or 0 **/
+	klass.signum = function signum(i) {
+		if (i < 0) return -1;
+		if (i > 0) return 1;
+		return 0;
+	};
+
+
+	// PROTOTYPE MEMBERS
+	/*** Evaluate the Expression using the given document as input.
+	| @returns the computed value
+	***/
+	proto.evaluate = function evaluate(obj) {
+		throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+	};
+
+	/** Optimize the Expression.
+	| This provides an opportunity to do constant folding, or to collapse nested
+	|  operators that have the same precedence, such as $add, $and, or $or.
+	| 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.
+	| @returns the optimized Expression
+	**/
+	proto.optimize = function optimize() {
+		throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
+	};
+
+	/** Add this expression's field dependencies to the set Expressions are trees, so this is often recursive.
+	| @param deps	output parameter
+	| @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 **/
+	proto.getIsSimple = function getIsSimple() {
+		return false;
+	};
+
+	return klass;
+})();

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

@@ -0,0 +1,80 @@
+var FieldPathExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** Create a field path expression. 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
+	**/
+	var klass = function FieldPathExpression(path){
+		if(arguments.length !== 1) throw new Error("args expected: path");
+		this.path = new FieldPath(path);
+	}, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var FieldPath = require("../FieldPath");
+
+	// PROTOTYPE MEMBERS
+	proto.evaluate = function evaluate(obj){
+		return this._evaluatePath(obj, 0, this.path.fields.length);
+	};
+
+	/** 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];
+
+		// 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.optimize = function(){
+		return this;
+	};
+
+	proto.addDependencies = function addDependencies(deps){
+		deps.push(this.path.getPath());
+		return deps;
+	};
+
+	proto.toJson = function toJson(){
+		return this.path.getPath(true);
+	};
+//TODO: proto.addToBsonObj = ...?
+//TODO: proto.addToBsonArray = ...?
+
+	return klass;
+})();

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

@@ -0,0 +1,189 @@
+var FieldRangeExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** 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.
+	| @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 klass = 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});
+	}, 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:...} **/
+		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);
+		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.
+	| @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...
+
+	return klass;
+})();

+ 27 - 0
lib/pipeline/expressions/HourExpression.js

@@ -0,0 +1,27 @@
+var HourExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** An $hour pipeline expression. @see evaluate **/
+	var klass = function HourExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$hour";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the hour between 0 and 23. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return date.getHours();
+	};
+
+	return klass;
+})();

+ 29 - 0
lib/pipeline/expressions/IfNullExpression.js

@@ -0,0 +1,29 @@
+var IfNullExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** An $ifNull pipeline expression. @see evaluate **/
+	var klass = function IfNullExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$ifNull";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(2);
+		base.addOperand(expr);
+	};
+
+	/** Use the $ifNull operator with the following syntax: { $ifNull: [ <expression>, <replacement-if-null> ] } **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(2);
+		var left = this.operands[0].evaluate(doc);
+		if(left !== undefined && left !== null) return left;
+		var right = this.operands[1].evaluate(doc);
+		return right;
+	};
+
+	return klass;
+})();

+ 27 - 0
lib/pipeline/expressions/MinuteExpression.js

@@ -0,0 +1,27 @@
+var MinuteExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $minute pipeline expression. @see evaluate **/
+	var klass = function MinuteExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$minute";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the minute between 0 and 59. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return date.getMinutes();
+	};
+
+	return klass;
+})();

+ 42 - 0
lib/pipeline/expressions/ModExpression.js

@@ -0,0 +1,42 @@
+var ModExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $mod pipeline expression. @see evaluate **/
+	var klass = function ModExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$mod";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(2);
+		base.addOperand(expr);
+	};
+
+	/** Takes an array that contains a pair of numbers and returns the remainder of the first number divided by the second number. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(2);
+		var left = this.operands[0].evaluate(doc),
+			right = this.operands[0].evaluate(doc);
+		if(left instanceof Date || right instanceof Date) throw new Error("$mod does not support dates; code 16374");
+
+		// pass along jstNULLs and Undefineds
+		if(left === undefined || left === null) return left;
+		if(right === undefined || right === null) return right;
+
+		// ensure we aren't modding by 0
+		right = Value.coerceToDouble(right);
+		if(right === 0) return undefined;
+
+		left = Value.coerceToDouble(left);
+		return left % right;
+	};
+
+	return klass;
+})();

+ 27 - 0
lib/pipeline/expressions/MonthExpression.js

@@ -0,0 +1,27 @@
+var MonthExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $month pipeline expression. @see evaluate **/
+	var klass = function MonthExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$month";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the month as a number between 1 and 12. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var date = this.operands[0].evalute(doc);
+		return date.getMonth() + 1;
+	};
+
+	return klass;
+})();

+ 39 - 0
lib/pipeline/expressions/MultiplyExpression.js

@@ -0,0 +1,39 @@
+var MultiplyExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $multiply pipeline expression. @see evaluate **/
+	var klass = function MultiplyExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$multiply";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes an array of one or more numbers and multiples them, returning the resulting product. **/
+	proto.evaluate = function evaluate(doc){
+		var product = 1;
+		for(var i = 0, n = this.operands.length; i < n; ++i){
+			var value = this.operands[i].evaluate(doc);
+			if(value instanceof Date) throw new Error("$multiply does not support dates; code 16375");
+			product *= Value.coerceToDouble(value);
+		}
+		if(typeof(product) != "number") throw new Error("$multiply resulted in a non-numeric type; code 16418");
+		return product;
+	};
+
+	proto.getFactory = function getFactory(){
+		return klass;	// using the ctor rather than a separate .create() method
+	};
+
+	return klass;
+})();

+ 123 - 0
lib/pipeline/expressions/NaryExpression.js

@@ -0,0 +1,123 @@
+var NaryExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	var klass = module.exports = function NaryExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		this.operands = [];
+		base.call(this);
+	}, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var ConstantExpression = require("./ConstantExpression");
+
+	// PROTOTYPE MEMBERS
+	proto.evaluate = undefined; // evaluate(doc){ ... defined by inheritor ... }
+
+	proto.optimize = function optimize(){
+		var constsFound = 0,
+			stringsFound = 0;
+		for (var i = 0, l = this.operands.length; i < l; i++) {
+			var optimizedExpr = this.operands[i].optimize();
+			if (optimizedExpr instanceof ConstantExpression) {
+				constsFound++;
+				if (typeof(optimizedExpr.value) == "string") stringsFound++;
+			}
+			this.operands[i] = optimizedExpr;
+		}
+		// If all the operands are constant, we can replace this expression with a constant.  We can find the value by evaluating this expression over a NULL Document because evaluating the ExpressionConstant never refers to the argument Document.
+		if (constsFound === l) return new ConstantExpression(this.evaluate());
+		// If there are any strings, we can't re-arrange anything, so stop now.     LATER:  we could concatenate adjacent strings as a special case.
+		if (stringsFound) return this;
+		// If there's no more than one constant, then we can't do any constant folding, so don't bother going any further.
+		if (constsFound <= 1) return this;
+		// If the operator isn't commutative or associative, there's nothing more we can do.  We test that by seeing if we can get a factory; if we can, we can use it to construct a temporary expression which we'll evaluate to collapse as many constants as we can down to a single one.
+		var IExpression = this.getFactory();
+		if (!(IExpression instanceof Function)) return this;
+        // Create a new Expression that will be the replacement for this one.  We actually create two:  one to hold constant expressions, and one to hold non-constants.
+        // Once we've got these, we evaluate the constant expression to produce a single value, as above.  We then add this operand to the end of the non-constant expression, and return that.
+		var expr = new IExpression(),
+			constExpr = new IExpression();
+		for (i = 0; i < l; ++i) {
+			var operandExpr = this.operands[i];
+			if (operandExpr instanceof ConstantExpression) {
+				constExpr.addOperand(operandExpr);
+			} else {
+				// If the child operand is the same type as this, then we can extract its operands and inline them here because we already know this is commutative and associative because it has a factory.  We can detect sameness of the child operator by checking for equality of the factory
+				// Note we don't have to do this recursively, because we called optimize() on all the children first thing in this call to optimize().
+				if (!(operandExpr instanceof NaryExpression)) {
+					expr.addOperand(operandExpr);
+				} else {
+					if (operandExpr.getFactory() !== IExpression) {
+						expr.addOperand(operandExpr);
+					} else { // same factory, so flatten
+						for (var i2 = 0, n2 = operandExpr.operands.length; i2 < n2; ++i2) {
+							var childOperandExpr = operandExpr.operands[i2];
+							if (childOperandExpr instanceof ConstantExpression) {
+								constExpr.addOperand(childOperandExpr);
+							} else {
+								expr.addOperand(childOperandExpr);
+							}
+						}
+					}
+				}
+			}
+		}
+
+		if (constExpr.operands.length === 1) { // If there was only one constant, add it to the end of the expression operand vector.
+			expr.addOperand(constExpr.operands[0]);
+		} else if (constExpr.operands.length > 1) { // If there was more than one constant, collapse all the constants together before adding the result to the end of the expression operand vector.
+			var pResult = constExpr.evaluate();
+			expr.addOperand(new ConstantExpression(pResult));
+		}
+
+        return expr;
+	};
+
+	proto.addDependencies = function addDependencies(deps){
+		for(var i = 0, l = this.operands.length; i < l; ++i)
+			this.operands[i].addDependencies(deps);
+		return deps;
+	};
+
+	/**Add an operand to the n-ary expression.
+	| @param pExpression the expression to add
+	**/
+	proto.addOperand = function addOperand(expr) {
+		this.operands.push(expr);
+	};
+
+	proto.getFactory = function getFactory() {
+		return undefined;
+	};
+
+	proto.toJson = function toJson() {
+		var o = {};
+		o[this.getOpName()] = this.operands.map(function(operand){
+			return operand.toJson();
+		});
+		return o;
+	};
+
+//TODO:	proto.toBson  ?
+//TODO:	proto.addToBsonObj  ?
+//TODO: proto.addToBsonArray  ?
+
+	/**Checks the current size of vpOperand; if the size equal to or
+	| greater than maxArgs, fires a user assertion indicating that this
+	| operator cannot have this many arguments.
+	| The equal is there because this is intended to be used in addOperand() to check for the limit *before* adding the requested argument.
+	| @param maxArgs the maximum number of arguments the operator accepts
+	**/
+	proto.checkArgLimit = function checkArgLimit(maxArgs) {
+		if (this.operands.length >= maxArgs) throw new Error(this.getOpName() + " only takes " + maxArgs + " operand" + (maxArgs == 1 ? "" : "s") + "; code 15993");
+	};
+
+	/**Checks the current size of vpOperand; if the size is not equal to reqArgs, fires a user assertion indicating that this must have exactly reqArgs arguments.
+	| This is meant to be used in evaluate(), *before* the evaluation takes place.
+	| @param reqArgs the number of arguments this operator requires
+	**/
+	proto.checkArgCount = function checkArgCount(reqArgs) {
+		if (this.operands.length !== reqArgs) throw new Error(this.getOpName() + ":  insufficient operands; " + reqArgs + " required, only got " + this.operands.length + "; code 15997");
+	};
+
+	return klass;
+})();

+ 30 - 0
lib/pipeline/expressions/NotExpression.js

@@ -0,0 +1,30 @@
+var NotExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** An $not pipeline expression. @see evaluate **/
+	var klass = function NotExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$not";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Returns the boolean opposite value passed to it. When passed a true value, $not returns false; when passed a false value, $not returns true. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var op = this.operands[0].evaluate(doc);
+		return !Value.coerceToBool(op);
+	};
+
+	return klass;
+})();

+ 396 - 0
lib/pipeline/expressions/ObjectExpression.js

@@ -0,0 +1,396 @@
+var ObjectExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** Create an empty expression.  Until fields are added, this will evaluate to an empty document (object). **/
+	var klass = function ObjectExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		this._excludeId = false;	/// <Boolean> for if _id is to be excluded
+		this._expressions = {};	/// <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
+		this._order = []; /// <Array<String>> this is used to maintain order for generated fields not in the source document
+	}, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+
+	// INSTANCE VARIABLES
+	/** <Boolean> for if _id is to be excluded **/
+	proto._excludeId = undefined;
+
+	/** <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document **/
+	proto._expressions = undefined;
+
+	/** <Array<String>> this is used to maintain order for generated fields not in the source document **/
+	proto._order = [];
+
+
+	// PROTOTYPE MEMBERS
+
+	/** evaluate(), but return a Document instead of a Value-wrapped Document.
+	| @param pDocument the input Document
+	| @returns the result document
+	**/
+	proto.evaluateDocument = function evaluateDocument(doc){
+		throw new Error("FINISH evaluateDocument");	//TODO:...
+		/*
+		intrusive_ptr<Document> ExpressionObject::evaluateDocument(
+			const intrusive_ptr<Document> &pDocument) const {
+			// create and populate the result
+			intrusive_ptr<Document> pResult(
+				Document::create(getSizeHint()));
+
+			addToDocument(pResult,
+						Document::create(), // No inclusion field matching.
+						pDocument);
+			return pResult;
+		}
+		*/
+	};
+
+	proto.evaluate = function evaluate(doc){
+		throw new Error("FINISH evaluate");	//TODO:...
+		//return Value::createDocument(evaluateDocument(pDocument));
+	};
+
+	proto.optimize = function optimize(){
+		for (var key in this._expressions) {
+			var expr = this._expressions[key];
+			if (expr !== undefined && expr !== null) this._expressions[key] = expr.optimize();
+		}
+		return this;
+	};
+
+	proto.getIsSimple = function getIsSimple(){
+		for (var key in this._expressions) {
+			var expr = this._expressions[key];
+			if (expr !== undefined && expr !== null && !expr.getIsSimple()) return false;
+		}
+		return true;
+	};
+
+	proto.addDependencies = function addDependencies(deps, path){
+		var pathStr;
+		if (path instanceof Array) {
+			if (path.length === 0) {
+				// we are in the top level of a projection so _id is implicit
+				if (!this._excludeId) deps.insert("_id");
+			} else {
+				pathStr = new FieldPath(path).getPath() + ".";
+			}
+		} else {
+			if (this._excludeId) throw new Error("_excludeId is true!");
+		}
+		for (var key in this._expressions) {
+			var expr = this._expressions[key];
+			if (expr !== undefined && expr !== null){
+				if (path instanceof Array) path.push(key);
+				expr.addDependencies(deps, path);
+				if (path instanceof Array) path.pop();
+			}else{ // inclusion
+				if(path === undefined || path === null) throw new Error("inclusion not supported in objects nested in $expressions; code 16407");
+				deps.insert(pathStr + key);
+			}
+		}
+		return deps;
+	};
+
+	/** evaluate(), but add the evaluated fields to a given document instead of creating a new one.
+	| @param pResult the Document to add the evaluated expressions to
+	| @param pDocument the input Document for this level
+	| @param rootDoc the root of the whole input document
+	**/
+	proto.addToDocument = function addToDocument(result, document, rootDoc){
+		throw new Error("FINISH addToDocument");	//TODO:...
+/*
+	void ExpressionObject::addToDocument(
+		const intrusive_ptr<Document> &pResult,
+		const intrusive_ptr<Document> &pDocument,
+		const intrusive_ptr<Document> &rootDoc
+		) const
+	{
+		const bool atRoot = (pDocument == rootDoc);
+
+		ExpressionMap::const_iterator end = _expressions.end();
+
+		// This is used to mark fields we've done so that we can add the ones we haven't
+		set<string> doneFields;
+
+		FieldIterator fields(pDocument);
+		while(fields.more()) {
+			Document::FieldPair field (fields.next());
+
+			ExpressionMap::const_iterator exprIter = _expressions.find(field.first);
+
+			// This field is not supposed to be in the output (unless it is _id)
+			if (exprIter == end) {
+				if (!_excludeId && atRoot && field.first == Document::idName) {
+					// _id from the root doc is always included (until exclusion is supported)
+					// not updating doneFields since "_id" isn't in _expressions
+					pResult->addField(field.first, field.second);
+				}
+				continue;
+			}
+
+			// make sure we don't add this field again
+			doneFields.insert(exprIter->first);
+
+			Expression* expr = exprIter->second.get();
+
+			if (!expr) {
+				// This means pull the matching field from the input document
+				pResult->addField(field.first, field.second);
+				continue;
+			}
+
+			ExpressionObject* exprObj = dynamic_cast<ExpressionObject*>(expr);
+			BSONType valueType = field.second->getType();
+			if ((valueType != Object && valueType != Array) || !exprObj ) {
+				// This expression replace the whole field
+
+				intrusive_ptr<const Value> pValue(expr->evaluate(rootDoc));
+
+				// don't add field if nothing was found in the subobject
+				if (exprObj && pValue->getDocument()->getFieldCount() == 0)
+					continue;
+
+				// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
+				// TODO make missing distinct from Undefined
+				if (pValue->getType() != Undefined)
+					pResult->addField(field.first, pValue);
+
+
+				continue;
+			}
+			// Check on the type of the input value.  If it's an object, just walk down into that recursively, and add it to the result.
+			if (valueType == Object) {
+				intrusive_ptr<Document> doc = Document::create(exprObj->getSizeHint());
+				exprObj->addToDocument(doc,
+									field.second->getDocument(),
+									rootDoc);
+				pResult->addField(field.first, Value::createDocument(doc));
+			}
+			else if (valueType == Array) {
+				// If it's an array, we have to do the same thing, but to each array element.  Then, add the array of results to the current document.
+				vector<intrusive_ptr<const Value> > result;
+				intrusive_ptr<ValueIterator> pVI(field.second->getArray());
+				while(pVI->more()) {
+					intrusive_ptr<const Value> next =  pVI->next();
+
+					// can't look for a subfield in a non-object value.
+					if (next->getType() != Object)
+						continue;
+
+					intrusive_ptr<Document> doc = Document::create(exprObj->getSizeHint());
+					exprObj->addToDocument(doc,
+										next->getDocument(),
+										rootDoc);
+					result.push_back(Value::createDocument(doc));
+				}
+
+				pResult->addField(field.first,
+									Value::createArray(result));
+			}
+			else {
+				verify( false );
+			}
+		}
+		if (doneFields.size() == _expressions.size())
+			return;
+
+		// add any remaining fields we haven't already taken care of
+		for (vector<string>::const_iterator i(_order.begin()); i!=_order.end(); ++i) {
+			ExpressionMap::const_iterator it = _expressions.find(*i);
+			string fieldName(it->first);
+
+			// if we've already dealt with this field, above, do nothing
+			if (doneFields.count(fieldName))
+				continue;
+
+			// this is a missing inclusion field
+			if (!it->second)
+				continue;
+
+			intrusive_ptr<const Value> pValue(it->second->evaluate(rootDoc));
+
+			// Don't add non-existent values (note:  different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
+			if (pValue->getType() == Undefined)
+				continue;
+
+			// don't add field if nothing was found in the subobject
+			if (dynamic_cast<ExpressionObject*>(it->second.get())
+					&& pValue->getDocument()->getFieldCount() == 0)
+				continue;
+
+
+			pResult->addField(fieldName, pValue);
+		}
+	}
+*/
+	};
+
+	/** estimated number of fields that will be output **/
+	proto.getSizeHint = function getSizeHint(){
+		throw new Error("FINISH getSizeHint");	//TODO:...
+		/*
+		// Note: this can overestimate, but that is better than underestimating
+		return _expressions.size() + (_excludeId ? 0 : 1);
+		*/
+	};
+
+	/** Add a field to the document expression.
+	| @param fieldPath the path the evaluated expression will have in the result Document
+	| @param pExpression the expression to evaluate obtain this field's Value in the result Document
+	**/
+	proto.addField = function addField(path, pExpression){
+		var fieldPart = path.fields[0],
+			haveExpr = this._expressions.hasOwnProperty(fieldPart),
+			expr = this._expressions[fieldPart];
+var subObj = expr instanceof ObjectExpression ? expr : undefined;
+
+		if(!haveExpr){
+			this._order.push(fieldPart);
+		}
+
+		throw new Error("FINISH addField");	//TODO:...
+		/*
+		void ExpressionObject::addField(const FieldPath &fieldPath, const intrusive_ptr<Expression> &pExpression) {
+			const string fieldPart = fieldPath.getFieldName(0);
+			const bool haveExpr = _expressions.count(fieldPart);
+
+			intrusive_ptr<Expression>& expr = _expressions[fieldPart]; // inserts if !haveExpr
+			intrusive_ptr<ExpressionObject> subObj = dynamic_cast<ExpressionObject*>(expr.get());
+
+			if (!haveExpr) {
+				_order.push_back(fieldPart);
+			}
+			else { // we already have an expression or inclusion for this field
+				if (fieldPath.getPathLength() == 1) {
+					// This expression is for right here
+
+					ExpressionObject* newSubObj = dynamic_cast<ExpressionObject*>(pExpression.get());
+					uassert(16400, str::stream()
+									<< "can't add an expression for field " << fieldPart
+									<< " because there is already an expression for that field"
+									<< " or one of its sub-fields.",
+							subObj && newSubObj); // we can merge them
+
+					// Copy everything from the newSubObj to the existing subObj
+					// This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
+					for (vector<string>::const_iterator it (newSubObj->_order.begin());
+														it != newSubObj->_order.end();
+														++it) {
+						// asserts if any fields are dupes
+						subObj->addField(*it, newSubObj->_expressions[*it]);
+					}
+					return;
+				}
+				else {
+					// This expression is for a subfield
+					uassert(16401, str::stream()
+							<< "can't add an expression for a subfield of " << fieldPart
+							<< " because there is already an expression that applies to"
+							<< " the whole field",
+							subObj);
+				}
+			}
+
+			if (fieldPath.getPathLength() == 1) {
+				verify(!haveExpr); // haveExpr case handled above.
+				expr = pExpression;
+				return;
+			}
+
+			if (!haveExpr)
+				expr = subObj = ExpressionObject::create();
+
+			subObj->addField(fieldPath.tail(), pExpression);
+		}
+		*/
+	};
+
+	/** Add a field path to the set of those to be included.
+	| Note that including a nested field implies including everything on the path leading down to it.
+	| @param fieldPath the name of the field to be included
+	**/
+	proto.includePath = function includePath(path){
+		this.addField(path);
+	};
+
+	/** Get a count of the added fields.
+	| @returns how many fields have been added
+	**/
+	proto.getFieldCount = function getFieldCount(){
+		var e; console.warn(e=new Error("CALLER SHOULD BE USING #expressions.length instead!")); console.log(e.stack);
+		return this._expressions.length;
+	};
+
+/*TODO: ... remove this?
+	inline ExpressionObject::BuilderPathSink::BuilderPathSink(
+		BSONObjBuilder *pB):
+		pBuilder(pB) {
+	}
+*/
+
+/*TODO: ... remove this?
+	inline ExpressionObject::PathPusher::PathPusher(
+		vector<string> *pTheVPath, const string &s):
+		pvPath(pTheVPath) {
+		pvPath->push_back(s);
+	}
+
+	inline ExpressionObject::PathPusher::~PathPusher() {
+		pvPath->pop_back();
+	}
+*/
+
+/** Specialized BSON conversion that allows for writing out a $project specification.
+| This creates a standalone object, which must be added to a containing object with a name
+| @param pBuilder where to write the object to
+| @param requireExpression see Expression::addToBsonObj
+**/
+//TODO:	proto.documentToBson = ...?
+//TODO:	proto.addToBsonObj = ...?
+//TODO: proto.addToBsonArray = ...?
+
+/*
+/// Visitor abstraction used by emitPaths().  Each path is recorded by calling path().
+		class PathSink {
+		public:
+			virtual ~PathSink() {};
+			/// Record a path.
+			/// @param path the dotted path string
+			/// @param include if true, the path is included; if false, the path is excluded
+			virtual void path(const string &path, bool include) = 0;
+		};
+
+/// Utility object for collecting emitPaths() results in a BSON object.
+		class BuilderPathSink :
+			public PathSink {
+		public:
+			// virtuals from PathSink
+			virtual void path(const string &path, bool include);
+
+/// Create a PathSink that writes paths to a BSONObjBuilder, to create an object in the form of { path:is_included,...}
+/// This object uses a builder pointer that won't guarantee the lifetime of the builder, so make sure it outlasts the use of this for an emitPaths() call.
+/// @param pBuilder to the builder to write paths to
+			BuilderPathSink(BSONObjBuilder *pBuilder);
+
+		private:
+			BSONObjBuilder *pBuilder;
+		};
+
+/// utility class used by emitPaths()
+		class PathPusher :
+			boost::noncopyable {
+		public:
+			PathPusher(vector<string> *pvPath, const string &s);
+			~PathPusher();
+
+		private:
+			vector<string> *pvPath;
+		};
+*/
+
+//void excludeId(bool b) { _excludeId = b; }
+
+	return klass;
+})();

+ 68 - 0
lib/pipeline/expressions/OrExpression.js

@@ -0,0 +1,68 @@
+var OrExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** An $or pipeline expression. @see evaluate **/
+	var klass = function OrExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value"),
+		ConstantExpression = require("./ConstantExpression"),
+		CoerceToBoolExpression = require("./CoerceToBoolExpression");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$or";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes an array of one or more values and returns true if any of the values in the array are true. Otherwise $or returns false. **/
+	proto.evaluate = function evaluate(doc){
+		for(var i = 0, n = this.operands.length; i < n; ++i){
+			var value = this.operands[i].evaluate(doc);
+			if(Value.coerceToBool(value)) return true;
+		}
+		return false;
+	};
+
+	proto.optimize = function optimize() {
+		var pE = base.optimize(); // optimize the disjunction as much as possible
+
+		if (!(pE instanceof OrExpression)) return pE; // if the result isn't a disjunction, we can't do anything
+		var pOr = pE;
+
+		// Check the last argument on the result; if it's not const (as promised
+		// by ExpressionNary::optimize(),) then there's nothing we can do.
+		if (!pOr.operands.length) throw new Error("OrExpression must have operands!");
+		var n = pOr.operands.length;
+		// ExpressionNary::optimize() generates an ExpressionConstant for {$or:[]}.
+		var pLast = pOr.operands[n - 1];
+		var pConst = pE;
+		if (!(pConst instanceof ConstantExpression)) return pE;
+
+		// Evaluate and coerce the last argument to a boolean.  If it's true, then we can replace this entire expression.
+		var last = Value.coerceToBool(pLast.evaluate());
+		if (last) return new ConstantExpression(true);
+
+		// If we got here, the final operand was false, so we don't need it anymore.
+		// If there was only one other operand, we don't need the conjunction either.  Note we still need to keep the promise that the result will be a boolean.
+		if (n == 2) return new CoerceToBoolExpression(pOr.operands[0]);
+
+		// Remove the final "false" value, and return the new expression.
+		pOr.operands.length = n - 1;
+		return pE;
+	};
+
+//TODO: proto.toMatcherBson = ...?
+
+	proto.getFactory = function getFactory(){
+		return klass;	// using the ctor rather than a separate .create() method
+	};
+
+	return klass;
+})();

+ 27 - 0
lib/pipeline/expressions/SecondExpression.js

@@ -0,0 +1,27 @@
+var SecondExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $second pipeline expression. @see evaluate **/
+	var klass = function SecondExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$second";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the second between 0 and 59, but can be 60 to account for leap seconds. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return date.getSeconds();	//TODO: incorrect for last second of leap year, need to fix...
+	};
+
+	return klass;
+})();

+ 34 - 0
lib/pipeline/expressions/StrcasecmpExpression.js

@@ -0,0 +1,34 @@
+var StrcasecmpExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $strcasecmp pipeline expression. @see evaluate **/
+	var klass = function StrcasecmpExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+var Value = require("../Value"),
+	NaryExpression = require("./NaryExpression");
+
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$strcasecmp";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(2);
+		base.addOperand(expr);
+	};
+
+	/** Takes in two strings. Returns a number. $strcasecmp is positive if the first string is “greater than” the second and negative if the first string is “less than” the second. $strcasecmp returns 0 if the strings are identical. **/
+	proto.evaluate = function evaluate(doc){
+		this.checkArgCount(2);
+		var val1 = this.operands[0].evaluate(doc),
+			val2 = this.operands[1].evaluate(doc),
+			str1 = Value.coerceToString(val1).toUpperCase(),
+			str2 = Value.coerceToString(val2).toUpperCase(),
+			cmp = Value.compare(str1, str2);
+		return cmp;
+	};
+
+	return klass;
+})();

+ 36 - 0
lib/pipeline/expressions/SubstrExpression.js

@@ -0,0 +1,36 @@
+var SubstrExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $substr pipeline expression. @see evaluate **/
+	var klass = function SubstrExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$substr";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(3);
+		base.addOperand(expr);
+	};
+
+	/** Takes a string and two numbers. The first number represents the number of bytes in the string to skip, and the second number specifies the number of bytes to return from the string. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(3);
+		var val = this.operands[0].evaluate(doc),
+			idx = this.operands[1].evaluate(doc),
+			len = this.operands[2].evaluate(doc),
+			str = Value.coerceToString(val);
+		if (typeof(idx) != "number") throw new Error(this.getOpName() + ": starting index must be a numeric type; code 16034");
+		if (typeof(len) != "number") throw new Error(this.getOpName() + ": length must be a numeric type; code 16035");
+		if (idx >= str.length) return "";
+		return str.substr(idx, len);
+	};
+
+	return klass;
+})();

+ 32 - 0
lib/pipeline/expressions/SubtractExpression.js

@@ -0,0 +1,32 @@
+var SubtractExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $subtract pipeline expression. @see evaluate **/
+	var klass = function SubtractExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$subtract";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(2);
+		base.addOperand(expr);
+	};
+
+	/** Takes an array that contains a pair of numbers and subtracts the second from the first, returning their difference. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(2);
+		var left = this.operands[0].evaluate(doc),
+			right = this.operands[1].evaluate(doc);
+		if(left instanceof Date || right instanceof Date) throw new Error("$subtract does not support dates; code 16376");
+		return left - right;
+	};
+
+	return klass;
+})();

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

@@ -0,0 +1,31 @@
+var ToLowerExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $toLower pipeline expression. @see evaluate **/
+	var klass = function ToLowerExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$toLower";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(1);
+		var val = this.operands[0].evaluate(doc),
+			str = Value.coerceToString(val);
+		return str.toLowerCase();
+	};
+
+	return klass;
+})();

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

@@ -0,0 +1,31 @@
+var ToUpperExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $toUpper pipeline expression. @see evaluate **/
+	var klass = function ToUpperExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$toUpper";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(1);
+		var val = this.operands[0].evaluate(doc),
+			str = Value.coerceToString(val);
+		return str.toUpperCase();
+	};
+
+	return klass;
+})();

+ 36 - 0
lib/pipeline/expressions/WeekExpression.js

@@ -0,0 +1,36 @@
+var WeekExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $week pipeline expression. @see evaluate **/
+	var klass = function WeekExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value"),
+		DayOfYearExpression = require("./DayOfYearExpression");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$week";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the week of the year as a number between 0 and 53. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc),
+			dayOfWeek = date.getDay(),
+			dayOfYear = DayOfYearExpression.getDateDayOfYear(date),
+			prevSundayDayOfYear = dayOfYear - dayOfWeek,	// may be negative
+			nextSundayDayOfYear = prevSundayDayOfYear + 7;	// must be positive
+        // Return the zero based index of the week of the next sunday, equal to the one based index of the week of the previous sunday, which is to be returned.
+		return (nextSundayDayOfYear / 7) | 0; // also, the `| 0` here truncates this so that we return an integer
+	};
+
+	return klass;
+})();

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

@@ -0,0 +1,31 @@
+var YearExpression = module.exports = (function(){
+	// CONSTRUCTOR
+	/** A $year pipeline expression. @see evaluate **/
+	var klass = function YearExpression(){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+	}, base = require("./NaryExpression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var Value = require("../Value"),
+		DayOfYearExpression = require("./DayOfYearExpression");
+
+	// PROTOTYPE MEMBERS
+	proto.getOpName = function getOpName(){
+		return "$year";
+	};
+
+	proto.addOperand = function addOperand(expr) {
+		this.checkArgLimit(1);
+		base.addOperand(expr);
+	};
+
+	/** Takes a date and returns the full year. **/
+	proto.evaluate = function evaluate(doc) {
+		this.checkArgCount(1);
+		var date = this.operands[0].evaluate(doc);
+		return date.getFullYear();
+	};
+
+	return klass;
+})();

+ 1 - 0
munge.js

@@ -0,0 +1 @@
+module.exports = require("./lib/munge");

+ 298 - 0
npm_scripts/test/test.sh

@@ -0,0 +1,298 @@
+#!/bin/bash -e
+# Script for running various package tests via the NPM 'test' sub-command.
+# Configuration occurs either through the environment variables set thru the
+# config section of the package.json file or via identical command line options.
+###############################################################################
+CMD_PWD=$(pwd)
+CMD="$0"
+CMD_DIR=$(cd "$(dirname "$CMD")"; pwd)
+
+# Defaults and command line options
+VERBOSE=
+DEBUG=
+NO_SYNTAX=
+NO_UNIT=
+NO_COVERAGE=
+
+# Shortcut for running echo and then exit
+die() {
+	echo "$1" 1>&2
+	[ -n "$2" ] && exit $2 || exit 1
+}
+# Show help function to be used below
+show_help() {
+	awk 'NR>1,/^(###|$)/{print $0; exit}' "$CMD"
+	echo "USAGE: $(basename "$CMD") [arguments]"
+	echo "ARGS:"
+	MSG=$(awk '/^NARGS=-1; while/,/^esac; done/' "$CMD" | sed -e 's/^[[:space:]]*/  /' -e 's/|/, /' -e 's/)//' | grep '^  -')
+	EMSG=$(eval "echo \"$MSG\"")
+	echo "$EMSG"
+}
+# Parse command line options (odd formatting to simplify show_help() above)
+NARGS=-1; while [ "$#" -ne "$NARGS" ]; do NARGS=$#; case $1 in
+	# SWITCHES
+	-h|--help)        # This help message
+		show_help; exit 1; ;;
+	-d|--debug)       # Enable debugging messages (implies verbose)
+		DEBUG=$(( $DEBUG + 1 )) && VERBOSE="$DEBUG" && shift && echo "#-INFO: DEBUG=$DEBUG (implies VERBOSE=$VERBOSE)"; ;;
+	-v|--verbose)     # Enable verbose messages
+		VERBOSE=$(( $VERBOSE + 1 )) && shift && echo "#-INFO: VERBOSE=$VERBOSE"; ;;
+	-S|--no-syntax)   # Disable syntax tests
+		NO_SYNTAX=$(( $NO_SYNTAX + 1 )) && shift && echo "#-INFO: NO_SYNTAX=$NO_SYNTAX"; ;;
+	-U|--no-unit)     # Disable unit tests
+		NO_UNIT=$(( $NO_UNIT + 1 )) && shift && echo "#-INFO: NO_UNIT=$NO_UNIT"; ;;
+	-C|--no-coverage) # Enable coverage tests
+		NO_COVERAGE=$(( $NO_COVERAGE + 1 )) && shift && echo "#-INFO: NO_COVERAGE=$NO_COVERAGE"; ;;
+	# PAIRS
+#	-t|--thing)	 # Set a thing to a value (DEFAULT: $THING)
+#		shift && THING="$1" && shift && [ -n "$VERBOSE" ] && echo "#-INFO: THING=$THING"; ;;
+esac; done
+
+###############################################################################
+
+[ $# -eq 0 ] || die "ERROR: Unexpected commands!"
+
+# Enable debug messages in silly mode
+[ "$npm_config_loglevel" = "silly" ] && DEBUG=1
+[ -n "$DEBUG" ] && set -x
+
+# Show all of the package config variables for debugging if non-standard loglevel
+[ -n "$npm_config_loglevel" ] && [ "$npm_config_loglevel" != "http" ] && VERBOSE=1
+[ -n "$VERBOSE" ] && env | egrep -i '^(npm|jenkins)_' | sort | sed 's/^/#-INFO: /g'
+
+# Change to root directory of package
+cd "$CMD_DIR/../../"	 # assuming that this is $PKG_ROOT/npm_scripts/MyAwesomeScript/MyAwesomeScript.sh or similar
+[ -f "package.json" ] || die "ERROR: Unable to find the \"package.json\" file in \"$(pwd)\"!"
+
+# Basic sanity check for node_modules directory (to ensure that 'npm install' has been run)
+[ -d "node_modules" ] || die "ERROR: Unable to find the \"node_modules\" dir in \"$(pwd)\"!. Run \"npm install\" first!"
+
+# Determing package name
+PKG_NAME="$npm_package_name"
+[ -n "$PKG_NAME" ] || PKG_NAME="$npm_config_package_name"
+[ -n "$PKG_NAME" ] || PKG_NAME=$(node -e 'console.log(require("./package.json").name)')
+[ -n "$PKG_NAME" ] || die "ERROR: Unable to determine package name! Broken package?"
+
+# Determine code directory
+CODE_DIR="$npm_package_config_code_dir"
+[ -n "$CODE_DIR" ] && [ -d "$CODE_DIR" ] || CODE_DIR="$npm_config_default_code_dir"
+[ -n "$CODE_DIR" ] && [ -d "$CODE_DIR" ] || CODE_DIR="lib"
+[ -n "$CODE_DIR" ] && [ -d "$CODE_DIR" ] || die "ERROR: Unable to find code directory at \"$CODE_DIR\"!"
+CODE_DIR=$(echo "$CODE_DIR" | sed 's/\/$//')	# remove trailing slash
+[ -n "$VERBOSE" ] && echo "CODE_DIR=$CODE_DIR"
+
+# Determine test directory
+TEST_DIR="$npm_package_config_test_dir"
+[ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ] || TEST_DIR="$npm_config_default_test_dir"
+[ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ] || TEST_DIR="test/lib"
+[ -d "$TEST_DIR" ] || die "ERROR: Unable to find test directory at \"$TEST_DIR\"!"
+TEST_DIR=$(echo "$TEST_DIR" | sed 's/\/$//')	# remove trailing slash
+[ -n "$VERBOSE" ] && echo "TEST_DIR=$TEST_DIR"
+
+# Helper to check if given file is valid XML
+XMLLINT_BIN=$(which xmllint || echo)
+validate_xml() {
+	REPORT_FILE="$1"
+	if [ -n "$XMLLINT_BIN" ]; then
+		if [ -s "$REPORT_FILE" ]; then
+			"$XMLLINT_BIN" --noout "$REPORT_FILE" || die "ERROR: Invalid XML in \"$REPORT_FILE\"!"
+		else
+			echo "WARNING: expected XML data in empty file at \"$REPORT_FILE\"."
+		fi
+	else
+		echo "WARNING: xmllint not in PATH so skipping XML check of \"$REPORT_FILE\"."
+	fi
+}
+
+# Syntax tests
+[ "$npm_package_config_test_syntax" = "false" ] && NO_SYNTAX=1
+if [ -z "$NO_SYNTAX" ]; then
+	echo "Running syntax checks ..."
+
+	# Deps
+	JSHINT_BIN="$npm_package_config_jshint_bin"
+	#[ -n "$JSHINT_BIN" ] && [ -x "$JSHINT_BIN" ] || JSHINT_BIN=$(which jshint || echo)
+	[ -n "$JSHINT_BIN" ] && [ -x "$JSHINT_BIN" ] || JSHINT_BIN="./node_modules/.bin/jshint"
+	[ -n "$JSHINT_BIN" ] && [ -x "$JSHINT_BIN" ] || die "ERROR: Unable to find 'jshint' binary! Install via 'npm install jshint' to proceed!"
+
+	# Prep
+	JSHINT_OUTPUT_DIR="$npm_package_config_jshint_output_dir"
+	[ -n "$JSHINT_OUTPUT_DIR" ] || JSHINT_OUTPUT_DIR="$npm_config_default_jshint_output_dir"
+	[ -n "$JSHINT_OUTPUT_DIR" ] || [ -n "$npm_config_default_reports_output_dir" ] && JSHINT_OUTPUT_DIR="$npm_config_default_reports_output_dir/syntax"
+	[ -n "$JSHINT_OUTPUT_DIR" ] || JSHINT_OUTPUT_DIR="reports/syntax"
+	[ -d "$JSHINT_OUTPUT_DIR" ] || mkdir -p "$JSHINT_OUTPUT_DIR" || die "ERROR: Unable to mkdir \"$JSHINT_OUTPUT_DIR\", the jshint output dir!"
+
+	# Exec require on all js files
+	echo "  Testing via NodeJS require function ..."
+    node -e "[$(find "./$CODE_DIR" "./$TEST_DIR" -type f -name '*.js' -not -iregex '.*/public/.*' -not -iregex '.*/node_modules/.*' | sed -e 's/^/ "/' -e 's/$/",/')].forEach(require);"	\
+		|| die "ERROR: NodeJS require error!"
+
+	# Exec jshint to get jslint output	#TODO: is this even needed?
+	echo "  Checking via JSHint jslint reporter ..."
+	REPORT_FILE="$JSHINT_OUTPUT_DIR/$PKG_NAME-jshint-jslint.xml"
+	"$JSHINT_BIN" --extra-ext ".js,.json" --jslint-reporter "$CODE_DIR" "$TEST_DIR" &> "$REPORT_FILE"	\
+		|| die "ERROR: JSHint errors on jslint reporter! $(echo; cat "$REPORT_FILE")"
+	[ -n "$VERBOSE" ] && echo "REPORT OUTPUT: $REPORT_FILE" && cat "$REPORT_FILE" && echo
+	validate_xml "$REPORT_FILE" || die "ERROR: INVALID REPORT FILE!"
+
+	# Exec jshint to get checkstyle output
+	echo "  Checking via JSHint checkstyle reporter ..."
+	REPORT_FILE="$JSHINT_OUTPUT_DIR/$PKG_NAME-jshint-checkstyle.xml"
+	"$JSHINT_BIN" --extra-ext ".js,.json" --checkstyle-reporter "$CODE_DIR" "$TEST_DIR" > "$REPORT_FILE"	\
+		|| die "ERROR: JSHint errors on checkstyle reporter! $(echo; cat "$REPORT_FILE")"
+	echo "    ERRORS: $(egrep -c '<error .* severity="error"' "$REPORT_FILE")"
+	echo "    WARNINGS: $(egrep -c '<error .* severity="warning"' "$REPORT_FILE")"
+	[ -n "$VERBOSE" ] && echo "REPORT OUTPUT: $REPORT_FILE" && cat "$REPORT_FILE" && echo
+	validate_xml "$REPORT_FILE" || die "ERROR: INVALID REPORT FILE!"
+
+	echo "  Checking custom code rules ..."
+	BAD_INSTANCEOF=$(egrep --include '*.js' --recursive ' instanceof (Boolean|Number|String)' "$CODE_DIR" || true)
+	[ -z "$BAD_INSTANCEOF" ] || die "ERROR: Found uses of instanceof that are likely to be broken! $(echo; echo "$BAD_INSTANCEOF")"
+
+	echo
+fi
+
+# Unit tests
+[ "$npm_package_config_test_unit" = "false" ] && NO_UNIT=1
+if [ -z "$NO_UNIT" ]; then
+	echo "Running unit tests ..."
+
+	# Deps
+	MOCHA_BIN="$npm_package_config_mocha_bin"
+	[ -n "$MOCHA_BIN" ] && [ -x "$MOCHA_BIN" ] || MOCHA_BIN=$(which mocha || echo)
+	[ -n "$MOCHA_BIN" ] && [ -x "$MOCHA_BIN" ] || die "ERROR: Unable to find 'mocha' binary! Install via 'npm install mocha' to proceed!"
+
+	# Prep
+	MOCHA_REPORTER="spec"
+	if [ -n "$JENKINS_URL" ]; then
+		MOCHA_REPORTER="$npm_package_config_test_reporter"
+		[ -n "$MOCHA_REPORTER" ] || MOCHA_REPORTER="xunit"
+	fi
+	MOCHA_OUTPUT_DIR="$npm_package_config_mocha_output_dir"
+	[ -n "$MOCHA_OUTPUT_DIR" ] || MOCHA_OUTPUT_DIR="$npm_config_default_mocha_output_dir"
+	[ -n "$MOCHA_OUTPUT_DIR" ] || [ -n "$npm_config_default_reports_output_dir" ] && MOCHA_OUTPUT_DIR="$npm_config_default_reports_output_dir/unit"
+	[ -n "$MOCHA_OUTPUT_DIR" ] || MOCHA_OUTPUT_DIR="reports/unit"
+	[ -d "$MOCHA_OUTPUT_DIR" ] || mkdir -p "$MOCHA_OUTPUT_DIR" || die "ERROR: Unable to mkdir \"$MOCHA_OUTPUT_DIR\", the mocha output dir!"
+
+	# Exec
+	[ "$MOCHA_REPORTER" == "xunit" ] && UNIT_TEST_EXTENSION=xml || UNIT_TEST_EXTENSION=txt
+	[ "$MOCHA_REPORTER" == "xunit" ] && MOCHA_EXTRA_FLAGS= || MOCHA_EXTRA_FLAGS=--colors
+
+	REPORT_FILE_BASE="$MOCHA_OUTPUT_DIR/$PKG_NAME-report"
+	REPORT_FILE="$REPORT_FILE_BASE.$UNIT_TEST_EXTENSION"
+	REPORT_FILE_ERR="$REPORT_FILE_BASE.err"
+
+	LOGGER_PREFIX='' LOGGER_LEVEL=NOTICE "$MOCHA_BIN" --ui exports --reporter "$MOCHA_REPORTER" $MOCHA_EXTRA_FLAGS --recursive "$TEST_DIR" 2> "$REPORT_FILE_ERR" 1> "$REPORT_FILE"	\
+		|| die "ERROR: Mocha errors during unit tests! $(echo; cat "$REPORT_FILE"; cat "$REPORT_FILE_ERR")"
+	[ -n "$VERBOSE" ] && echo "REPORT OUTPUT: $REPORT_FILE" && cat "$REPORT_FILE" && echo
+
+	[ -s "$REPORT_FILE" ] || die "ERROR: no report data, units tests probably failed!"
+
+	echo
+fi
+
+# Coverage tests
+[ "$npm_package_config_test_coverage" = "false" ] && NO_COVERAGE=1
+if [ -z "$NO_COVERAGE" ]; then
+	echo "Running coverage tests ..."
+
+	# Deps
+	JSCOVERAGE_BIN="$npm_package_config_jscoverage_bin"
+	[ -n "$JSCOVERAGE_BIN" ] && [ -x "$JSCOVERAGE_BIN" ] || JSCOVERAGE_BIN=$(which jscoverage || echo "./node_modules/visionmedia-jscoverage/jscoverage") # TODO: jscoverage does not install itself.  Renable this when it does.
+	[ -n "$JSCOVERAGE_BIN" ] && [ -x "$JSCOVERAGE_BIN" ] || die "$(cat<<-ERROR_DOCS_EOF
+		ERROR: Unable to find node.js jscoverage binary!
+		To install the nodejs jscoverage binary run the following commands:
+		# git clone https://github.com/visionmedia/node-jscoverage.git
+		# cd node-coverage
+		# ./configure && make && make install
+	ERROR_DOCS_EOF
+	)"
+
+	# Prep
+	JSCOVERAGE_OUTPUT_DIR="$npm_package_config_jscoverage_output_dir"
+	[ -n "$JSCOVERAGE_OUTPUT_DIR" ] || JSCOVERAGE_OUTPUT_DIR="$npm_config_default_jscoverage_output_dir"
+	[ -n "$JSCOVERAGE_OUTPUT_DIR" ] || [ -n "$npm_config_default_reports_output_dir" ] && JSCOVERAGE_OUTPUT_DIR="$npm_config_default_reports_output_dir/html/jscoverage"
+	[ -n "$JSCOVERAGE_OUTPUT_DIR" ] || JSCOVERAGE_OUTPUT_DIR="reports/html/jscoverage"
+	[ -d "$JSCOVERAGE_OUTPUT_DIR" ] || mkdir -p "$JSCOVERAGE_OUTPUT_DIR" || die "ERROR: Unable to mkdir \"$MOCHA_OUTPUT_DIR\", the mocha output dir!"
+	JSCOVERAGE_TMP_DIR="$CODE_DIR.jscoverage"
+	if [ -d "$JSCOVERAGE_TMP_DIR" ]; then
+		rm -fr "$JSCOVERAGE_TMP_DIR" || die "ERROR: Unable to remove obstruting \"$JSCOVERAGE_TMP_DIR\" temp directory!"
+	fi
+
+	# Exec
+	"$JSCOVERAGE_BIN" "$CODE_DIR" "$JSCOVERAGE_TMP_DIR"
+	# - Backup the actual code and replace it with jscoverage results
+	[ -n "$VERBOSE" ] && echo "Replacing $CODE_DIR with $JSCOVERAGE_TMP_DIR ..."
+
+	REPORT_FILE_BASE="$JSCOVERAGE_OUTPUT_DIR/$PKG_NAME-coverage"
+	REPORT_FILE="$REPORT_FILE_BASE.html"
+	REPORT_FILE_ERR="$REPORT_FILE_BASE.err"
+
+	mv "$CODE_DIR" "$CODE_DIR.ORIGINAL"	\
+		&& mv "$JSCOVERAGE_TMP_DIR" "$CODE_DIR"	\
+		&& LOGGER_PREFIX='' LOGGER_LEVEL=NOTICE "$MOCHA_BIN" --ui exports --reporter "html-cov" --recursive "$TEST_DIR" 2> "$REPORT_FILE_ERR" 1> "$REPORT_FILE"	\
+		|| echo "WARNING: JSCoverage: insufficient coverage (exit code $?)."
+#		|| die "ERROR: JSCoverage errors during coverage tests! $(rm -fr "$CODE_DIR" && mv "$CODE_DIR.ORIGINAL" "$CODE_DIR"; echo; cat "$REPORT_FILE")"
+#	[ -n "$VERBOSE" ] && echo "REPORT OUTPUT: $REPORT_FILE" && cat "$REPORT_FILE" && echo
+
+	# Cleanup
+	rm -rf "$CODE_DIR"	\
+		&& mv "$CODE_DIR.ORIGINAL" "$CODE_DIR"	\
+		|| die "ERROR: Unable to put code directory \"$CODE_DIR.ORIGNAL\" back where it belongs!"
+
+	#TODO: verifying reports should be part of checking test coverage
+
+	echo
+fi
+
+# This is used by both the PMD and jscheckstyle.
+ANALYSIS_TARGET="$npm_package_config_analyze_dirs"
+[ -n "$ANALYSIS_TARGET" ] || ANALYSIS_TARGET="$CODE_DIR"
+
+# Static analysis.
+[ "$npm_package_config_test_static_analysis" = "false" ] && NO_STATIC_ANALYSIS=1
+if [ -z "$NO_STATIC_ANALYSIS" ]; then
+	echo "Running static analysis ..."
+
+	PMD_BIN="$npm_package_config_pmd_bin"
+	[ -n "$PMD_BIN" ] && [ -x "$PMD_BIN" ] || PMD_BIN="/srv/jenkins/tools/pmd/bin/run.sh"
+
+	if [ -n "$PMD_BIN" ] && [ -x "$PMD_BIN" ]; then
+
+        PMD_OUTPUT_DIR="$npm_package_config_pmd_output_dir"
+        [ -n "$PMD_OUTPUT_DIR" ] || PMD_OUTPUT_DIR="$npm_package_config_pmd_output_dir"
+        [ -n "$PMD_OUTPUT_DIR" ] || [ -n "$npm_config_default_reports_output_dir" ] && PMD_OUTPUT_DIR="$npm_config_default_reports_output_dir/static-analysis"
+        [ -n "$PMD_OUTPUT_DIR" ] || PMD_OUTPUT_DIR="reports/static-analysis"
+        [ -d "$PMD_OUTPUT_DIR" ] || mkdir -p "$PMD_OUTPUT_DIR" || die "ERROR: Unable to mkdir \"$PMD_OUTPUT_DIR\", the PMD static analysis output dir!"
+
+        REPORT_FILE="$PMD_OUTPUT_DIR/$PKG_NAME-cpd.xml"
+
+        "$PMD_BIN" cpd --minimum-tokens 90 $(for TARGET in $ANALYSIS_TARGET; do echo "--files $TARGET "; done) --format xml --language js > "$REPORT_FILE" || echo "WARNING: PMD found issues (exit code $?)."
+		validate_xml "$REPORT_FILE" || die "ERROR: INVALID REPORT FILE!"
+
+        echo
+    fi
+fi
+
+# jscheckstyle, different than mocha's checkstyle.
+[ "$npm_package_config_test_jscheckstyle" = "false" ] && NO_JSCHECKSTYLE=1
+if [ -z "$NO_JSCHECKSTYLE" ]; then
+	echo "Running jscheckstyle ..."
+
+	JSCHECKSTYLE_BIN="$npm_package_config_jscheckstyle_bin"
+	#[ -n "$JSCHECKSTYLE_BIN" ] && [ -x "$JSCHECKSTYLE_BIN" ] || JSCHECKSTYLE_BIN=$(which jscheckstyle || echo)
+	[ -n "$JSCHECKSTYLE_BIN" ] && [ -x "$JSCHECKSTYLE_BIN" ] || JSCHECKSTYLE_BIN="./node_modules/.bin/jscheckstyle"
+	[ -n "$JSCHECKSTYLE_BIN" ] && [ -x "$JSCHECKSTYLE_BIN" ] || die "ERROR: Unable to find 'jscheckstyle' binary! Install via 'npm install jscheckstyle' to proceed!"
+
+	JSCHECKSTYLE_OUTPUT_DIR="$npm_package_config_jscheckstyle_output_dir"
+	[ -n "$JSCHECKSTYLE_OUTPUT_DIR" ] || JSCHECKSTYLE_OUTPUT_DIR="$npm_package_config_jscheckstyle_output_dir"
+	[ -n "$JSCHECKSTYLE_OUTPUT_DIR" ] || [ -n "$npm_config_default_reports_output_dir" ] && JSCHECKSTYLE_OUTPUT_DIR="$npm_config_default_reports_output_dir/jscheckstyle"
+	[ -n "$JSCHECKSTYLE_OUTPUT_DIR" ] || JSCHECKSTYLE_OUTPUT_DIR="reports/jscheckstyle"
+	[ -d "$JSCHECKSTYLE_OUTPUT_DIR" ] || mkdir -p "$JSCHECKSTYLE_OUTPUT_DIR" || die "ERROR: Unable to mkdir \"$JSCHECKSTYLE_OUTPUT_DIR\", the jscheckstyle output dir!"
+
+    REPORT_FILE="$JSCHECKSTYLE_OUTPUT_DIR/$PKG_NAME-jscheckstyle.xml"
+
+    "$JSCHECKSTYLE_BIN" --checkstyle $ANALYSIS_TARGET 2> /dev/null 1> "$REPORT_FILE" || echo "WARNING: jscheckstyle: code is too complex"
+	validate_xml "$REPORT_FILE" || die "ERROR: INVALID REPORT FILE!"
+fi
+

+ 43 - 0
package.json

@@ -0,0 +1,43 @@
+{
+	"name": "munge",
+	"version": "0.4.2+2013.02.22",
+	"description": "A JavaScript data munging pipeline based on the MongoDB aggregation framework.",
+	"author": "Rivera Group <support@riverainc.com>",
+	"contributors": [
+		"Adam Bell <ABell@riverainc.com>",
+		"Kyle Davis <KDavis@riverainc.com>",
+		"Phil Murray <PMurray@riverainc.com>",
+		"Spencer Rathbun <SRathbun@riverainc.com>",
+		"Charles Ezell <CEzell@riverainc.com>"
+	],
+	"main": "./munge.js",
+	"scripts": {
+		"test": "npm_scripts/test/test.sh"
+	},
+	"repository": {
+		"type": "svn",
+		"url": "http://svn.rcg.local/svn/devrd/eagle6/"
+	},
+	"keywords": [
+		"manipulation",
+		"alteration"
+	],
+	"dependencies": {
+	},
+	"devDependencies": {
+		"mocha": "*",
+		"jshint": "*",
+		"visionmedia-jscoverage": "*",
+		"jscheckstyle": "git+https://github.com/RiveraGroup/jscheckstyle.git"
+	},
+	"license": "AGPL",
+	"private": true,
+	"engine": {
+		"node": ">=0.8"
+	},
+	"config": {
+		"test_syntax": false,
+		"test_unit": false,
+		"test_coverage": false
+	}
+}

+ 146 - 0
test/lib/munge.js

@@ -0,0 +1,146 @@
+var jsext = require("jsext").install(),	//TODO: remove this...
+    assert = require("assert"),
+	alter = require("../../");
+
+module.exports = {
+
+	"alter": {
+
+		"should be able to use an empty pipeline (no-op)": function(){
+console.debug("");
+			var i = [1, 2, 3],
+				p = [],
+				e = [1, 2, 3],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+		"should be able to use a $skip operator": function(){
+console.debug("");
+			var i = [{_id:0}, {_id:1}, {_id:2}, {_id:3}, {_id:4}, {_id:5}],
+				p = [{$skip:2}, {$skip:1}],	//testing w/ 2 ensures independent state variables
+				e = [{_id:3}, {_id:4}, {_id:5}],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+		"should be able to use a $limit operator": function(){
+console.debug("");
+			var i = [{_id:0}, {_id:1}, {_id:2}, {_id:3}, {_id:4}, {_id:5}],
+				p = [{$limit:2}],
+				e = [{_id:0}, {_id:1}],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+		"should be able to use a $skip and then a $limit operator together in the same pipeline": function(){
+console.debug("");
+			var i = [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
+				p = [{$skip:2}, {$limit:1}],
+				e = [{_id:2, e:1}],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+		"should be able to use a $match operator": function(){
+console.debug("");
+			var i = [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
+				p = [{$match:{e:1}}],
+				e = [{_id:0, e:1}, {_id:2, e:1}, {_id:4, e:1}],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+		"should be able to use a $project operator": function(){
+console.debug("");
+			var i = [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
+				p = [{$project:{e:1}}],
+				e = [{_id:0, e:1}, {_id:2, e:1}, {_id:4, e:1}],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+//TODO: $project w/ expressions
+
+/*
+		"should be able to construct an instance with $unwind operators properly": function(){
+console.debug("");
+			var i = [
+					{_id:0, nodes:[
+						{one:[11], two:[2,2]},
+						{one:[1,1], two:[22]}
+					]},
+					{_id:1, nodes:[
+						{two:[22], three:[333]},
+						{one:[1], three:[3,3,3]}
+					]}
+				],
+				p = [{$unwind:"$nodes"}, {$unwind:"$nodes.two"}],
+				e = [
+					{_id:0,nodes:{one:[11],two:2}},
+					{_id:0,nodes:{one:[11],two:2}},
+					{_id:0,nodes:{one:[1,1],two:22}},
+					{_id:1,nodes:{two:22,three:[333]}}
+				],
+				alterer = alter(p),
+				a = alterer(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(alterer(i)), JSON.stringify(e), "Reuse of alterer should yeild the same results!");
+			assert.equal(JSON.stringify(alter(p, i)), JSON.stringify(e), "Alternate use of alter should yeild the same results!");
+		},
+
+		"should be able to construct an instance with $sort operators properly (ascending)": function(){
+			var i = [
+						{_id:3.14159}, {_id:-273.15},
+						{_id:42}, {_id:11}, {_id:1},
+						{_id:false},{_id:true},
+						{_id:""}, {_id:"a"}, {_id:"A"}, {_id:"Z"}, {_id:"z"},
+						{_id:null}, {_id:NaN},
+						//TODO: test with Objects; e.g., {_id:{a:{b:1}},
+						{_id:new Date("2012-10-22T08:01:21.235Z")}, {_id:new Date("2012-10-15T15:48:55.181Z")}
+					],
+				p = [{$sort:{_id:1}}],
+				e = [
+						{_id:null}, {_id:NaN},
+						{_id:-273.15}, {_id:1}, {_id:3.14159}, {_id:11}, {_id:42},
+						{_id:""}, {_id:"A"}, {_id:"Z"}, {_id:"a"}, {_id:"z"},
+						{_id:false}, {_id:true},
+						{_id:new Date("2012-10-15T15:48:55.181Z")}, {_id:new Date("2012-10-22T08:01:21.235Z")}
+					];
+			console.debug("\nINPUTS:\n", i);
+			console.debug("\nPIPELINE OPS:\n", p);
+			var a = alter(p, i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			console.debug("\n");
+		}
+*/
+	}
+
+};
+
+if(!module.parent) (new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run();

+ 152 - 0
test/lib/pipeline/FieldPath.js

@@ -0,0 +1,152 @@
+var assert = require("assert"),
+	FieldPath = require("../../../lib/pipeline/FieldPath");
+
+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");
+			}
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run();

+ 133 - 0
test/lib/pipeline/expressions/AddExpression.js

@@ -0,0 +1,133 @@
+var assert = require("assert"),
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
+
+//TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
+module.exports = {
+
+	"AddExpression": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
+					new AddExpression();
+				});
+			}
+
+		},
+
+		"#getOpName()": {
+
+			"should return the correct op name; $add": function testOpName(){
+				assert.equal(new AddExpression().getOpName(), "$add");
+			}
+
+		},
+
+		"#getFactory()": {
+
+			"should return the constructor for this class": function factoryIsConstructor(){
+				assert.equal((new AddExpression()).getFactory(), AddExpression);
+			}
+
+		},
+
+		"#evaluate()": {
+
+			"should return the operand if null document is given": function nullDocument(){
+				var expr = new AddExpression();
+				expr.addOperand(new ConstantExpression(2));
+				assert.equal(expr.evaluate(null), 2);
+			},
+
+			"should return 0 if no operands were given": function noOperands(){
+				var expr = new AddExpression();
+				assert.equal(expr.evaluate({}), 0);
+			},
+
+			"should throw Error if a Date operand was given": function date(){
+				var expr = new AddExpression();
+				expr.addOperand(new ConstantExpression(new Date()));
+				assert.throws(function(){
+					expr.evaluate({});
+				});
+			},
+
+			"should throw Error if a String operand was given": function string(){
+				var expr = new AddExpression();
+				expr.addOperand(new ConstantExpression(""));
+				assert.throws(function(){
+					expr.evaluate({});
+				});
+			},
+
+			"should throw Error if a Boolean operand was given": function bool() {
+				var expr = new AddExpression();
+				expr.addOperand(new ConstantExpression(true));
+				assert.throws(function() {
+					expr.evaluate({});
+				});
+			},
+
+			"should pass thru a single number": function number() {
+				var expr = new AddExpression(),
+					input = 123,
+					expected = 123;
+				expr.addOperand(new ConstantExpression(input));
+				assert.equal(expr.evaluate({}), expected);
+			},
+
+			"should pass thru a single null": function nullSupport() {
+				var expr = new AddExpression(),
+					input = null,
+					expected = 0;
+				expr.addOperand(new ConstantExpression(input));
+				assert.equal(expr.evaluate({}), expected);
+			},
+
+			"should pass thru a single undefined": function undefinedSupport() {
+				var expr = new AddExpression(),
+					input,
+					expected = 0;
+				expr.addOperand(new ConstantExpression(input));
+				assert.equal(expr.evaluate({}), expected);
+			},
+
+			"should add two numbers": function numbers() {
+				var expr = new AddExpression(),
+					inputs = [1, 5],
+					expected = 6;
+				inputs.forEach(function(input) {
+					expr.addOperand(new ConstantExpression(input));
+				});
+				assert.equal(expr.evaluate({}), expected);
+			},
+
+			"should add a number and a null": function numberAndNull() {
+				var expr = new AddExpression(),
+					inputs = [1, null],
+					expected = 1;
+				inputs.forEach(function(input) {
+					expr.addOperand(new ConstantExpression(input));
+				});
+				assert.equal(expr.evaluate({}), expected);
+			},
+
+			"should add a number and an undefined": function numberAndUndefined() {
+				var expr = new AddExpression(),
+					inputs = [1, undefined],
+					expected = 1;
+				inputs.forEach(function(input) {
+					expr.addOperand(new ConstantExpression(input));
+				});
+				assert.equal(expr.evaluate({}), expected);
+			}
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

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

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

@@ -0,0 +1,58 @@
+var assert = require("assert"),
+	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression");
+
+module.exports = {
+
+	"CoerceToBoolExpression": {
+
+		"constructor()": {
+
+			"should throw Error if no args": function construct(){
+				assert.throws(function(){
+					new CoerceToBoolExpression();
+				});
+			}
+
+		},
+
+		"#evaluate()": {
+
+			"should return true if nested expression is coerced to true; {$const:5}": function testEvaluateTrue(){
+				var expr = new CoerceToBoolExpression(new ConstantExpression(5));
+				assert.equal(expr.evaluate({}), true);
+			},
+
+			"should return false if nested expression is coerced to false; {$const:0}": function testEvaluateFalse(){
+				var expr = new CoerceToBoolExpression(new ConstantExpression(0));
+				assert.equal(expr.evaluate({}), false);
+			}
+
+		},
+
+		"#addDependencies()": {
+
+			"should forward dependencies of nested expression": function testDependencies(){
+				var expr = new CoerceToBoolExpression(new FieldPathExpression('a.b')),
+					deps = expr.addDependencies([]);
+				assert.equal(deps.length, 1);
+				assert.equal(deps[0], 'a.b');
+			}
+
+		},
+
+		"#toJson()": {
+
+			"should serialize as $and which will coerceToBool; '$foo'": function(){
+				var expr = new CoerceToBoolExpression(new FieldPathExpression('foo'));
+				assert.deepEqual(expr.toJson(), {$and:['$foo']});
+			}
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 293 - 0
test/lib/pipeline/expressions/CompareExpression.js

@@ -0,0 +1,293 @@
+var assert = require("assert"),
+	CompareExpression = require("../../../../lib/pipeline/expressions/CompareExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	FieldRangeExpression = require("../../../../lib/pipeline/expressions/FieldRangeExpression");
+
+module.exports = {
+
+	"CompareExpression": {
+
+		"constructor()": {
+
+			"should throw Error if no args": function testConstructor(){
+				assert.throws(function(){
+					new CompareExpression();
+				});
+			}
+
+		},
+
+		"#getOpName()": {
+
+			"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function testOpName(){
+				assert.equal((new CompareExpression(Expression.CmpOp.EQ)).getOpName(), "$eq");
+				assert.equal((new CompareExpression(Expression.CmpOp.NE)).getOpName(), "$ne");
+				assert.equal((new CompareExpression(Expression.CmpOp.GT)).getOpName(), "$gt");
+				assert.equal((new CompareExpression(Expression.CmpOp.GTE)).getOpName(), "$gte");
+				assert.equal((new CompareExpression(Expression.CmpOp.LT)).getOpName(), "$lt");
+				assert.equal((new CompareExpression(Expression.CmpOp.LTE)).getOpName(), "$lte");
+				assert.equal((new CompareExpression(Expression.CmpOp.CMP)).getOpName(), "$cmp");
+			}
+
+		},
+
+		"#evaluate()": {
+
+			"$eq": {
+
+				"should return false if first < second; {$eq:[1,2]}": function testEqLt(){
+					assert.equal(Expression.parseOperand({$eq:[1,2]}).evaluate({}), false);
+				},
+
+				"should return true if first == second; {$eq:[1,1]}": function testEqEq(){
+					assert.equal(Expression.parseOperand({$eq:[1,1]}).evaluate({}), true);
+				},
+
+				"should return false if first > second {$eq:[1,0]}": function testEqGt(){
+					assert.equal(Expression.parseOperand({$eq:[1,0]}).evaluate({}), false);
+				},
+
+			},
+
+			"$ne": {
+
+				"should return true if first < second; {$ne:[1,2]}": function testNeLt(){
+					assert.equal(Expression.parseOperand({$ne:[1,2]}).evaluate({}), true);
+				},
+
+				"should return false if first == second; {$ne:[1,1]}": function testNeLt(){
+					assert.equal(Expression.parseOperand({$ne:[1,1]}).evaluate({}), false);
+				},
+
+				"should return true if first > second; {$ne:[1,0]}": function testNeGt(){
+					assert.equal(Expression.parseOperand({$ne:[1,0]}).evaluate({}), true);
+				}
+
+			},
+
+			"$gt": {
+
+				"should return false if first < second; {$gt:[1,2]}": function testGtLt(){
+					assert.equal(Expression.parseOperand({$gt:[1,2]}).evaluate({}), false);
+				},
+
+				"should return false if first == second; {$gt:[1,1]}": function testGtLt(){
+					assert.equal(Expression.parseOperand({$gt:[1,1]}).evaluate({}), false);
+				},
+
+				"should return true if first > second; {$gt:[1,0]}": function testGtGt(){
+					assert.equal(Expression.parseOperand({$gt:[1,0]}).evaluate({}), true);
+				}
+
+			},
+
+			"$gte": {
+
+				"should return false if first < second; {$gte:[1,2]}": function testGteLt(){
+					assert.equal(Expression.parseOperand({$gte:[1,2]}).evaluate({}), false);
+				},
+
+				"should return true if first == second; {$gte:[1,1]}": function testGteLt(){
+					assert.equal(Expression.parseOperand({$gte:[1,1]}).evaluate({}), true);
+				},
+
+				"should return true if first > second; {$gte:[1,0]}": function testGteGt(){
+					assert.equal(Expression.parseOperand({$gte:[1,0]}).evaluate({}), true);
+				}
+
+			},
+
+			"$lt": {
+
+				"should return true if first < second; {$lt:[1,2]}": function testLtLt(){
+					assert.equal(Expression.parseOperand({$lt:[1,2]}).evaluate({}), true);
+				},
+
+				"should return false if first == second; {$lt:[1,1]}": function testLtLt(){
+					assert.equal(Expression.parseOperand({$lt:[1,1]}).evaluate({}), false);
+				},
+
+				"should return false if first > second; {$lt:[1,0]}": function testLtGt(){
+					assert.equal(Expression.parseOperand({$lt:[1,0]}).evaluate({}), false);
+				}
+
+			},
+
+			"$lte": {
+
+				"should return true if first < second; {$lte:[1,2]}": function testLteLt(){
+					assert.equal(Expression.parseOperand({$lte:[1,2]}).evaluate({}), true);
+				},
+
+				"should return true if first == second; {$lte:[1,1]}": function testLteLt(){
+					assert.equal(Expression.parseOperand({$lte:[1,1]}).evaluate({}), true);
+				},
+
+				"should return false if first > second; {$lte:[1,0]}": function testLteGt(){
+					assert.equal(Expression.parseOperand({$lte:[1,0]}).evaluate({}), false);
+				}
+
+			},
+
+			"$cmp": {
+
+				"should return -1 if first < second; {$cmp:[1,2]}": function testCmpLt(){
+					assert.equal(Expression.parseOperand({$cmp:[1,2]}).evaluate({}), -1);
+				},
+
+				"should return 0 if first < second; {$cmp:[1,1]}": function testCmpLt(){
+					assert.equal(Expression.parseOperand({$cmp:[1,1]}).evaluate({}), 0);
+				},
+
+				"should return 1 if first < second; {$cmp:[1,0]}": function testCmpLt(){
+					assert.equal(Expression.parseOperand({$cmp:[1,0]}).evaluate({}), 1);
+				},
+
+				"should return 1 even if comparison is larger; {$cmp:['z','a']}": function testCmpBracketed(){
+					assert.equal(Expression.parseOperand({$cmp:['z','a']}).evaluate({}), 1);
+				}
+
+			},
+
+			"should throw Error": {
+
+				"if zero operands are provided; {$ne:[]}": function testZeroOperands(){
+					assert.throws(function(){
+						Expression.parseOperand({$ne:[]}).evaluate({});
+					});
+				},
+
+				"if one operand is provided; {$eq:[1]}": function testOneOperand(){
+					assert.throws(function(){
+						Expression.parseOperand({$eq:[1]}).evaluate({});
+					});
+				},
+
+				"if three operands are provided; {$gt:[2,3,4]}": function testThreeOperands(){
+					assert.throws(function(){
+						Expression.parseOperand({$gt:[2,3,4]}).evaluate({});
+					});
+				}
+
+			}
+
+		},
+
+		"#optimize()": {
+
+			"should optimize constants; {$eq:[1,1]}": function testOptimizeConstants(){
+				assert.deepEqual(Expression.parseOperand({$eq:[1,1]}).optimize().toJson(true), {$const:true});
+			},
+
+			"should not optimize if $cmp op; {$cmp:[1,'$a']}": function testNoOptimizeCmp(){
+				assert.deepEqual(Expression.parseOperand({$cmp:[1,'$a']}).optimize().toJson(), {$cmp:[1,'$a']});
+			},
+
+			"should not optimize if $ne op; {$ne:[1,'$a']}": function testNoOptimizeNe(){
+				assert.deepEqual(Expression.parseOperand({$ne:[1,'$a']}).optimize().toJson(), {$ne:[1,'$a']});
+			},
+
+			"should not optimize if no constants; {$ne:['$a','$b']}": function testNoOptimizeNoConstant(){
+				assert.deepEqual(Expression.parseOperand({$ne:['$a','$b']}).optimize().toJson(), {$ne:['$a','$b']});
+			},
+
+			"should not optimize without an immediate field path;": {
+
+				"{$eq:[{$and:['$a']},1]}": function testNoOptimizeWithoutFieldPath(){
+					assert.deepEqual(Expression.parseOperand({$eq:[{$and:['$a']},1]}).optimize().toJson(), {$eq:[{$and:['$a']},1]});
+				},
+
+				"(reversed); {$eq:[1,{$and:['$a']}]}": function testNoOptimizeWithoutFieldPathReverse(){
+					assert.deepEqual(Expression.parseOperand({$eq:[1,{$and:['$a']}]}).optimize().toJson(), {$eq:[1,{$and:['$a']}]});
+				}
+
+			},
+
+			"should optimize $eq expressions;": {
+
+				"{$eq:['$a',1]}": function testOptimizeEq(){
+					var expr = Expression.parseOperand({$eq:['$a',1]}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$eq:['$a',1]});
+				},
+
+				"{$eq:[1,'$a']} (reversed)": function testOptimizeEqReverse(){
+					var expr = Expression.parseOperand({$eq:[1,'$a']}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$eq:['$a',1]});
+				}
+
+			},
+
+			"should optimize $lt expressions;": {
+
+				"{$lt:['$a',1]}": function testOptimizeLt(){
+					var expr = Expression.parseOperand({$lt:['$a',1]}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$lt:['$a',1]});
+				},
+
+				"{$lt:[1,'$a']} (reversed)": function testOptimizeLtReverse(){
+					var expr = Expression.parseOperand({$lt:[1,'$a']}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$gt:['$a',1]});
+				}
+
+			},
+
+			"should optimize $lte expressions;": {
+
+				"{$lte:['$b',2]}": function testOptimizeLte(){
+					var expr = Expression.parseOperand({$lte:['$b',2]}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$lte:['$b',2]});
+				},
+
+				"{$lte:[2,'$b']} (reversed)": function testOptimizeLteReverse(){
+					var expr = Expression.parseOperand({$lte:[2,'$b']}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$gte:['$b',2]});
+				}
+
+			},
+
+			"should optimize $gt expressions;": {
+
+				"{$gt:['$b',2]}": function testOptimizeGt(){
+					var expr = Expression.parseOperand({$gt:['$b',2]}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$gt:['$b',2]});
+				},
+
+				"{$gt:[2,'$b']} (reversed)": function testOptimizeGtReverse(){
+					var expr = Expression.parseOperand({$gt:[2,'$b']}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$lt:['$b',2]});
+				}
+
+			},
+
+			"should optimize $gte expressions;": {
+
+				"{$gte:['$b',2]}": function testOptimizeGte(){
+					var expr = Expression.parseOperand({$gte:['$b',2]}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$gte:['$b',2]});
+				},
+
+				"{$gte:[2,'$b']} (reversed)": function testOptimizeGteReverse(){
+					var expr = Expression.parseOperand({$gte:[2,'$b']}).optimize();
+					assert(expr instanceof FieldRangeExpression, "not optimized");
+					assert.deepEqual(expr.toJson(), {$lte:['$b',2]});
+				}
+
+			},
+
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

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

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

@@ -0,0 +1,47 @@
+var assert = require("assert"),
+	DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+module.exports = {
+
+	"DayOfMonthExpression": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
+					new DayOfMonthExpression();
+				});
+			}
+
+		},
+
+		"#getOpName()": {
+
+			"should return the correct op name; $dayOfMonth": function testOpName(){
+				assert.equal(new DayOfMonthExpression().getOpName(), "$dayOfMonth");
+			}
+
+		},
+
+		"#getFactory()": {
+
+			"should return the constructor for this class": function factoryIsConstructor(){
+				assert.strictEqual(new DayOfMonthExpression().getFactory(), undefined);
+			}
+
+		},
+
+		"#evaluate()": {
+
+			"should return day of month; 18 for 2013-02-18": function testStuff(){
+				assert.strictEqual(Expression.parseOperand({$dayOfMonth:"$someDate"}).evaluate({someDate:new Date("2013-02-18")}), 18);
+			}
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 129 - 0
test/lib/pipeline/expressions/FieldPathExpression.js

@@ -0,0 +1,129 @@
+var assert = require("assert"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression");
+
+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(new FieldPathExpression('a').evaluate({}), undefined);
+			},
+
+			"should return value if field path is present": function testPresent(){
+				assert.strictEqual(new FieldPathExpression('a').evaluate({a:123}), 123);
+			},
+
+			"should return undefined if field path is nested below null": function testNestedBelowNull(){
+				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:null}), undefined);
+			},
+
+			"should return undefined if field path is nested below undefined": function NestedBelowUndefined(){
+				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:undefined}), undefined);
+			},
+
+			"should return undefined if field path is nested below Number": function testNestedBelowInt(){
+				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:2}), undefined);
+			},
+
+			"should return value if field path is nested": function testNestedValue(){
+				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:{b:55}}), 55);
+			},
+
+			"should return undefined if field path is nested below empty Object": function testNestedBelowEmptyObject(){
+				assert.strictEqual(new FieldPathExpression('a.b').evaluate({a:{}}), undefined);
+			},
+
+			"should return empty Array if field path is nested below empty Array": function testNestedBelowEmptyArray(){
+				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[]}), []);
+			},
+
+			"should return Array with null if field path is nested below Array containing null": function testNestedBelowArrayWithNull(){
+				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[null]}), [null]);
+			},
+
+			"should return Array with undefined if field path is nested below Array containing undefined": function testNestedBelowArrayWithUndefined(){
+				assert.deepEqual(new FieldPathExpression('a.b').evaluate({a:[undefined]}), [undefined]);
+			},
+
+			"should throw Error if field path is nested below Array containing a Number": function testNestedBelowArrayWithInt(){
+				assert.throws(function(){
+					new FieldPathExpression('a.b').evaluate({a:[1]});
+				});
+			},
+
+			"should return Array with value if field path is in Object within Array": function testNestedWithinArray(){
+				assert.deepEqual(new FieldPathExpression('a.b').evaluate({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,null,undefined,undefined,20,undefined];
+				assert.deepEqual(new FieldPathExpression(path).evaluate(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(new FieldPathExpression(path).evaluate(doc), expected);
+			},
+
+			"should return null if field path points to a null value": function testPresentNull(){
+				assert.strictEqual(new FieldPathExpression('a').evaluate({a:null}), null);
+			},
+
+			"should return undefined if field path points to a undefined value": function testPresentUndefined(){
+				assert.strictEqual(new FieldPathExpression('a').evaluate({a:undefined}), undefined);
+			},
+
+			"should return Number if field path points to a Number value": function testPresentNumber(){
+				assert.strictEqual(new FieldPathExpression('a').evaluate({a:42}), 42);
+			}
+
+		},
+
+		"#optimize()": {
+
+			"should not optimize anything": function testOptimize(){
+				var expr = new FieldPathExpression('a');
+				assert.strictEqual(expr, expr.optimize());
+			}
+
+		},
+
+		"#addDependencies()": {
+
+			"should return the field path itself as a dependency": function testDependencies(){
+				var deps = new FieldPathExpression('a.b').addDependencies([]);
+				assert.strictEqual(deps.length, 1);
+				assert.strictEqual(deps[0], 'a.b');
+			}
+
+		},
+
+		"#toJson()": {
+
+			"should output path String with a '$'-prefix": function testJson(){
+				assert.equal(new FieldPathExpression('a.b.c').toJson(), "$a.b.c");
+			}
+
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

@@ -0,0 +1,137 @@
+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(deps.length, 1);
+				assert.strictEqual(deps[0], "a.b.c");
+			}
+
+		},
+
+//		"#intersect()": {
+//		},
+
+//		"#toJson()": {
+//		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 147 - 0
test/lib/pipeline/expressions/NaryExpression.js

@@ -0,0 +1,147 @@
+var assert = require("assert"),
+	NaryExpression = require("../../../../lib/pipeline/expressions/NaryExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+/** A dummy child of NaryExpression used for testing **/
+var TestableExpression = (function(){
+	// CONSTRUCTOR
+	var klass = module.exports = function TestableExpression(operands, haveFactory){
+		base.call(this);
+		if (operands) {
+			var self = this;
+			operands.forEach(function(operand) {
+				self.addOperand(operand);
+			});
+		}
+		this.haveFactory = !!haveFactory;
+	}, base = NaryExpression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// PROTOTYPE MEMBERS
+	proto.evaluate = function evaluate(doc) {
+		// Just put all the values in a list.  This is not associative/commutative so
+		// the results will change if a factory is provided and operations are reordered.
+		return this.operands.map(function(operand) {
+			return operand.evaluate(doc);
+		});
+	};
+
+	proto.getFactory = function getFactory(){
+		return this.haveFactory ? this.factory : klass;
+	};
+
+	proto.getOpName = function getOpName() {
+		return "$testable";
+	};
+
+	return klass;
+})();
+
+module.exports = {
+
+	"NaryExpression": {
+
+		"constructor()": {
+
+		},
+
+		"#optimize()": {
+
+		},
+
+		"#addOperand() should be able to add operands to expressions": function testAddOperand(){
+			assert.deepEqual(new TestableExpression([new ConstantExpression(9)]).toJson(), {$testable:[9]});
+			assert.deepEqual(new TestableExpression([new FieldPathExpression("ab.c")]).toJson(), {$testable:["$ab.c"]});
+		},
+
+		"#checkArgLimit() should throw Error iff number of operands is over given limit": function testCheckArgLimit(){
+			var testableExpr = new TestableExpression();
+
+			// no arguments
+			assert.doesNotThrow(function(){
+				testableExpr.checkArgLimit(1);
+			});
+
+			// one argument
+			testableExpr.addOperand(new ConstantExpression(1));
+			assert.throws(function(){
+				testableExpr.checkArgLimit(1);
+			});
+			assert.doesNotThrow(function(){
+				testableExpr.checkArgLimit(2);
+			});
+
+			// two arguments
+			testableExpr.addOperand(new ConstantExpression(2));
+			assert.throws(function(){
+				testableExpr.checkArgLimit(1);
+			});
+			assert.throws(function(){
+				testableExpr.checkArgLimit(2);
+			});
+			assert.doesNotThrow(function(){
+				testableExpr.checkArgLimit(3);
+			});
+		},
+
+		"#checkArgCount() should throw Error iff number of operands is not equal to given count": function testCheckArgCount(){
+			var testableExpr = new TestableExpression();
+
+			// no arguments
+			assert.doesNotThrow(function(){
+				testableExpr.checkArgCount(0);
+			});
+			assert.throws(function(){
+				testableExpr.checkArgCount(1);
+			});
+
+			// one argument
+			testableExpr.addOperand(new ConstantExpression(1));
+			assert.throws(function(){
+				testableExpr.checkArgCount(0);
+			});
+			assert.doesNotThrow(function(){
+				testableExpr.checkArgCount(1);
+			});
+			assert.throws(function(){
+				testableExpr.checkArgCount(2);
+			});
+
+			// two arguments
+			testableExpr.addOperand(new ConstantExpression(2));
+			assert.throws(function(){
+				testableExpr.checkArgCount(1);
+			});
+			assert.doesNotThrow(function(){
+				testableExpr.checkArgCount(2);
+			});
+			assert.throws(function(){
+				testableExpr.checkArgCount(3);
+			});
+		},
+
+		"#addDependencies()": function testDependencies(){
+			var testableExpr = new TestableExpression();
+
+			// no arguments
+			assert.deepEqual(testableExpr.addDependencies([]), []);
+
+			// add a constant argument
+			testableExpr.addOperand(new ConstantExpression(1));
+			assert.deepEqual(testableExpr.addDependencies([]), []);
+
+			// add a field path argument
+			testableExpr.addOperand(new FieldPathExpression("ab.c"));
+			assert.deepEqual(testableExpr.addDependencies([]), ["ab.c"]);
+
+			// add an object expression
+			testableExpr.addOperand(Expression.parseObject({a:"$x",q:"$r"}, new Expression.ObjectCtx({isDocumentOk:1})));
+			assert.deepEqual(testableExpr.addDependencies([]), ["ab.c", "r", "x"]);
+		}
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);