Просмотр исходного кода

EAGLESIX-2689 updated Nary to 2.6.5

Jared Hall 11 лет назад
Родитель
Сommit
74a53fb8b7
2 измененных файлов с 389 добавлено и 179 удалено
  1. 122 94
      lib/pipeline/expressions/NaryExpression.js
  2. 267 85
      test/lib/pipeline/expressions/NaryExpression.js

+ 122 - 94
lib/pipeline/expressions/NaryExpression.js

@@ -8,127 +8,155 @@
  * @extends mungedb-aggregate.pipeline.expressions.Expression
  * @constructor
  **/
-var Expression = require("./Expression");
-
-var NaryExpression = module.exports = function NaryExpression(){
-	if (arguments.length !== 0) throw new Error("Zero args expected");
-	this.operands = [];
-	base.call(this);
-}, klass = NaryExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+var Expression = require("./Expression"),
+	Variables = require("./Variables");
+
+var NaryExpressionT = module.exports = function NaryExpressionT(SubClass) {
+
+	var NaryExpression = function NaryExpression(){
+		if (arguments.length !== 0) throw new Error("Zero args expected");
+		this.operands = [];
+		base.call(this);
+	}, klass = NaryExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	klass.parseArguments = function(exprElement, vps) {
+		var out = [];
+		if(exprElement instanceof Array) {
+			for(var ii = 0; ii < exprElement.length; ii++) {
+				out.push(Expression.parseOperand(exprElement[ii], vps));
+			}
+		} else {
+			out.push(Expression.parseOperand(exprElement, vps));
+		}
+		return out;
+	};
 
-klass.parse = function(SubClass) {
-	return function parse(expr, vps) {
+	klass.parse = function(expr, vps) {
 		var outExpr = new SubClass(),
 			args = NaryExpression.parseArguments(expr, vps);
 		outExpr.validateArguments(args);
 		outExpr.operands = args;
 		return outExpr;
 	};
-};
 
-klass.parseArguments = function(exprElement, vps) {
-	var out = [];
-	if(exprElement instanceof Array) {
-		for(var ii = 0; ii < exprElement.length; ii++) {
-			out.push(Expression.parseOperand(exprElement[ii], vps));
-		}
-	} else {
-		out.push(Expression.parseOperand(exprElement, vps));
-	}
-	return out;
-};
+	proto.optimize = function optimize(){
+		var n = this.operands.length,
+			constCount = 0;
 
+		// optimize sub-expressions and count constants
+		for(var ii = 0; ii < n; ii++) {
+			var optimized = this.operands[ii].optimize();
 
-function partitionBy(fn, coll) {
-	var ret = {pass:[],
-			   fail:[]};
-	coll.forEach(function(x) {
-		if(fn(x)) {
-			ret.pass.push(x);
-		} else {
-			ret.fail.push(x);
-		}
-	});
-	return ret;
-}
-// DEPENDENCIES
-var ConstantExpression = require("./ConstantExpression");
+			// substitute the optimized expression
+			this.operands[ii] = optimized;
 
-// PROTOTYPE MEMBERS
-proto.evaluate = undefined; // evaluate(doc){ ... defined by inheritor ... }
+			// check to see if the result was a constant
+			if(optimized instanceof ConstantExpression) {
+				constCount++;
+			}
+		}
 
-proto.getOpName = function getOpName(doc){
-	throw new Error("NOT IMPLEMENTED BY INHERITOR");
-};
+		// If all the operands are constant, we can replace this expression with a constant. Using
+		// an empty Variables since it will never be accessed.
+		if(constCount === n) {
+			var emptyVars = new Variables(),
+				result = this.evaluateInternal(emptyVars),
+				replacement = new ConstantExpression(result);
+			return replacement;
+		}
 
-proto.optimize = function optimize(){
-	var n = this.operands.length,
-		constantCount = 0;
+		// Remaining optimizations are only for associative and commutative expressions.
+		if(!this.isAssociativeAndCommutative()) {
+			return this;
+		}
 
-	for(var ii = 0; ii < n; ii++) {
-		if(this.operands[ii] instanceof ConstantExpression) {
-			constantCount++;
-		} else {
-			this.operands[ii] = this.operands[ii].optimize();
-						}
+		// Process vpOperand to split it into constant and nonconstant vectors.
+		// This can leave vpOperand in an invalid state that is cleaned up after the loop.
+		var constExprs = [],
+			nonConstExprs = [];
+		for(var i=0; i<this.operands.length; i++) { // NOTE: vpOperand grows in loop
+			var expr = this.operands[i];
+			if(expr instanceof ConstantExpression) {
+				constExprs.push(expr);
+			} else {
+				// If the child operand is the same type as this, then we can
+				// extract its operands and inline them here because we know
+				// this is commutative and associative.  We detect sameness of
+				// the child operator by checking for equality of the opNames
+				var nary = expr;
+				if(!(nary instanceof NaryExpression) || nary.getOpName() !== this.getOpName) {
+					nonConstExprs.push(expr);
+				} else {
+					// same expression, so flatten by adding to vpOperand which
+					// will be processed later in this loop.
+					for(var j=0; j<nary.operands.length; j++) {
+						this.operands.push(nary.operands[j]);
 					}
-
-	if(constantCount === n) {
-		return new ConstantExpression(this.evaluateInternal({}));
 				}
+			}
+		}
 
-	if(!this.isAssociativeAndCommutative) {
-		return this;
-	}
+		// collapse all constant expressions (if any)
+		var constValue;
+		if(constExprs.length > 0) {
+			this.operands = constExprs;
+			var emptyVars = new Variables();
+			constValue = this.evaluateInternal(emptyVars);
+		}
 
-	// Flatten and inline nested operations of the same type
+		// now set the final expression list with constant (if any) at the end
+		this.operands = nonConstExprs;
+		if (constExprs.length > 0) {
+			this.operands.push(new ConstantExpression(constValue));
+		}
 
-	var similar = partitionBy(function(x){ return x.getOpName() === this.getOpName();}, this.operands);
+		return this;
+	};
 
-	this.operands = similar.fail;
-	similar.pass.forEach(function(x){
-		this.operands.concat(x.operands);
-	});
+	// DEPENDENCIES
+	var ConstantExpression = require("./ConstantExpression");
 
-	// Partial constant folding
+	proto.getOpName = function getOpName(doc){
+		throw new Error("NOT IMPLEMENTED BY INHERITOR");
+	};
 
-	var constantOperands = partitionBy(function(x) {return x instanceof ConstantExpression;}, this.operands);
+	proto.addDependencies = function addDependencies(deps, path){
+		for(var i = 0, l = this.operands.length; i < l; ++i)
+			this.operands[i].addDependencies(deps);
+	};
 
-	this.operands = constantOperands.pass;
-	this.operands = [new ConstantExpression(this.evaluateInternal({}))].concat(constantOperands.fail);
+	/**
+	 * Add an operand to the n-ary expression.
+	 * @method addOperand
+	 * @param pExpression the expression to add
+	 **/
+	proto.addOperand = function addOperand(expr) {
+		this.operands.push(expr);
+	};
 
-	return this;
-};
+	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+		return false;
+	};
 
-proto.addDependencies = function addDependencies(deps){
-	for(var i = 0, l = this.operands.length; i < l; ++i)
-		this.operands[i].addDependencies(deps);
-};
+	proto.serialize = function serialize(explain) {
+		var nOperand = this.operands.length,
+			array = [];
 
-/**
- * Add an operand to the n-ary expression.
- * @method addOperand
- * @param pExpression the expression to add
- **/
-proto.addOperand = function addOperand(expr) {
-	this.operands.push(expr);
-};
+		for(var i=0; i<nOperand; i++){
+			array.push(this.operands[i].serialize(explain));
+		}
 
-proto.serialize = function serialize() {
-	var ret = {}, subret = [];
-	for(var ii = 0; ii < this.operands.length; ii++) {
-		subret.push(this.operands[ii].serialize());
-	}
-	ret[this.getOpName()] = subret;
-	return ret;
-};
+		var obj = {};
+		obj[this.getOpName()] = array;
+		return obj;
+	};
 
-proto.fixedArity = function(nargs) {
-	this.nargs = nargs;
-};
+	proto.validateArguments = function(args) {
+		if(this.nargs !== args.length) {
+			throw new Error("Expression " + this.getOpName() + " takes exactly " + this.nargs + " arguments. " + args.length + " were passed in.");
+		}
+	};
 
-proto.validateArguments = function(args) {
-	if(this.nargs !== args.length) {
-		throw new Error("Expression " + this.getOpName() + " takes exactly " + this.nargs + " arguments. " + args.length + " were passed in.");
-	}
+	return NaryExpression;
 };
+

+ 267 - 85
test/lib/pipeline/expressions/NaryExpression.js

@@ -2,46 +2,91 @@
 var assert = require("assert"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	NaryExpression = require("../../../../lib/pipeline/expressions/NaryExpression"),
+	NaryExpressionT = require("../../../../lib/pipeline/expressions/NaryExpression"),
 	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+function constify(obj, parentIsArray) {
+	parentIsArray = !parentIsArray ? false : true;
+	var bob = parentIsArray ? [] : {};
+	Object.keys(obj).forEach(function(key) {
+		var elem = obj[key];
+		if(elem.constructor === Object) {
+			bob[key] = constify(elem, false);
+		}
+		else if(elem.constructor === Array && !parentIsArray) {
+			bob[key] = constify(elem, true);
+		}
+		else if(key === "$const" ||
+			elem.constructor === String && elem[0] === '$') {
+			bob[key] = obj[key];
+		}
+		else {
+			bob[key] = {$const:obj[key]}
+		}
+	});
+	return bob;
+};
+
+function expressionToJson(expr) {
+	return expr.serialize(false);
+};
+
+function assertDependencies(expectedDeps, expr) {
+	var deps = new DepsTracker(),
+		depsJson = [];
+	expr.addDependencies(deps);
+	deps.forEach(function(dep) {
+		depsJson.push(dep);
+	});
+	assert.deepEqual(depsJson, expectedDeps);
+	assert.equal(deps.needWholeDocument, false);
+	assert.equal(deps.needTextScore, false);
+};
 
 // A dummy child of NaryExpression used for testing
 var TestableExpression = (function(){
-	// CONSTRUCTOR
-	var klass = function TestableExpression(operands, haveFactory){
+		// CONSTRUCTOR
+	var klass = function TestableExpression(isAssociativeAndCommutative){
+		this._isAssociativeAndCommutative = isAssociativeAndCommutative;
 		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}});
+	}, base = NaryExpressionTemplate(TestableExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 	// PROTOTYPE MEMBERS
-	proto.evaluateInternal = function evaluateInternal(vps) {
+	proto.evaluateInternal = function evaluateInternal(vars) {
 		// Just put all the values in a list.  This is not associative/commutative so
 		// the results will change if a factory is provided and operations are reordered.
 		return this.operands.map(function(operand) {
-			return operand.evaluateInternal(vps);
+			return operand.evaluateInternal(vars);
 		});
 	};
 
 	proto.isAssociativeAndCommutative = function isAssociativeAndCommutative(){
-		return this.isAssociativeAndCommutative;
+		return this._isAssociativeAndCommutative;
+	};
+
+	klass.create = function create(associativeAndCommutative) {
+		associativeAndCommutative = !associativeAndCommutative ? false : true; //NOTE: coercing to bool -- defaults to false
+		return new TestableExpression(associativeAndCommutative);
+	};
+
+	klass.factory = function factory() {
+		return new TestableExpression(true);
 	};
 
 	proto.getOpName = function getOpName() {
 		return "$testable";
 	};
 
-	klass.createFromOperands = function(operands) {
+	proto.assertContents = function assertContents(expectedContents) {
+		assert.deepEqual(constify({$testable:expectedContents}), expressionToJson(this));
+	};
+
+	klass.createFromOperands = function(operands, haveFactory) {
+		haveFactory = !haveFactory ? false : true; //NOTE: coercing to bool -- defaults to false
 		var vps = new VariablesParseState(new VariablesIdGenerator()),
-			testable = new TestableExpression();
+			testable = new TestableExpression(haveFactory);
 		operands.forEach(function(x) {
 			testable.addOperand(Expression.parseOperand(x, vps));
 		});
@@ -54,98 +99,235 @@ var TestableExpression = (function(){
 
 module.exports = {
 
+	"NaryExpressionTemplate": {
+
+		"generator": {
+
+			"can generate a NaryExpression class": function() {
+				assert.doesNotThrow(function() {
+					var NaryExpressionClass = NaryExpressionTemplate(String),
+						naryEpressionIntance = new NaryExpressionClass();
+				});
+			}
+
+		}
+
+	},
+
 	"NaryExpression": {
 
-		"constructor()": {
+		"statics": {
+
+			"parseArguments":{
+
+				"should parse a fieldPathExpression": function parsesFieldPathExpression() {
+					var NaryExpressionClass = NaryExpressionTemplate(String),
+						vps = new VariablesParseState(new VariablesIdGenerator()),
+						parsedArguments = NaryExpressionClass.parseArguments("$field.path.expression", vps);
+						assert.equal(parsedArguments.length, 1);
+						assert(parsedArguments[0] instanceof FieldPathExpression);
+				},
+
+				"should parse an array of fieldPathExpressions": function parsesFieldPathExpression() {
+					var NaryExpressionClass = NaryExpressionTemplate(String),
+						vps = new VariablesParseState(new VariablesIdGenerator()),
+						parsedArguments = NaryExpressionClass.parseArguments(["$field.path.expression", "$another.FPE"], vps);
+						assert.equal(parsedArguments.length, 2);
+						assert(parsedArguments[0] instanceof FieldPathExpression);
+						assert(parsedArguments[1] instanceof FieldPathExpression);
+				}
+			}
 
 		},
 
-		"#optimize()": {
+		"addOperand": {
+			"run" : function run() {
+				var testable = new TestableExpression.create();
+				testable.addOperand(new ConstantExpression(9));
+				debugger;
+				testable.assertContents([9]);
+				testable.addOperand(new FieldPathExpression("ab.c"));
+				testable.assertContents([9, "$ab.c"]); //NOTE: Broken, not sure if problem with assertConents or FPE serialize
+			}
+		},
 
-			"should suboptimize": function() {
-				var testable = TestableExpression.createFromOperands([{"$and": []}, "$abc"], true);
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$testable: [true,"$abc"]});
-			},
-			"should fold constants": function() {
-				var testable = TestableExpression.createFromOperands([1,2], true);
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$const: [1,2]});
-			},
+		"Dependencies": {
+			"run": function run() {
+				var testable = new TestableExpression.create();
 
-			"should place constants at the end of operands array": function() {
-				var testable = TestableExpression.createFromOperands([55,65, "$path"], true);
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$testable:["$path", [55,66]]});
-			},
+				// No arguments.
+				assertDependencies([], testable);
 
-			"should flatten two layers" : function() {
-				var testable = TestableExpression.createFromOperands([55, "$path", {$add: [5,6,"$q"]}], true);
-				testable.addOperand(TestableExpression.createFromOperands([99,100,"$another_path"], true));
-				testable = testable.optimize();
-				assert.deepEqual(testable.serialize(), {$testable: ["$path", {$add: [5,6,"$q"]}, "$another_path", [55,66,[99,100]]]});
-			},
+				// Add a constant argument.
+				testable.addOperand(new ConstantExpression(1));
+				assertDependencies([], testable);
+
+				// Add a field path argument.
+				testable.addOperand(new FieldPathExpression("ab.c"));
+				assertDependencies(["ab.c"], testable);
+
+				// Add an object expression.
+				var spec = {a:"$x", q:"$r"},
+					specElement = spec,
+					ctx = new Expression.ObjectCtx({isDocumentOk:true}),
+					vps = new VariablesParseState(new VariablesIdGenerator());
+				testable.addOperand(Expression.parseObject(specElement, ctx, vps));
+				assertDependencies(["ab.c", "r", "x"]);
+			}
+		},
 
-			"should flatten three layers": function(){
-				var bottom = TestableExpression.createFromOperands([5,6,"$c"], true),
-					middle = TestableExpression.createFromOperands([3,4,"$b"], true).addOperand(bottom),
-					top = TestableExpression.createFromOperands([1,2,"$a"], true);
-				var testable = top.optimize();
-				assert.deepEqual(testable.serialize(), {$testable: ["$a", "$b", "$c", [1,2,[3,4,[5,6]]]]});
+		"AddToJsonObj": {
+			"run": function run() {
+				var testable = new TestableExpression.create();
+				testable.addOperand(new ConstantExpression(5));
+				assert.deepEqual(
+						{foo:{$testable:[{$const:5}]}},
+						{foo:testable.serialize(false)}
+					);
 			}
+		},
 
+		"AddToJsonArray": {
+			"run": function run() {
+				var testable = new TestableExpression.create();
+				testable.addOperand(new ConstantExpression(5));
+				assert.deepEqual(
+						[{$testable:[{$const:5}]}],
+						[testable.serialize(false)]
+					);
+			}
 		},
 
-		"#addOperand() should be able to add operands to expressions": function testAddOperand(){
-			var foo = new TestableExpression([new ConstantExpression(9)]).serialize();
-			var bar = new TestableExpression([new ConstantExpression(9)]).serialize();
-			var baz = {"$testable":[{"$const":9}]};
+		"OptimizeOneOperand": {
+			"run": function run() {
+				var spec = [{$and:[]},"$abc"],
+					testable = TestableExpression.createFromOperands(spec);
+				testable.assertContents(spec);
+				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+				assertContents([true, "$abc"])
+			}
+		},
 
-			assert.deepEqual(foo,bar);
-			assert.deepEqual(foo, baz);
-			assert.deepEqual(baz,foo);
-			assert.deepEqual(new TestableExpression([new ConstantExpression(9)]).serialize(), {"$testable":[{"$const":9}]});
-			assert.deepEqual(new TestableExpression([new FieldPathExpression("ab.c")]).serialize(), {$testable:["$ab.c"]});
+		"EvaluateAllConstantOperands": {
+			"run": function run() {
+				var spec = [1,2],
+					testable = TestableExpression.createFromOperands(spec);
+				testable.assertContents(spec);
+				var optimized = testable.optimize();
+				assert.notDeepEqual(testable.serialize(), optimized.serialize());
+				assert.deepEqual({$const:[1,2]}, expressionToJson(optimized));
+			}
 		},
 
+		"NoFactoryOptimize": {
+			// Without factory optimization, optimization will not produce a new expression.
+
+			/** A string constant prevents factory optimization. */
+			"StringConstant": function run() {
+				var testable = TestableExpression.createFromOperands(["abc","def","$path"], true);
+				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+			},
+
+			/** A single (instead of multiple) constant prevents optimization.  SERVER-6192 */
+			"SingleConstant": function run() {
+				var testable = TestableExpression.createFromOperands([55,"$path"], true);
+				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+			},
 
-		"#serialize() should convert an object to json": function(){
-			var testable = new TestableExpression();
-			testable.addOperand(new ConstantExpression(5));
-			assert.deepEqual({foo: testable.serialize()}, {foo:{$testable:[{$const: 5}]}});
+			/** Factory optimization is not used without a factory. */
+			"NoFactory": function run() {
+				var testable = TestableExpression.createFromOperands([55,66,"$path"], false);
+				assert.deepEqual(testable.serialize(), testable.optimize().serialize());
+			}
 		},
 
+		/** Factory optimization separates constant from non constant expressions. */
+		"FactoryOptimize": {
 
-		//the following test case is eagerly awaiting ObjectExpression
-		"#addDependencies()": function testDependencies(){
-			var testableExpr = new TestableExpression();
-			var deps = {};
-			// no arguments
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {});
-
-			// add a constant argument
-			testableExpr.addOperand(new ConstantExpression(1));
-
-			deps = {};
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {});
-
-			// add a field path argument
-			testableExpr.addOperand(new FieldPathExpression("ab.c"));
-			deps = {};
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {"ab.c":1});
-
-			// add an object expression
-			testableExpr.addOperand(Expression.parseObject({a:"$x",q:"$r"}, new Expression.ObjectCtx({isDocumentOk:1})));
-			deps = {};
-			testableExpr.addDependencies(deps);
-			assert.deepEqual(deps, {"ab.c":1, "x":1, "r":1});
+			// The constant expressions are evaluated separately and placed at the end.
+			"run": function run() {
+				var testable = TestableExpression.createFromOperands([55,66,"$path"], false),
+					optimized = testable.optimize();	
+				assert.deepEqual({$testable:["$path", [55,66]]}, expressionToJson(optimized));
+			}
+		},
+
+		/** Factory optimization flattens nested operators of the same type. */
+		"FlattenOptimize": {
+			"run": function run() {
+				var testable = TestableExpression.createFromOperands(
+						[55,"$path",{$add:[5,6,"$q"]},66],
+					true);
+				testable.addOperand(Testable.createFromOperands(
+						[99,100,"$another_path"],
+					true));
+				var optimized = testable.optimize();
+				assert.deepEqual(
+					constify({$testable:[
+							"$path",
+							{$add:["$q", 11]},
+							"$another_path",
+							[55, 66, [99, 100]]
+						]}),
+					expressionToJson(optimized));
+			}
+		},
+
+		/** Three layers of factory optimization are flattened. */
+		"FlattenThreeLayers": {
+			"run": function run() {
+				var top = TestableExpression.createFromOperands([1,2,"$a"], true),
+					nested = TestableExpression.createFromOperands([3,4,"$b"], true);
+				nested.addOperand(TestableExpression.createFromOperands([5,6,"$c"],true));	
+				top.addOperand(nested);
+				var optimized = top.optimize();
+				assert.deepEqual(
+					constify({$testable:[
+						"$a",
+						"$b",
+						"$c",
+						[1,2,[3,4,[5,6]]]]}),
+					expressionToJson(optimized));
+			}
+		},
+
+		"constify": {
+			"simple": function simple() {
+				var obj = {a:'s'},
+					constified = constify(obj);
+				assert.deepEqual(constified, { a: { '$const': 's' } });
+			},
+			"array": function array() {
+				var obj = {a:['s']},
+					constified = constify(obj);
+				assert.deepEqual(constified, { a: [ { '$const': 's' } ] });
+			},
+			"array2": function array2() {
+				var obj = {a:['s', [5], {a:5}]},
+					constified = constify(obj);
+				assert.deepEqual(constified,
+					{ a: 
+						[{ '$const': 's' },
+						 { '$const': [ 5 ] },
+						 { a: { '$const': 5 } }]
+					});
+			},
+			"object": function object() {
+				var obj = {a:{b:{c:5}, d:'hi'}},
+					constified = constify(obj);
+				assert.deepEqual(constified, 
+					{ a: 
+						{ b: { c: { '$const': 5 } },
+							d: { '$const': 'hi' } } });
+			},
+			"fieldPathExpression": function fieldPathExpression() {
+				var obj = {a:"$field.path"},
+					constified = constify(obj);
+				assert.deepEqual(constified, obj);
+			}
 		}
 
 	}
-
 };
 
 if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);