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

Merge pull request #82 from RiveraGroup/feature/mongo_2.6.5_expressions_SetIntersection

Feature/mongo 2.6.5 expressions SetIntersection
Kyle P Davis 11 лет назад
Родитель
Сommit
cba6389fbc

+ 10 - 10
lib/pipeline/Value.js

@@ -8,7 +8,7 @@
  * @constructor
  **/
 var Value = module.exports = function Value(){
-	if(this.constructor == Value) throw new Error("Never create instances of this! Use the static helpers only.");
+	if(this.constructor === Value) throw new Error("Never create instances of this! Use the static helpers only.");
 }, klass = Value, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
 var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
@@ -27,7 +27,7 @@ var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate
  * @static
  */
 klass.coerceToBool = function coerceToBool(value) {
-	if (typeof(value) == "string") return true;
+	if (typeof value === "string") return true;
 	return !!value;	// including null or undefined
 };
 
@@ -71,7 +71,7 @@ klass.coerceToDate = function coerceToDate(value) {
 //SKIPPED: tmToISODateString -- not required; just use Date
 klass.coerceToString = function coerceToString(value) {
 	var type = typeof(value);
-	if (type == "object") type = value === null ? "null" : value.constructor.name;
+	if (type === "object") type = value === null ? "null" : value.constructor.name;
 	switch (type) {
 		//TODO: BSON numbers?
 		case "number":
@@ -131,7 +131,7 @@ klass.compare = function compare(l, r) {
 	}
 	// Compare MinKey and MaxKey cases
 	if (l instanceof Object && ["MinKey", "MaxKey"].indexOf(l.constructor.name) !== -1) {
-		if (l.constructor.name == r.constructor.name) {
+		if (l.constructor.name === r.constructor.name) {
 			return 0;
 		} else if (l.constructor.name === "MinKey") {
 			return -1;
@@ -154,7 +154,7 @@ klass.compare = function compare(l, r) {
 	case "string":
 		return klass.cmp(l, r);
 	case "boolean":
-		return l == r ? 0 : l ? 1 : -1;
+		return l === r ? 0 : l ? 1 : -1;
 	case "undefined": //NOTE: deviation from mongo code: we are comparing null to null or undefined to undefined (otherwise the ret stuff above would have caught it)
 	case "null":
 		return 0;
@@ -201,12 +201,12 @@ klass.consume = function consume(consumed) {
 };
 
 //NOTE: DEVIATION FROM MONGO: many of these do not apply or are inlined (code where relevant)
-// missing(val):  val == undefined
-// nullish(val):  val == null || val == undefined
-// numeric(val):  typeof val == "number"
+// missing(val):  val === undefined
+// nullish(val):  val === null || val === undefined
+// numeric(val):  typeof val === "number"
 klass.getType = function getType(v) {
 	var t = typeof v;
-	if (t == "object") t = (v === null ? "null" : v.constructor.name || t);
+	if (t === "object") t = (v === null ? "null" : v.constructor.name || t);
 	return t;
 };
 // getArrayLength(arr): arr.length
@@ -226,7 +226,7 @@ klass.getType = function getType(v) {
 // from bsontypes
 klass.canonicalize = function canonicalize(x) {
 	var xType = typeof(x);
-	if (xType == "object") xType = x === null ? "null" : x.constructor.name;
+	if (xType === "object") xType = x === null ? "null" : x.constructor.name;
 	switch (xType) {
 		case "MinKey":
 			return -1;

+ 43 - 22
lib/pipeline/expressions/SetIntersectionExpression.js

@@ -2,39 +2,60 @@
 
 /**
  * A $setintersection pipeline expression.
- * @see evaluateInternal
  * @class SetIntersectionExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetIntersectionExpression = module.exports = function SetIntersectionExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetIntersectionExpression, base = require("./NaryBaseExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = SetIntersectionExpression, base = require("./VariadicExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("./ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setIntersection";
-};
-
-/**
- * Takes 2 objects. Returns the intersects of the objects.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var object1 = this.operands[0].evaluateInternal(vars),
-		object2 = this.operands[1].evaluateInternal(vars);
-	if (object1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an object");
-	if (object2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an object");
+	var n = this.operands.length,
+		currentIntersection = new ValueSet();
+	for (var i = 0; i < n; i++){
+		var nextEntry = this.operands[i].evaluateInternal(vars);
+		if (nextEntry === undefined || nextEntry === null){
+			return null;
+		}
+		if (!(nextEntry instanceof Array))
+			 throw new Error("All operands of " + this.getOpName() + "must be arrays. One " +
+				"argument is of type: " + Value.getType(nextEntry) + "; uassert code 17047");
 
-	var result = object1.filter(function(n) {
-		return object2.indexOf(n) > -1;
-	});
+		if (i === 0){
+			currentIntersection.insertRange(nextEntry);
+		} else {
+			var nextSet = new ValueSet(nextEntry);
+			if (currentIntersection.size() > nextSet.size()) {
+				// to iterate over whichever is the smaller set
+				nextSet.swap(currentIntersection);
+			}
+			for (var itKey in currentIntersection.set) {
+				if (!nextSet.hasKey(itKey)) {
+					currentIntersection.eraseKey(itKey);
+				}
+			}
+		}
+		if (currentIntersection.empty()) {
+			break;
+		}
+	}
+	var result = currentIntersection.values();
+	return result;
 };
 
-/** Register Expression */
 Expression.registerExpression("$setIntersection", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setIntersection";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 38 - 0
lib/pipeline/expressions/ValueSet.js

@@ -1,3 +1,12 @@
+"use strict";
+
+/**
+ * Somewhat mimics the ValueSet in mongo (std::set<Value>)
+ * @class valueSet
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ */
 var ValueSet = module.exports = function ValueSet(vals) {
 	this.set = {};
 	if (vals instanceof Array)
@@ -9,6 +18,7 @@ proto._getKey = JSON.stringify;
 proto.hasKey = function hasKey(key) {
 	return key in this.set;
 };
+//SKIPPED: proto.count -- use hasKey instead
 
 proto.has = function has(val) {
 	return this._getKey(val) in this.set;
@@ -48,3 +58,31 @@ proto.values = function values() {
 		vals.push(this.set[key]);
 	return vals;
 };
+
+proto.size = function values() {
+	var n = 0;
+	for (var key in this.set) //jshint ignore:line
+		n++;
+	return n;
+};
+
+proto.swap = function swap(other) {
+	var tmp = this.set;
+	this.set = other.set;
+	other.set = tmp;
+};
+
+proto.eraseKey = function eraseKey(key) {
+	delete this.set[key];
+};
+
+proto.erase = function erase(val) {
+	var key = this._getKey(val);
+	this.eraseKey(key);
+};
+
+proto.empty = function empty() {
+	for (var key in this.set) //jshint ignore:line
+		return false;
+	return true;
+};

+ 319 - 111
test/lib/pipeline/expressions/SetIntersectionExpression.js

@@ -1,117 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetIntersectionExpression = require("../../../../lib/pipeline/expressions/SetIntersectionExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetIntersectionExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetIntersectionExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setintersection": function testOpName() {
-								assert.equal(new SetIntersectionExpression([1, 2, 3], [4, 5, 6]).getOpName(), "$setintersection");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setintersection: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setintersection: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setintersection: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and return an interested set1": function testBasicAssignment() {
-								var array1 = {
-										"a": "3",
-										"c": "4"
-								},
-										array2 = {
-												"a": "3",
-												"b": "3"
-										};
-								assert.strictEqual(Expression.parseOperand({
-										$setintersection: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), {
-										"a": "3"
-								});
-						},
-
-						"Should pass and return an intersected set1": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [2, 3, 6, 7, 8];
-								assert.strictEqual(Expression.parseOperand({
-										$setintersection: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [2, 3]);
-						},
-
-						"Should pass and return an intersected set2": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [7, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setintersection: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), []);
-						},
-
-				}
+	SetIntersectionExpression = require("../../../../lib/pipeline/expressions/SetIntersectionExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.SetIntersectionExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetIntersectionExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetIntersectionExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setIntersection": function() {
+			assert.equal(new SetIntersectionExpression().getOpName(), "$setIntersection");
 		}
 
-};
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: false,
+						$setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						$setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						$setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						$setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						$setIntersection: [1, 2, 4],
+						// $setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};