Prechádzať zdrojové kódy

Refs #1005. Almost done porting sort, just finishing 'Ordering of a missing value' down

http://source.rd.rcg.local/trac/eagle6/changeset/1353/Eagle6_SVN
Spencer Rathbun 12 rokov pred
rodič
commit
db95eb73ff

+ 259 - 0
lib/pipeline/documentSources/SortDocumentSource.js

@@ -0,0 +1,259 @@
+var SortDocumentSource = module.exports = (function(){
+	// CONSTRUCTOR
+	/**
+	 * A document source sorter
+	 *
+	 * Since we don't have shards, this inherits from DocumentSource, instead of SplittableDocumentSource
+	 * 
+	 * @class SortDocumentSource
+	 * @namespace munge.pipepline.documentsource
+	 * @module munge
+	 * @constructor
+	**/
+	var klass = module.exports = SortDocumentSource = function SortDocumentSource(/* pCtx*/){
+		if(arguments.length !== 0) throw new Error("zero args expected");
+		base.call(this);
+		/*
+		* Before returning anything, this source must fetch everything from
+		* the underlying source and group it.  populate() is used to do that
+		* on the first call to any method on this source.  The populated
+		* boolean indicates that this has been done
+		**/
+		this.populated = false;
+		this.current = null;
+		this.docIterator = null; // a number tracking our position in the documents array
+		this.documents = []; // an array of documents
+
+		this.vSortKey = [];
+		this.vAscending = [];
+	}, base = require('./DocumentSource'), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+	// DEPENDENCIES
+	var FieldPathExpression = require("../expressions/FieldPathExpression"),
+		Value = require("../Value");
+
+	klass.sortName = "$sort";
+	proto.getSourceName = function getSourceName(){
+		return klass.sortName;
+	};
+	
+	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.getDependencies = function getDependencies() {
+		for(var i = 0; i < this.vSortKey.length; ++i) {
+			this.vSortKey[i].addDependencies(deps);
+		}
+		return this.GetDepsReturn.SEE_NEXT;
+	};
+
+	/**
+	 * Is the source at EOF?
+	 * 
+	 * @method	eof
+	 * @return {bool} return if we have hit the end of input
+	**/
+	proto.eof = function eof() {
+		if (!this.populated)
+			this.populate();
+		return (this.docIterator == this.documents.length);
+	};
+
+	/**
+	 * 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() {
+		if (!this.populated)
+			this.populate();
+		return this.current;
+	};
+
+	/**
+	 * 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
+
+		if (!this.populated) 
+			this.populate();
+
+		if (this.docIterator == this.documents.length) throw new Error("This should never happen");
+		++this.docIterator;
+
+		if (this.docIterator == this.documents.length) {
+			this.current = null;
+			return false;
+		}
+		this.current = this.documents[this.docIterator];
+		return true;
+	};
+
+	/**
+	 * 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) {
+		var insides = {};
+		this.sortKeyToJson(insides, false);
+		builder[this.getSourceName()] = insides;
+	};
+
+	/**
+	* Add sort key field.
+	*
+	* Adds a sort key field to the key being built up.  A concatenated
+	* key is built up by calling this repeatedly.
+	*
+	* @param {String} fieldPath the field path to the key component
+	* @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);
+		this.vSortKey.push(pathExpr);
+		if (ascending == -1)
+			this.vAscending.push(0);
+		else if (typeof ascending !== "undefined" && typeof ascending !== null)
+			this.vAscending.push(ascending);
+		else
+			this.vAscending.push(0);
+
+	};
+
+	proto.populate = function populate() {
+		/* make sure we've got a sort key */
+		if (this.vSortKey.length === null) throw new Error("This should never happen");
+			
+		/* pull everything from the underlying source */
+		for(var hasNext = !this.pSource.eof(); hasNext; hasNext = this.pSource.advance()) {
+			var doc = this.pSource.getCurrent();
+			this.documents.push(doc);
+		}
+
+		/* sort the list */
+		this.documents.sort(SortDocumentSource.prototype.compare.bind(this));
+
+		/* start the sort iterator */
+		this.docIterator = 0;
+
+		if (this.docIterator < this.documents.length)
+			this.current = this.documents[this.docIterator];
+		this.populated = true;
+	};
+
+	/**
+	 * Compare two documents according to the specified sort key.
+	 *
+	 * @param {Object} pL the left side doc
+	 * @param {Object} pR 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) {
+			/* evaluate the sort keys */
+			var pathExpr = new FieldPathExpression(this.vSortKey[i].getFieldPath(false));
+			var left = pathExpr.evaluate(pL), right = pathExpr.evaluate(pR);
+
+			/*
+			Compare the two values; if they differ, return.  If they are
+			the same, move on to the next key.
+			*/
+			var cmp = Value.compare(left, right);
+			if (cmp) {
+				/* if necessary, adjust the return value by the key ordering */
+				if (!this.vAscending[i])
+					cmp = -cmp;
+				return cmp;
+			}
+		}
+		/**
+		* If we got here, everything matched (or didn't exist), so we'll
+		* consider the documents equal for purposes of this sort
+		**/
+		return 0;
+	};
+
+	/**
+	* Write out an object whose contents are the sort key.
+	*
+	* @param {Object} builder initialized object builder.
+	* @param {bool} fieldPrefix specify whether or not to include the field 
+	**/
+	proto.sortKeyToJson = function sortKeyToJson(builder, usePrefix) {
+		/* add the key fields */
+		var n = this.vSortKey.length;
+		for(var i = 0; i < n; ++i) {
+			/* create the "field name" */
+			var ss = this.vSortKey[i].getFieldPath(usePrefix); // renamed write to get
+			/* push a named integer based on the sort order */
+			builder[ss] = (this.vAscending[i] ? 1 : 0);
+		}
+	};
+
+	/**
+	 * Creates a new SortDocumentSource 
+	 *
+	 * @param {Object} JsonElement
+	**/
+	klass.createFromJson = function createFromJson(JsonElement) {
+		if (typeof JsonElement !== "object") throw new Error("code 15973; the " + klass.sortName + " key specification must be an object");
+
+		var Sort = proto.getFactory(),
+			nextSort = new Sort();
+
+		/* check for then iterate over the sort object */
+		var sortKeys = 0;
+		for(var key in JsonElement) {
+			var sortOrder = 0;
+
+			if (typeof JsonElement[key] !== "number") throw new Error("code 15974; " + klass.sortName + " key ordering must be specified using a number");
+
+			sortOrder = JsonElement[key];
+			if ((sortOrder != 1) && (sortOrder !== 0)) throw new Error("code 15975; " + klass.sortName + " key ordering must be 1 (for ascending) or -1 (for descending)");
+
+			nextSort.addKey(key, (sortOrder > 0));
+			++sortKeys;
+		}
+
+		if (sortKeys <= 0) throw new Error("code 15976; " + klass.sortName + " must have at least one sort key");
+		return nextSort;
+	};
+	
+	/**
+	 * 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.count = 0;
+	};
+
+	return klass;
+})();

+ 5 - 0
lib/pipeline/expressions/FieldPathExpression.js

@@ -79,6 +79,11 @@ var FieldPathExpression = module.exports = (function(){
 		return deps;
 	};
 
+	// renamed write to get because there are no streams
+	proto.getFieldPath = function getFieldPath(usePrefix){
+		return this.path.getPath(usePrefix);
+	};
+
 	proto.toJson = function toJson(){
 		return this.path.getPath(true);
 	};

+ 289 - 0
test/lib/pipeline/documentSources/SortDocumentSource.js

@@ -0,0 +1,289 @@
+var assert = require("assert"),
+	SortDocumentSource = require("../../../../lib/pipeline/documentSources/SortDocumentSource"),
+	CursorDocumentSource = require("../../../../lib/pipeline/documentsources/CursorDocumentSource"),
+	Cursor = require("../../../../lib/Cursor"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression");
+
+module.exports = {
+
+	"SortDocumentSource": {
+
+		"constructor()": {
+
+			"should not throw Error when constructing without args": function testConstructor(){
+				assert.doesNotThrow(function(){
+					new SortDocumentSource();
+				});
+			}
+
+		},
+
+		"#getSourceName()": {
+
+			"should return the correct source name; $sort": function testSourceName(){
+				var sds = new SortDocumentSource();
+				assert.strictEqual(sds.getSourceName(), "$sort");
+			}
+
+		},
+
+		"#getFactory()": {
+
+			"should return the constructor for this class": function factoryIsConstructor(){
+				assert.strictEqual(new SortDocumentSource().getFactory(), SortDocumentSource);
+			}
+
+		},
+
+		"#eof()": {
+
+			"should return true if there are no more sources": function noSources(){
+				var sds = new SortDocumentSource();
+				sds.pSource = {
+					eof: function(){
+						return true;
+					}
+				};
+				assert.equal(sds.eof(), true);
+			},
+			"should return false if there are more documents": function hitSort(){
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				cwc._cursor = new Cursor( [{a: 1}] );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = new SortDocumentSource();
+				sds.setSource(cds);
+				assert.equal(sds.eof(), false);
+			}
+
+		},
+
+		"#getCurrent()": {
+
+			"should return the current document source": function currSource(){
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				cwc._cursor = new Cursor( [{a: 1}] );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = new SortDocumentSource();
+				sds.setSource(cds);
+				assert.deepEqual(sds.getCurrent(), { a:1 }); 
+			}
+
+		},
+
+		"#advance()": {
+
+			"should return true for moving to the next source": function nextSource(){
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				cwc._cursor = new Cursor( [{a: 1}, {b:2}] );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = new SortDocumentSource();
+				sds.setSource(cds);
+				assert.strictEqual(sds.advance(), true); 
+			},
+
+			"should return false for no sources remaining": function noMoar(){
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				cwc._cursor = new Cursor( [{a: 1}, {b:2}] );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = new SortDocumentSource();
+				sds.setSource(cds);
+				sds.advance();
+				assert.strictEqual(sds.advance(), false); 
+			}
+
+		},
+
+		"#sourceToJson()": {
+
+			"should create an object representation of the SortDocumentSource": function sourceToJsonTest(){
+				var sds = new SortDocumentSource();
+				sds.vSortKey.push(new FieldPathExpression("b") );
+				var t = {};
+				sds.sourceToJson(t, false);
+				assert.deepEqual(t, { "$sort": { "b": -1 } });
+			}
+
+		},
+
+		"#createFromJson()": {
+
+			"should return a new SortDocumentSource object from an input JSON object": function createTest(){
+				var sds = SortDocumentSource.createFromJson({a:1});
+				assert.strictEqual(sds.constructor, SortDocumentSource);
+				var t = {};
+				sds.sourceToJson(t, false);
+				assert.deepEqual(t, { "$sort": { "a": 1 } });
+			},
+
+			"should return a new SortDocumentSource object from an input JSON object with a descending field": function createTest(){
+				var sds = SortDocumentSource.createFromJson({a:-1});
+				assert.strictEqual(sds.constructor, SortDocumentSource);
+				var t = {};
+				sds.sourceToJson(t, false);
+				assert.deepEqual(t, { "$sort": { "a": -1 } });
+			},
+
+			"should return a new SortDocumentSource object from an input JSON object with dotted paths": function createTest(){
+				var sds = SortDocumentSource.createFromJson({ "a.b":1 });
+				assert.strictEqual(sds.constructor, SortDocumentSource);
+				var t = {};
+				sds.sourceToJson(t, false);
+				assert.deepEqual(t, { "$sort": { "a.b" : 1  } });
+			},
+
+			"should throw an exception when not passed an object": function createTest(){
+				assert.throws(function() {
+					var sds = SortDocumentSource.createFromJson(7);
+				});
+			},
+
+			"should throw an exception when passed an empty object": function createTest(){
+				assert.throws(function() {
+					var sds = SortDocumentSource.createFromJson({});
+				});
+			},
+
+			"should throw an exception when passed an object with a non number value": function createTest(){
+				assert.throws(function() {
+					var sds = SortDocumentSource.createFromJson({a:"b"});
+				});
+			},
+
+			"should throw an exception when passed an object with a non valid number value": function createTest(){
+				assert.throws(function() {
+					var sds = SortDocumentSource.createFromJson({a:0});
+				});
+			}
+
+		},
+
+		"#sort": {
+
+			"should sort a single document": function singleValue() {
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				cwc._cursor = new Cursor( [{_id:0, a: 1}] );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = new SortDocumentSource();
+				sds.addKey("_id");
+				sds.setSource(cds);
+				assert.deepEqual(sds.getCurrent(), {_id:0, a:1}); 
+			},
+
+			"should sort two documents": function twoValue() {
+				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 sds = new SortDocumentSource();
+				sds.addKey("_id");
+				sds.setSource(cds);
+				var c = [];
+				while (!sds.eof()) {
+					c.push(sds.getCurrent());
+					sds.advance();
+				}
+				assert.deepEqual(c, [{_id:1, a: 0}, {_id:0, a:1}]); 
+			},
+
+			"should sort two documents in ascending order": function ascendingValue() {
+				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 sds = new SortDocumentSource();
+				sds.addKey("_id", true);
+				sds.setSource(cds);
+				var c = [];
+				while (!sds.eof()) {
+					c.push(sds.getCurrent());
+					sds.advance();
+				}
+				assert.deepEqual(c, [{_id:0, a: 1}, {_id:1, a:0}, {_id:5, a:12}]); 
+			},
+
+			"should sort documents with a compound key": function compoundKeySort() {
+				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();
+				sds.addKey("a");
+				sds.addKey("b");
+				sds.setSource(cds);
+				var c = [];
+				while (!sds.eof()) {
+					c.push(sds.getCurrent());
+					sds.advance();
+				}
+				assert.deepEqual(c, [{_id:5, a:12, b:7}, {_id:0, a:1, b:3}, {_id:1, a:0, b:2}]); 
+			},
+
+			"should sort documents with a compound key in ascending order": function compoundAscendingKeySort() {
+				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();
+				sds.addKey("a", true);
+				sds.addKey("b", true);
+				sds.setSource(cds);
+				var c = [];
+				while (!sds.eof()) {
+					c.push(sds.getCurrent());
+					sds.advance();
+				}
+				assert.deepEqual(c, [{_id:1, a:0, b:2}, {_id:0, a:1, b:3}, {_id:5, a:12, b:7}]); 
+			},
+
+			"should sort documents with a compound key in mixed order": function compoundMixedKeySort() {
+				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 sds = new SortDocumentSource();
+				sds.addKey("a", true);
+				sds.addKey("b", false);
+				sds.setSource(cds);
+				var c = [];
+				while (!sds.eof()) {
+					c.push(sds.getCurrent());
+					sds.advance();
+				}
+				assert.deepEqual(c, [{_id:1, a:0, b:2}, {_id:0, a:1, b:3}, {_id:8, a:7, b:42}, {_id:5, a:12, b:7}]); 
+			},
+
+			"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);
+				var sds = new SortDocumentSource();
+				sds.addKey("a");
+				assert.throws(sds.setSource(cds));
+			},
+
+			"should sort docs with missing fields": function missingFields() {
+				var cwc = new CursorDocumentSource.CursorWithContext();
+				var l = [{_id:0, a: 1}, {_id:1}];
+				cwc._cursor = new Cursor( l );
+				var cds = new CursorDocumentSource(cwc);
+				var sds = new SortDocumentSource();
+				sds.addKey("a");
+				sds.setSource(cds);
+				var c = [];
+				while (!sds.eof()) {
+					c.push(sds.getCurrent());
+					sds.advance();
+				}
+				assert.deepEqual(c, [{_id:1}, {_id:0, a:1}]); 
+			},
+
+		}
+
+
+	}
+
+};
+
+if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+