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

DEVOPS-228 Done

Ported Mongo 2.5 Expressions to JS. Significant changes to the way
Objects inheritting from Expression are indexed for later retrieval.
Hopefully this lets us get rid of the SetTimeouts in our code to deal
with circular dependencies.
Brennan Chesley 12 лет назад
Родитель
Сommit
c21bb2736c
1 измененных файлов с 74 добавлено и 222 удалено
  1. 74 222
      lib/pipeline/expressions/Expression.js

+ 74 - 222
lib/pipeline/expressions/Expression.js

@@ -57,150 +57,18 @@ var ObjectCtx = Expression.ObjectCtx = (function(){
 	return klass;
 })();
 
-/**
- * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.OpDesc` class
- * @static
- * @property OpDesc
- **/
-var OpDesc = Expression.OpDesc = (function(){
-	// CONSTRUCTOR
-	/**
-	 * Decribes how and when to create an Op instance
-	 *
-	 * @class OpDesc
-	 * @namespace mungedb-aggregate.pipeline.expressions.Expression
-	 * @module mungedb-aggregate
-	 * @constructor
-	 * @param name
-	 * @param factory
-	 * @param flags
-	 * @param argCount
-	 **/
-	var klass = function OpDesc(name, factory, flags, argCount){
-		var firstArg = arguments[0];
-		if (firstArg instanceof Object && firstArg.constructor == Object) { //TODO: using this?
-			var opts = firstArg;
-			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;
-
-	/**
-	 * internal `OpDesc#name` comparer
-	 * @method cmp
-	 * @param that the other `OpDesc` instance
-	 **/
-	proto.cmp = function cmp(that) {
-		return this.name < that.name ? -1 : this.name > that.name ? 1 : 0;
-	};
-
-	return klass;
-})();
-// END OF NESTED CLASSES
-/**
- * @class Expression
- * @namespace mungedb-aggregate.pipeline.expressions
- * @module mungedb-aggregate
- **/
-
-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.
- *
- * @static
- * @property CmpOp
- **/
-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
+proto.removeFieldPrefix = function removeFieldPrefix( prefixedField ) {
+    if(prefixedField.indexOf("\0") !== -1 ) {
+        // field path must not contain embedded null characters - 16419
+    }
+    if(prefixedField[0] !== '$') {
+        // "field path references must be prefixed with a '$'"
+    }
+    return prefixedField.slice(1);
 };
-
-// DEPENDENCIES (later in this file as compared to others to ensure that the required statics are setup first)
-var FieldPathExpression = require("./FieldPathExpression"),
-	ObjectExpression = require("./ObjectExpression"),
-	ConstantExpression = require("./ConstantExpression"),
-	CompareExpression = require("./CompareExpression");
-
-// DEFERRED DEPENDENCIES
-/**
- * Expressions, as exposed to users
- *
- * @static
- * @property opMap
- **/
-setTimeout(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});
-}, 0);
-Object.defineProperty(klass, "opMap", {	//NOTE: deferred requires using a getter to allow circular requires (to maintain the ported API)
-	configurable: true,
-	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("$concat", require("./ConcatExpression"), 0),
-				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;
-	}
-});
-
+var KIND_UNKNOWN = 0,
+    KIND_NOTOPERATOR = 1,
+    KIND_OPERATOR = 2;
 /**
  * Parse an Object.  The object could represent a functional expression or a Document expression.
  *
@@ -215,63 +83,84 @@ Object.defineProperty(klass, "opMap", {	//NOTE: deferred requires using a getter
  * @param ctx	a MiniCtx representing the options above
  * @returns the parsed Expression
  **/
-klass.parseObject = function parseObject(obj, ctx){
+klass.parseObject = function parseObject(obj, ctx, vps){
 	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 kind = KIND_UNKNOWN,
+	    pExpression, // the result
+	    pExpressionObject; // the alt result
+	if (obj === undefined || obj == {}) return new ObjectExpression();
 	var fieldNames = Object.keys(obj);
 	if(fieldNames.length === 0) { //NOTE: Added this for mongo 2.5 port of document sources. Should reconsider when porting the expressions themselves
 		return new ObjectExpression();
 	}
-	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]);
+	for (var fieldCount = 0, n = fieldNames.length; fieldCount < n; ++fieldCount) {
+	    var pFieldName = fieldNames[fieldCount];
+
+	    if (pFieldName[0] === "$") {
+		if (fieldCount !== 0)
+                    throw new Error("the operator must be the only field in a pipeline object (at '" + pFieldName + "'.; code 16410");
+
+		if(ctx.isTopLevel)
+                    throw new Error("$expressions are not allowed at the top-level of $project; code 16404");
+		kind = KIND_OPERATOR;	//we've determined this "object" is an operator expression
+		pExpression = Expression.parseExpression(pFieldName, obj[pFieldName], vps);
 		} 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();
+		    if (kind === KIND_OPERATOR)
+                        throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + pFieldName + "'.; code 15990");
+
+                    if (!ctx.isTopLevel && pFieldName.indexOf(".") != -1)
+                        throw new Error("dotted field names are only allowed at the top level; code 16405");
+		    if (pExpression === 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
+				pExpression = pExpressionObject = new ObjectExpression(); //check for top level?
 				kind = kinds.NOT_OPERATOR;	//this "object" is not an operator expression
 			}
-			var fv = obj[fn];
-			switch (typeof(fv)) {
+			var fieldValue = obj[pFieldName];
+			switch (typeof(fieldValue)) {
 			case "object":
 				// it's a nested document
 				var subCtx = new ObjectCtx({
 					isDocumentOk: ctx.isDocumentOk,
 					isInclusionOk: ctx.isInclusionOk
 				});
-				exprObj.addField(fn, Expression.parseObject(fv, subCtx));
+				pExpressionObject.addField(pFieldName, Expression.parseObject(fieldValue, subCtx, vps));
 				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);
+				var pathExpr = new FieldPathExpression.parse(fieldValue);
+				pExpressionObject.addField(pFieldName, 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);
+				if (fieldValue) {
+				    if (!ctx.isInclusionOk)
+                                        throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
+					pExpressionObject.includePath(pFieldName);
 				} else {
-					if (!(ctx.isTopLevel && fn == Document.ID_PROPERTY_NAME)) throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; code 16406");
-					exprObj.excludeId = true;
+					if (!(ctx.isTopLevel && fn == Document.ID_PROPERTY_NAME))
+                                            throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; code 16406");
+					pExpressionObject.excludeId = true;
 				}
 				break;
 			default:
-				throw new Error("disallowed field type " + (fv ? fv.constructor.name + ":" : "") + typeof(fv) + " in object expression (at '" + fn + "')");
+				throw new Error("disallowed field type " + (fieldValue ? fieldValue.constructor.name + ":" : "") + typeof(fieldValue) + " in object expression (at '" + pFieldName + "')");
 			}
 		}
 	}
-	return expr;
+	return pExpression;
+};
+
+
+klass.expressionParserMap = {};
+
+klass.registerExpression = function registerExpression(key, parserFunc) {
+    if( key in klass.expressionParserMap ) {
+        throw new Error("Duplicate expression registrarion for " + key);
+    }
+    klass.expressionParserMap[key] = parserFunc;
+    return 0; // Should
 };
 
 /**
@@ -283,36 +172,11 @@ klass.parseObject = function parseObject(obj, ctx){
  * @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;
+klass.parseExpression = function parseExpression(exprKey, exprValue, vps) {
+    if( !(exprKey in Expression.expressionParserMap) ) {
+        throw new Error("Invalid operator : " + exprKey);
+    }
+    return Expression.expressionParserMap[exprKey](exprValue, vps);
 };
 
 /**
@@ -322,14 +186,15 @@ klass.parseExpression = function parseExpression(opName, obj) {
  * @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);
+klass.parseOperand = function parseOperand(exprElement, vps){
+	var t = typeof(exprElement);
+	if (t === "string" && exprElement[0] == "$") { //if we got here, this is a field path expression
+		return new FieldPathExpression.parse(exprElement, vps);
 	}
-	else if (t === "object" && obj && obj.constructor === Object) return Expression.parseObject(obj, new ObjectCtx({isDocumentOk: true}));
-	else return new ConstantExpression(obj);
+	else
+            if (t === "object" && exprElement && exprElement.constructor === Object)
+                return Expression.parseObject(exprElement, new ObjectCtx({isDocumentOk: true}), vps);
+	else return ConstantExpression.parse(exprElement, vps);
 };
 
 /**
@@ -346,19 +211,6 @@ klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
 	return prefixedField.substr(1);
 };
 
-/**
- * returns the signe of a number
- *
- * @static
- * @method signum
- * @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
 /**