Browse Source

Merge pull request #126 from RiveraGroup/feature/mongo_2.6.5_documentSource_SortDocumentSource

Feature/mongo 2.6.5 document source sort document source
Chris Sexton 11 years ago
parent
commit
068af52543

+ 294 - 76
lib/pipeline/documentSources/SortDocumentSource.js

@@ -2,7 +2,8 @@
 
 var async = require("async"),
 	DocumentSource = require("./DocumentSource"),
-	LimitDocumentSource = require("./LimitDocumentSource");
+	LimitDocumentSource = require("./LimitDocumentSource"),
+	Document = require('../Document');
 
 /**
  * A document source sorter
@@ -34,6 +35,9 @@ var SortDocumentSource = module.exports = function SortDocumentSource(ctx){
 
 // DEPENDENCIES
 var FieldPathExpression = require("../expressions/FieldPathExpression"),
+	VariablesIdGenerator = require("../expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../expressions/VariablesParseState"),
+	Variables = require("../expressions/Variables"),
 	Value = require("../Value");
 
 klass.sortName = "$sort";
@@ -46,13 +50,10 @@ proto.getFactory = function getFactory(){
 	return klass;	// using the ctor rather than a separate .create() method
 };
 
-klass.GetDepsReturn = {
-	SEE_NEXT: "SEE_NEXT" // Add the next Source's deps to the set
-};
-
 proto.dispose = function dispose() {
 	this.docIterator = 0;
 	this.documents = [];
+	this._output.reset();
 	this.source.dispose();
 };
 
@@ -64,7 +65,7 @@ proto.getDependencies = function getDependencies(deps) {
 	for(var i = 0; i < this.vSortKey.length; ++i) {
 		this.vSortKey[i].addDependencies(deps);
 	}
-	return klass.GetDepsReturn.SEE_NEXT;
+	return DocumentSource.GetDepsReturn.SEE_NEXT;
 };
 
 proto.coalesce = function coalesce(nextSource) {
@@ -82,28 +83,33 @@ proto.coalesce = function coalesce(nextSource) {
 proto.getNext = function getNext(callback) {
 	if (!callback) throw new Error(this.getSourceName() + ' #getNext() requires callback');
 
+	if (this.expCtx instanceof Object && this.expCtx.checkForInterrupt && this.expCtx.checkForInterrupt() === false)
+		return callback(new Error("Interrupted"));
+
 	var self = this,
 		out;
 	async.series(
 		[
 			function(next) {
 				if (!self.populated)
+				{
 					self.populate(function(err) {
 						return next(err);
 					});
-				else
-					next();
+				} else {
+					return next();
+				}
 			},
 			function(next) {
 				if (self.docIterator >= self.documents.length) {
-					out = DocumentSource.EOF;
-					return next(null, DocumentSource.EOF);
+					out = null;
+					return next(null, null);
 				}
 
 				var output = self.documents[self.docIterator++];
-				if (!output || output === DocumentSource.EOF) {
-					out = DocumentSource.EOF;
-					return next(null, DocumentSource.EOF);
+				if (!output || output === null) {
+					out = null;
+					return next(null, null);
 				}
 
 				out = output;
@@ -118,18 +124,26 @@ proto.getNext = function getNext(callback) {
 	return out;
 };
 
+/**
+* Serialize to Array.
+*
+* @param {Array} array
+* @param {bool} explain
+**/
 proto.serializeToArray = function serializeToArray(array, explain) {
 	var doc = {};
-	if (explain) {
-		doc.sortKey = this.serializeSortKey();
+	if (explain) { // always one obj for combined $sort + $limit
+		doc.sortKey = this.serializeSortKey(explain);
 		doc.mergePresorted = this._mergePresorted;
 		doc.limit = this.limitSrc ? this.limitSrc.getLimit() : undefined;
 		array.push(doc);
-	} else {
-		var inner = this.serializeSortKey();
+	} else { // one Value for $sort and maybe a Value for $limit
+		var inner = {};
+		inner = this.serializeSortKey(explain);
 		if (this._mergePresorted)
 			inner.$mergePresorted = true;
 		doc[this.getSourceName()] = inner;
+
 		array.push(doc);
 
 		if (this.limitSrc)
@@ -151,7 +165,10 @@ proto.serialize = function serialize(explain) {
 * @param {bool} ascending if true, use the key for an ascending sort, otherwise, use it for descending
 **/
 proto.addKey = function addKey(fieldPath, ascending) {
-	var pathExpr = new FieldPathExpression(fieldPath);
+	var idGenerator = new VariablesIdGenerator(),
+		vps = new VariablesParseState(idGenerator);
+
+	var pathExpr = FieldPathExpression.parse("$$ROOT." + fieldPath, vps);
 	this.vSortKey.push(pathExpr);
 	if (ascending === true || ascending === false) {
 		this.vAscending.push(ascending);
@@ -161,62 +178,173 @@ proto.addKey = function addKey(fieldPath, ascending) {
 	}
 };
 
-proto.populate = function populate(callback) {
+proto.makeSortOptions = function makeSortOptions(){
 	/* make sure we've got a sort key */
 	if (!this.vSortKey.length) throw new Error("no sort key for " + this.getSourceName());
 
-	// Skipping stuff about mergeCursors and commandShards
+	// Skipping memory checks
+
+	var opts;
+	if ( this.limitSrc)
+		opts.limit = limitSrc.getLimt();
+
+	return opts;
+}
+
+
+proto.populate = function populate(callback) {
+	if ( this._mergePresorted ){
+		// Skipping stuff about mergeCursors and commandShards
+
+		if ( this.source instanceof MergeCursorDocumentSouce ){
+			populateFromCursors( this.source);
+		} else if ( this.source instanceof CommandShardsDocumentSource){
+			populateFromJsonArrays(this.source);
+		} else {
+			throw new Error("code 17196; the " + klass.sortName + "can only mergePresorted from MergeCursors and CommandShards");
+		}
+	} else {
+
+		/* pull everything from the underlying source */
+		var self = this,
+			next;
+
+		async.doWhilst(
+			function (cb) {
+				self.source.getNext(function(err, doc) {
+					next = doc;
+
+					// Don't add EOF; it doesn't sort well.
+					if (doc !== null)
+						self.documents.push(doc);
+
+					return cb();
+				});
+			},
+			function() {
+				return next !== null;
+			},
+			function(err) {
+				/* sort the list */
+				self.documents.sort(SortDocumentSource.prototype.compare.bind(self));
+
+				/* start the sort iterator */
+				self.docIterator = 0;
+
+				self.populated = true;
+				//self._output.reset(true);
+				return callback();
+		}
+		);
+
 
-	/* pull everything from the underlying source */
-	var self = this,
-		next;
-	async.doWhilst(
-		function (cb) {
-			self.source.getNext(function(err, doc) {
-				next = doc;
-
-				// Don't add EOF; it doesn't sort well.
-				if (doc !== DocumentSource.EOF)
-					self.documents.push(doc);
-				return cb();
-			});
-		},
-		function() {
-			return next !== DocumentSource.EOF;
-		},
-		function(err) {
-	/* sort the list */
-			self.documents.sort(SortDocumentSource.prototype.compare.bind(self));
-
-	/* start the sort iterator */
-			self.docIterator = 0;
-
-			self.populated = true;
-			return callback();
 	}
-	);
+	this.populated = true;
 };
 
+klass.IteratorFromCursor = (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 IteratorFromCursor(sorter, cursor){
+		this._sorter = new SortDocumentSource(sorter);
+		//this._cursor = new DBClientCursor(cursor);
+
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	proto.more = function more() {
+		return this._cursor.more();
+	};
+
+	proto.next = function next() {
+		var doc = DocumentSourceMergeCursors(this._cursor);
+		// TODO: make_pair for return
+		//return {this._sorter.extractKey(doc): doc};
+	};
+	return klass;
+})();
+
+proto.populateFromCursors = function populateFromCursors(cursors){
+	for (var i = 0; i < cursors.length; i++) {
+		// TODO Create class
+		//this.iterators.push(boost::make_shared<IteratorFromBsonArray>(this, cursors[i]));
+	}
+
+	this._output.reset( ); // TODO: MySorter::Iterator::merge(iterators, makeSortOptions(), Comparator(*this))
+
+}
+
+klass.IteratorFromBsonArray = (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 IteratorFromBsonArray(sorter, array){
+		this._sorter = new SortDocumentSource(sorter);
+		//this._iterator = new BSONObjIterator(array);
+
+	}, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	proto.next = function next() {
+		var doc = DocumentSourceMergeCursors(this._cursor);
+		// TODO: make_pair for return
+		//return {this._sorter.extractKey(doc): doc};
+	};
+
+	proto.more = function more() {
+		return this._cursor.more();
+	};
+
+	return klass;
+})();
+
+proto.populateFromBsonArrays = function populateFromBsonArrays(arrays){
+	for (var i = 0; i < arrays.lenth; i++) {
+		// TODO Create class
+		//this.iterators.push(boost::make_shared<IteratorFromBsonArray>(this, arrays[i]));
+	}
+	this._output.reset( ); // TODO: MySorter::Iterator::merge(iterators, makeSortOptions(), Comparator(*this))
+}
+/**
+* Extract the key
+*
+* @param  {d} document
+* @returns {keys} extracted key
+**/
+proto.extractKey = function extractKey(d){
+	var vars = new Variables(0,d);
+
+	if ( this.vSortKey.length == 1)
+		return this.vSortKey[0].evaluate(vars);
+
+	var keys;
+	for (var i=0; i < this.vSortKey.length; i++) {
+		keys.push(this.vSortKey[i].evaluate(vars));
+	}
+	return keys;
+}
+
 /**
  * Compare two documents according to the specified sort key.
  *
- * @param {Object} pL the left side doc
- * @param {Object} pR the right side doc
+ * @param {Object} lhs the left side doc
+ * @param {Object} rhs the right side doc
  * @returns {Number} a number less than, equal to, or greater than zero, indicating pL < pR, pL == pR, or pL > pR, respectively
 **/
-proto.compare = function compare(pL,pR) {
-	/**
-	* populate() already checked that there is a non-empty sort key,
-	* so we shouldn't have to worry about that here.
-	*
-	* However, the tricky part is what to do is none of the sort keys are
-	* present.  In this case, consider the document less.
-	**/
-	var n = this.vSortKey.length;
-	for(var i = 0; i < n; ++i) {
+proto.compare = function compare(lhs,rhs) {
+	/*
+	  populate() already checked that there is a non-empty sort key,
+	  so we shouldn't have to worry about that here.
+	  However, the tricky part is what to do is none of the sort keys are
+	  present.  In this case, consider the document less.
+	*/
+
+	for(var i = 0, n = this.vSortKey.length; i < n; ++i) {
+		var pathExpr = FieldPathExpression.create(this.vSortKey[i].getFieldPath(false).fieldNames.slice(1).join('.'));
+
 		/* evaluate the sort keys */
-		var pathExpr = new FieldPathExpression(this.vSortKey[i].getFieldPath(false));
-		var left = pathExpr.evaluate(pL), right = pathExpr.evaluate(pR);
+		var left = pathExpr.evaluate(lhs), right = pathExpr.evaluate(rhs);
 
 		/*
 		Compare the two values; if they differ, return.  If they are
@@ -238,43 +366,133 @@ proto.compare = function compare(pL,pR) {
 };
 
 /**
-* Write out an object whose contents are the sort key.
+ * Write out an object whose contents are the sort key.
+ *
+ * @param {bool} explain
+ * @return {Object} key
 **/
-proto.serializeSortKey = function sortKeyToJson() {
+proto.serializeSortKey = function serializeSortKey(explain) {
 	var keyObj = {};
-
+	// add the key fields
 	var n = this.vSortKey.length;
 	for (var i = 0; i < n; i++) {
-		var fieldPath = this.vSortKey[i].getFieldPath();
-		keyObj[fieldPath] = this.vAscending[i] ? 1 : -1;
+		if ( this.vSortKey[i] instanceof FieldPathExpression ) {
+			var fieldPath = this.vSortKey[i].getFieldPath(false).fieldNames.slice(1).join('.');
+			// append a named integer based on the sort order
+			keyObj[fieldPath] = this.vAscending[i] ? 1 : -1;
+		} else {
+			// other expressions use a made-up field name
+			keyObj[{"$computed":i}] = this.vSortKey[i].serialize(explain);
+		}
 	}
 	return keyObj;
 };
 
+/**
+ * Creates a new SortDocumentSource from Json
+ *
+ * @param {Object} elem
+ * @param {Object} expCtx
+ *
+**/
+klass.createFromJson = function createFromJson(elem, expCtx) {
+	if (typeof elem !== "object") throw new Error("code 15973; the " + klass.sortName + " key specification must be an object");
+
+	return this.create(expCtx, elem);
+};
+
 /**
  * Creates a new SortDocumentSource
- * @param {Object} jsonElement
+ *
+ * @param {Object} expCtx
+ * @param {object} sortorder
+ * @param {int} limit
+ *
 **/
-klass.createFromJson = function createFromJson(jsonElement, ctx) {
-	if (typeof jsonElement !== "object") throw new Error("code 15973; the " + klass.sortName + " key specification must be an object");
+klass.create = function create(expCtx, sortOrder, limit) {
 
 	var Sort = proto.getFactory(),
-		nextSort = new Sort(ctx);
+		nextSort = new Sort(expCtx);
 
 	/* check for then iterate over the sort object */
 	var sortKeys = 0;
-	for(var key in jsonElement) {
-		var sortOrder = 0;
+	for(var keyField in sortOrder) {
+		var fieldName = keyField.fieldName;
+
+		if ( fieldName === "$mergePresorted" ){
+			Sort._mergePresorted = true;
+			continue;
+		}
+
+		if ( keyField instanceof Object) {
+			// this restriction is due to needing to figure out sort direction
+			throw new Error("code 17312; " + klass.sortName + "the only expression supported by $sort right now is {$meta: 'textScore'}");
+
+			nextSort.vSortKey.push(new ExpressionMeta());
+			nextSort.vAscending.push(false); // best scoring documents first
+			continue;
+		}
 
-		if (typeof jsonElement[key] !== "number") throw new Error("code 15974; " + klass.sortName + " key ordering must be specified using a number");
+		if (typeof sortOrder[keyField] !== "number") throw new Error("code 15974; " + klass.sortName + "$sort key ordering must be specified using a number or {$meta: 'text'}");
 
-		sortOrder = jsonElement[key];
-		if ((sortOrder != 1) && (sortOrder !== -1)) throw new Error("code 15975; " + klass.sortName + " key ordering must be 1 (for ascending) or 0 (for descending)");
+		// RedBeard0531 can the thanked.
+		var sortDirection = 0;
+		sortDirection = sortOrder[keyField];
+		if ((sortDirection != 1) && (sortDirection !== -1)) throw new Error("code 15975; " + klass.sortName + " $sort key ordering must be 1 (for ascending) or -1 (for descending)");
 
-		nextSort.addKey(key, (sortOrder > 0));
+		nextSort.addKey(keyField, (sortDirection > 0));
 		++sortKeys;
 	}
 
 	if (sortKeys <= 0) throw new Error("code 15976; " + klass.sortName + " must have at least one sort key");
+
+
+	if ( limit > 0) {
+		var coalesced = nextSort.coalesce( create(expCtx, limit));
+		// should always coalesce
+	}
+
 	return nextSort;
 };
+
+// SplittableDocumentSource implementation.
+klass.isSplittableDocumentSource = true;
+
+/**
+ * Get dependencies.
+ *
+ * @param deps
+ * @returns {number}
+ */
+proto.getDependencies = function getDependencies(deps) {
+	for(var i = 0; i < this.vSortKey.length; i++) {
+		this.vSortKey[i].addDependencies(deps);
+	}
+
+	return DocumentSource.GetDepsReturn.SEE_NEXT;
+};
+
+/**
+ * Get shard source.
+ *
+ * @returns {this}
+ */
+proto.getShardSource = function getShardSource() {
+	if (this._mergePresorted) throw new Error("getShardSource", + klass.sortName + " should not be merging presorted");
+	return this;
+};
+
+/**
+ * Get merge source.
+ *
+ * @returns {SortDocumentSource}
+ */
+proto.getMergeSource = function getMergeSource() {
+	if ( this._mergingPresorted) throw new Error("getMergeSource", + klass.sortName + " should not be merging presorted");
+	var other = new SortDocumentSource();
+	other.vAscending = this.vAscending;
+	other.vSortKey = this.vSortKey;
+	other.limitSrc = this.limitSrc;
+	other._mergingPresorted = true;
+	return other;
+};

+ 254 - 156
test/lib/pipeline/documentSources/SortDocumentSource.js

@@ -5,9 +5,65 @@ var assert = require("assert"),
 	SortDocumentSource = require("../../../../lib/pipeline/documentSources/SortDocumentSource"),
 	LimitDocumentSource = require("../../../../lib/pipeline/documentSources/LimitDocumentSource"),
 	CursorDocumentSource = require("../../../../lib/pipeline/documentSources/CursorDocumentSource"),
-	Cursor = require("../../../../lib/Cursor"),
+	ArrayRunner = require("../../../../lib/query/ArrayRunner"),
 	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression");
 
+var getCursorDocumentSource = function(values) {
+	return new CursorDocumentSource(null, new ArrayRunner(values), null);
+};
+
+
+/// 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 sds = SortDocumentSource.createFromJson(args.spec),
+			next,
+			results = [],
+			cds = new CursorDocumentSource(null, new ArrayRunner(args.docs), null);
+		sds.setSource(cds);
+		async.whilst(
+			function() {
+				return next !== null;
+			},
+			function(done) {
+				sds.getNext(function(err, doc) {
+					if(err) return done(err);
+					next = doc;
+					if(next === null) {
+						return done();
+					} else {
+						results.push(next);
+						return done();
+					}
+				});
+			},
+			function(err) {
+				assert.equal(JSON.stringify(results), JSON.stringify(args.expected));
+				if(args.done) {
+					return args.done();
+				}
+			}
+		);
+	}else{
+		if(args.throw) {
+			assert.throws(function(){
+				SortDocumentSource.createFromJson(args.spec);
+			});
+		} else {
+			assert.doesNotThrow(function(){
+				var gds = SortDocumentSource.createFromJson(args.spec);
+			});
+		}
+	}
+}
 
 module.exports = {
 
@@ -15,11 +71,28 @@ module.exports = {
 
 		"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SortDocumentSource();
+			// $sort spec is not an object
+			"should throw Error when constructing without args": function testConstructor(){
+				assertExpectedResult({"throw":true});
+			},
+
+			// $sort spec is not an object
+			"should throw Error when $sort spec is not an object": function testConstructor(){
+				assertExpectedResult({spec:"Foo"});
+			},
+
+			// $sort spec is an empty object
+			"should throw Error when $sort spec is an empty object": function testConstructor(){
+				assertExpectedResult({spec:{}});
+			},
+
+
+			// $sort _id is specified as an invalid object expression
+			"should throw error when _id is an invalid object expression": function testConstructor(){
+				assertExpectedResult({
+					spec:{_id:{$add:1, $and:1}},
 				});
-			}
+			},
 
 		},
 
@@ -41,71 +114,82 @@ module.exports = {
 		},
 
 		"#getNext()": {
-
+			/** Assert that iterator state accessors consistently report the source is exhausted. */
 			"should return EOF if there are no more sources": function noSources(next){
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				cwc._cursor = new Cursor( [{a: 1}] );
-				var cds = new CursorDocumentSource(cwc);
-				var sds = SortDocumentSource.createFromJson({a:1});
+				var cds = getCursorDocumentSource([{"a": 1}]);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
 				sds.setSource(cds);
 				sds.getNext(function(err, val) {
 					assert.deepEqual(val, {a:1});
 					sds.getNext(function(err, val) {
-						assert.equal(val, DocumentSource.EOF);
-						next();
+						if (err) throw err;
+						assert.equal(val, null);
+						return next();
 					});
 				});
+
 			},
-			"should return EOF if there are more documents": function hitSort(next){
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				cwc._cursor = new Cursor( [{a: 1}] );
-				var cds = new CursorDocumentSource(cwc);
-				var sds = SortDocumentSource.createFromJson({a:1});
+
+			"should not return EOF if there are documents": function hitSort(next){
+				var cds = getCursorDocumentSource([{a: 1}]);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
 				sds.setSource(cds);
-				sds.getNext(function(err, doc) {
-					assert.notEqual(doc, DocumentSource.EOF);
-					next();
-				});
+				async.series([
+						cds.getNext.bind(cds),
+					],
+					function(err,res) {
+						if (err) throw err;
+						assert.notEqual(res, null);
+						return next();
+					}
+				);
 			},
 
 			"should return the current document source": function currSource(next){
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				cwc._cursor = new Cursor( [{a: 1}] );
-				var cds = new CursorDocumentSource(cwc);
-				var sds = SortDocumentSource.createFromJson({a:1});
+				var cds = getCursorDocumentSource([{a: 1}]);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
 				sds.setSource(cds);
-				sds.getNext(function(err, doc) {
-					assert.deepEqual(doc, { a:1 });
-					next();
-				});
+				async.series([
+						cds.getNext.bind(cds),
+					],
+					function(err,res) {
+						if (err) throw err;
+						assert.deepEqual(res, [ { a: 1 } ]);
+						return next();
+					}
+				);
 			},
 
-			"should return next document when moving to the next source": function nextSource(next){
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				cwc._cursor = new Cursor( [{a: 1}, {b:2}] );
-				var cds = new CursorDocumentSource(cwc);
-				var sds = SortDocumentSource.createFromJson({a:1});
+			"should return next document when moving to the next source sorted descending": function nextSource(next){
+				var cds = getCursorDocumentSource([{a: 1}, {b:2}]);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
 				sds.setSource(cds);
-				sds.getNext(function(err, doc) {
-					assert.deepEqual(doc, {b:2});
-					next();
-				});
+				async.series([
+						cds.getNext.bind(cds),
+					],
+					function(err,res) {
+						if (err) throw err;
+						assert.deepEqual(res, [ { a: 1 } ]);
+						return next();
+					}
+				);
 			},
 
-			"should return false for no sources remaining": function noMoar(next){
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				cwc._cursor = new Cursor( [{a: 1}, {b:2}] );
-				var cds = new CursorDocumentSource(cwc);
-				var sds = SortDocumentSource.createFromJson({a:1});
+			"should return false for no sources remaining sorted descending": function noMoar(next){
+				var cds = getCursorDocumentSource([{a: 1}, {b:2}]);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
 				sds.setSource(cds);
-				sds.getNext(function(err, doc) {
-					sds.getNext(function(err, doc) {
-						assert.deepEqual(doc, {a:1});
-						next();
-					});
-				});
+				async.series([
+						cds.getNext.bind(cds),
+						cds.getNext.bind(cds),
+					],
+					function(err,res) {
+						if (err) throw err;
+						assert.deepEqual(res,  [ { a: 1 }, { b: 2 } ]);
+						return next();
+					}
+				);
 			}
-
 		},
 
 		"#serialize()": {
@@ -114,93 +198,108 @@ module.exports = {
 				var sds = new SortDocumentSource();
 				assert.throws(sds.serialize.bind(sds));
 			}
-
 		},
 
 		"#serializeToArray()": {
 
-			"should create an object representation of the SortDocumentSource": function serializeToArrayTest(){
+			/**
+            * Check that the BSON representation generated by the souce matches the BSON it was
+            * created with.
+            */
+            "should have equal json representation": function serializeToArrayCheck(next){
+				var sds = SortDocumentSource.createFromJson({"sort":1}, {});
+				var array = [];
+				sds.serializeToArray(array, false);
+				assert.deepEqual(array, [{"$sort":{"sort":1}}]);
+				return next();
+			},
+
+			"should create an object representation of the SortDocumentSource": function serializeToArrayTest(next){
 				var sds = new SortDocumentSource();
-				sds.vSortKey.push(new FieldPathExpression("b") );
-				var t = [];
-				sds.serializeToArray(t, false);
-				assert.deepEqual(t, [{ "$sort": { "b": -1 } }]);
+				var fieldPathVar;
+				sds.vSortKey.push(new FieldPathExpression("b", fieldPathVar) );
+				var array = [];
+				sds.serializeToArray(array, false);
+				assert.deepEqual(array, [{"$sort":{"":-1}}] );
+				return next();
 			}
 
 		},
 
 		"#createFromJson()": {
 
-			"should return a new SortDocumentSource object from an input JSON object": function createTest(){
+			"should return a new SortDocumentSource object from an input JSON object": function createTest(next){
 				var sds = SortDocumentSource.createFromJson({a:1});
 				assert.strictEqual(sds.constructor, SortDocumentSource);
 				var t = [];
 				sds.serializeToArray(t, false);
-				assert.deepEqual(t, [{ "$sort": { "a": 1 } }]);
+				assert.deepEqual(t, [{"$sort":{"a":1}}] );
+				return next();
 			},
 
-			"should return a new SortDocumentSource object from an input JSON object with a descending field": function createTest(){
+			"should return a new SortDocumentSource object from an input JSON object with a descending field": function createTest(next){
 				var sds = SortDocumentSource.createFromJson({a:-1});
 				assert.strictEqual(sds.constructor, SortDocumentSource);
 				var t = [];
 				sds.serializeToArray(t, false);
-				assert.deepEqual(t, [{ "$sort": { "a": -1 } }]);
+				assert.deepEqual(t,  [{"$sort":{"a":-1}}]);
+				return next();
 			},
 
-			"should return a new SortDocumentSource object from an input JSON object with dotted paths": function createTest(){
+			"should return a new SortDocumentSource object from an input JSON object with dotted paths": function createTest(next){
 				var sds = SortDocumentSource.createFromJson({ "a.b":1 });
 				assert.strictEqual(sds.constructor, SortDocumentSource);
 				var t = [];
 				sds.serializeToArray(t, false);
-				assert.deepEqual(t, [{ "$sort": { "a.b" : 1  } }]);
+				assert.deepEqual(t, [{"$sort":{"a.b":1}}]);
+				return next();
 			},
 
-			"should throw an exception when not passed an object": function createTest(){
+			"should throw an exception when not passed an object": function createTest(next){
 				assert.throws(function() {
 					var sds = SortDocumentSource.createFromJson(7);
 				});
+				return next();
 			},
 
-			"should throw an exception when passed an empty object": function createTest(){
+			"should throw an exception when passed an empty object": function createTest(next){
 				assert.throws(function() {
 					var sds = SortDocumentSource.createFromJson({});
 				});
+				return next();
 			},
 
-			"should throw an exception when passed an object with a non number value": function createTest(){
+			"should throw an exception when passed an object with a non number value": function createTest(next){
 				assert.throws(function() {
 					var sds = SortDocumentSource.createFromJson({a:"b"});
 				});
+				return next();
 			},
 
-			"should throw an exception when passed an object with a non valid number value": function createTest(){
+			"should throw an exception when passed an object with a non valid number value": function createTest(next){
 				assert.throws(function() {
 					var sds = SortDocumentSource.createFromJson({a:14});
 				});
+				next();
 			}
-
 		},
 
 		"#sort": {
 
 			"should sort a single document": function singleValue(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				cwc._cursor = new Cursor( [{_id:0, a: 1}] );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("_id", false);
 				sds.setSource(cds);
 				sds.getNext(function(err, actual) {
+					if (err) throw err;
 					assert.deepEqual(actual, {_id:0, a:1});
-					next();
+					return next();
 				});
 			},
 
 			"should sort two documents": function twoValue(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1}, {_id:1, a:0}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1}, {_id:1, a:0}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("_id", false);
 				sds.setSource(cds);
@@ -210,17 +309,15 @@ module.exports = {
 						sds.getNext.bind(sds),
 					],
 					function(err,res) {
-						assert.deepEqual([{_id:1, a: 0}, {_id:0, a:1}], res);
-						next();
+						if (err) throw err;
+						assert.deepEqual([ { _id: 1, a: 0 }, { _id: 0, a: 1 } ], res);
+						return next();
 					}
 				);
 			},
 
 			"should sort two documents in ascending order": function ascendingValue(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1}, {_id:5, a:12}, {_id:1, a:0}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1}, {_id:5, a:12}, {_id:1, a:0}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("_id", true);
 				sds.setSource(cds);
@@ -234,21 +331,20 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:0, a: 1}, {_id:1, a:0}, {_id:5, a:12}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:0, a: 1}, {_id:1, a:0}, {_id:5, a:12}, null], docs);
+						return next();
 					}
 				);
 			},
 
 			"should sort documents with a compound key": function compoundKeySort(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
-				var sds = new SortDocumentSource();
+				var cds = getCursorDocumentSource([{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}]);
+				var sds = SortDocumentSource.createFromJson({"sort":1});
+
 				sds.addKey("a", false);
 				sds.addKey("b", false);
 				sds.setSource(cds);
@@ -262,20 +358,18 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:5, a:12, b:7}, {_id:0, a:1, b:3}, {_id:1, a:0, b:2}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:5, a:12, b:7}, {_id:0, a:1, b:3}, {_id:1, a:0, b:2}, null], docs);
+						return next();
 					}
 				);
 			},
 
 			"should sort documents with a compound key in ascending order": function compoundAscendingKeySort(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("a", true);
 				sds.addKey("b", true);
@@ -290,20 +384,18 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:1, a:0, b:2}, {_id:0, a:1, b:3}, {_id:5, a:12, b:7}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:1, a:0, b:2}, {_id:0, a:1, b:3}, {_id:5, a:12, b:7}, null], docs);
+						return next();
 					}
 				);
 			},
 
 			"should sort documents with a compound key in mixed order": function compoundMixedKeySort(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}, {_id:8, a:7, b:42}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1, b:3}, {_id:5, a:12, b:7}, {_id:1, a:0, b:2}, {_id:8, a:7, b:42}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("a", true);
 				sds.addKey("b", false);
@@ -318,30 +410,26 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:1, a:0, b:2}, {_id:0, a:1, b:3}, {_id:8, a:7, b:42}, {_id:5, a:12, b:7}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:1, a:0, b:2}, {_id:0, a:1, b:3}, {_id:8, a:7, b:42}, {_id:5, a:12, b:7}, null], docs);
+						return next();
 					}
 				);
 			},
 
-			"should not sort different types": function diffTypesSort() {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1}, {_id:1, a:"foo"}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+			"should not sort different types": function diffTypesSort(next) {
+				var cds = getCursorDocumentSource([{_id:0, a: 1}, {_id:1, a:"foo"}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("a", false);
 				assert.throws(sds.setSource(cds));
+				return next();
 			},
 
 			"should sort docs with missing fields": function missingFields(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1}, {_id:1}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1}, {_id:1}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("a", true);
 				sds.setSource(cds);
@@ -355,20 +443,18 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:1}, {_id:0, a:1}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:1}, {_id:0, a:1}, null], docs);
+						return next();
 					}
 				);
 			},
 
 			"should sort docs with null fields": function nullFields(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: 1}, {_id:1, a: null}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0, a: 1}, {_id:1, a: null}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("a", true);
 				sds.setSource(cds);
@@ -382,20 +468,18 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:1, a:null}, {_id:0, a:1}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:1, a:null}, {_id:0, a:1}, null], docs);
+						return next();
 					}
 				);
 			},
 
-			"should not support a missing object nested in an array": function missingObjectWithinArray() {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0, a: [1]}, {_id:1, a:[0]}];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+			"should not support a missing object nested in an array": function missingObjectWithinArray(next) {
+				var cds = getCursorDocumentSource([{_id:0, a: [1]}, {_id:1, a:[0]}]);
 				var sds = new SortDocumentSource();
 				assert.throws(function() {
 					sds.addKey("a.b", false);
@@ -406,13 +490,11 @@ module.exports = {
 						sds.advance();
 					}
 				});
+				return next();
 			},
 
 			"should compare nested values from within an array": function extractArrayValues(next) {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0,a:[{b:1},{b:2}]}, {_id:1,a:[{b:1},{b:1}]} ];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc);
+				var cds = getCursorDocumentSource([{_id:0,a:[{b:1},{b:2}]}, {_id:1,a:[{b:1},{b:1}]}]);
 				var sds = new SortDocumentSource();
 				sds.addKey("a.b", true);
 				sds.setSource(cds);
@@ -426,33 +508,37 @@ module.exports = {
 						});
 					},
 					function() {
-						return docs[i++] !== DocumentSource.EOF;
+						return docs[i++] !== null;
 					},
 					function(err) {
-						assert.deepEqual([{_id:1,a:[{b:1},{b:1}]},{_id:0,a:[{b:1},{b:2}]}, DocumentSource.EOF], docs);
-						next();
+						if (err) throw err;
+						assert.deepEqual([{_id:1,a:[{b:1},{b:1}]},{_id:0,a:[{b:1},{b:2}]}, null], docs);
+						return next();
 					}
 				);
 			}
-
 		},
 
 		"#coalesce()": {
-			"should return false when coalescing a non-limit source": function nonLimitSource() {
-				var cwc = new CursorDocumentSource.CursorWithContext();
-				var l = [{_id:0,a:[{b:1},{b:2}]}, {_id:1,a:[{b:1},{b:1}]} ];
-				cwc._cursor = new Cursor( l );
-				var cds = new CursorDocumentSource(cwc),
-					sds = SortDocumentSource.createFromJson({a:1});
+			"should return false when coalescing a non-limit source": function nonLimitSource(next) {
+				var cds = getCursorDocumentSource([{_id:0,a:[{b:1},{b:2}]}, {_id:1,a:[{b:1},{b:1}]} ]);
+				var	sds = SortDocumentSource.createFromJson({a:1});
 
 				var newSrc = sds.coalesce(cds);
 				assert.equal(newSrc, false);
+				return next();
 			},
 
-			"should return limit source when coalescing a limit source": function limitSource() {
+
+			"should return limit source when coalescing a limit source": function limitSource(next) {
 				var sds = SortDocumentSource.createFromJson({a:1}),
 					lds = LimitDocumentSource.createFromJson(1);
 
+				// TODO: add missing test cases.
+				// array json getLimit
+				// getShardSource
+				// getMergeSource
+
 				var newSrc = sds.coalesce(LimitDocumentSource.createFromJson(10));
 				assert.ok(newSrc instanceof LimitDocumentSource);
 				assert.equal(sds.getLimit(), 10);
@@ -464,24 +550,36 @@ module.exports = {
 				var arr = [];
 				sds.serializeToArray(arr);
 				assert.deepEqual(arr, [{$sort: {a:1}}, {$limit: 5}]);
+
+				// TODO: add missing test cases
+				// doc array get limit
+				// getShardSource
+				// get MergeSource
+				return next();
 			},
 		},
 
 		"#dependencies": {
-			"should have Dependant field paths": function dependencies() {
-				var sds = new SortDocumentSource();
-				sds.addKey("a", true);
-				sds.addKey("b.c", false);
-				var deps = {};
-				assert.equal("SEE_NEXT", sds.getDependencies(deps));
-				assert.equal(2, Object.keys(deps).length);
-				assert.ok(deps.a);
-				assert.ok(deps["b.c"]);
+			/** Dependant field paths. */
+			"should have Dependant field paths": function dependencies(next) {
+			 	var sds = SortDocumentSource.createFromJson({sort: 1});
+
+				sds.addKey('a', true);
+			 	sds.addKey('b.c', false);
+
+				var deps = {fields: {}, needWholeDocument: false, needTextScore: false};
+
+				assert.equal('SEE_NEXT', sds.getDependencies(deps));
+				// Sort keys are now part of deps fields.
+				assert.equal(3, Object.keys(deps.fields).length);
+			 	assert.equal(1, deps.fields.a);
+				assert.equal(1, deps.fields['b.c']);
+				assert.equal(false, deps.needWholeDocument);
+				assert.equal(false, deps.needTextScore);
+				return next();
 			}
 		}
-
 	}
-
 };
 
 if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).grep(process.env.MOCHA_GREP || '').run(process.exit);