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