Prechádzať zdrojové kódy

Added $unwind document source and test cases. fixes #1008

http://source.rd.rcg.local/trac/eagle6/changeset/1352/Eagle6_SVN
Philip Murray 12 rokov pred
rodič
commit
59ecca9c11

+ 14 - 1
lib/pipeline/Document.js

@@ -61,7 +61,20 @@ var Document = module.exports = (function(){
 		throw new Error("This should never happen");	//verify(false)
 //		return 0;
 	};
-
+	klass.clone = function(document){
+		var obj = {};
+		for(var key in document){
+			if(document.hasOwnProperty(key)){
+				var withObjVal = document[key];
+				if(withObjVal.constructor === Object){
+					obj[key] = Document.clone(withObjVal);
+				}else{
+					obj[key] = withObjVal;
+				}
+			}
+		}
+		return obj;
+	};
 //	proto.addField = function addField(){ throw new Error("Instead of `Document#addField(key,val)` you should just use `obj[key] = val`"); }
 //	proto.setField = function addField(){ throw new Error("Instead of `Document#setField(key,val)` you should just use `obj[key] = val`"); }
 //  proto.getField = function getField(){ throw new Error("Instead of `Document#getField(key)` you should just use `var val = obj[key];`"); }

+ 4 - 4
lib/pipeline/Pipeline.js

@@ -9,19 +9,19 @@ var Pipeline = module.exports = (function(){
 	var LimitDocumentSource = require('./documentSources/LimitDocumentSource'),
 		MatchDocumentSource = require('./documentSources/MatchDocumentSource'),
 		ProjectDocumentSource = require('./documentSources/ProjectDocumentSource'),
-		SkipDocumentSource = require('./documentSources/SkipDocumentSource');
+		SkipDocumentSource = require('./documentSources/SkipDocumentSource'),
+		UnwindDocumentSource = require('./documentSources/UnwindDocumentSource');
 //		GroupDocumentSource = require('./documentSources/GroupDocumentSource'),
-//		SortDocumentSource = require('./documentSources/SortDocumentSource'),
-//		UnwindDocumentSource = require('./documentSources/UnwindDocumentSource');
+//		SortDocumentSource = require('./documentSources/SortDocumentSource');
 	
 	klass.StageDesc = {};//attaching this to the class for test cases
 	klass.StageDesc[LimitDocumentSource.limitName] = LimitDocumentSource.createFromJson;
 	klass.StageDesc[MatchDocumentSource.matchName] = MatchDocumentSource.createFromJson;
 	klass.StageDesc[ProjectDocumentSource.projectName] = ProjectDocumentSource.createFromJson;
 	klass.StageDesc[SkipDocumentSource.skipName] = SkipDocumentSource.createFromJson;
+	klass.StageDesc[UnwindDocumentSource.unwindName] = UnwindDocumentSource.createFromJson;
 //	klass.StageDesc[GroupDocumentSource.groupName] = GroupDocumentSource.createFromJson;
 //	klass.StageDesc[SortDocumentSource.sortName] = SortDocumentSource.createFromJson;
-//	klass.StageDesc[UnwindDocumentSource.unwindName] = UnwindDocumentSource.createFromJson;
 	
     /**
      * Create a pipeline from the command.

+ 352 - 0
lib/pipeline/documentSources/UnwindDocumentSource.js

@@ -0,0 +1,352 @@
+var UnwindDocumentSource = module.exports = (function(){
+	// CONSTRUCTOR
+	/**
+	 * A document source unwindper
+	 * 
+	 * @class UnwindDocumentSource
+	 * @namespace munge.pipepline.documentsource
+	 * @module munge
+	 * @constructor
+	 * @param {Object} query the match query to use
+	**/
+	var klass = module.exports = UnwindDocumentSource = function UnwindDocumentSource(/* pCtx*/){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+		
+        // Configuration state.
+        this._unwindPath = null;
+        
+        // Iteration state.
+        this._unwinder = null;
+		
+	}, base = require('./DocumentSource'), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	var DocumentSource = base,
+		FieldPath = require('../FieldPath'),
+		Document = require('../Document'),
+		Expression = require('../expressions/Expression');
+
+	klass.Unwinder = (function(){
+		/** 
+		 * Helper class to unwind arrays within a series of documents. 
+		 * 
+		 * @param	{String}	unwindPath is the field path to the array to unwind.
+		**/
+		var klass = function Unwinder(unwindPath){
+
+			// Path to the array to unwind.
+			this._unwindPath = unwindPath;
+			// The souce document to unwind.
+			this._document = null;
+			// Document indexes of the field path components.
+			this._unwindPathFieldIndexes = [];
+			// Iterator over the array within _document to unwind.
+			this._unwindArrayIterator = null;
+			// The last value returned from _unwindArrayIterator.
+			//this._unwindArrayIteratorCurrent = undefined; //dont define this yet
+
+		}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+		
+		/** 
+		 * Reset the unwinder to unwind a new document. 
+		 * 
+		 * @param	{Object}	document
+		**/
+        proto.resetDocument = function resetDocument(document){
+			if (!document){
+				throw new Error("document is required!");
+			}
+			
+			// Reset document specific attributes.
+			this._document = document;
+			this._unwindPathFieldIndexes.length = 0;
+			this._unwindArrayIterator = null;
+			delete this._unwindArrayIteratorCurrent;
+	
+			var pathValue = this.extractUnwindValue(); // sets _unwindPathFieldIndexes
+			if (!pathValue || pathValue.length === 0) {
+				// The path does not exist.
+				return;
+			}
+			if (pathValue.constructor !== Array){
+				throw new Error(UnwindDocumentSource.unwindName + ":  value at end of field path must be an array; code 15978");
+			}
+			
+			// Start the iterator used to unwind the array.
+			this._unwindArrayIterator = pathValue.slice(0);
+			this._unwindArrayIteratorCurrent = this._unwindArrayIterator.splice(0,1)[0];
+        };
+        
+        /** 
+         * eof
+         * 
+         * @returns	{Boolean}	true if done unwinding the last document passed to resetDocument(). 
+        **/
+        proto.eof = function eof(){
+			return !this.hasOwnProperty("_unwindArrayIteratorCurrent");
+        };
+        
+        /**
+         * Try to advance to the next document unwound from the document passed to resetDocument().
+         * 
+         * @returns	{Boolean} true if advanced to a new unwound document, but false if done advancing.
+        **/
+        proto.advance = function advance(){
+			if (!this._unwindArrayIterator) {
+				// resetDocument() has not been called or the supplied document had no results to
+				// unwind.
+				delete this._unwindArrayIteratorCurrent;
+			} else if (!this._unwindArrayIterator.length) {
+				// There are no more results to unwind.
+				delete this._unwindArrayIteratorCurrent;
+			} else {
+				this._unwindArrayIteratorCurrent = this._unwindArrayIterator.splice(0, 1)[0];
+			}
+        };
+        
+        /**
+         * Get the current document unwound from the document provided to resetDocument(), using
+         * the current value in the array located at the provided unwindPath.  But return
+         * intrusive_ptr<Document>() if resetDocument() has not been called or the results to unwind
+         * have been exhausted.
+         * 
+         * @returns	{Object}
+        **/
+        proto.getCurrent = function getCurrent(){
+			if (!this.hasOwnProperty("_unwindArrayIteratorCurrent")) {
+				return null;
+			}
+			
+			// Clone all the documents along the field path so that the end values are not shared across
+			// documents that have come out of this pipeline operator.  This is a partial deep clone.
+			// Because the value at the end will be replaced, everything along the path leading to that
+			// will be replaced in order not to share that change with any other clones (or the
+			// original).
+			
+			var clone = Document.clone(this._document);
+			var current = clone;
+			var n = this._unwindPathFieldIndexes.length;
+			if (!n) {
+				throw new Error("unwindFieldPathIndexes are empty");
+			}
+			for (var i = 0; i < n; ++i) {
+				var fi = this._unwindPathFieldIndexes[i];
+				var fp = current[fi];
+				if (i + 1 < n) {
+					// For every object in the path but the last, clone it and continue on down.
+					var next = Document.clone(fp);
+					current[fi] = next;
+					current = next;
+				} else {
+					// In the last nested document, subsitute the current unwound value.
+					current[fi] = this._unwindArrayIteratorCurrent;
+				}
+			}
+			
+			return clone;
+        };
+        
+        /**
+         * Get the value at the unwind path, otherwise an empty pointer if no such value
+         * exists.  The _unwindPathFieldIndexes attribute will be set as the field path is traversed
+         * to find the value to unwind.
+         * 
+         * @returns	{Object}
+        **/
+        proto.extractUnwindValue = function extractUnwindValue(){
+			var current = this._document;
+			var pathValue;
+			var pathLength = this._unwindPath.getPathLength();
+			for (var i = 0; i < pathLength; ++i) {
+			
+				var idx = this._unwindPath.getFieldName(i);
+			
+				if (!current.hasOwnProperty(idx)) {
+					// The target field is missing.
+					return null;
+				}
+			
+				// Record the indexes of the fields down the field path in order to quickly replace them
+				// as the documents along the field path are cloned.
+				this._unwindPathFieldIndexes.push(idx);
+			
+				pathValue = current[idx];
+			
+				if (i < pathLength - 1) {
+			
+					if (typeof pathValue !== 'object') {
+						// The next field in the path cannot exist (inside a non object).
+						return null;
+					}
+			
+					// Move down the object tree.
+					current = pathValue;
+				}
+			}
+			
+			return pathValue;
+        };
+		
+		return klass;
+	})();
+	
+	/**
+	 * Lazily construct the _unwinder and initialize the iterator state of this DocumentSource.
+	 * To be called by all members that depend on the iterator state.
+	**/
+	proto.lazyInit = function lazyInit(){
+        if (!this._unwinder) {
+            if (!this._unwindPath){
+				throw new Error("unwind path does not exist!");
+            }
+            this._unwinder = new klass.Unwinder(this._unwindPath);
+            if (!this.pSource.eof()) {
+                // Set up the first source document for unwinding.
+                this._unwinder.resetDocument(this.pSource.getCurrent());
+            }
+            this.mayAdvanceSource();
+        }
+	};
+	
+	/**
+	 * If the _unwinder is exhausted and the source may be advanced, advance the pSource and
+	 * reset the _unwinder's source document.
+	**/
+	proto.mayAdvanceSource = function mayAdvanceSource(){
+        while(this._unwinder.eof()) {
+            // The _unwinder is exhausted.
+
+            if (this.pSource.eof()) {
+                // The source is exhausted.
+                return;
+            }
+            if (!this.pSource.advance()) {
+                // The source is exhausted.
+                return;
+            }
+            // Reset the _unwinder with pSource's next document.
+            this._unwinder.resetDocument(this.pSource.getCurrent());
+        }
+	};
+	
+	/** 
+	 * Specify the field to unwind. 
+	**/
+	proto.unwindPath = function unwindPath(fieldPath){
+        // Can't set more than one unwind path.
+        if (this._unwindPath){
+			throw new Error(this.getSourceName() + " can't unwind more than one path; code 15979");
+        }
+                
+        // Record the unwind path.
+        this._unwindPath = new FieldPath(fieldPath);
+	};
+
+	klass.unwindName = "$unwind";
+	proto.getSourceName = function getSourceName(){
+		return klass.unwindName;
+	};
+	
+	/**
+     * Get the fields this operation needs to do its job.
+     * Deps should be in "a.b.c" notation
+     * 
+     * @method	getDependencies
+     * @param	{Object} deps	set (unique array) of strings
+     * @returns	DocumentSource.GetDepsReturn
+    **/
+    proto.getDependencies = function getDependencies(deps) {
+        if (!this._unwindPath){
+			throw new Error("unwind path does not exist!");
+        }
+        deps[this._unwindPath.getPath(false)] = 1;
+        return DocumentSource.GetDepsReturn.SEE_NEXT;
+    };
+
+
+    /**
+     * Is the source at EOF?
+     * 
+     * @method	eof
+    **/
+    proto.eof = function eof() {
+        this.lazyInit();
+        return this._unwinder.eof();
+    };
+
+    /**
+     * some implementations do the equivalent of verify(!eof()) so check eof() first
+     * 
+     * @method	getCurrent
+     * @returns	{Document}	the current Document without advancing
+    **/
+    proto.getCurrent = function getCurrent() {
+        this.lazyInit();
+        return this._unwinder.getCurrent();
+    };
+
+	/**
+	 * Advance the state of the DocumentSource so that it will return the next Document.  
+	 * The default implementation returns false, after checking for interrupts. 
+	 * Derived classes can call the default implementation in their own implementations in order to check for interrupts.
+	 * 
+	 * @method	advance
+	 * @returns	{Boolean}	whether there is another document to fetch, i.e., whether or not getCurrent() will succeed.  This default implementation always returns false.
+	**/
+	proto.advance = function advance() {
+        base.prototype.advance.call(this); // check for interrupts
+        this.lazyInit();
+        this._unwinder.advance();
+        this.mayAdvanceSource();
+        return !this._unwinder.eof();
+	};
+
+	/**
+	 * Create an object that represents the document source.  The object
+	 * will have a single field whose name is the source's name.  This
+	 * will be used by the default implementation of addToJsonArray()
+	 * to add this object to a pipeline being represented in JSON.
+	 * 
+	 * @method	sourceToJson
+	 * @param	{Object} builder	JSONObjBuilder: a blank object builder to write to
+	 * @param	{Boolean}	explain	create explain output
+	**/
+	proto.sourceToJson = function sourceToJson(builder, explain) {
+        if (!this._unwindPath){
+			throw new Error("unwind path does not exist!");
+        }
+        builder[this.getSourceName()] = this._unwindPath.getPath(true);
+	};
+
+	/**
+	 * Creates a new UnwindDocumentSource with the input path as the path to unwind
+	 *
+	 * @param {String} JsonElement this thing is *called* Json, but it expects a string
+	**/
+	klass.createFromJson = function createFromJson(JsonElement) {
+        /*
+          The value of $unwind should just be a field path.
+         */
+        if (JsonElement.constructor !== String){
+			throw new Error("the " + klass.unwindName + " field path must be specified as a string; code 15981");
+        }
+
+        var pathString = Expression.removeFieldPrefix(JsonElement);
+        var unwind = new UnwindDocumentSource();
+        unwind.unwindPath(pathString);
+
+        return unwind;
+	};
+	
+    /**
+     * Reset the document source so that it is ready for a new stream of data.
+     * Note that this is a deviation from the mongo implementation.
+     * 
+     * @method	reset
+    **/
+	proto.reset = function reset(){
+        this._unwinder = null;
+	};
+
+	return klass;
+})();

+ 16 - 16
test/lib/munge.js

@@ -65,22 +65,6 @@ module.exports = {
 			assert.equal(JSON.stringify(munge(p, i)), JSON.stringify(e), "Alternate use of munge should yield the same results!");
 		},
 
-
-/*
-		"should be able to use a $project operator": function(){
-			var i = [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
-				p = [{$project:{e:1}}],
-				e = [{_id:0, e:1}, {_id:2, e:1}, {_id:4, e:1}],
-				munger = munge(p),
-				a = munger(i);
-			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
-			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
-			assert.equal(JSON.stringify(munger(i)), JSON.stringify(e), "Reuse of munger should yield the same results!");
-			assert.equal(JSON.stringify(munge(p, i)), JSON.stringify(e), "Alternate use of munge should yield the same results!");
-		},
-
-//TODO: $project w/ expressions
-
 		"should be able to construct an instance with $unwind operators properly": function(){
 			var i = [
 					{_id:0, nodes:[
@@ -105,8 +89,24 @@ module.exports = {
 			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
 			assert.equal(JSON.stringify(munger(i)), JSON.stringify(e), "Reuse of munger should yield the same results!");
 			assert.equal(JSON.stringify(munge(p, i)), JSON.stringify(e), "Alternate use of munge should yield the same results!");
+		}
+
+/*
+		"should be able to use a $project operator": function(){
+			var i = [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
+				p = [{$project:{e:1}}],
+				e = [{_id:0, e:1}, {_id:2, e:1}, {_id:4, e:1}],
+				munger = munge(p),
+				a = munger(i);
+			assert.equal(JSON.stringify(a), JSON.stringify(e), "Unexpected value!");
+			assert.deepEqual(a, e, "Unexpected value (not deepEqual)!");
+			assert.equal(JSON.stringify(munger(i)), JSON.stringify(e), "Reuse of munger should yield the same results!");
+			assert.equal(JSON.stringify(munge(p, i)), JSON.stringify(e), "Alternate use of munge should yield the same results!");
 		},
 
+//TODO: $project w/ expressions
+
+
 		"should be able to construct an instance with $sort operators properly (ascending)": function(){
 			var i = [
 						{_id:3.14159}, {_id:-273.15},

+ 295 - 0
test/lib/pipeline/documentSources/UnwindDocumentSource.js

@@ -0,0 +1,295 @@
+
+var assert = require("assert"),
+DocumentSource = require("../../../../lib/pipeline/documentSources/DocumentSource"),
+UnwindDocumentSource = require("../../../../lib/pipeline/documentSources/UnwindDocumentSource"),
+CursorDocumentSource = require("../../../../lib/pipeline/documentSources/CursorDocumentSource"),
+Cursor = require("../../../../lib/Cursor");
+
+//HELPERS
+var assertExhausted = function assertExhausted(pds) {
+    assert.ok(pds.eof());
+    assert.ok(!pds.advance());
+};
+
+/**
+*   Tests if the given rep is the same as what the pds resolves to as JSON.
+*   MUST CALL WITH A PDS AS THIS (e.g. checkJsonRepresentation.call(this, rep) where this is a PDS)
+**/
+var checkJsonRepresentation = function checkJsonRepresentation(self, rep) {
+    var pdsRep = {};
+    self.sourceToJson(pdsRep, true);
+    assert.deepEqual(pdsRep, rep);
+};
+
+var createUnwind = function createUnwind(unwind) {
+    //let unwind be optional
+    if(!unwind){
+        unwind = "$a";
+    }
+    var spec = {"$unwind": unwind},
+        specElement = unwind,
+		unwindDs = UnwindDocumentSource.createFromJson(specElement);
+    checkJsonRepresentation(unwindDs, spec);
+    return unwindDs;
+};
+
+var addSource = function addSource(unwind, data) {
+    var cwc = new CursorDocumentSource.CursorWithContext();
+    cwc._cursor = new Cursor( data );
+    var cds = new CursorDocumentSource(cwc);
+    var pds = new UnwindDocumentSource();
+    unwind.setSource(cds);
+};
+
+var checkResults = function checkResults(data, expectedResults, path){
+	var unwind = createUnwind(path);
+	addSource(unwind, data || []);
+	
+	expectedResults = expectedResults || [];
+	
+	var resultSet = [];
+    while( !unwind.eof() ) {
+
+        // If not eof, current is non null.
+        assert.ok( unwind.getCurrent() );
+
+        // Get the current result.
+        resultSet.push( unwind.getCurrent() );
+
+        // Advance.
+        if ( unwind.advance() ) {
+            // If advance succeeded, eof() is false.
+            assert.equal( unwind.eof(), false );
+        }
+    }
+    assert.deepEqual( resultSet, expectedResults );
+};
+
+var throwsException = function throwsException(data, path, expectedResults){
+	assert.throws(function(){
+		checkResults(data, path, expectedResults);
+	});
+};
+
+//TESTS
+module.exports = {
+
+    "UnwindDocumentSource": {
+
+        "constructor()": {
+
+            "should throw Error when constructing with args": function (){
+                assert.throws(function(){
+                    new UnwindDocumentSource("$a");
+                });
+            }
+
+        },
+
+        "#getSourceName()": {
+
+            "should return the correct source name; $unwind": function (){
+                var pds = new UnwindDocumentSource();
+                assert.strictEqual(pds.getSourceName(), "$unwind");
+            }
+
+        },
+
+        "#eof()": {
+
+            "should return true if source is empty": function (){
+                var pds = createUnwind();
+                addSource(pds, []);
+                assert.strictEqual(pds.eof(), true);
+            },
+            
+            "should return false if source documents exist": function (){
+                var pds = createUnwind();
+                addSource(pds, [{_id:0, a:[1]}]);
+                assert.strictEqual(pds.eof(), false);
+            }
+
+        },
+
+        "#advance()": {
+
+            "should return false if there are no documents in the parent source": function () {
+                var pds = createUnwind();
+                addSource(pds, []);
+                assert.strictEqual(pds.advance(), false);
+            },
+            "should return true if source documents exist and advance the source": function (){
+                var pds = createUnwind();
+                addSource(pds, [{_id:0, a:[1,2]}]);
+                assert.strictEqual(pds.advance(), true);
+                assert.strictEqual(pds.getCurrent().a, 2);
+            }
+        },
+
+        "#getCurrent()": {
+
+            "should return null if there are no documents in the parent source": function () {
+                var pds = createUnwind();
+                addSource(pds, []);
+                assert.strictEqual(pds.getCurrent(), null);
+            },
+            "should return unwound documents": function (){
+                var pds = createUnwind();
+                addSource(pds, [{_id:0, a:[1,2]}]);
+                assert.ok(pds.getCurrent());
+                assert.strictEqual(pds.getCurrent().a, 1);
+            },
+            
+            "A document without the unwind field produces no results.": function(){
+				checkResults([{}]);
+            },
+            "A document with a null field produces no results.": function(){
+				checkResults([{a:null}]);
+            },
+            "A document with an empty array produces no results.": function(){
+				checkResults([{a:[]}]);
+            },
+            "A document with a number field produces a UserException.": function(){
+				throwsException([{a:1}]);
+            },
+            "An additional document with a number field produces a UserException.": function(){
+				throwsException([{a:[1]}, {a:1}]);
+            },
+            "A document with a string field produces a UserException.": function(){
+				throwsException([{a:"foo"}]);
+            },
+            "A document with an object field produces a UserException.": function(){
+				throwsException([{a:{}}]);
+            },
+            "Unwind an array with one value.": function(){
+				checkResults(
+					[{_id:0, a:[1]}],
+					[{_id:0,a:1}]
+				);
+            },
+            "Unwind an array with two values.": function(){
+				checkResults(
+					[{_id:0, a:[1, 2]}],
+					[{_id:0,a:1}, {_id:0,a:2}]
+				);
+            },
+            "Unwind an array with two values, one of which is null.": function(){
+				checkResults(
+					[{_id:0, a:[1, null]}],
+					[{_id:0,a:1}, {_id:0,a:null}]
+				);
+            },
+            "Unwind two documents with arrays.": function(){
+				checkResults(
+					[{_id:0, a:[1,2]}, {_id:0, a:[3,4]}],
+					[{_id:0,a:1}, {_id:0,a:2}, {_id:0,a:3}, {_id:0,a:4}]
+				);
+            },
+            "Unwind an array in a nested document.": function(){
+				checkResults(
+					[{_id:0,a:{b:[1,2],c:3}}],
+					[{_id:0,a:{b:1,c:3}},{_id:0,a:{b:2,c:3}}],
+					"$a.b"
+				);
+            },
+            "A missing array (that cannot be nested below a non object field) produces no results.": function(){
+				checkResults(
+					[{_id:0,a:4}],
+					[],
+					"$a.b"
+				);
+            },
+            "Unwind an array in a doubly nested document.": function(){
+				checkResults(
+					[{_id:0,a:{b:{d:[1,2],e:4},c:3}}],
+					[{_id:0,a:{b:{d:1,e:4},c:3}},{_id:0,a:{b:{d:2,e:4},c:3}}],
+					"$a.b.d"
+				);
+            },
+            "Unwind several documents in a row.": function(){
+				checkResults(
+					[
+						{_id:0,a:[1,2,3]},
+						{_id:1},
+						{_id:2},
+						{_id:3,a:[10,20]},
+						{_id:4,a:[30]}
+					],
+					[
+						{_id:0,a:1},
+						{_id:0,a:2},
+						{_id:0,a:3},
+						{_id:3,a:10},
+                        {_id:3,a:20},
+                        {_id:4,a:30}
+                    ]
+				);
+            },
+            "Unwind several more documents in a row.": function(){
+				checkResults(
+					[
+						{_id:0,a:null},
+						{_id:1},
+						{_id:2,a:['a','b']},
+						{_id:3},
+						{_id:4,a:[1,2,3]},
+						{_id:5,a:[4,5,6]},
+						{_id:6,a:[7,8,9]},
+						{_id:7,a:[]}
+					],
+					[
+						{_id:2,a:'a'},
+						{_id:2,a:'b'},
+						{_id:4,a:1},
+						{_id:4,a:2},
+                        {_id:4,a:3},
+                        {_id:5,a:4},
+                        {_id:5,a:5},
+                        {_id:5,a:6},
+                        {_id:6,a:7},
+                        {_id:6,a:8},
+                        {_id:6,a:9}
+					]
+				);
+            }
+            
+        },
+
+        "#createFromJson()": {
+
+            "should error if called with non-string": function testNonObjectPassed() {
+                //Date as arg
+                assert.throws(function() {
+                    var pds = createUnwind(new Date());
+                });
+                //Array as arg
+                assert.throws(function() {
+                    var pds = createUnwind([]);
+                });
+                //Empty args
+                assert.throws(function() {
+                    var pds = UnwindDocumentSource.createFromJson();
+                });
+                //Top level operator
+                assert.throws(function() {
+                    var pds = createUnwind({$add: []});
+                });
+
+            }
+
+        },
+        "#getDependencies": {
+			"should Dependant field paths.": function () {
+                var pds = createUnwind("$x.y.z"),
+					deps = {};
+                assert.strictEqual(pds.getDependencies(deps), DocumentSource.GetDepsReturn.SEE_NEXT);
+                assert.deepEqual(deps, {"x.y.z":1});
+                
+			}
+        }
+
+    }
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);