(#6096) - clean up and reduce memory usage of mapreduce
diff --git a/packages/node_modules/pouchdb-collate/src/index.js b/packages/node_modules/pouchdb-collate/src/index.js
index 049a5fe..cca89b8 100644
--- a/packages/node_modules/pouchdb-collate/src/index.js
+++ b/packages/node_modules/pouchdb-collate/src/index.js
@@ -20,14 +20,11 @@
if ((ai - bi) !== 0) {
return ai - bi;
}
- if (a === null) {
- return 0;
- }
switch (typeof a) {
case 'number':
return a - b;
case 'boolean':
- return a === b ? 0 : (a < b ? -1 : 1);
+ return a < b ? -1 : 1;
case 'string':
return stringCollate(a, b);
}
diff --git a/packages/node_modules/pouchdb-for-coverage/src/index.js b/packages/node_modules/pouchdb-for-coverage/src/index.js
index 51d340a..1fc76a2 100644
--- a/packages/node_modules/pouchdb-for-coverage/src/index.js
+++ b/packages/node_modules/pouchdb-for-coverage/src/index.js
@@ -2,9 +2,11 @@
import ajax from 'pouchdb-ajax';
import utils from './utils';
import errors from './errors';
+import * as collate from 'pouchdb-collate';
PouchDB.ajax = ajax;
PouchDB.utils = utils;
PouchDB.Errors = errors;
+PouchDB.collate = collate;
export default PouchDB;
diff --git a/packages/node_modules/pouchdb-mapreduce/src/index.js b/packages/node_modules/pouchdb-mapreduce/src/index.js
index 0ff29ae..af8b634 100644
--- a/packages/node_modules/pouchdb-mapreduce/src/index.js
+++ b/packages/node_modules/pouchdb-mapreduce/src/index.js
@@ -31,7 +31,7 @@
NotFoundError,
BuiltInError
} from './errors';
-import { Map } from 'pouchdb-collections';
+import { Map, Set } from 'pouchdb-collections';
import Promise from 'pouchdb-promise';
import sum from './sum';
@@ -356,8 +356,8 @@
var metaDocId = '_local/doc_' + docId;
var defaultMetaDoc = {_id: metaDocId, keys: []};
var docData = docIdsToChangesAndEmits.get(docId);
- var indexableKeysToKeyValues = docData.indexableKeysToKeyValues;
- var changes = docData.changes;
+ var indexableKeysToKeyValues = docData[0];
+ var changes = docData[1];
function getMetaDoc() {
if (isGenOne(changes)) {
@@ -379,9 +379,9 @@
});
}
- function processKvDocs(metaDoc, kvDocsRes) {
+ function processKeyValueDocs(metaDoc, kvDocsRes) {
var kvDocs = [];
- var oldKeysMap = {};
+ var oldKeys = new Set();
for (var i = 0, len = kvDocsRes.rows.length; i < len; i++) {
var row = kvDocsRes.rows[i];
@@ -390,24 +390,23 @@
continue;
}
kvDocs.push(doc);
- oldKeysMap[doc._id] = true;
- doc._deleted = !indexableKeysToKeyValues[doc._id];
+ oldKeys.add(doc._id);
+ doc._deleted = !indexableKeysToKeyValues.has(doc._id);
if (!doc._deleted) {
- var keyValue = indexableKeysToKeyValues[doc._id];
+ var keyValue = indexableKeysToKeyValues.get(doc._id);
if ('value' in keyValue) {
doc.value = keyValue.value;
}
}
}
-
- var newKeys = Object.keys(indexableKeysToKeyValues);
+ var newKeys = mapToKeysArray(indexableKeysToKeyValues);
newKeys.forEach(function (key) {
- if (!oldKeysMap[key]) {
+ if (!oldKeys.has(key)) {
// new doc
var kvDoc = {
_id: key
};
- var keyValue = indexableKeysToKeyValues[key];
+ var keyValue = indexableKeysToKeyValues.get(key);
if ('value' in keyValue) {
kvDoc.value = keyValue.value;
}
@@ -422,7 +421,7 @@
return getMetaDoc().then(function (metaDoc) {
return getKeyValueDocs(metaDoc).then(function (kvDocsRes) {
- return processKvDocs(metaDoc, kvDocsRes);
+ return processKeyValueDocs(metaDoc, kvDocsRes);
});
});
}
@@ -505,48 +504,59 @@
style: 'all_docs',
since: currentSeq,
limit: CHANGES_BATCH_SIZE
- }).then(function (response) {
- var results = response.results;
- if (!results.length) {
- return;
- }
- var docIdsToChangesAndEmits = new Map();
- for (var i = 0, l = results.length; i < l; i++) {
- var change = results[i];
- if (change.doc._id[0] !== '_') {
- mapResults = [];
- doc = change.doc;
+ }).then(processBatch);
+ }
- if (!doc._deleted) {
- tryMap(view.sourceDB, mapFun, doc);
- }
- mapResults.sort(sortByKeyThenValue);
+ function processBatch(response) {
+ var results = response.results;
+ if (!results.length) {
+ return;
+ }
+ var docIdsToChangesAndEmits = createDocIdsToChangesAndEmits(results);
+ queue.add(processChange(docIdsToChangesAndEmits, currentSeq));
+ if (results.length < CHANGES_BATCH_SIZE) {
+ return;
+ }
+ return processNextBatch();
+ }
- var indexableKeysToKeyValues = {};
- var lastKey;
- for (var j = 0, jl = mapResults.length; j < jl; j++) {
- var obj = mapResults[j];
- var complexKey = [obj.key, obj.id];
- if (collate(obj.key, lastKey) === 0) {
- complexKey.push(j); // dup key+id, so make it unique
- }
- var indexableKey = toIndexableString(complexKey);
- indexableKeysToKeyValues[indexableKey] = obj;
- lastKey = obj.key;
- }
- docIdsToChangesAndEmits.set(change.doc._id, {
- indexableKeysToKeyValues: indexableKeysToKeyValues,
- changes: change.changes
- });
+ function createDocIdsToChangesAndEmits(results) {
+ var docIdsToChangesAndEmits = new Map();
+ for (var i = 0, len = results.length; i < len; i++) {
+ var change = results[i];
+ if (change.doc._id[0] !== '_') {
+ mapResults = [];
+ doc = change.doc;
+
+ if (!doc._deleted) {
+ tryMap(view.sourceDB, mapFun, doc);
}
- currentSeq = change.seq;
+ mapResults.sort(sortByKeyThenValue);
+
+ var indexableKeysToKeyValues = createIndexableKeysToKeyValues(mapResults);
+ docIdsToChangesAndEmits.set(change.doc._id, [
+ indexableKeysToKeyValues,
+ change.changes
+ ]);
}
- queue.add(processChange(docIdsToChangesAndEmits, currentSeq));
- if (results.length < CHANGES_BATCH_SIZE) {
- return;
+ currentSeq = change.seq;
+ }
+ return docIdsToChangesAndEmits;
+ }
+
+ function createIndexableKeysToKeyValues(mapResults) {
+ var indexableKeysToKeyValues = new Map();
+ var lastKey;
+ for (var i = 0, len = mapResults.length; i < len; i++) {
+ var emittedKeyValue = mapResults[i];
+ var complexKey = [emittedKeyValue.key, emittedKeyValue.id];
+ if (i > 0 && collate(emittedKeyValue.key, lastKey) === 0) {
+ complexKey.push(i); // dup key+id, so make it unique
}
- return processNextBatch();
- });
+ indexableKeysToKeyValues.set(toIndexableString(complexKey), emittedKeyValue);
+ lastKey = emittedKeyValue.key;
+ }
+ return indexableKeysToKeyValues;
}
return processNextBatch().then(function () {
diff --git a/tests/mapreduce/test.mapreduce.js b/tests/mapreduce/test.mapreduce.js
index abb22fa..a69ed49 100644
--- a/tests/mapreduce/test.mapreduce.js
+++ b/tests/mapreduce/test.mapreduce.js
@@ -426,6 +426,505 @@
});
});
+ it('Test complex key collation', function () {
+ var map = function () {
+ emit(null);
+ emit(false);
+ emit(true);
+ emit(1);
+ emit(2);
+ emit(3);
+ emit(4);
+ emit("a");
+ emit("aa");
+ emit("b");
+ emit("ba");
+ emit("bb");
+ emit(["a"]);
+ emit(["b"]);
+ emit(["b","c"]);
+ emit(["b","c","a"]);
+ emit(["b","d"]);
+ emit(["b","d","e"]);
+ emit({"a":1});
+ emit({"a":2});
+ emit({"b":1});
+ emit({"b":2});
+ emit({"b":2,"a":1});
+ emit({"b":2,"c":2});
+ };
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, { map: map });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { id: '1', key: null, value: null },
+ { id: '2', key: null, value: null },
+ { id: '1', key: false, value: null },
+ { id: '2', key: false, value: null },
+ { id: '1', key: true, value: null },
+ { id: '2', key: true, value: null },
+ { id: '1', key: 1, value: null },
+ { id: '2', key: 1, value: null },
+ { id: '1', key: 2, value: null },
+ { id: '2', key: 2, value: null },
+ { id: '1', key: 3, value: null },
+ { id: '2', key: 3, value: null },
+ { id: '1', key: 4, value: null },
+ { id: '2', key: 4, value: null },
+ { id: '1', key: 'a', value: null },
+ { id: '2', key: 'a', value: null },
+ { id: '1', key: 'aa', value: null },
+ { id: '2', key: 'aa', value: null },
+ { id: '1', key: 'b', value: null },
+ { id: '2', key: 'b', value: null },
+ { id: '1', key: 'ba', value: null },
+ { id: '2', key: 'ba', value: null },
+ { id: '1', key: 'bb', value: null },
+ { id: '2', key: 'bb', value: null },
+ { id: '1', key: [ 'a' ], value: null },
+ { id: '2', key: [ 'a' ], value: null },
+ { id: '1', key: [ 'b' ], value: null },
+ { id: '2', key: [ 'b' ], value: null },
+ { id: '1', key: [ 'b', 'c' ], value: null },
+ { id: '2', key: [ 'b', 'c' ], value: null },
+ { id: '1', key: [ 'b', 'c', 'a' ], value: null },
+ { id: '2', key: [ 'b', 'c', 'a' ], value: null },
+ { id: '1', key: [ 'b', 'd' ], value: null },
+ { id: '2', key: [ 'b', 'd' ], value: null },
+ { id: '1', key: [ 'b', 'd', 'e' ], value: null },
+ { id: '2', key: [ 'b', 'd', 'e' ], value: null },
+ { id: '1', key: { a: 1 }, value: null },
+ { id: '2', key: { a: 1 }, value: null },
+ { id: '1', key: { a: 2 }, value: null },
+ { id: '2', key: { a: 2 }, value: null },
+ { id: '1', key: { b: 1 }, value: null },
+ { id: '2', key: { b: 1 }, value: null },
+ { id: '1', key: { b: 2 }, value: null },
+ { id: '2', key: { b: 2 }, value: null },
+ { id: '1', key: { b: 2, a: 1 }, value: null },
+ { id: '2', key: { b: 2, a: 1 }, value: null },
+ { id: '1', key: { b: 2, c: 2 }, value: null },
+ { id: '2', key: { b: 2, c: 2 }, value: null }
+ ]);
+ });
+ });
+ });
+
+ it('Test duplicate collation of objects', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'a' }, { b: 'b' });
+ emit({ a: 'a' }, { b: 'b' });
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": { b: 'b' }},
+ { "id": "1", "key": { "a": "a" }, "value": { b: 'b' }},
+ { "id": "2", "key": { "a": "a" }, "value": { b: 'b' }},
+ { "id": "2", "key": { "a": "a" }, "value": { b: 'b' }}
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of undefined/null', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit();
+ emit(null);
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": null, "value": null},
+ { "id": "1", "key": null, "value": null},
+ { "id": "2", "key": null, "value": null},
+ { "id": "2", "key": null, "value": null}
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of null/undefined', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit(null);
+ emit();
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": null, "value": null},
+ { "id": "1", "key": null, "value": null},
+ { "id": "2", "key": null, "value": null},
+ { "id": "2", "key": null, "value": null}
+ ]);
+ });
+ });
+ });
+
+ it('Test duplicate collation of nulls', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit(null);
+ emit(null);
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": null, "value": null},
+ { "id": "1", "key": null, "value": null},
+ { "id": "2", "key": null, "value": null},
+ { "id": "2", "key": null, "value": null}
+ ]);
+ });
+ });
+ });
+
+ it('Test duplicate collation of booleans', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit(true);
+ emit(true);
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": true, "value": null},
+ { "id": "1", "key": true, "value": null},
+ { "id": "2", "key": true, "value": null},
+ { "id": "2", "key": true, "value": null}
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different objects', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'b' }, { a: 'a' });
+ emit({ a: 'a' }, { b: 'b' });
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": { "b": "b" } },
+ { "id": "2", "key": { "a": "a" }, "value": { "b": "b" } },
+ { "id": "1", "key": { "a": "b" }, "value": { "a": "a" } },
+ { "id": "2", "key": { "a": "b" }, "value": { "a": "a" } }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different objects 2', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'b', b: 'c' }, { a: 'a' });
+ emit({ a: 'a' }, { b: 'b' });
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": { "b": "b" } },
+ { "id": "2", "key": { "a": "a" }, "value": { "b": "b" } },
+ { "id": "1", "key": { "a": "b", "b": "c" }, "value": { "a": "a" } },
+ { "id": "2", "key": { "a": "b", "b": "c" }, "value": { "a": "a" } }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different objects 3', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'a' }, { b: 'b' });
+ emit({ a: 'b'}, { a: 'a' });
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": { "b": "b" } },
+ { "id": "2", "key": { "a": "a" }, "value": { "b": "b" } },
+ { "id": "1", "key": { "a": "b" }, "value": { "a": "a" } },
+ { "id": "2", "key": { "a": "b" }, "value": { "a": "a" } }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different objects 4', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'a'});
+ emit({ b: 'b'});
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": null },
+ { "id": "2", "key": { "a": "a" }, "value": null },
+ { "id": "1", "key": { "b": "b" }, "value": null },
+ { "id": "2", "key": { "b": "b" }, "value": null }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different objects 5', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'a'});
+ emit({ a: 'a', b: 'b'});
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": null },
+ { "id": "2", "key": { "a": "a" }, "value": null },
+ { "id": "1", "key": { "a": "a", "b": "b" }, "value": null },
+ { "id": "2", "key": { "a": "a", "b": "b" }, "value": null }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different objects 6', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit({ a: 'a'});
+ emit({ a: 'a', b: 'b'});
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": { "a": "a" }, "value": null },
+ { "id": "2", "key": { "a": "a" }, "value": null },
+ { "id": "1", "key": { "a": "a", "b": "b" }, "value": null },
+ { "id": "2", "key": { "a": "a", "b": "b" }, "value": null }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different booleans', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit(true);
+ emit(false);
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": false, "value": null },
+ { "id": "2", "key": false, "value": null },
+ { "id": "1", "key": true, "value": null },
+ { "id": "2", "key": true, "value": null }
+ ]);
+ });
+ });
+ });
+
+ it('Test collation of different booleans 2', function () {
+ var db = new PouchDB(dbName);
+ return db.bulkDocs([
+ { _id: '1' },
+ { _id: '2' }
+ ]).then(function () {
+ return createView(db, {
+ map: function () {
+ emit(false);
+ emit(true);
+ }
+ });
+ }).then(function (queryFun) {
+ return db.query(queryFun).then(function (res) {
+ var rows = res.rows.map(function (x) {
+ return {
+ id: x.id,
+ key: x.key,
+ value: x.value
+ };
+ });
+ assert.deepEqual(rows, [
+ { "id": "1", "key": false, "value": null },
+ { "id": "2", "key": false, "value": null },
+ { "id": "1", "key": true, "value": null },
+ { "id": "2", "key": true, "value": null }
+ ]);
+ });
+ });
+ });
+
it("Test joins", function () {
var db = new PouchDB(dbName);
return createView(db, {
diff --git a/tests/unit/test.collate.js b/tests/unit/test.collate.js
new file mode 100644
index 0000000..922821f
--- /dev/null
+++ b/tests/unit/test.collate.js
@@ -0,0 +1,469 @@
+'use strict';
+
+var PouchDB = require('../../packages/node_modules/pouchdb-for-coverage');
+var should = require('chai').should();
+var pouchCollate = PouchDB.collate;
+var collate = pouchCollate.collate;
+var normalizeKey = pouchCollate.normalizeKey;
+var toIndexableString = pouchCollate.toIndexableString;
+var parseIndexableString = pouchCollate.parseIndexableString;
+
+function stringLexCompare(a, b) {
+
+ var aLen = a.length;
+ var bLen = b.length;
+
+ var i;
+ for (i = 0; i < aLen; i++) {
+ if (i === bLen) {
+ // b is shorter substring of a
+ return 1;
+ }
+ var aChar = a.charAt(i);
+ var bChar = b.charAt(i);
+ if (aChar !== bChar) {
+ return aChar < bChar ? -1 : 1;
+ }
+ }
+
+ if (aLen < bLen) {
+ // a is shorter substring of b
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * returns the decimal form for the given integer, i.e. writes
+ * out all the digits (in base-10) instead of using scientific notation
+ */
+function intToDecimalForm(int) {
+
+ var isNeg = int < 0;
+ var result = '';
+
+ do {
+ var remainder = isNeg ? -Math.ceil(int % 10) : Math.floor(int % 10);
+
+ result = remainder + result;
+ int = isNeg ? Math.ceil(int / 10) : Math.floor(int / 10);
+ } while (int);
+
+
+ if (isNeg && result !== '0') {
+ result = '-' + result;
+ }
+
+ return result;
+}
+
+var verifyLexicalKeysSort = function (keys) {
+ var lexical = keys.map(function (key) {
+ return [key, pouchCollate.toIndexableString(key)];
+ });
+ lexical.sort(function (a, b) {
+ return stringLexCompare(a[1], b[1]);
+ });
+ keys.sort(pouchCollate.collate);
+
+ keys.forEach(function (expected, i) {
+ var actual = lexical[i][0];
+
+ should.equal(actual, expected, 'expect ' + JSON.stringify(actual) +
+ ' is ' + JSON.stringify(expected));
+ });
+};
+
+
+describe('test.collate.js', function () {
+ var a = {
+ array: [1, 2, 3],
+ bool: true,
+ string: '123',
+ object: {
+ a: 3,
+ b: 2
+ },
+ number: 1
+ };
+ var b = {
+ array: ['a', 'b'],
+ bool: false,
+ string: 'ab',
+ object: {
+ c: 1,
+ b: 3
+ },
+ number: 2
+ };
+ var c = {
+ object: {
+ a: 1,
+ b: 2,
+ c: 3
+ },
+ array: [1, 2]
+ };
+ it('compare array to itself', function () {
+ collate(a.array, a.array).should.equal(0);
+ collate(b.array, b.array).should.equal(0);
+ collate(c.array, c.array).should.equal(0);
+ });
+ it('compare boolean to itself', function () {
+ collate(a.bool, a.bool).should.equal(0);
+ collate(b.bool, b.bool).should.equal(0);
+ });
+ it('compare string to itself', function () {
+ collate(a.string, a.string).should.equal(0);
+ collate(b.string, b.string).should.equal(0);
+ });
+ it('compare number to itself', function () {
+ collate(a.number, a.number).should.equal(0);
+ collate(b.number, b.number).should.equal(0);
+ });
+ it('compare null to itself', function () {
+ collate(null, null).should.equal(0);
+ });
+ it('compare object to itself', function () {
+ collate(a.object, a.object).should.equal(0);
+ collate(b.object, b.object).should.equal(0);
+ collate(c.object, c.object).should.equal(0);
+ });
+ it('compare array to array', function () {
+ collate(a.array, b.array).should.equal(-1);
+ collate(b.array, a.array).should.equal(1);
+ collate(c.array, b.array).should.equal(-1);
+ collate(b.array, c.array).should.equal(1);
+ collate(a.array, c.array).should.equal(1);
+ collate(c.array, a.array).should.equal(-1);
+ });
+ it('compare array to array', function () {
+ collate([a.array], [b.array]).should.equal(-1);
+ collate([b.array], [a.array]).should.equal(1);
+ });
+ it('compare boolean to boolean', function () {
+ collate(a.bool, b.bool).should.equal(1);
+ collate(b.bool, a.bool).should.equal(-1);
+ });
+ it('compare string to string', function () {
+ collate(a.string, b.string).should.equal(-1);
+ collate(b.string, a.string).should.equal(1);
+ });
+ it('compare number to number', function () {
+ collate(a.number, b.number).should.equal(-1);
+ collate(b.number, a.number).should.equal(1);
+ });
+ it('compare object to object', function () {
+ collate(a.object, b.object).should.equal(-1);
+ collate(b.object, a.object).should.equal(1);
+ collate(c.object, b.object).should.equal(-1);
+ collate(b.object, c.object).should.equal(1);
+ collate(c.object, a.object).should.be.below(0);
+ collate(a.object, c.object).should.be.above(0);
+ });
+ it('objects differing only in num of keys', function () {
+ collate({1: 1}, {1: 1, 2: 2}).should.equal(-1);
+ collate({1: 1, 2: 2}, {1: 1}).should.equal(1);
+ });
+ it('compare number to null', function () {
+ collate(a.number, null).should.be.above(0);
+ });
+ it('compare number to function', function () {
+ collate(a.number, function () {
+ }).should.not.equal(collate(a.number, function () {}));
+ collate(b.number, function () {
+ }).should.not.equal(collate(b.number, function () {}));
+ collate(function () {
+ }, a.number).should.not.equal(collate(function () {}, a.number));
+ collate(function () {
+ }, b.number).should.not.equal(collate(function () {}, b.number));
+ });
+
+});
+
+describe('normalizeKey', function () {
+
+ it('verify key normalizations', function () {
+ var normalizations = [
+ [null, null],
+ [NaN, null],
+ [undefined, null],
+ [Infinity, null],
+ [-Infinity, null],
+ ['', ''],
+ ['foo', 'foo'],
+ ['0', '0'],
+ ['1', '1'],
+ [0, 0],
+ [1, 1],
+ [Number.MAX_VALUE, Number.MAX_VALUE],
+ [new Date('1982-11-30T00:00:00.000Z'), '1982-11-30T00:00:00.000Z'] // Thriller release date
+ ];
+
+ normalizations.forEach(function (normalization) {
+ var original = normalization[0];
+ var expected = normalization[1];
+ var normalized = normalizeKey(original);
+
+ var message = 'check normalization of ' + JSON.stringify(original) +
+ ' to ' + JSON.stringify(expected) +
+ ', got ' + JSON.stringify(normalized);
+ should.equal(normalized, expected, message);
+ });
+ });
+});
+
+describe('indexableString', function () {
+
+ it('verify intToDecimalForm', function () {
+ intToDecimalForm(0).should.equal('0');
+ intToDecimalForm(Number.MIN_VALUE).should.equal('0');
+ intToDecimalForm(-Number.MIN_VALUE).should.equal('0');
+
+ var maxValueStr = '1797693134862316800886484642206468426866682428440286464' +
+ '42228680066046004606080400844208228060084840044686866242482868202680268' +
+ '82040288406280040662242886466688240606642242682208668042640440204020242' +
+ '48802248082808208888442866208026644060866608420408868240026826626668642' +
+ '46642840408646468824200860804260804068888';
+
+ intToDecimalForm(Number.MAX_VALUE).should.equal(maxValueStr);
+ intToDecimalForm(-Number.MAX_VALUE).should.equal('-' + maxValueStr);
+
+ var simpleNums = [-3000, 3000, 322, 2308, -32, -1, 0, 1, 2, -2, -10, 10, -100, 100];
+
+ simpleNums.forEach(function (simpleNum) {
+ intToDecimalForm(simpleNum).should.equal(simpleNum.toString());
+ });
+ });
+
+ it('verify toIndexableString()', function () {
+ var keys = [
+ null,
+ false,
+ true,
+ -Number.MAX_VALUE,
+ -300,
+ -200,
+ -100,
+ -10,
+ -2.5,
+ -2,
+ -1.5,
+ -1,
+ -0.5,
+ -0.0001,
+ -Number.MIN_VALUE,
+ 0,
+ Number.MIN_VALUE,
+ 0.0001,
+ 0.1,
+ 0.5,
+ 1,
+ 1.5,
+ 2,
+ 3,
+ 10,
+ 15,
+ 100,
+ 200,
+ 300,
+ Number.MAX_VALUE,
+ '',
+ '1',
+ '10',
+ '100',
+ '2',
+ '20',
+ '[]',
+ //'é',
+ 'foo',
+ 'mo',
+ 'moe',
+ //'moé',
+ //'moët et chandon',
+ 'moz',
+ 'mozilla',
+ 'mozilla with a super long string see how far it can go',
+ 'mozzy',
+ [],
+ [ null ],
+ [ null, null ],
+ [ null, 'foo' ],
+ [ false ],
+ [ false, 100 ],
+ [ true ],
+ [ true, 100 ],
+ [ 0 ],
+ [ 0, null ],
+ [ 0, 1 ],
+ [ 0, '' ],
+ [ 0, 'foo' ],
+ [ '', '' ],
+ [ 'foo' ],
+ [ 'foo', 1 ],
+ {},
+ { '0': null },
+ { '0': false },
+ { '0': true },
+ { '0': 0 },
+ { '0': 1 },
+ { '0': 'bar' },
+ { '0': 'foo' },
+ { '0': 'foo', '1': false },
+ { '0': 'foo', '1': true },
+ { '0': 'foo', '1': 0 },
+ { '0': 'foo', '1': '0' },
+ { '0': 'foo', '1': 'bar' },
+ { '0': 'quux' },
+ { '1': 'foo' }
+ //{ '1': 'foo', '0' : 'foo' } // key order actually matters, but node sorts them
+ ];
+ verifyLexicalKeysSort(keys);
+ });
+
+ it('verify toIndexableString()', function () {
+ var keys = [
+ ['test', 'test'],
+ ['test\u0000']
+ ];
+ verifyLexicalKeysSort(keys);
+ });
+
+ it('verify deep normalization', function () {
+ var a = {
+ list : [undefined, '1982-11-30T00:00:00.000Z'],
+ obj : {
+ foo: null,
+ date: '1982-11-30T00:00:00.000Z'
+ },
+ brokenList : [undefined, 1, 2, undefined, 3, 4, 5, undefined]
+ };
+ var b = {
+ list : [null, new Date('1982-11-30T00:00:00.000Z')],
+ obj : {
+ foo: NaN,
+ date: new Date('1982-11-30T00:00:00.000Z')
+ },
+ ignoredParam : undefined,
+ brokenList : [null, 1, 2, null, 3, 4, 5, null]
+ };
+
+ // sanity check
+ JSON.stringify(a).should.equal(JSON.stringify(b), 'stringify a,b');
+
+ toIndexableString(a).should.equal(toIndexableString(b), 'string a,b');
+ toIndexableString(a).should.equal(toIndexableString(b), 'string a,a');
+ toIndexableString(b).should.equal(toIndexableString(b), 'string b,b');
+
+ normalizeKey(a).should.deep.equal(normalizeKey(b), 'normalize a,b');
+ normalizeKey(a).should.deep.equal(normalizeKey(a), 'normalize a,a');
+ normalizeKey(b).should.deep.equal(normalizeKey(b), 'normalize b,b');
+
+ collate(a, b).should.equal(0, 'collate a,b');
+ collate(a, a).should.equal(0, 'collate a,a');
+ collate(b, b).should.equal(0, 'collate b,b');
+ });
+
+ it('verify parseIndexableString', function () {
+ var keys = [null, false, true, 0, 1, -1, 9, -9, 10, -10, 0.1, -0.1, -0.01,
+ 100, 200, 20, -20, -200, -30, Number.MAX_VALUE, Number.MIN_VALUE,
+ 'foo', '', '\u0000', '\u0001', '\u0002', [1], {foo: true},
+ {foo: 'bar', 'baz': 'quux', foobaz: {bar: 'bar', baz: 'baz', quux: {foo: 'bar'}}},
+ {foo: {bar: true}},
+ [{foo: 'bar'}, {bar: 'baz'}, {}, ['foo', 'bar', 'baz']],
+ [[[['foo']], [], [['bar']]]],
+ -Number.MAX_VALUE,
+ -300,
+ -200,
+ -100,
+ -10,
+ -2.5,
+ -2,
+ -1.5,
+ -1,
+ -0.5,
+ -0.0001,
+ -Number.MIN_VALUE,
+ 0,
+ Number.MIN_VALUE,
+ 0.0001,
+ 0.1,
+ 0.5,
+ 1,
+ 1.5,
+ 2,
+ 3,
+ 10,
+ 15,
+ 100,
+ 200,
+ 300,
+ Number.MAX_VALUE,
+ '',
+ '1',
+ '10',
+ '100',
+ '2',
+ '20',
+ '[]',
+ //'é',
+ 'foo',
+ 'mo',
+ 'moe',
+ //'moé',
+ //'moët et chandon',
+ 'moz',
+ 'mozilla',
+ 'mozilla with a super long string see how far it can go',
+ 'mozzy',
+ [],
+ [ null ],
+ [ null, null ],
+ [ null, 'foo' ],
+ [ false ],
+ [ false, 100 ],
+ [ true ],
+ [ true, 100 ],
+ [ 0 ],
+ [ 0, null ],
+ [ 0, 1 ],
+ [ 0, '' ],
+ [ 0, 'foo' ],
+ [ '', '' ],
+ [ 'foo' ],
+ [ 'foo', 1 ],
+ {},
+ { '0': null },
+ { '0': false },
+ { '0': true },
+ { '0': 0 },
+ { '0': 1 },
+ { '0': 'bar' },
+ { '0': 'foo' },
+ { '0': 'foo', '1': false },
+ { '0': 'foo', '1': true },
+ { '0': 'foo', '1': 0 },
+ { '0': 'foo', '1': '0' },
+ { '0': 'foo', '1': 'bar' },
+ { '0': 'quux' },
+ { '1': 'foo' }
+ ];
+
+ keys.forEach(function (key) {
+ var indexableString = toIndexableString(key);
+ JSON.stringify(parseIndexableString(indexableString)).should.equal(
+ JSON.stringify(key), 'check parseIndexableString for key: ' + key +
+ '(indexable string is: ' + indexableString + ')');
+ });
+ });
+ it('throws error in parseIndexableString on invalid input', function () {
+
+ try {
+ parseIndexableString('');
+ should.fail("didn't expect to parse correctly");
+ } catch (err) {
+ should.exist(err);
+ }
+ });
+});
\ No newline at end of file