Browse Source

Refs #1001 GroupDocumentSource has been aded and all test cases have been built. There are test cases that are dependent on upon ObjectExpression working correctly so they are commented out for now

http://source.rd.rcg.local/trac/eagle6/changeset/1346/Eagle6_SVN
Adam Bell 12 years ago
parent
commit
3a15648752

+ 214 - 0
lib/pipeline/documentSources/GroupDocumentSource.js

@@ -0,0 +1,214 @@
+var DocumentSource = require("./DocumentSource"),
+	Document = require("../Document"),
+	Expression = require("../expressions/Expression"),
+	Accumulators = require("../accumulators/"),
+	GroupDocumentSource = module.exports = (function(){
+	// CONSTRUCTOR
+	/**
+	 * A base class for all document sources
+	 * @param	{ExpressionContext}	
+	**/
+	var klass = module.exports = GroupDocumentSource = function GroupDocumentSource(groupElement){
+		if(!(groupElement instanceof Object && groupElement.constructor.name === "Object") || Object.keys(groupElement).length < 1)
+			throw new Error("a group's fields must be specified in an object");			
+	
+		this.populated = false;
+		this.idExpression = null;
+		this.groups = {}; // GroupsType Value -> Accumulators[]
+		this.groupsKeys = []; // This is to faciliate easier look up of groups
+
+		this.fieldNames = [];
+		this.accumulatorFactories = [];
+		this.expressions = [];
+		this.currentDocument = null;
+		this.groupCurrentIndex = 0;
+
+		var groupObj = groupElement[this.getSourceName()];
+		for(var groupFieldName in groupObj){
+			if(groupObj.hasOwnProperty(groupFieldName)){
+				var groupField = groupObj[groupFieldName];
+				
+				if(groupFieldName === "_id"){
+					if(groupField instanceof Object && groupField.constructor.name === "Object"){
+						var objCtx = new Expression.ObjectCtx({isDocumentOk:true});
+						this.idExpression = Expression.parseObject(groupField, objCtx);
+
+					}else if( typeof groupField === "string"){
+						if(groupField[0] !== "$")
+							this.idExpression = new ConstantExpression(groupField);		
+						var pathString = Expression.removeFieldPrefix(groupField);
+						this.idExpression = new FieldPathExpression(pathString);
+					}else{
+						var typeStr = this._getTypeStr(groupField);
+						switch(typeStr){
+							case "number":
+							case "string":
+							case "boolean":
+							case "object":
+								this.idExpression = new ConstantExpression(groupField);
+								break;
+							default:
+								throw new Error("a group's _id may not include fields of type " + typeStr  + ""); 
+						}
+					}
+
+
+				}else{
+					if(groupFieldName.indexOf(".") !== -1)
+						throw new Error("16414 the group aggregate field name '" + groupFieldName + "' cannot contain '.'");
+					if(groupFieldName[0] === "$")
+						throw new Error("15950 the group aggregate field name '" + groupFieldName + "' cannot be an operator name");
+					if(this._getTypeStr(groupFieldName) === "object")
+						throw new Error("15951 the group aggregate field '" + groupFieldName + "' must be defined as an expression inside an object");
+
+					var subFieldCount = 0;
+					for(var subFieldName in groupField){
+						if(groupField.hasOwnProperty(subFieldName)){
+							var subField = groupField[subField],
+								op = DocumentSource.GroupOps[subFieldName];
+							if(!op)
+								throw new Exception("15952 unknown group operator '" + subFieldName + "'");
+
+							var groupExpression,
+								subFieldTypeStr = this._getTypeStr(subField);
+							if(subFieldTypeStr === "object"){
+								var subFieldObjCtx = new Expression.ObjectCtx({isDocumentOk:true});
+								groupExpression = Expression.parseObject(groupField, subFieldObjCtx);
+							}else if(subFieldTypeStr === "Array"){
+								throw new Exception("15953 aggregating group operators are unary (" + subFieldName + ")");
+							}else{
+								groupExpression = Expression.parseOperand(subField);
+							}
+							this.addAccumulator(groupFieldName,op, groupExpression); 
+							
+							++subFieldCount;
+						}
+					if(subFieldCount != 1)
+						throw new Error("15954 the computed aggregate '" + groupFieldName + "' must specify exactly one operator");
+					}
+				}
+			}
+		}	
+
+
+	}, base = DocumentSource, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+
+	klass.GroupOps = {
+		"$addToSet": Accumulators.AddToSet,
+		"$avg": Accumulators.Avg,
+		"$first": Accumulators.First,
+		"$last": Accumulators.Last,
+		"$max": Accumulators.MinMax.bind(null, 1),
+		"$min": Accumulators.MinMax.bind(null, -1),
+		"$push": Accumulators.Push,
+		"$sum": Accumulators.Sum
+	};
+
+	proto._getTypeStr = function _getTypeStr(obj){
+		var typeofStr=typeof groupField, typeStr = (typeofStr == "object" ? groupField.constructor.name : typeStr);
+		return typeofStr;	
+	};
+
+
+	proto.getSourceName = function getSourceName(){
+		return "$group";
+	};
+
+	proto.advance = function advance(){
+		base.prototype.advance.call(this); // Check for interupts ????
+
+		if(!this.populated)
+			this.populate();
+
+		++this.currentGroupsKeysIndex;
+		if(this.currentGroupsKeysIndex === this.groupKeys.length){
+			this.currentDocument = null;
+			return false;	
+		}
+
+		
+		return true;
+	};
+
+	proto.eof = function eof(){
+		if(!this.populated)
+			this.populate();
+		
+		return this.currentGroupsKeysIndex === this.groupsKeys.length; 
+
+	};
+
+	proto.getCurrent = function getCurrent(){
+		if(!this.populated)
+			this.populate();
+
+		return this.currentDocument;
+	};
+
+
+	
+
+	proto.addAccumulator = function addAccumulator(fieldName, accumulatorFactory, expression){
+		this.fieldNames.push(fieldName);
+		this.accumulatorFactories.push(accumulatorFactory);
+		this.expressions.push(expression);
+	};
+
+
+	proto.populate = function populate(){
+		for(var hasNext = !this.pSource.eof(); hasNext; hasNext = this.pSource.advance()){
+			var currentDocument = this.pSource.getCurrent(),
+				_id = this.idExpression.evaluate(currentDocument) || null,
+				group;
+
+			if(_id in this.groups){
+				group = this.groups[_id];
+			}else{
+				this.groups[_id] = group = [];
+				this.groupsKeys[this.currentGroupsKeysIndex] = _id;
+				for(var ai =0; ai < this.accumulators.length; ++ai){
+					var accumulator = new this.accumulatorFactories[ai]();
+					accumulators.addOperand(this.expressions[ai]);
+					group.push(accumulator);
+				}
+			}
+
+
+			// tickle all the accumulators for the group we found
+			for(var gi=0; gi < group.length; ++gi)
+				group[gi].evaluate(currentDocument);
+
+		
+			this.currentGroupsKeysIndex = 0; // Start the group
+			if(this.currentGroupsKeysIndex === this.groups.length-1)
+				this.currentDocument = makeDocument(this.currentGroupsKeysIndex);
+			this.populated = true;
+
+		}
+
+	};
+
+
+	proto.makeDocument = function makeDocument(groupKeyIndex){
+		var groupKey = this.groupKeys[groupKeyIndex],
+			group = this.groups[groupKey],
+			doc = {};
+		doc[Document.ID_PROPERTY_NAME] = groupKey;
+			
+	
+		for(var i = 0; i < this.fieldNames.length; ++i){
+			var fieldName = this.fieldNames[i],
+				value = this.group[i].getValue();
+			if(typeof value !== "undefined"){
+				doc[fieldName] = value;
+			}
+		}
+
+		return doc;
+	};
+
+	return klass;
+
+
+})();

+ 197 - 0
test/lib/pipeline/documentSources/GroupDocumentSource.js

@@ -0,0 +1,197 @@
+var assert = require("assert"),
+	CursorDocumentSource = require("../../../../lib/pipeline/documentsources/CursorDocumentSource"),
+	Cursor = require("../../../../lib/Cursor"),
+	GroupDocumentSource = require("../../../../lib/pipeline/documentSources/GroupDocumentSource");
+
+
+/// An assertion for `ObjectExpression` instances based on Mongo's `ExpectedResultBase` class
+function assertExpectedResult(args) {
+	{// check for required args
+		if (args === undefined) throw new TypeError("missing arg: `args` is required");
+		if (args.spec && args.throw === undefined) args.throw = true; // Assume that spec only tests expect an error to be thrown 
+		//if (args.spec === undefined) throw new Error("missing arg: `args.spec` is required");
+		if (args.expected !== undefined && args.docs === undefined) throw new Error("must provide docs with expected value");
+	}// check for required args
+
+	// run implementation
+	if(args.expected && args.docs){
+		var gds = new GroupDocumentSource(args.spec),
+			cwc = new CursorDocumentSource.CursorWithContext();
+		cwc._cursor = new Cursor( args.docs );
+		var cds = new CursorDocumentSource(cwc);
+		gds.setSource(cds);
+		gds.advance();
+		var result = gds.getCurrent();
+		assert.deepEqual(result, expected);
+	}else{
+		if(args.throw)
+			assert.throws(function(){
+				new GroupDocumentSource(args.spec);
+			});
+		else
+			assert.doesNotThrow(function(){
+				new GroupDocumentSource(args.spec);
+			});
+	}
+
+}
+
+module.exports = {
+
+	"GroupDocumentSource": {
+
+		"constructor()": {
+
+			/** $group spec is not an object. */
+			"should throw Error when constructing without args": function testConstructor(){
+				assertExpectedResult({throw:true});
+			},
+
+			/** $group spec is not an object. */
+			"should throw Error when $group spec is not an object": function testConstructor(){
+				assertExpectedResult({spec:"Foo"});
+			},
+
+			/** $group spec is an empty object. */
+			"should throw Error when $group spec is an empty object": function testConstructor(){
+				assertExpectedResult({spec:{}});
+			},
+
+//			/** $group _id is an empty object. */
+//			"should not throw when _id is an empty object": function advanceTest(){
+//				assertExpectedResult({spec:{$group:{_id:{}}}, throw:false});
+//			},
+
+			/** $group _id is specified as an invalid object expression. */
+			"should throw error when  _id is an invalid object expression": function testConstructor(){
+				assertExpectedResult({
+					spec:{$group:{_id:{$add:1, $and:1}}},
+				});	
+			},
+
+
+			/** $group with two _id specs. */
+			//NOT Implemented can't do this in Javascript
+
+
+
+//			/** $group _id is the empty string. */
+//			"should not throw when _id is an empty string": function advanceTest(){
+//				assertExpectedResult({spec:{$group:{_id:""}}, throw:false});
+//			},
+
+//			/** $group _id is a string constant. */
+//			"should not throw when _id is a string constant": function advanceTest(){
+//				assertExpectedResult({spec:{$group:{_id:"abc"}}, throw:false});
+//			},
+
+			/** $group with _id set to an invalid field path. */
+			"should throw when _id is an invalid field path": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:"$a.."}}});
+			},
+		
+//			/** $group _id is a numeric constant. */
+//			"should not throw when _id is a numeric constant": function advanceTest(){
+//				assertExpectedResult({spec:{$group:{_id:2}}, throw:false});
+//			},
+
+//			/** $group _id is an array constant. */
+//			"should not throw when _id is an array constant": function advanceTest(){
+//				assertExpectedResult({spec:{$group:{_id:[1,2]}}, throw:false});
+//			},
+
+			/** $group _id is a regular expression (not supported). */
+			"should throw when _id is a regex": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:/a/}}});
+			},
+
+			/** The name of an aggregate field is specified with a $ prefix. */
+			"should throw when aggregate field spec is specified with $ prefix": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:1, $foo:{$sum:1}}}});
+			},
+
+			/** An aggregate field spec that is not an object. */
+			"should throw when aggregate field spec is not an object": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:1, a:1}}});
+			},
+
+			/** An aggregate field spec that is not an object. */
+			"should throw when aggregate field spec is an empty object": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:1, a:{}}}});
+			},
+
+			/** An aggregate field spec with an invalid accumulator operator. */
+			"should throw when aggregate field spec is an invalid accumulator": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:1, a:{$bad:1}}}});
+			},
+
+			/** An aggregate field spec with an array argument. */
+			"should throw when aggregate field spec with an array as an argument": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:1, a:{$sum:[]}}}});
+			},
+
+			/** Multiple accumulator operators for a field. */
+			"should throw when aggregate field spec with multiple accumulators": function advanceTest(){
+				assertExpectedResult({spec:{$group:{_id:1, a:{$sum:1, $push:1}}}});
+			}
+
+			//Not Implementing, not way to support this in Javascript Objects
+			/** Aggregation using duplicate field names is allowed currently. */
+
+
+
+		},
+
+		"#getSourceName()": {
+
+//			"should return the correct source name; $group": function testSourceName(){
+//				var gds = new GroupDocumentSource({$group:{_id:{}}});
+//				assert.strictEqual(gds.getSourceName(), "$group");
+//			}
+		},
+
+		"#advance": {
+
+//			/** $group _id is computed from an object expression. */
+//			"should compute _id from an object expression": function advanceTest(){
+//				assertExpectedResult({
+//					docs:[{a:6}],
+//					spec:{$group:{_id:{z:"$a"}}},
+//					expected:{_id:{z:6}}
+//				});	
+//			},
+
+//			/** $group _id is a field path expression. */
+//			"should compute _id a field path expression": function advanceTest(){
+//				assertExpectedResult({
+//					docs:[{a:5}],
+//					spec:{$group:{_id:"$a"}},
+//					expected:{_id:{z:5}}
+//				});	
+//			},
+
+//			/** Aggregate the value of an object expression. */
+//			"should aggregate the value of an object expression": function advanceTest(){
+//				assertExpectedResult({
+//					docs:[{a:6}],
+//					spec:{$group:{_id:0, z:{$first:{x:"$a"}}}},
+//					expected:{_id:0, z:6}
+//				});	
+//			},
+
+//			/** Aggregate the value of an operator expression. */
+//			"should aggregate the value of an operator expression": function advanceTest(){
+//				assertExpectedResult({
+//					docs:[{a:6}],
+//					spec:{$group:{_id:0, z:{$first:"$a"}}},
+//					expected:{_id:0, z:6}
+//				});	
+//			}
+		}
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+
+