From bda36e32a25e5efd80f808e3ad7dc5a346b174ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Mon, 25 Mar 2013 18:40:02 +0100 Subject: [PATCH 01/22] (#583) - compact callback after compaction's done Now http adapter behaves like others. The code manually pings database to check if it's done compaction. --- src/adapters/pouch.http.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/adapters/pouch.http.js b/src/adapters/pouch.http.js index 2ce84fa217..8fd3cd05d4 100644 --- a/src/adapters/pouch.http.js +++ b/src/adapters/pouch.http.js @@ -209,16 +209,31 @@ var HttpPouch = function(opts, callback) { // Sends a POST request to the host calling the couchdb _compact function // version: The version of CouchDB it is running - api.compact = function(callback) { + api.compact = function(opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('compact', arguments); return; } + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } ajax({ auth: host.auth, url: genDBUrl(host, '_compact'), method: 'POST' - }, callback); + }); + if (typeof callback === "function") { + // Ping the http if it's finished compaction + var id = setInterval(function() { + api.info(function(err, res) { + if (!res.compact_running) { + clearInterval(id); + call(callback, null); + } + }); + }, opts.interval || 200); + } }; // Calls GET on the host, which gets back a JSON string containing From 610c615567de6751bc1ec01b42dedf49c6c18aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Mon, 25 Mar 2013 18:42:11 +0100 Subject: [PATCH 02/22] (#583) - uncomment compaction test This test was failing due to http compaction took some time and callback was fired before it finished. Now it's fixed --- tests/test.get.js | 73 +++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/tests/test.get.js b/tests/test.get.js index 032c07d3f4..6ef990285d 100644 --- a/tests/test.get.js +++ b/tests/test.get.js @@ -256,47 +256,38 @@ adapters.map(function(adapter) { }); }); -// uncomment once issue #583 is fixed -// asyncTest("Test get with revs_info on compacted tree", 7, function() { -// console.log("compacted"); -// initTestDB(this.name, function(err, db) { -// var simpleTree = [ -// [ -// {_id: "foo", _rev: "1-a", value: "foo a"}, -// {_id: "foo", _rev: "2-b", value: "foo d"}, -// {_id: "foo", _rev: "3-c", value: "foo c"} -// ], -// [ -// {_id: "foo", _rev: "1-a", value: "foo a"}, -// {_id: "foo", _rev: "2-d", value: "foo d"}, -// {_id: "foo", _rev: "3-e", _deleted: true} -// ] -// ]; -// putTree(db, simpleTree, function() { -// db.compact(function(err, ok) { -// setTimeout(function(){ -// db.get("foo", {revs_info: true}, function(err, doc) { -// -// var revs = doc._revs_info; -// -// console.log("revs", revs, revs[0].status, revs[1].status, revs[2].status); -// console.log(revs[1].status === "missing"); -// -// strictEqual(revs.length, 3, "correct number of revs"); -// strictEqual(revs[0].rev, "3-c", "rev ok"); -// strictEqual(revs[0].status, "available", "not compacted"); -// strictEqual(revs[1].rev, "2-b", "rev ok"); -// strictEqual(revs[1].status, "missing", "compacted"); -// strictEqual(revs[2].rev, "1-a", "rev ok"); -// strictEqual(revs[2].status, "missing", "compacted"); -// start(); -// }); -// }, 1000); -// }); -// }); -// }); -// }); -// + asyncTest("Test get with revs_info on compacted tree", 7, function() { + initTestDB(this.name, function(err, db) { + var simpleTree = [ + [ + {_id: "foo", _rev: "1-a", value: "foo a"}, + {_id: "foo", _rev: "2-b", value: "foo d"}, + {_id: "foo", _rev: "3-c", value: "foo c"} + ], + [ + {_id: "foo", _rev: "1-a", value: "foo a"}, + {_id: "foo", _rev: "2-d", value: "foo d"}, + {_id: "foo", _rev: "3-e", _deleted: true} + ] + ]; + putTree(db, simpleTree, function() { + db.compact(function(err, ok) { + db.get("foo", {revs_info: true}, function(err, doc) { + var revs = doc._revs_info; + strictEqual(revs.length, 3, "correct number of revs"); + strictEqual(revs[0].rev, "3-c", "rev ok"); + strictEqual(revs[0].status, "available", "not compacted"); + strictEqual(revs[1].rev, "2-b", "rev ok"); + strictEqual(revs[1].status, "missing", "compacted"); + strictEqual(revs[2].rev, "1-a", "rev ok"); + strictEqual(revs[2].status, "missing", "compacted"); + start(); + }); + }); + }); + }); + }); + asyncTest("Test get with conflicts", 3, function() { initTestDB(this.name, function(err, db) { From 282d433c75f3652706f3465924903e8effd6ee7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Mon, 25 Mar 2013 18:51:52 +0100 Subject: [PATCH 03/22] (#583) - add compaction documentation --- docs/api.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/api.md b/docs/api.md index b40d0f7c8b..be203aeb7e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -25,6 +25,7 @@ Most of the Pouch API is exposed as `fun(arg, [options], [callback])` Where both * [Get database information](#get_database_information) * [Listen to database changes](#listen_to_database_changes) * [Replicate a database](#replicate_a_database) +* [Compact the database](#compact_the_database) * [Get document revision diffs](#document_revisions_diff) ## List all databases @@ -499,6 +500,14 @@ Replicate one database to another. // }) +## Compact the database + + db.compact([opts], [callback]) + +Runs compaction of the database. Fires callback when compaction is done. If you use http adapter and have specified callback Pouch will ping the remote database in regular intervals unless the compaction is finished. + +* `options.interval`: Number of milliseconds Pouch waits before asking again if compaction is already done. Only for http adapter. + ## Document Revisions Diff db.revsDiff(diff, [callback]) From e78b9defafdf0e94cdce01154e06ca0cc963cc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Wed, 27 Mar 2013 21:37:56 +0100 Subject: [PATCH 04/22] (#583) - ping remote server correctly Better now: 1. starting pinging _after_ the compaction started 2. try again _only_ if we got answer --- src/adapters/pouch.http.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/adapters/pouch.http.js b/src/adapters/pouch.http.js index 8fd3cd05d4..c63bc99af6 100644 --- a/src/adapters/pouch.http.js +++ b/src/adapters/pouch.http.js @@ -222,18 +222,21 @@ var HttpPouch = function(opts, callback) { auth: host.auth, url: genDBUrl(host, '_compact'), method: 'POST' - }); - if (typeof callback === "function") { - // Ping the http if it's finished compaction - var id = setInterval(function() { + }, function() { + function ping() { api.info(function(err, res) { if (!res.compact_running) { - clearInterval(id); call(callback, null); + } else { + setTimeout(ping, opts.interval || 200); } }); - }, opts.interval || 200); - } + } + // Ping the http if it's finished compaction + if (typeof callback === "function") { + ping(); + } + }); }; // Calls GET on the host, which gets back a JSON string containing From a52578e2e449eca1f87a4497132665f7a737effd Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Thu, 28 Mar 2013 13:08:32 +0000 Subject: [PATCH 05/22] (#538) - Switch to Linux to test Firefox I did a review of the bulk_docs code and cannot see where our tests could intermittently fail, this seems to be a quirck with FileReader on windows. Linux tests have been far more stable. I also removed some redundant error handling code and unneeded backwards compatibility code --- Gruntfile.js | 4 ++-- src/adapters/pouch.idb.js | 25 +++++++------------------ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 4d811cdcd5..2ab975c9ef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,8 +29,8 @@ var browserConfig = [{ }, { browserName: 'firefox', version: '19', - platform: 'Windows 2003', - name: 'win2003/firefox' + platform: 'Linux', + name: 'linux/firefox' // }, { // browserName: 'opera', // version: '12', diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index faf34c6542..683afc88b1 100644 --- a/src/adapters/pouch.idb.js +++ b/src/adapters/pouch.idb.js @@ -404,28 +404,18 @@ var IdbPouch = function(opts, callback) { function saveAttachment(docInfo, digest, data, callback) { var objectStore = txn.objectStore(ATTACH_STORE); var getReq = objectStore.get(digest).onsuccess = function(e) { + var originalRefs = e.target.result && e.target.result.refs || {}; var ref = [docInfo.metadata.id, docInfo.metadata.rev].join('@'); - var newAtt = {digest: digest, body: data}; - - if (e.target.result) { - if (e.target.result.refs) { - // only update references if this attachment already has them - // since we cannot migrate old style attachments here without - // doing a full db scan for references - newAtt.refs = e.target.result.refs; - newAtt.refs[ref] = true; - } - } else { - newAtt.refs = {}; - newAtt.refs[ref] = true; - } - + var newAtt = { + digest: digest, + body: data, + refs: originalRefs + }; + newAtt.refs[ref] = true; var putReq = objectStore.put(newAtt).onsuccess = function(e) { call(callback); }; - putReq.onerror = putReq.ontimeout = idbError(callback); }; - getReq.onerror = getReq.ontimeout = idbError(callback); } var txn; @@ -438,7 +428,6 @@ var IdbPouch = function(opts, callback) { processDocs(); }); - }; function sortByBulkSeq(a, b) { From 2bc7f657271e426c11604389c124cf8155dccc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Wed, 27 Mar 2013 22:39:28 +0100 Subject: [PATCH 06/22] (#611) - move opts cloning to generic adapter That is: get rid of extend in specific adapters as it can be done in generic one. Also - use new Pouch.error method --- src/adapters/pouch.idb.js | 6 ++---- src/adapters/pouch.leveldb.js | 6 +++--- src/adapters/pouch.websql.js | 6 ++---- src/pouch.adapter.js | 26 ++++++++++++++------------ 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index 683afc88b1..3233b2c96d 100644 --- a/src/adapters/pouch.idb.js +++ b/src/adapters/pouch.idb.js @@ -162,7 +162,7 @@ var IdbPouch = function(opts, callback) { api._bulkDocs = function idb_bulkDocs(req, opts, callback) { var newEdits = opts.new_edits; - var userDocs = extend(true, [], req.docs); + var userDocs = req.docs; // Parse the docs, give them a sequence number for the result var docInfos = userDocs.map(function(doc, i) { @@ -456,7 +456,7 @@ var IdbPouch = function(opts, callback) { return; } if (isDeleted(metadata) && !opts.rev) { - result = extend({}, Pouch.Errors.MISSING_DOC, {reason:"deleted"}); + result = Pouch.error(Pouch.Errors.MISSING_DOC, "deleted"); return; } @@ -676,8 +676,6 @@ var IdbPouch = function(opts, callback) { console.log(name + ': Start Changes Feed: continuous=' + opts.continuous); } - opts = extend(true, {}, opts); - if (!opts.since) { opts.since = 0; } diff --git a/src/adapters/pouch.leveldb.js b/src/adapters/pouch.leveldb.js index 05fd276395..0b6acad8d0 100644 --- a/src/adapters/pouch.leveldb.js +++ b/src/adapters/pouch.leveldb.js @@ -169,7 +169,7 @@ var LevelPouch = function(opts, callback) { return call(callback, Pouch.Errors.MISSING_DOC); } if (isDeleted(metadata) && !opts.rev) { - return call(callback, extend({}, Pouch.Errors.MISSING_DOC, {reason:"deleted"})); + return call(callback, Pouch.error(Pouch.Errors.MISSING_DOC, "deleted")); } var rev = Pouch.merge.winningRev(metadata); @@ -256,7 +256,7 @@ var LevelPouch = function(opts, callback) { var results = []; // parse the docs and give each a sequence number - var userDocs = extend(true, [], req.docs); + var userDocs = req.docs; info = userDocs.map(function(doc, i) { var newDoc = Pouch.utils.parseDoc(doc, newEdits); newDoc._bulk_seq = i; @@ -848,4 +848,4 @@ LevelPouch.destroy = function(name, callback) { Pouch.adapter('ldb', LevelPouch); Pouch.adapter('leveldb', LevelPouch); -module.exports = LevelPouch; \ No newline at end of file +module.exports = LevelPouch; diff --git a/src/adapters/pouch.websql.js b/src/adapters/pouch.websql.js index e736983d4f..521545407b 100644 --- a/src/adapters/pouch.websql.js +++ b/src/adapters/pouch.websql.js @@ -108,7 +108,7 @@ var webSqlPouch = function(opts, callback) { api._bulkDocs = function idb_bulkDocs(req, opts, callback) { var newEdits = opts.new_edits; - var userDocs = extend(true, [], req.docs); + var userDocs = req.docs; // Parse the docs, give them a sequence number for the result var docInfos = userDocs.map(function(doc, i) { @@ -410,7 +410,7 @@ var webSqlPouch = function(opts, callback) { } metadata = JSON.parse(results.rows.item(0).json); if (isDeleted(metadata) && !opts.rev) { - result = extend({}, Pouch.Errors.MISSING_DOC, {reason:"deleted"}); + result = Pouch.error(Pouch.Errors.MISSING_DOC, "deleted"); return; } @@ -536,8 +536,6 @@ var webSqlPouch = function(opts, callback) { console.log(name + ': Start Changes Feed: continuous=' + opts.continuous); } - opts = extend(true, {}, opts); - if (!opts.since) { opts.since = 0; } diff --git a/src/pouch.adapter.js b/src/pouch.adapter.js index 3a575df43b..42646941aa 100644 --- a/src/pouch.adapter.js +++ b/src/pouch.adapter.js @@ -256,16 +256,14 @@ var PouchAdapter = function(opts, callback) { var l = leaves[i]; // looks like it's the only thing couchdb checks if (!(typeof(l) === "string" && /^\d+-/.test(l))) { - return call(callback, extend({}, Pouch.Errors.BAD_REQUEST, { - reason: "Invalid rev format" - })); + return call(callback, Pouch.error(Pouch.Errors.BAD_REQUEST, + "Invalid rev format" )); } } finishOpenRevs(); } else { - return call(callback, extend({}, Pouch.Errors.UNKNOWN_ERROR, { - reason: 'function_clause' - })); + return call(callback, Pouch.error(Pouch.Errors.UNKNOWN_ERROR, + 'function_clause')); } } return; // open_revs does not like other options @@ -363,15 +361,15 @@ var PouchAdapter = function(opts, callback) { } if ('keys' in opts) { if ('startkey' in opts) { - call(callback, extend({ - reason: 'Query parameter `start_key` is not compatible with multi-get' - }, Pouch.Errors.QUERY_PARSE_ERROR)); + call(callback, Pouch.error(Pouch.Errors.QUERY_PARSE_ERROR, + 'Query parameter `start_key` is not compatible with multi-get' + )); return; } if ('endkey' in opts) { - call(callback, extend({ - reason: 'Query parameter `end_key` is not compatible with multi-get' - }, Pouch.Errors.QUERY_PARSE_ERROR)); + call(callback, Pouch.error(Pouch.Errors.QUERY_PARSE_ERROR, + 'Query parameter `end_key` is not compatible with multi-get' + )); return; } } @@ -384,6 +382,7 @@ var PouchAdapter = function(opts, callback) { api.taskqueue.addTask('changes', arguments); return; } + opts = extend(true, {}, opts); return customApi._changes(opts); }; @@ -422,6 +421,8 @@ var PouchAdapter = function(opts, callback) { } if (!opts) { opts = {}; + } else { + opts = extend(true, {}, opts); } if (!req || !req.docs || req.docs.length < 1) { @@ -432,6 +433,7 @@ var PouchAdapter = function(opts, callback) { return call(callback, Pouch.Errors.QUERY_PARSE_ERROR); } + req = extend(true, {}, req); if (!('new_edits' in opts)) { opts.new_edits = true; } From f098aec6539c63001c8b9d201a17cf598a2fc985 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Wed, 27 Mar 2013 13:59:26 +0000 Subject: [PATCH 07/22] (#300) - Add id to stored revsisions to ensure uniqueness Thanks to @neojski for the test and @trokster for the original implementation of the fix. --- src/adapters/pouch.idb.js | 28 +++++++++++++++++++--------- src/adapters/pouch.websql.js | 24 +++++++++++++++--------- tests/test.replication.js | 17 +++++++++++++++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index 3233b2c96d..862c10a4dc 100644 --- a/src/adapters/pouch.idb.js +++ b/src/adapters/pouch.idb.js @@ -85,7 +85,7 @@ var IdbPouch = function(opts, callback) { db.createObjectStore(DOC_STORE, {keyPath : 'id'}) .createIndex('seq', 'seq', {unique: true}); db.createObjectStore(BY_SEQ_STORE, {autoIncrement : true}) - .createIndex('_rev', '_rev', {unique: true}); + .createIndex('_doc_id_rev', '_doc_id_rev', {unique: true}); db.createObjectStore(ATTACH_STORE, {keyPath: 'digest'}); db.createObjectStore(META_STORE, {keyPath: 'id', autoIncrement: false}); db.createObjectStore(DETECT_BLOB_SUPPORT_STORE); @@ -348,6 +348,7 @@ var IdbPouch = function(opts, callback) { } function finish() { + docInfo.data._doc_id_rev = docInfo.data._id + "::" + docInfo.data._rev; var dataReq = txn.objectStore(BY_SEQ_STORE).put(docInfo.data); dataReq.onsuccess = function(e) { if (Pouch.DEBUG) { @@ -461,11 +462,14 @@ var IdbPouch = function(opts, callback) { } var rev = Pouch.merge.winningRev(metadata); - var key = opts.rev ? opts.rev : rev; - var index = txn.objectStore(BY_SEQ_STORE).index('_rev'); + var key = metadata.id + '::' + (opts.rev ? opts.rev : rev); + var index = txn.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); index.get(key).onsuccess = function(e) { var doc = e.target.result; + if(doc && doc._doc_id_rev) { + delete(doc._doc_id_rev); + } if (!doc) { result = Pouch.Errors.MISSING_DOC; return; @@ -611,6 +615,9 @@ var IdbPouch = function(opts, callback) { if (opts.include_docs) { doc.doc = data; doc.doc._rev = Pouch.merge.winningRev(metadata); + if (doc.doc._doc_id_rev) { + delete(doc.doc._doc_id_rev); + } if (opts.conflicts) { doc.doc._conflicts = collectConflicts(metadata); } @@ -634,9 +641,10 @@ var IdbPouch = function(opts, callback) { if (!opts.include_docs) { allDocsInner(metadata); } else { - var index = transaction.objectStore(BY_SEQ_STORE).index('_rev'); + var index = transaction.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); var mainRev = Pouch.merge.winningRev(metadata); - index.get(mainRev).onsuccess = function(event) { + var key = metadata.id + "::" + mainRev; + index.get(key).onsuccess = function(event) { allDocsInner(cursor.value, event.target.result); }; } @@ -762,8 +770,9 @@ var IdbPouch = function(opts, callback) { } var mainRev = Pouch.merge.winningRev(metadata); - var index = txn.objectStore(BY_SEQ_STORE).index('_rev'); - index.get(mainRev).onsuccess = function(docevent) { + var key = metadata.id + "::" + mainRev; + var index = txn.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); + index.get(key).onsuccess = function(docevent) { var doc = docevent.target.result; var changeList = [{rev: mainRev}]; if (opts.style === 'all_docs') { @@ -841,9 +850,10 @@ var IdbPouch = function(opts, callback) { api._removeDocRevisions = function(docId, revs, callback) { var txn = idb.transaction([BY_SEQ_STORE], IDBTransaction.READ_WRITE); - var index = txn.objectStore(BY_SEQ_STORE).index('_rev'); + var index = txn.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); revs.forEach(function(rev) { - index.getKey(rev).onsuccess = function(e) { + var key = docId + "::" + rev; + index.getKey(key).onsuccess = function(e) { var seq = e.target.result; if (!seq) { return; diff --git a/src/adapters/pouch.websql.js b/src/adapters/pouch.websql.js index 521545407b..f47e2c562b 100644 --- a/src/adapters/pouch.websql.js +++ b/src/adapters/pouch.websql.js @@ -55,7 +55,7 @@ var webSqlPouch = function(opts, callback) { var doc = 'CREATE TABLE IF NOT EXISTS ' + DOC_STORE + ' (id unique, seq, json, winningseq)'; var seq = 'CREATE TABLE IF NOT EXISTS ' + BY_SEQ_STORE + - ' (seq INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, rev UNIQUE, json)'; + ' (seq INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, doc_id_rev UNIQUE, json)'; tx.executeSql(attach); tx.executeSql(doc); @@ -241,8 +241,9 @@ var webSqlPouch = function(opts, callback) { function finish() { var data = docInfo.data; - var sql = 'INSERT INTO ' + BY_SEQ_STORE + ' (rev, json) VALUES (?, ?);'; - tx.executeSql(sql, [data._rev, JSON.stringify(data)], dataWritten); + var sql = 'INSERT INTO ' + BY_SEQ_STORE + ' (doc_id_rev, json) VALUES (?, ?);'; + tx.executeSql(sql, [data._id + "::" + data._rev, + JSON.stringify(data)], dataWritten); } function collectResults(attachmentErr) { @@ -297,11 +298,14 @@ var webSqlPouch = function(opts, callback) { var mainRev = Pouch.merge.winningRev(docInfo.metadata); var sql = isUpdate ? - 'UPDATE ' + DOC_STORE + ' SET seq=?, json=?, winningseq=(SELECT seq FROM ' + BY_SEQ_STORE + ' WHERE rev=?) WHERE id=?' : + 'UPDATE ' + DOC_STORE + ' SET seq=?, json=?, winningseq=(SELECT seq FROM ' + + BY_SEQ_STORE + ' WHERE doc_id_rev=?) WHERE id=?' : 'INSERT INTO ' + DOC_STORE + ' (id, seq, winningseq, json) VALUES (?, ?, ?, ?);'; + var metadataStr = JSON.stringify(docInfo.metadata); + var key = docInfo.metadata.id + "::" + mainRev; var params = isUpdate ? - [seq, JSON.stringify(docInfo.metadata), mainRev, docInfo.metadata.id] : - [docInfo.metadata.id, seq, seq, JSON.stringify(docInfo.metadata)]; + [seq, metadataStr, key, docInfo.metadata.id] : + [docInfo.metadata.id, seq, seq, metadataStr]; tx.executeSql(sql, params, function(tx, result) { results.push(docInfo); call(callback, null); @@ -416,7 +420,8 @@ var webSqlPouch = function(opts, callback) { var rev = Pouch.merge.winningRev(metadata); var key = opts.rev ? opts.rev : rev; - var sql = 'SELECT * FROM ' + BY_SEQ_STORE + ' WHERE rev=?'; + key = metadata.id + '::' + key; + var sql = 'SELECT * FROM ' + BY_SEQ_STORE + ' WHERE doc_id_rev=?'; tx.executeSql(sql, [key], function(tx, results) { if (!results.rows.length) { result = Pouch.Errors.MISSING_DOC; @@ -682,8 +687,9 @@ var webSqlPouch = function(opts, callback) { }; api._removeDocRevisions = function(docId, revs, callback) { db.transaction(function (tx) { - var sql = 'DELETE FROM ' + BY_SEQ_STORE + ' WHERE rev IN (' + - revs.map(function(rev){return quote(rev);}).join(',') + ')'; + var sql = 'DELETE FROM ' + BY_SEQ_STORE + ' WHERE doc_id_rev IN (' + + revs.map(function(rev){return quote(docId + '::' + rev);}).join(',') + ')'; + console.log(sql); tx.executeSql(sql, [], function(tx, result) { callback(); }); diff --git a/tests/test.replication.js b/tests/test.replication.js index dfa634c1a8..3503bbf938 100644 --- a/tests/test.replication.js +++ b/tests/test.replication.js @@ -667,6 +667,23 @@ deletedDocAdapters.map(function(adapters) { workflow(self.name, self.remote, runs); }); }); + + asyncTest("issue #300 rev id unique per doc", 3, function() { + var docs = [{_id: "a"}, {_id: "b"}]; + var self = this; + initDBPair(this.name, this.remote, function(db, remote) { + remote.bulkDocs({docs: docs}, {}, function(err, _){ + db.replicate.from(self.remote, function(err, _){ + db.allDocs(function(err, result){ + ok(result.rows.length === 2, "correct number of rows"); + ok(result.rows[0].id === "a", "first doc ok"); + ok(result.rows[1].id === "b", "second doc ok"); + start(); + }); + }); + }); + }); + }); }); // This test only needs to run for one configuration, and it slows stuff From c44be4033163a98255cfbd4f21fc747a5cb80d8f Mon Sep 17 00:00:00 2001 From: Adam Polak Date: Tue, 26 Mar 2013 09:53:45 +0100 Subject: [PATCH 08/22] (#577) - Initial implementation of auto-compaction --- src/pouch.adapter.js | 50 ++++++++++++++++++++++++++++++++-------- src/pouch.utils.js | 28 ++++++++++++++++++++++ tests/test.compaction.js | 30 ++++++++++++++++++++++++ tests/test.utils.js | 12 ++++++---- 4 files changed, 107 insertions(+), 13 deletions(-) diff --git a/src/pouch.adapter.js b/src/pouch.adapter.js index 42646941aa..57a95b173d 100644 --- a/src/pouch.adapter.js +++ b/src/pouch.adapter.js @@ -35,13 +35,43 @@ var PouchAdapter = function(opts, callback) { } }); + var auto_compaction = (opts.auto_compaction === true); + + // wraps a callback with a function that runs compaction after each edit + var autoCompact = function(callback) { + if (!auto_compaction) { + return callback; + } + return function(err, res) { + if (err) { + call(callback, err); + } else { + var count = res.length; + var decCount = function() { + count--; + if (!count) { + call(callback, null, res); + } + }; + res.forEach(function(doc) { + if (doc.ok) { + // TODO: we need better error handling + compactDocument(doc.id, 1, decCount); + } else { + decCount(); + } + }); + } + }; + } api.post = function (doc, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } - return customApi.bulkDocs({docs: [doc]}, opts, yankError(callback)); + return customApi.bulkDocs({docs: [doc]}, opts, + autoCompact(yankError(callback))); }; api.put = function(doc, opts, callback) { @@ -53,7 +83,8 @@ var PouchAdapter = function(opts, callback) { if (!doc || !('_id' in doc)) { return call(callback, Pouch.Errors.MISSING_ID); } - return customApi.bulkDocs({docs: [doc]}, opts, yankError(callback)); + return customApi.bulkDocs({docs: [doc]}, opts, + autoCompact(yankError(callback))); }; api.putAttachment = function (id, rev, blob, type, callback) { @@ -166,22 +197,23 @@ var PouchAdapter = function(opts, callback) { // compact one document and fire callback // by compacting we mean removing all revisions which - // are not leaves in revision tree - var compactDocument = function(docId, callback) { + // are further from the leaf in revision tree than max_height + var compactDocument = function(docId, max_height, callback) { customApi._getRevisionTree(docId, function(err, rev_tree){ if (err) { return call(callback); } + var height = computeHeight(rev_tree); var nonLeaves = []; - traverseRevTree(rev_tree, function(isLeaf, pos, id) { - var rev = pos + '-' + id; - if (!isLeaf) { + Object.keys(height).forEach(function(rev) { + if (height[rev] > max_height) { nonLeaves.push(rev); } }); customApi._removeDocRevisions(docId, nonLeaves, callback); }); }; + // compact the whole database using single document // compaction api.compact = function(callback) { @@ -192,7 +224,7 @@ var PouchAdapter = function(opts, callback) { return; } res.rows.forEach(function(row) { - compactDocument(row.key, function() { + compactDocument(row.key, 0, function() { count--; if (!count) { call(callback); @@ -438,7 +470,7 @@ var PouchAdapter = function(opts, callback) { opts.new_edits = true; } - return customApi._bulkDocs(req, opts, callback); + return customApi._bulkDocs(req, opts, autoCompact(callback)); }; /* End Wrappers */ diff --git a/src/pouch.utils.js b/src/pouch.utils.js index 6a655bf499..5d155c8ef8 100644 --- a/src/pouch.utils.js +++ b/src/pouch.utils.js @@ -249,6 +249,33 @@ var collectConflicts = function(metadata) { return conflicts; }; + +// for every node in a revision tree computes its distance from the closest +// leaf +var computeHeight = function(revs) { + var height = {}; + var edges = []; + traverseRevTree(revs, function(isLeaf, pos, id, prnt) { + var rev = pos + "-" + id; + if (isLeaf) { + height[rev] = 0; + } + if (!(prnt === undefined)) { + edges.push({from: prnt, to: rev}); + } + return rev; + }); + edges.reverse(); + edges.forEach(function(edge) { + if (height[edge.from] === undefined) { + height[edge.from] = 1 + height[edge.to]; + } else { + height[edge.from] = Math.min(height[edge.from], 1 + height[edge.to]); + } + }); + return height; +} + // returns first element of arr satisfying callback predicate var arrayFirst = function(arr, callback) { for (var i = 0; i < arr.length; i++) { @@ -319,6 +346,7 @@ if (typeof module !== 'undefined' && module.exports) { collectRevs: collectRevs, collectLeaves: collectLeaves, collectConflicts: collectConflicts, + computeHeight: computeHeight, arrayFirst: arrayFirst, filterChange: filterChange, atob: function(str) { diff --git a/tests/test.compaction.js b/tests/test.compaction.js index e13a94d774..7aafabcca9 100644 --- a/tests/test.compaction.js +++ b/tests/test.compaction.js @@ -195,4 +195,34 @@ adapters.map(function(adapter) { }); }); }); + + asyncTest('Auto-compaction test', function() { + initTestDB(this.name, {auto_compaction: true}, function(err, db) { + var doc = {_id: "doc", val: "1"}; + db.post(doc, function(err, res) { + var rev1 = res.rev; + doc._rev = rev1; + doc.val = "2"; + db.post(doc, function(err, res) { + var rev2 = res.rev; + doc._rev = rev2; + doc.val = "3"; + db.post(doc, function(err, res) { + var rev3 = res.rev; + db.get("doc", {rev: rev1}, function(err, doc) { + ok(err.status === 404 && err.error === "not_found", + "compacted document is missing"); + db.get("doc", {rev: rev2}, function(err, doc) { + ok(!err, "leaf's parent does not get compacted"); + db.get("doc", {rev: rev3}, function(err, doc) { + ok(!err, "leaf revision does not get compacted"); + start(); + }); + }); + }); + }); + }); + }); + }); + }); }); diff --git a/tests/test.utils.js b/tests/test.utils.js index 369b7720bd..031a150a29 100644 --- a/tests/test.utils.js +++ b/tests/test.utils.js @@ -65,8 +65,12 @@ function openTestAsyncDB(name) { }); } -function openTestDB(name, callback) { - new Pouch(name, function(err, db) { +function openTestDB(name, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } + new Pouch(name, opts, function(err, db) { if (err) { console.error(err); ok(false, 'failed to open database'); @@ -76,7 +80,7 @@ function openTestDB(name, callback) { }); } -function initTestDB(name, callback) { +function initTestDB(name, opts, callback) { // ignore errors, the database might not exist Pouch.destroy(name, function(err) { if (err && err.status !== 404 && err.statusText !== 'timeout') { @@ -84,7 +88,7 @@ function initTestDB(name, callback) { ok(false, 'failed to open database'); return start(); } - openTestDB(name, callback); + openTestDB(name, opts, callback); }); } From d89e8f30cd84ab539460cab413f27ce6e922acf1 Mon Sep 17 00:00:00 2001 From: Adam Polak Date: Wed, 27 Mar 2013 15:39:35 +0100 Subject: [PATCH 09/22] Fix linting errors --- src/pouch.adapter.js | 6 +++--- src/pouch.utils.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pouch.adapter.js b/src/pouch.adapter.js index 57a95b173d..e9e5523d93 100644 --- a/src/pouch.adapter.js +++ b/src/pouch.adapter.js @@ -1,5 +1,5 @@ -/*globals Pouch:true, yankError: false, extend: false, call: false, parseDocId: false, traverseRevTree: false, collectLeaves: false */ -/*globals collectConflicts: false, arrayFirst: false, rootToLeaf: false */ +/*globals Pouch: true, yankError: false, extend: false, call: false, parseDocId: false, traverseRevTree: false, collectLeaves: false */ +/*globals collectConflicts: false, arrayFirst: false, rootToLeaf: false, computeHeight: false */ "use strict"; @@ -63,7 +63,7 @@ var PouchAdapter = function(opts, callback) { }); } }; - } + }; api.post = function (doc, opts, callback) { if (typeof opts === 'function') { diff --git a/src/pouch.utils.js b/src/pouch.utils.js index 5d155c8ef8..33cda3d4bc 100644 --- a/src/pouch.utils.js +++ b/src/pouch.utils.js @@ -260,7 +260,7 @@ var computeHeight = function(revs) { if (isLeaf) { height[rev] = 0; } - if (!(prnt === undefined)) { + if (prnt !== undefined) { edges.push({from: prnt, to: rev}); } return rev; @@ -274,7 +274,7 @@ var computeHeight = function(revs) { } }); return height; -} +}; // returns first element of arr satisfying callback predicate var arrayFirst = function(arr, callback) { From a417b87455ce24071f5d5aae1bd88a7872ea957a Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Thu, 28 Mar 2013 21:40:12 +0000 Subject: [PATCH 10/22] (#623) - Switch to self hosts couchdb for test results --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2ab975c9ef..f0f66c419e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -215,7 +215,7 @@ module.exports = function(grunt) { } }, 'publish-results': { - server: 'http://pouchdb.iriscouch.com', + server: 'http://couchdb.pouchdb.com', db: 'test_results' } }); From 4d7460581237a1c2585cc46d8564b3a59390cf94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Thu, 28 Mar 2013 20:48:06 +0100 Subject: [PATCH 11/22] (#578) - enable local adapters in test.issue211.js Also: * compaction now fires callback when it's finished so #280 looks no longer needed * remove empty braces in get which looked like some legacy * replace ok with strictEqual --- tests/test.issue221.js | 60 +++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/tests/test.issue221.js b/tests/test.issue221.js index 843329bbf6..5e730dcaaa 100644 --- a/tests/test.issue221.js +++ b/tests/test.issue221.js @@ -5,7 +5,10 @@ "use strict"; var adapters = [ - ['local-1', 'http-1'] + ['local-1', 'http-1'], + ['http-1', 'http-2'], + ['http-1', 'local-1'], + ['local-1', 'local-2'] ]; var qunit = module; var LevelPouch; @@ -23,7 +26,7 @@ if (typeof module !== undefined && module.exports) { adapters.map(function(adapters) { - qunit('replication + compaction', { + qunit('replication + compaction: ' + adapters[0] + ':' + adapters[1], { setup: function() { this.local = generateAdapterUrl(adapters[0]); this.remote = generateAdapterUrl(adapters[1]); @@ -49,41 +52,26 @@ adapters.map(function(adapters) { // Create databases. initDBPair(self.local, self.remote, function(local, remote) { // Write a doc in CouchDB. - remote.put(doc, {}, function(err, results) { + remote.put(doc, function(err, results) { // Update the doc. doc._rev = results.rev; doc.integer = 1; - remote.put(doc, {}, function(err, results) { + remote.put(doc, function(err, results) { // Compact the db. - remote.compact(function(data, status, jqXHR) { - // Wait until compaction has affected the doc. - var interval; - var checkDoc = function() { - remote.get(doc._id,{revs_info:true},function(err, data) { - var correctRev = data._revs_info[0]; - if (data._revs_info[1].status === 'missing') { - // We already got a successful compaction, but did a whole - // new request before we figured it out, yay races - if (!interval) { - return; - } - clearInterval(interval); - interval = null; - // Replicate to PouchDB. - local.replicate.from(remote, function(err, results) { - // Check the PouchDB doc. - local.get(doc._id, function(err, results) { - ok(results._rev === correctRev.rev, - 'correct rev stored after replication'); - ok(results.integer === 1, - 'correct content stored after replication'); - start(); - }); - }); - } + remote.compact(function() { + remote.get(doc._id, {revs_info:true},function(err, data) { + var correctRev = data._revs_info[0]; + local.replicate.from(remote, function(err, results) { + // Check the PouchDB doc. + local.get(doc._id, function(err, results) { + strictEqual(results._rev, correctRev.rev, + 'correct rev stored after replication'); + strictEqual(results.integer, 1, + 'correct content stored after replication'); + start(); + }); }); - }; - interval = setInterval(checkDoc, 100); + }); }); }); }); @@ -102,19 +90,19 @@ adapters.map(function(adapters) { // Create databases. initDBPair(self.local, self.remote, function(local, remote) { // Write a doc in CouchDB. - remote.put(doc, {}, function(err, results) { + remote.put(doc, function(err, results) { doc._rev = results.rev; // Second doc so we get 2 revisions from replicate. - remote.put(doc, {}, function(err, results) { + remote.put(doc, function(err, results) { doc._rev = results.rev; local.replicate.from(remote, function(err, results) { doc.integer = 1; // One more change - remote.put(doc, {}, function(err, results) { + remote.put(doc, function(err, results) { // Testing if second replications fails now local.replicate.from(remote, function(err, results) { local.get(doc._id, function(err, results) { - ok(results.integer === 1, 'correct content stored after replication'); + strictEqual(results.integer, 1, 'correct content stored after replication'); start(); }); }); From 1455c57cdf8a79b25a764ff22f55b036a3f6ef54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ko=C5=82odziejski?= Date: Thu, 28 Mar 2013 22:46:08 +0100 Subject: [PATCH 12/22] (#578) - fix linting error --- tests/test.issue221.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.issue221.js b/tests/test.issue221.js index 5e730dcaaa..0f06574400 100644 --- a/tests/test.issue221.js +++ b/tests/test.issue221.js @@ -1,4 +1,4 @@ -/*globals initTestDB: false, emit: true, generateAdapterUrl: false */ +/*globals initTestDB: false, emit: true, generateAdapterUrl: false, strictEqual: false */ /*globals PERSIST_DATABASES: false, initDBPair: false, utils: true */ /*globals ajax: true, LevelPouch: true */ From e61a266ac2fd17f5a2ca74fe59efb65260183f30 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Thu, 28 Mar 2013 22:52:59 +0000 Subject: [PATCH 13/22] (#563) - Remove feature detection for compaction in tests --- tests/test.issue221.js | 82 ++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/tests/test.issue221.js b/tests/test.issue221.js index 0f06574400..715ddbce0b 100644 --- a/tests/test.issue221.js +++ b/tests/test.issue221.js @@ -43,33 +43,26 @@ adapters.map(function(adapters) { asyncTest('Testing issue #221', function() { var self = this; - // Feature detection for pouchdb-server - ajax({ url: 'http://localhost:2020/' }, function (err, ret) { - if (!ret.couchdb) { - ok(true, 'Test not applicable to backend.'); - return start(); - } - // Create databases. - initDBPair(self.local, self.remote, function(local, remote) { - // Write a doc in CouchDB. + // Create databases. + initDBPair(self.local, self.remote, function(local, remote) { + // Write a doc in CouchDB. + remote.put(doc, function(err, results) { + // Update the doc. + doc._rev = results.rev; + doc.integer = 1; remote.put(doc, function(err, results) { - // Update the doc. - doc._rev = results.rev; - doc.integer = 1; - remote.put(doc, function(err, results) { - // Compact the db. - remote.compact(function() { - remote.get(doc._id, {revs_info:true},function(err, data) { - var correctRev = data._revs_info[0]; - local.replicate.from(remote, function(err, results) { - // Check the PouchDB doc. - local.get(doc._id, function(err, results) { - strictEqual(results._rev, correctRev.rev, - 'correct rev stored after replication'); - strictEqual(results.integer, 1, - 'correct content stored after replication'); - start(); - }); + // Compact the db. + remote.compact(function() { + remote.get(doc._id, {revs_info:true},function(err, data) { + var correctRev = data._revs_info[0]; + local.replicate.from(remote, function(err, results) { + // Check the PouchDB doc. + local.get(doc._id, function(err, results) { + strictEqual(results._rev, correctRev.rev, + 'correct rev stored after replication'); + strictEqual(results.integer, 1, + 'correct content stored after replication'); + start(); }); }); }); @@ -81,30 +74,23 @@ adapters.map(function(adapters) { asyncTest('Testing issue #221 again', function() { var self = this; - // Feature detection for pouchdb-server - ajax({ url: 'http://localhost:2020/' }, function (err, ret) { - if (!ret.couchdb) { - ok(true, 'Test not applicable to backend.'); - return start(); - } - // Create databases. - initDBPair(self.local, self.remote, function(local, remote) { - // Write a doc in CouchDB. + // Create databases. + initDBPair(self.local, self.remote, function(local, remote) { + // Write a doc in CouchDB. + remote.put(doc, function(err, results) { + doc._rev = results.rev; + // Second doc so we get 2 revisions from replicate. remote.put(doc, function(err, results) { doc._rev = results.rev; - // Second doc so we get 2 revisions from replicate. - remote.put(doc, function(err, results) { - doc._rev = results.rev; - local.replicate.from(remote, function(err, results) { - doc.integer = 1; - // One more change - remote.put(doc, function(err, results) { - // Testing if second replications fails now - local.replicate.from(remote, function(err, results) { - local.get(doc._id, function(err, results) { - strictEqual(results.integer, 1, 'correct content stored after replication'); - start(); - }); + local.replicate.from(remote, function(err, results) { + doc.integer = 1; + // One more change + remote.put(doc, function(err, results) { + // Testing if second replications fails now + local.replicate.from(remote, function(err, results) { + local.get(doc._id, function(err, results) { + strictEqual(results.integer, 1, 'correct content stored after replication'); + start(); }); }); }); From 73c4c185ace2a54d4592f70cefb21ce68b8b16b1 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Fri, 29 Mar 2013 00:04:00 +0000 Subject: [PATCH 14/22] (#612) - Add seperate travis test target --- CONTRIBUTING.md | 3 ++- Gruntfile.js | 11 ++++++++--- package.json | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f59bd20dd0..964d57882c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,7 @@ Guide to Contributions * Looking for something to work on? look for bugs marked [goodfirstbug](https://github.com/daleharvey/pouchdb/issues?labels=goodfirstbug&page=1&state=open) * We follow [Felix's Node.js Style Guide](http://nodeguide.com/style.html) * Almost all Pull Requests for features or bug fixes will need tests (seriously, its really important) + * Before opening a pull request run `$ grunt test` to lint test the changes and run node tests. Preferably run the browser tests as well. * Commit messages should follow the following style: ``` @@ -57,7 +58,7 @@ Run single test file `test.basics.js` with: ### Browser Tests - $ grunt browser + $ grunt browser # Now visit http://127.0.0.1:8000/tests/test.html in your browser # add ?testFiles=test.basics.js to run single test file diff --git a/Gruntfile.js b/Gruntfile.js index f0f66c419e..70e46ac6d0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -278,11 +278,16 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.registerTask("build", ["concat:amd", "concat:all" , "uglify:dist"]); - grunt.registerTask("browser", ["connect", "cors-server", "forever"]) - grunt.registerTask("testSetup", ["jshint", "build", "connect", "cors-server"]); - grunt.registerTask("test", ["testSetup", "node-qunit" ,"saucelabs-qunit", "publish-results"]); + grunt.registerTask("browser", ["connect", "cors-server", "forever"]); grunt.registerTask("full", ["concat", "uglify"]); + grunt.registerTask("spatial", ["concat:spatial", "uglify:spatial"]); grunt.registerTask("gql", ["concat:gql", "uglify:gql"]); + + grunt.registerTask("test", ["jshint", "cors-server"]); + grunt.registerTask("test-travis", ["jshint", "build", "connect", "cors-server", + "node-qunit", "saucelabs-qunit", + "publish-results"]); + grunt.registerTask('default', 'build'); }; diff --git a/package.json b/package.json index 7310acda40..b7d2f867ed 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,6 @@ } ], "scripts": { - "test": "grunt test" + "test": "grunt test-travis" } } From b7d5f3cfee1aa0406d8f9371072928ad2f03b94d Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Fri, 29 Mar 2013 16:26:53 +0800 Subject: [PATCH 15/22] (#628) - Added Pouch.enableAllDbs flag to make allDbs optional --- src/pouch.js | 62 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/pouch.js b/src/pouch.js index 478c586cb9..6258bd404f 100644 --- a/src/pouch.js +++ b/src/pouch.js @@ -125,36 +125,49 @@ Pouch.destroy = function(name, callback) { // call destroy method of the particular adaptor Pouch.adapters[opts.adapter].destroy(opts.name, callback); }; + + // remove Pouch from allDBs + Pouch.removeFromAllDbs(opts, cb); +}; + +Pouch.removeFromAllDbs = function(opts, callback) { + // Only execute function if flag is enabled + if (!Pouch.enableAllDbs) { + callback(); + return; + } // skip http and https adaptors for allDbs var adapter = opts.adapter; if (adapter === "http" || adapter === "https") { - cb(); + callback(); return; } // remove db from Pouch.ALL_DBS new Pouch(Pouch.allDBName(opts.adapter), function(err, db) { if (err) { - callback(err); + // don't fail when allDbs fail + console.log(err); + callback(); return; } // check if db has been registered in Pouch.ALL_DBS var dbname = Pouch.dbName(opts.adapter, opts.name); db.get(dbname, function(err, doc) { if (err) { - if (err.status === 404) { - cb(); - } else { - cb(err); - } + callback(); } else { db.remove(doc, function(err, response) { - cb(err); + if (err) { + console.log(err); + } + callback(); }); } }); }); + }; Pouch.adapter = function (id, obj) { @@ -167,6 +180,9 @@ Pouch.plugin = function(id, obj) { Pouch.plugins[id] = obj; }; +// flag to toggle allDbs (off by default) +Pouch.enableAllDbs = false; + // name of database used to keep track of databases Pouch.ALL_DBS = "_allDbs"; Pouch.dbName = function(adapter, name) { @@ -180,6 +196,12 @@ Pouch.allDBName = function(adapter) { }; Pouch.open = function(opts, callback) { + // Only register pouch with allDbs if flag is enabled + if (!Pouch.enableAllDbs) { + callback(); + return; + } + var adapter = opts.adapter; // skip http and https adaptors for allDbs if (adapter === "http" || adapter === "https") { @@ -189,22 +211,26 @@ Pouch.open = function(opts, callback) { new Pouch(Pouch.allDBName(adapter), function(err, db) { if (err) { - callback(err); + // don't fail when allDb registration fails + console.log(err); + callback(); return; } // check if db has been registered in Pouch.ALL_DBS var dbname = Pouch.dbName(adapter, opts.name); db.get(dbname, function(err, response) { - if (err) { - if (err.status === 404) { - db.put({ - _id: dbname, - dbname: opts.originalName - }, callback); - } else { - callback(err); - } + if (err && err.status === 404) { + db.put({ + _id: dbname, + dbname: opts.originalName + }, function(err) { + if (err) { + console.log(err); + } + + callback(); + }); } else { callback(); } From 446225cc514b0ef31b5fec601598dc27556342ad Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Fri, 29 Mar 2013 16:28:22 +0800 Subject: [PATCH 16/22] (#628) - updated allDbs tests (enable allDbs for the particular tests) --- tests/test.all_dbs.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/test.all_dbs.js b/tests/test.all_dbs.js index 7e0ab500d5..7756c7aeee 100644 --- a/tests/test.all_dbs.js +++ b/tests/test.all_dbs.js @@ -47,6 +47,15 @@ function async(functions, callback) { series(functions); } +// Remove old allDbs to prevent DOM exception +Object.keys(Pouch.adapters).forEach(function(adapter) { + if (adapter === "http" || adapter === "https") { + return; + } + + Pouch.destroy(Pouch.allDBName(adapter), function(){}); +}); + // Loop through all availible adapters Object.keys(Pouch.adapters).forEach(function(adapter) { // allDbs method only works for local adapters @@ -56,16 +65,20 @@ Object.keys(Pouch.adapters).forEach(function(adapter) { qunit('allDbs: ' + adapter, { setup: function() { - // DummyDB Names - this.pouchNames = []; + // enable allDbs + Pouch.enableAllDbs = true; - var pouchName; - for (var i = 0; i < 5; i++) { - pouchName = 'testdb_' + uuid(); - this.pouchNames.push([adapter, "://", pouchName].join('')); - } + // DummyDB Names + this.pouchNames = []; + + var pouchName; + for (var i = 0; i < 5; i++) { + pouchName = 'testdb_' + uuid(); + this.pouchNames.push([adapter, "://", pouchName].join('')); + } }, teardown: function() { + Pouch.enableAllDbs = false; } }); @@ -293,6 +306,9 @@ Object.keys(Pouch.adapters).forEach(function(adapter) { // 2. Otherwise, the dbname will just contain the dbname (without the adapter prefix) qunit("allDbs return value", { setup: function() { + // enable allDbs + Pouch.enableAllDbs = true; + // DummyDB Names var pouchNames = []; @@ -317,6 +333,7 @@ qunit("allDbs return value", { this.pouchNames = pouchNames; }, teardown: function() { + Pouch.enableAllDbs = false; } }); From 2764513e39cde8833144d9d001cd65c23674f4a6 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Fri, 29 Mar 2013 05:26:07 +0000 Subject: [PATCH 17/22] (#579) - Merge rev_map into rev_tree --- src/adapters/pouch.idb.js | 28 +++---- src/adapters/pouch.leveldb.js | 18 ++--- src/adapters/pouch.websql.js | 24 +++--- src/pouch.adapter.js | 16 ++-- src/pouch.merge.js | 97 ++++++++++++++++++------ src/pouch.utils.js | 126 +++++++++---------------------- tests/test.compaction.js | 4 +- tests/test.merge_rev_tree.js | 136 +++++++++++++++++----------------- tests/test.utils.js | 4 +- 9 files changed, 212 insertions(+), 241 deletions(-) diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index 862c10a4dc..ea5f4836ec 100644 --- a/src/adapters/pouch.idb.js +++ b/src/adapters/pouch.idb.js @@ -1,6 +1,5 @@ /*globals call: false, extend: false, parseDoc: false, Crypto: false */ -/*globals isLocalId: false, isDeleted: false, collectConflicts: false */ -/*globals collectLeaves: false, Changes: false */ +/*globals isLocalId: false, isDeleted: false, Changes: false */ 'use strict'; @@ -160,7 +159,6 @@ var IdbPouch = function(opts, callback) { }; api._bulkDocs = function idb_bulkDocs(req, opts, callback) { - var newEdits = opts.new_edits; var userDocs = req.docs; @@ -168,12 +166,6 @@ var IdbPouch = function(opts, callback) { var docInfos = userDocs.map(function(doc, i) { var newDoc = parseDoc(doc, newEdits); newDoc._bulk_seq = i; - if (doc._deleted) { - if (!newDoc.metadata.deletions) { - newDoc.metadata.deletions = {}; - } - newDoc.metadata.deletions[doc._rev.split('-')[1]] = true; - } return newDoc; }); @@ -371,12 +363,10 @@ var IdbPouch = function(opts, callback) { } function updateDoc(oldDoc, docInfo) { - docInfo.metadata.deletions = extend(docInfo.metadata.deletions, oldDoc.deletions); - var merged = Pouch.merge(oldDoc.rev_tree, docInfo.metadata.rev_tree[0], 1000); - - var inConflict = (isDeleted(oldDoc) && isDeleted(docInfo.metadata)) || - (!isDeleted(oldDoc) && newEdits && merged.conflicts !== 'new_leaf'); + var wasPreviouslyDeleted = isDeleted(oldDoc); + var inConflict = (wasPreviouslyDeleted && isDeleted(docInfo.metadata)) || + (!wasPreviouslyDeleted && newEdits && merged.conflicts !== 'new_leaf'); if (inConflict) { results.push(makeErr(Pouch.Errors.REV_CONFLICT, docInfo._bulk_seq)); @@ -619,7 +609,8 @@ var IdbPouch = function(opts, callback) { delete(doc.doc._doc_id_rev); } if (opts.conflicts) { - doc.doc._conflicts = collectConflicts(metadata); + doc.doc._conflicts = Pouch.merge.collectConflicts(metadata) + .map(function(x) { return x.id; }); } } if ('keys' in opts) { @@ -776,8 +767,8 @@ var IdbPouch = function(opts, callback) { var doc = docevent.target.result; var changeList = [{rev: mainRev}]; if (opts.style === 'all_docs') { - // console.log('all docs', changeList, collectLeaves(metadata.rev_tree)); - changeList = collectLeaves(metadata.rev_tree); + changeList = Pouch.merge.collectLeaves(metadata.rev_tree) + .map(function(x) { return {rev: x.rev}; }); } var change = { id: metadata.id, @@ -789,7 +780,8 @@ var IdbPouch = function(opts, callback) { change.deleted = true; } if (opts.conflicts) { - change.doc._conflicts = collectConflicts(metadata); + change.doc._conflicts = Pouch.merge.collectConflicts(metadata) + .map(function(x) { return x.id; }); } // Dedupe the changes feed diff --git a/src/adapters/pouch.leveldb.js b/src/adapters/pouch.leveldb.js index 0b6acad8d0..9fa99b2750 100644 --- a/src/adapters/pouch.leveldb.js +++ b/src/adapters/pouch.leveldb.js @@ -263,13 +263,6 @@ var LevelPouch = function(opts, callback) { if (newDoc.metadata && !newDoc.metadata.rev_map) { newDoc.metadata.rev_map = {}; } - if (doc._deleted) { - if (!newDoc.metadata.deletions) { - newDoc.metadata.deletions = {}; - } - newDoc.metadata.deletions[doc._rev.split('-')[1]] = true; - } - return newDoc; }); @@ -325,8 +318,6 @@ var LevelPouch = function(opts, callback) { } function updateDoc(oldDoc, docInfo, callback) { - docInfo.metadata.deletions = extend(docInfo.metadata.deletions, oldDoc.deletions); - var merged = Pouch.merge(oldDoc.rev_tree, docInfo.metadata.rev_tree[0], 1000); var conflict = (isDeleted(oldDoc) && isDeleted(docInfo.metadata)) || @@ -491,7 +482,7 @@ var LevelPouch = function(opts, callback) { var change = { id: metadata.id, seq: metadata.seq, - changes: Pouch.utils.collectLeaves(metadata.rev_tree), + changes: Pouch.merge.collectLeaves(metadata.rev_tree), doc: result.data }; change.doc._rev = rev; @@ -546,7 +537,7 @@ var LevelPouch = function(opts, callback) { doc.doc = data; doc.doc._rev = doc.value.rev; if (opts.conflicts) { - doc.doc._conflicts = Pouch.utils.collectConflicts(metadata); + doc.doc._conflicts = Pouch.merge.collectConflicts(metadata); } } if ('keys' in opts) { @@ -630,7 +621,8 @@ var LevelPouch = function(opts, callback) { var change = { id: metadata.id, seq: metadata.seq, - changes: Pouch.utils.collectLeaves(metadata.rev_tree), + changes: Pouch.merge.collectLeaves(metadata.rev_tree) + .map(function(x) { return {rev: x.rev}; }), doc: data.value }; @@ -640,7 +632,7 @@ var LevelPouch = function(opts, callback) { change.deleted = true; } if (opts.conflicts) { - change.doc._conflicts = Pouch.utils.collectConflicts(metadata); + change.doc._conflicts = Pouch.merge.collectConflicts(metadata); } // dedupe changes (TODO: more efficient way to accomplish this?) diff --git a/src/adapters/pouch.websql.js b/src/adapters/pouch.websql.js index f47e2c562b..5225d14d13 100644 --- a/src/adapters/pouch.websql.js +++ b/src/adapters/pouch.websql.js @@ -1,6 +1,5 @@ /*globals call: false, extend: false, parseDoc: false, Crypto: false */ -/*globals isLocalId: false, isDeleted: false, collectConflicts: false */ -/*globals collectLeaves: false, Changes: false */ +/*globals isLocalId: false, isDeleted: false, Changes: false */ 'use strict'; @@ -114,12 +113,6 @@ var webSqlPouch = function(opts, callback) { var docInfos = userDocs.map(function(doc, i) { var newDoc = parseDoc(doc, newEdits); newDoc._bulk_seq = i; - if (doc._deleted) { - if (!newDoc.metadata.deletions) { - newDoc.metadata.deletions = {}; - } - newDoc.metadata.deletions[doc._rev.split('-')[1]] = true; - } return newDoc; }); @@ -314,8 +307,6 @@ var webSqlPouch = function(opts, callback) { } function updateDoc(oldDoc, docInfo) { - docInfo.metadata.deletions = extend(docInfo.metadata.deletions, oldDoc.deletions); - var merged = Pouch.merge(oldDoc.rev_tree, docInfo.metadata.rev_tree[0], 1000); var inConflict = (isDeleted(oldDoc) && isDeleted(docInfo.metadata)) || (!isDeleted(oldDoc) && newEdits && merged.conflicts !== 'new_leaf'); @@ -455,6 +446,13 @@ var webSqlPouch = function(opts, callback) { }); }; + function makeRevs(arr) { + return arr.map(function(x) { return {rev: x.rev}; }); + } + function makeIds(arr) { + return arr.map(function(x) { return x.id; }); + } + api._allDocs = function(opts, callback) { var results = []; var resultsMap = {}; @@ -496,7 +494,7 @@ var webSqlPouch = function(opts, callback) { doc.doc = data; doc.doc._rev = Pouch.merge.winningRev(metadata); if (opts.conflicts) { - doc.doc._conflicts = collectConflicts(metadata); + doc.doc._conflicts = makeIds(Pouch.merge.collectConflicts(metadata)); } } if ('keys' in opts) { @@ -586,7 +584,7 @@ var webSqlPouch = function(opts, callback) { var change = { id: metadata.id, seq: doc.seq, - changes: collectLeaves(metadata.rev_tree), + changes: makeRevs(Pouch.merge.collectLeaves(metadata.rev_tree)), doc: JSON.parse(doc.data) }; change.doc._rev = Pouch.merge.winningRev(metadata); @@ -594,7 +592,7 @@ var webSqlPouch = function(opts, callback) { change.deleted = true; } if (opts.conflicts) { - change.doc._conflicts = collectConflicts(metadata); + change.doc._conflicts = makeIds(Pouch.merge.collectConflicts(metadata)); } results.push(change); } diff --git a/src/pouch.adapter.js b/src/pouch.adapter.js index e9e5523d93..5fa2f5af42 100644 --- a/src/pouch.adapter.js +++ b/src/pouch.adapter.js @@ -1,5 +1,5 @@ -/*globals Pouch: true, yankError: false, extend: false, call: false, parseDocId: false, traverseRevTree: false, collectLeaves: false */ -/*globals collectConflicts: false, arrayFirst: false, rootToLeaf: false, computeHeight: false */ +/*globals Pouch: true, yankError: false, extend: false, call: false, parseDocId: false, traverseRevTree: false */ +/*globals arrayFirst: false, rootToLeaf: false, computeHeight: false */ "use strict"; @@ -276,7 +276,7 @@ var PouchAdapter = function(opts, callback) { // situation the same way as if revision tree was empty rev_tree = []; } - leaves = collectLeaves(rev_tree).map(function(leaf){ + leaves = Pouch.merge.collectLeaves(rev_tree).map(function(leaf){ return leaf.rev; }); finishOpenRevs(); @@ -316,14 +316,18 @@ var PouchAdapter = function(opts, callback) { } if (opts.conflicts) { - var conflicts = collectConflicts(metadata); + var conflicts = Pouch.merge.collectConflicts(metadata); if (conflicts.length) { doc._conflicts = conflicts; } } if (opts.revs || opts.revs_info) { - var path = arrayFirst(rootToLeaf(metadata.rev_tree), function(arr) { + var paths = rootToLeaf(metadata.rev_tree); + paths.map(function(path, i) { + paths[i].ids = path.ids.map(function(x) { return x.id; }); + }); + var path = arrayFirst(paths, function(arr) { return arr.ids.indexOf(doc._rev.split('-')[1]) !== -1; }); path.ids.splice(path.ids.indexOf(doc._rev.split('-')[1]) + 1); @@ -366,7 +370,7 @@ var PouchAdapter = function(opts, callback) { } else { finish(); } - + }); }; diff --git a/src/pouch.merge.js b/src/pouch.merge.js index f52f3a9d71..f1d2ab2d29 100644 --- a/src/pouch.merge.js +++ b/src/pouch.merge.js @@ -1,4 +1,4 @@ -/*globals rootToLeaf: false, extend: false, traverseRevTree: false */ +/*globals rootToLeaf: false, extend: false */ 'use strict'; @@ -20,17 +20,19 @@ if (typeof module !== 'undefined' && module.exports) { // // KeyTree = [Path ... ] // Path = {pos: position_from_root, ids: Tree} -// Tree = [Key, [Tree, ...]], in particular single node: [Key, []] +// Tree = [Key, Opts, [Tree, ...]], in particular single node: [Key, []] // Turn a path as a flat array into a tree with a single branch function pathToTree(path) { - var root = [path.shift(), []]; + var doc = path.shift(); + var root = [doc.id, doc.opts, []]; var leaf = root; var nleaf; while (path.length) { - nleaf = [path.shift(), []]; - leaf[1].push(nleaf); + doc = path.shift(); + nleaf = [doc.id, doc.opts, []]; + leaf[2].push(nleaf); leaf = nleaf; } return root; @@ -46,24 +48,24 @@ function mergeTree(in_tree1, in_tree2) { var tree1 = item.tree1; var tree2 = item.tree2; - for (var i = 0; i < tree2[1].length; i++) { - if (!tree1[1][0]) { + for (var i = 0; i < tree2[2].length; i++) { + if (!tree1[2][0]) { conflicts = 'new_leaf'; - tree1[1][0] = tree2[1][i]; + tree1[2][0] = tree2[2][i]; continue; } var merged = false; - for (var j = 0; j < tree1[1].length; j++) { - if (tree1[1][j][0] === tree2[1][i][0]) { - queue.push({tree1: tree1[1][j], tree2: tree2[1][i]}); + for (var j = 0; j < tree1[2].length; j++) { + if (tree1[2][j][0] === tree2[2][i][0]) { + queue.push({tree1: tree1[2][j], tree2: tree2[2][i]}); merged = true; } } if (!merged) { conflicts = 'new_branch'; - tree1[1].push(tree2[1][i]); - tree1[1].sort(); + tree1[2].push(tree2[2][i]); + tree1[2].sort(); } } } @@ -114,7 +116,7 @@ function doMerge(tree, path, dontExpand) { continue; } /*jshint loopfunc:true */ - item.ids[1].forEach(function(el, idx) { + item.ids[2].forEach(function(el, idx) { trees.push({ids: el, diff: item.diff-1, parent: item.ids, parentIdx: idx}); }); } @@ -125,7 +127,7 @@ function doMerge(tree, path, dontExpand) { restree.push(branch); } else { res = mergeTree(el.ids, t2.ids); - el.parent[1][el.parentIdx] = res.tree; + el.parent[2][el.parentIdx] = res.tree; restree.push({pos: t1.pos, ids: t1.ids}); conflicts = conflicts || res.conflicts; merged = true; @@ -185,19 +187,13 @@ Pouch.merge = function(tree, path, depth) { // The final sort algorithm is slightly documented in a sidebar here: // http://guide.couchdb.org/draft/conflicts.html Pouch.merge.winningRev = function(metadata) { - var deletions = metadata.deletions || {}; var leafs = []; - - traverseRevTree(metadata.rev_tree, function(isLeaf, pos, id) { + Pouch.merge.traverseRevTree(metadata.rev_tree, + function(isLeaf, pos, id, something, opts) { if (isLeaf) { - leafs.push({pos: pos, id: id}); + leafs.push({pos: pos, id: id, deleted: !!opts.deleted}); } }); - - leafs.forEach(function(leaf) { - leaf.deleted = leaf.id in deletions; - }); - leafs.sort(function(a, b) { if (a.deleted !== b.deleted) { return a.deleted > b.deleted ? 1 : -1; @@ -207,6 +203,59 @@ Pouch.merge.winningRev = function(metadata) { } return a.id < b.id ? 1 : -1; }); + return leafs[0].pos + '-' + leafs[0].id; }; +// Pretty much all below can be combined into a higher order function to +// traverse revisions +// Callback has signature function(isLeaf, pos, id, [context]) +// The return value from the callback will be passed as context to all +// children of that node +Pouch.merge.traverseRevTree = function(revs, callback) { + var toVisit = []; + + revs.forEach(function(tree) { + toVisit.push({pos: tree.pos, ids: tree.ids}); + }); + while (toVisit.length > 0) { + var node = toVisit.pop(); + var pos = node.pos; + var tree = node.ids; + var newCtx = callback(tree[2].length === 0, pos, tree[0], node.ctx, tree[1]); + /*jshint loopfunc: true */ + tree[2].forEach(function(branch) { + toVisit.push({pos: pos+1, ids: branch, ctx: newCtx}); + }); + } +}; + +Pouch.merge.collectLeaves = function(revs) { + var leaves = []; + Pouch.merge.traverseRevTree(revs, function(isLeaf, pos, id, acc, opts) { + if (isLeaf) { + leaves.unshift({rev: pos + "-" + id, pos: pos, opts: opts}); + } + }); + leaves.sort(function(a, b) { + return b.pos - a.pos; + }); + leaves.map(function(leaf) { delete leaf.pos; }); + return leaves; +}; + +// returns all conflicts that is leaves such that +// 1. are not deleted and +// 2. are different than winning revision +Pouch.merge.collectConflicts = function(metadata) { + var win = Pouch.merge.winningRev(metadata); + var leaves = Pouch.merge.collectLeaves(metadata.rev_tree); + var conflicts = []; + leaves.forEach(function(leaf) { + if (leaf.rev !== win && !leaf.opts.deleted) { + conflicts.push(leaf.rev); + } + }); + return conflicts; +}; + diff --git a/src/pouch.utils.js b/src/pouch.utils.js index 33cda3d4bc..25c7171a3c 100644 --- a/src/pouch.utils.js +++ b/src/pouch.utils.js @@ -45,23 +45,6 @@ var parseDocId = function(id) { }; }; -// check if a specific revision of a doc has been deleted -// - metadata: the metadata object from the doc store -// - rev: (optional) the revision to check. defaults to winning revision -var isDeleted = function(metadata, rev) { - if (!metadata || !metadata.deletions) { - return false; - } - if (!rev) { - rev = Pouch.merge.winningRev(metadata); - } - if (rev.indexOf('-') >= 0) { - rev = rev.split('-')[1]; - } - - return metadata.deletions[rev] === true; -}; - // Determine id an ID is valid // - invalid IDs begin with an underescore that does not begin '_design' or '_local' // - any other string value is a valid id @@ -96,6 +79,10 @@ var parseDoc = function(doc, newEdits) { var nRevNum; var newRevId; var revInfo; + var opts = {}; + if (doc._deleted) { + opts.deleted = true; + } if (newEdits) { if (!doc._id) { @@ -109,13 +96,13 @@ var parseDoc = function(doc, newEdits) { } doc._rev_tree = [{ pos: parseInt(revInfo[1], 10), - ids: [revInfo[2], [[newRevId, []]]] + ids: [revInfo[2], {}, [[newRevId, opts, []]]] }]; nRevNum = parseInt(revInfo[1], 10) + 1; } else { doc._rev_tree = [{ pos: 1, - ids : [newRevId, []] + ids : [newRevId, opts, []] }]; nRevNum = 1; } @@ -125,9 +112,9 @@ var parseDoc = function(doc, newEdits) { pos: doc._revisions.start - doc._revisions.ids.length + 1, ids: doc._revisions.ids.reduce(function(acc, x) { if (acc === null) { - return [x, []]; + return [x, opts, []]; } else { - return [x, [acc]]; + return [x, {}, [acc]]; } }, null) }]; @@ -140,7 +127,7 @@ var parseDoc = function(doc, newEdits) { newRevId = revInfo[2]; doc._rev_tree = [{ pos: parseInt(revInfo[1], 10), - ids: [revInfo[2], []] + ids: [revInfo[2], {}, []] }]; } } @@ -186,76 +173,13 @@ var compareRevs = function(a, b) { return (a.rev_tree[0].start < b.rev_tree[0].start ? -1 : 1); }; -// Pretty much all below can be combined into a higher order function to -// traverse revisions -// Callback has signature function(isLeaf, pos, id, [context]) -// The return value from the callback will be passed as context to all children of that node -var traverseRevTree = function(revs, callback) { - var toVisit = []; - - revs.forEach(function(tree) { - toVisit.push({pos: tree.pos, ids: tree.ids}); - }); - - while (toVisit.length > 0) { - var node = toVisit.pop(); - var pos = node.pos; - var tree = node.ids; - var newCtx = callback(tree[1].length === 0, pos, tree[0], node.ctx); - /*jshint loopfunc: true */ - tree[1].forEach(function(branch) { - toVisit.push({pos: pos+1, ids: branch, ctx: newCtx}); - }); - } -}; - -var collectRevs = function(path) { - var revs = []; - - traverseRevTree([path], function(isLeaf, pos, id) { - revs.push({rev: pos + "-" + id, status: 'available'}); - }); - - return revs; -}; - -var collectLeaves = function(revs) { - var leaves = []; - traverseRevTree(revs, function(isLeaf, pos, id) { - if (isLeaf) { - leaves.unshift({rev: pos + "-" + id, pos: pos}); - } - }); - leaves.sort(function(a, b) { - return b.pos - a.pos; - }); - leaves.map(function(leaf) { delete leaf.pos; }); - return leaves; -}; - -// returns all conflicts that is leaves such that -// 1. are not deleted and -// 2. are different than winning revision -var collectConflicts = function(metadata) { - var win = Pouch.merge.winningRev(metadata); - var leaves = collectLeaves(metadata.rev_tree); - var conflicts = []; - leaves.forEach(function(leaf) { - var rev = leaf.rev.split("-")[1]; - if ((!metadata.deletions || !metadata.deletions[rev]) && leaf.rev !== win) { - conflicts.push(leaf.rev); - } - }); - return conflicts; -}; - // for every node in a revision tree computes its distance from the closest // leaf var computeHeight = function(revs) { var height = {}; var edges = []; - traverseRevTree(revs, function(isLeaf, pos, id, prnt) { + Pouch.merge.traverseRevTree(revs, function(isLeaf, pos, id, prnt) { var rev = pos + "-" + id; if (isLeaf) { height[rev] = 0; @@ -302,9 +226,9 @@ var filterChange = function(opts) { // [[id, ...], ...] var rootToLeaf = function(tree) { var paths = []; - traverseRevTree(tree, function(isLeaf, pos, id, history) { + Pouch.merge.traverseRevTree(tree, function(isLeaf, pos, id, history, opts) { history = history ? history.slice(0) : []; - history.push(id); + history.push({id: id, opts: opts}); if (isLeaf) { var rootPos = pos + 1 - history.length; paths.unshift({pos: rootPos, ids: history}); @@ -314,6 +238,26 @@ var rootToLeaf = function(tree) { return paths; }; +// check if a specific revision of a doc has been deleted +// - metadata: the metadata object from the doc store +// - rev: (optional) the revision to check. defaults to winning revision +var isDeleted = function(metadata, rev) { + if (!rev) { + rev = Pouch.merge.winningRev(metadata); + } + if (rev.indexOf('-') >= 0) { + rev = rev.split('-')[1]; + } + var deleted = false; + Pouch.merge.traverseRevTree(metadata.rev_tree, function(isLeaf, pos, id, acc, opts) { + if (id === rev) { + deleted = !!opts.deleted; + } + }); + + return deleted; +}; + var isChromeApp = function(){ return (typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.local !== "undefined"); }; @@ -343,9 +287,6 @@ if (typeof module !== 'undefined' && module.exports) { parseDoc: parseDoc, isDeleted: isDeleted, compareRevs: compareRevs, - collectRevs: collectRevs, - collectLeaves: collectLeaves, - collectConflicts: collectConflicts, computeHeight: computeHeight, arrayFirst: arrayFirst, filterChange: filterChange, @@ -357,7 +298,6 @@ if (typeof module !== 'undefined' && module.exports) { }, extend: extend, ajax: ajax, - traverseRevTree: traverseRevTree, rootToLeaf: rootToLeaf, isChromeApp: isChromeApp }; @@ -409,7 +349,7 @@ var Changes = function() { api.notify = function(db_name) { if (!listeners[db_name]) { return; } - + Object.keys(listeners[db_name]).forEach(function (i) { var opts = listeners[db_name][i].opts; listeners[db_name][i].db.changes({ diff --git a/tests/test.compaction.js b/tests/test.compaction.js index 7aafabcca9..9f7ebd1d04 100644 --- a/tests/test.compaction.js +++ b/tests/test.compaction.js @@ -210,9 +210,9 @@ adapters.map(function(adapter) { db.post(doc, function(err, res) { var rev3 = res.rev; db.get("doc", {rev: rev1}, function(err, doc) { - ok(err.status === 404 && err.error === "not_found", + ok(err.status === 404 && err.error === "not_found", "compacted document is missing"); - db.get("doc", {rev: rev2}, function(err, doc) { + db.get("doc", {rev: rev2}, function(err, doc) { ok(!err, "leaf's parent does not get compacted"); db.get("doc", {rev: rev3}, function(err, doc) { ok(!err, "leaf revision does not get compacted"); diff --git a/tests/test.merge_rev_tree.js b/tests/test.merge_rev_tree.js index 078038edcb..acd1bc9b8f 100644 --- a/tests/test.merge_rev_tree.js +++ b/tests/test.merge_rev_tree.js @@ -16,35 +16,35 @@ if (typeof module !== undefined && module.exports) { qunit('merge rev tree'); -var simple = {pos: 1, ids: ['1', []]}; -var two0 = {pos: 1, ids: ['1', [['2_0', []]]]}; -var two1 = {pos: 1, ids: ['1', [['2_1', []]]]}; -var newleaf = {pos: 2, ids: ['2_0', [['3', []]]]}; -var withnewleaf = {pos: 1, ids: ['1', [['2_0', [['3', []]]]]]}; -var newbranch = {pos: 1, ids: ['1', [['2_0', []], ['2_1', []]]]}; -var newdeepbranch = {pos: 2, ids: ['2_0', [['3_1', []]]]}; - -var stemmededit = {pos: 3, ids: ['3', []]}; +var simple = {pos: 1, ids: ['1', {}, []]}; +var two0 = {pos: 1, ids: ['1', {}, [['2_0', {}, []]]]}; +var two1 = {pos: 1, ids: ['1', {}, [['2_1', {}, []]]]}; +var newleaf = {pos: 2, ids: ['2_0', {}, [['3', {}, []]]]}; +var withnewleaf = {pos: 1, ids: ['1', {}, [['2_0', {}, [['3', {}, []]]]]]}; +var newbranch = {pos: 1, ids: ['1', {}, [['2_0', {}, []], ['2_1', {}, []]]]}; +var newdeepbranch = {pos: 2, ids: ['2_0', {}, [['3_1', {}, []]]]}; + +var stemmededit = {pos: 3, ids: ['3', {}, []]}; var stemmedconflicts = [simple, stemmededit]; -var newbranchleaf = {pos: 1, ids: ['1', [['2_0', [['3', []]]], ['2_1', []]]]}; -var newbranchleafbranch = {pos: 1, ids: ['1', [['2_0', [['3', []], ['3_1', []]]], ['2_1', []]]]}; +var newbranchleaf = {pos: 1, ids: ['1', {}, [['2_0', {}, [['3', {}, []]]], ['2_1', {}, []]]]}; +var newbranchleafbranch = {pos: 1, ids: ['1', {}, [['2_0', {}, [['3', {}, []], ['3_1', {}, []]]], ['2_1', {}, []]]]}; -var stemmed2 = [{pos: 1, ids: ['1', [['2_1', []]]]}, - {pos: 2, ids: ['2_0', [['3', []], ['3_1', []]]]}]; +var stemmed2 = [{pos: 1, ids: ['1', {}, [['2_1', {}, []]]]}, + {pos: 2, ids: ['2_0', {}, [['3', {}, []], ['3_1', {}, []]]]}]; -var stemmed3 = [{pos: 2, ids: ['2_1', []]}, - {pos: 3, ids: ['3', []]}, - {pos: 3, ids: ['3_1', []]}]; -var partialrecover = [{pos: 1, ids: ['1', [['2_0', [['3', []]]]]]}, - {pos: 2, ids: ['2_1', []]}, - {pos: 3, ids: ['3_1', []]}]; +var stemmed3 = [{pos: 2, ids: ['2_1', {}, []]}, + {pos: 3, ids: ['3', {}, []]}, + {pos: 3, ids: ['3_1', {}, []]}]; +var partialrecover = [{pos: 1, ids: ['1', {}, [['2_0', {}, [['3', {}, []]]]]]}, + {pos: 2, ids: ['2_1', {}, []]}, + {pos: 3, ids: ['3_1', {}, []]}]; test('Merging a path into an empty tree is the path', function() { - deepEqual(Pouch.merge([], simple, 10), { + deepEqual(JSON.stringify(Pouch.merge([], simple, 10)), JSON.stringify({ tree: [simple], conflicts: 'new_leaf' - }, ''); + }, '')); }); test('Remerge path into path is reflexive', function() { @@ -148,10 +148,10 @@ test('Merging unstemmed recovers as much as possible without losing info', funct test('winningRev returns the longest leaf', function() { var tree = [ {"pos":1,"ids":[ - "bfe70372c90ded1087239e5191984f76",[ - ["44d71a718b90e4696c06a90e08912c8f",[]], - ["56e657612d55ab1a402dcb281c874f2a",[ - ["93c3db16462f656f7172ccabd3cf6cd6",[]] + "bfe70372c90ded1087239e5191984f76", {}, [ + ["44d71a718b90e4696c06a90e08912c8f", {}, []], + ["56e657612d55ab1a402dcb281c874f2a", {}, [ + ["93c3db16462f656f7172ccabd3cf6cd6", {}, []] ]] ]] }]; @@ -164,17 +164,17 @@ test('winningRev returns the longest leaf again', function() { // this one is from issue #293 var tree = [ {"pos": 1,"ids": [ - "203db1a1810a838895d561f67b224b5d", [ - ["bf5e08a4f9fa6d33a53f4a00ae3ea399", [ - ["28cd77a3ca30f79e1cfffcd6a41ca308", []] + "203db1a1810a838895d561f67b224b5d", {}, [ + ["bf5e08a4f9fa6d33a53f4a00ae3ea399", {}, [ + ["28cd77a3ca30f79e1cfffcd6a41ca308", {}, []] ]] ] ]}, {"pos": 1,"ids": [ - "c6d5cce35bcfbef90b20f140d723cbdb", [ - ["1b8dfbb1267e213328920bae43f2f597", []], - ["59ed830b84b276ab776c3c51aaf93a16", [ - ["64a9842c6aea50bf24660378e496e853", []] + "c6d5cce35bcfbef90b20f140d723cbdb", {}, [ + ["1b8dfbb1267e213328920bae43f2f597", {}, []], + ["59ed830b84b276ab776c3c51aaf93a16", {}, [ + ["64a9842c6aea50bf24660378e496e853", {}, []] ]] ]] } @@ -186,7 +186,7 @@ test('winningRev returns the longest leaf again', function() { ///// These are tests from CouchDB's kt-merging.erl test suite -var one = {pos: 1, ids: ['1', []]}; +var one = {pos: 1, ids: ['1', {}, []]}; test('The empty tree is the identity for merge.', function() { deepEqual(Pouch.merge([], one, 10), { tree: [one], @@ -201,7 +201,7 @@ test('Merging is reflexive', function() { }); }); -var two = {pos: 1, ids: ['2', []]}; +var two = {pos: 1, ids: ['2', {}, []]}; var twoSibs = [one, two]; test('Merging a prefix of a tree with the tree yields the tree.', function() { deepEqual(Pouch.merge(twoSibs, one, 10), { @@ -210,7 +210,7 @@ test('Merging a prefix of a tree with the tree yields the tree.', function() { }); }); -var three = {pos: 1, ids: ['3', []]}; +var three = {pos: 1, ids: ['3', {}, []]}; var threeSibs = [one, two, three]; test('Merging a third unrelated branch leads to a conflict.', function() { deepEqual(Pouch.merge(twoSibs, three, 10), { @@ -219,9 +219,9 @@ test('Merging a third unrelated branch leads to a conflict.', function() { }); }); -var twoChild = {pos: 1, ids: ['1', [ - ['1a', [ - ['1aa', []] +var twoChild = {pos: 1, ids: ['1', {}, [ + ['1a', {}, [ + ['1aa', {}, []] ]] ]]}; test('Merging two children is still reflexive.', function() { @@ -231,9 +231,9 @@ test('Merging two children is still reflexive.', function() { }); }); -var twoChildSibs = {pos: 1, ids: ['1', [ - ['1a', []], - ['1b', []] +var twoChildSibs = {pos: 1, ids: ['1', {}, [ + ['1a', {}, []], + ['1b', {}, []] ]]}; test('Merging a tree to itself is itself.', function() { deepEqual(Pouch.merge([twoChildSibs], twoChildSibs, 10), { @@ -242,11 +242,11 @@ test('Merging a tree to itself is itself.', function() { }); }); -var twoChildPlusSibs = {pos: 1, ids: ['1', [ - ['1a', [ - ['1aa', []] +var twoChildPlusSibs = {pos: 1, ids: ['1', {}, [ + ['1a', {}, [ + ['1aa', {}, []] ]], - ['1b', []] + ['1b', {}, []] ]]}; test('Merging tree of uneven length at node 2.', function() { deepEqual(Pouch.merge([twoChild], twoChildSibs, 10), { @@ -255,7 +255,7 @@ test('Merging tree of uneven length at node 2.', function() { }); }); -var stemmed1b = {pos: 2, ids: ['1a', []]}; +var stemmed1b = {pos: 2, ids: ['1a', {}, []]}; test('Merging a tree with a stem.', function() { deepEqual(Pouch.merge([twoChildSibs], stemmed1b, 10), { tree: [twoChildSibs], @@ -263,13 +263,13 @@ test('Merging a tree with a stem.', function() { }); }); -var twoChildPlusSibs2 = {pos: 1, ids: ['1', [ - ['1a', []], - ['1b', [ - ['1bb', []] +var twoChildPlusSibs2 = {pos: 1, ids: ['1', {}, [ + ['1a', {}, []], + ['1b', {}, [ + ['1bb', {}, []] ]] ]]}; -var stemmed1bb = {pos:3, ids: ['1bb', []]}; +var stemmed1bb = {pos:3, ids: ['1bb', {}, []]}; test('Merging a stem at a deeper level.', function() { deepEqual(Pouch.merge([twoChildPlusSibs2], stemmed1bb, 10), { tree: [twoChildPlusSibs2], @@ -278,8 +278,8 @@ test('Merging a stem at a deeper level.', function() { }); var stemmedTwoChildSibs2 = [ - {pos: 2, ids: ['1a', []]}, - {pos: 2, ids: ['1b', [['1bb', []]]]} + {pos: 2, ids: ['1a', {}, []]}, + {pos: 2, ids: ['1b', {}, [['1bb', {}, []]]]} ]; test('Merging a stem at a deeper level against paths at deeper levels.', function() { deepEqual(Pouch.merge(stemmedTwoChildSibs2, stemmed1bb, 10), { @@ -288,7 +288,7 @@ test('Merging a stem at a deeper level against paths at deeper levels.', functio }); }); -var stemmed1aa = {pos: 3, ids: ['1aa', []]}; +var stemmed1aa = {pos: 3, ids: ['1aa', {}, []]}; test("Merging a single tree with a deeper stem.", function() { deepEqual(Pouch.merge([twoChild], stemmed1aa, 10), { tree: [twoChild], @@ -296,7 +296,7 @@ test("Merging a single tree with a deeper stem.", function() { }); }); -var stemmed1a = {pos: 2, ids: ['1a', [['1aa', []]]]}; +var stemmed1a = {pos: 2, ids: ['1a', {}, [['1aa', {}, []]]]}; test('Merging a larger stem.', function() { deepEqual(Pouch.merge([twoChild], stemmed1a, 10), { tree: [twoChild], @@ -311,7 +311,7 @@ test('More merging.', function() { }); }); -var oneChild = {pos: 1, ids: ['1', [['1a', []]]]}; +var oneChild = {pos: 1, ids: ['1', {}, [['1a', {}, []]]]}; test('Merging should create conflicts.', function() { deepEqual(Pouch.merge([oneChild], stemmed1aa, 10), { tree: [oneChild, stemmed1aa], @@ -326,19 +326,19 @@ test('Merging should have no conflicts.', function() { }); }); -var foo = {pos: 1, ids: ['foo', [ - ['foo2', []], - ['foo3', []] +var foo = {pos: 1, ids: ['foo', {}, [ + ['foo2', {}, []], + ['foo3', {}, []] ]]}; -var bar = {pos: 1, ids: ['foo', [ - ['foo3', [ - ['foo4', []] +var bar = {pos: 1, ids: ['foo', {}, [ + ['foo3', {}, [ + ['foo4', {}, []] ]] ]]}; -var fooBar = {pos: 1, ids: ['foo', [ - ['foo2', []], - ['foo3', [ - ['foo4', []] +var fooBar = {pos: 1, ids: ['foo', {}, [ + ['foo2', {}, []], + ['foo3', {}, [ + ['foo4', {}, []] ]] ]]}; test('Merging trees with conflicts ought to behave.', function() { @@ -352,7 +352,3 @@ test('Merging trees with conflicts ought to behave.', function() { - - - - diff --git a/tests/test.utils.js b/tests/test.utils.js index 031a150a29..22500e6922 100644 --- a/tests/test.utils.js +++ b/tests/test.utils.js @@ -115,7 +115,7 @@ function generateAdapterUrl(id) { } // Put doc after prevRev (so that doc is a child of prevDoc -// in rev_tree). Doc must have _rev. If prevRev is not specified +// in rev_tree). Doc must have _rev. If prevRev is not specified // just insert doc with correct _rev (new_edits=false!) function putAfter(db, doc, prevRev, callback){ var newDoc = extend({}, doc); @@ -148,7 +148,7 @@ var putBranch = function(db, docs, callback) { } db.get(doc._id, {rev: doc._rev}, function(err, ok){ if(err){ - putAfter(db, docs[i], prev, function() { + putAfter(db, docs[i], prev, function(err, doc) { next(); }); }else{ From 60f3cfafc463490bdc8bf733cdd0ca5761aec8ac Mon Sep 17 00:00:00 2001 From: Stephen Bartell Date: Thu, 28 Mar 2013 17:48:56 -0700 Subject: [PATCH 18/22] (#585) - Store checkpoint on target. --- src/pouch.replicate.js | 14 +++++++------- tests/test.replication.js | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/pouch.replicate.js b/src/pouch.replicate.js index fff83bb6aa..deaaabc5bd 100644 --- a/src/pouch.replicate.js +++ b/src/pouch.replicate.js @@ -61,8 +61,8 @@ var genReplicationId = function(src, target, opts) { }; // A checkpoint lets us restart replications from when they were last cancelled -var fetchCheckpoint = function(src, id, callback) { - src.get(id, function(err, doc) { +var fetchCheckpoint = function(target, id, callback) { + target.get(id, function(err, doc) { if (err && err.status === 404) { callback(null, 0); } else { @@ -71,16 +71,16 @@ var fetchCheckpoint = function(src, id, callback) { }); }; -var writeCheckpoint = function(src, id, checkpoint, callback) { +var writeCheckpoint = function(target, id, checkpoint, callback) { var check = { _id: id, last_seq: checkpoint }; - src.get(check._id, function(err, doc) { + target.get(check._id, function(err, doc) { if (doc && doc._rev) { check._rev = doc._rev; } - src.put(check, function(err, doc) { + target.put(check, function(err, doc) { callback(); }); }); @@ -183,13 +183,13 @@ function replicate(src, target, opts, promise) { function isCompleted() { if (completed && pending === 0) { result.end_time = Date.now(); - writeCheckpoint(src, repId, last_seq, function(err, res) { + writeCheckpoint(target, repId, last_seq, function(err, res) { call(opts.complete, err, result); }); } } - fetchCheckpoint(src, repId, function(err, checkpoint) { + fetchCheckpoint(target, repId, function(err, checkpoint) { if (err) { return call(opts.complete, err); diff --git a/tests/test.replication.js b/tests/test.replication.js index 3503bbf938..60afffbce9 100644 --- a/tests/test.replication.js +++ b/tests/test.replication.js @@ -684,6 +684,26 @@ deletedDocAdapters.map(function(adapters) { }); }); }); + + asyncTest("issue #585 Store checkpoint on target db.", function() { + console.info('Starting Test: Local DB contains documents'); + var docs = [{_id: "a"}, {_id: "b"}]; + var self = this; + initDBPair(this.name, this.remote, function(db, remote) { + db.bulkDocs({docs: docs}, {}, function(err, _) { + db.replicate.to(self.remote, function(err, result) { + ok(result.docs_written === docs.length, 'docs replicated ok'); + Pouch.destroy(self.remote, function (err, result) { + ok(result.ok === true, 'remote was deleted'); + db.replicate.to(self.remote, function (err, result) { + ok(result.docs_written === docs.length, 'docs were written again because target was deleted.'); + start(); + }); + }); + }); + }); + }); + }); }); // This test only needs to run for one configuration, and it slows stuff From 41b7c1b1a2c31ab3cba1979d36c29a5d64e48215 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Fri, 29 Mar 2013 20:29:10 +0000 Subject: [PATCH 19/22] (#642) - Ensure only winningRev is included in leveldb changes --- src/adapters/pouch.idb.js | 7 +++++-- src/adapters/pouch.leveldb.js | 11 +++++------ tests/test.changes.js | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index ea5f4836ec..849de336c0 100644 --- a/src/adapters/pouch.idb.js +++ b/src/adapters/pouch.idb.js @@ -744,9 +744,11 @@ var IdbPouch = function(opts, callback) { var cursor = event.target.result; // Try to pre-emptively dedup to save us a bunch of idb calls - var changeId = cursor.value._id, changeIdIndex = resultIndices[changeId]; + var changeId = cursor.value._id; + var changeIdIndex = resultIndices[changeId]; if (changeIdIndex !== undefined) { - results[changeIdIndex].seq = cursor.key; // update so it has the later sequence number + results[changeIdIndex].seq = cursor.key; + // update so it has the later sequence number results.push(results[changeIdIndex]); results[changeIdIndex] = null; resultIndices[changeId] = results.length - 1; @@ -776,6 +778,7 @@ var IdbPouch = function(opts, callback) { changes: changeList, doc: doc }; + if (isDeleted(metadata, mainRev)) { change.deleted = true; } diff --git a/src/adapters/pouch.leveldb.js b/src/adapters/pouch.leveldb.js index 9fa99b2750..c54eb99411 100644 --- a/src/adapters/pouch.leveldb.js +++ b/src/adapters/pouch.leveldb.js @@ -628,18 +628,17 @@ var LevelPouch = function(opts, callback) { change.doc._rev = Pouch.merge.winningRev(metadata); - if (isDeleted(metadata)) { + if (isDeleted(metadata)) { change.deleted = true; } if (opts.conflicts) { change.doc._conflicts = Pouch.merge.collectConflicts(metadata); } - // dedupe changes (TODO: more efficient way to accomplish this?) - results = results.filter(function(doc) { - return doc.id !== change.id; - }); - results.push(change); + // Ensure duplicated dont overwrite winning rev + if (+data.key === metadata.rev_map[change.doc._rev]) { + results.push(change); + } }); }) .on('error', function(err) { diff --git a/tests/test.changes.js b/tests/test.changes.js index 4308bc1c03..7245e17312 100644 --- a/tests/test.changes.js +++ b/tests/test.changes.js @@ -320,7 +320,6 @@ adapters.map(function(adapter) { var localname = this.name, remotename = this.name + "-remote"; initDBPair(localname, remotename, function(localdb, remotedb) { - localdb.bulkDocs({docs: docs1}, function(err, info) { docs2[0]._rev = info[2].rev; var rev1 = docs2[1]._rev = info[3].rev; @@ -328,7 +327,8 @@ adapters.map(function(adapter) { localdb.put(docs2[1], function(err, info) { var rev2 = info.rev; Pouch.replicate(localdb, remotedb, function(err, done) { - // update remote once, local twice, then replicate from remote to local so the remote losing conflict is later in the tree + // update remote once, local twice, then replicate from + // remote to local so the remote losing conflict is later in the tree localdb.put({_id: "3", _rev: rev2, integer: 20}, function(err, resp) { var rev3local = resp.rev; localdb.put({_id: "3", _rev: rev3local, integer: 30}, function(err, resp) { From 58e2a343c1e39d9bf80cbcecd6f5817b9a158211 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Fri, 29 Mar 2013 20:54:15 +0000 Subject: [PATCH 20/22] (#612) - Follow up to fix test target --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 70e46ac6d0..694a602ee7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -284,7 +284,7 @@ module.exports = function(grunt) { grunt.registerTask("spatial", ["concat:spatial", "uglify:spatial"]); grunt.registerTask("gql", ["concat:gql", "uglify:gql"]); - grunt.registerTask("test", ["jshint", "cors-server"]); + grunt.registerTask("test", ["jshint", "node-qunit"]); grunt.registerTask("test-travis", ["jshint", "build", "connect", "cors-server", "node-qunit", "saucelabs-qunit", "publish-results"]); From 6b9fe0454f514eb334e47a75538369920ee3b944 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Fri, 29 Mar 2013 22:44:52 +0000 Subject: [PATCH 21/22] (#629) - Add boilerplate for idb upgrade migrations --- src/adapters/pouch.idb.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index 849de336c0..8d78edef8e 100644 --- a/src/adapters/pouch.idb.js +++ b/src/adapters/pouch.idb.js @@ -77,10 +77,18 @@ var IdbPouch = function(opts, callback) { console.log(name + ': Open Database'); } - // TODO: before we release, make sure we write upgrade needed - // in a way that supports a future upgrade path req.onupgradeneeded = function(e) { var db = e.target.result; + var currentVersion = e.oldVersion; + while (currentVersion !== e.newVersion) { + if (currentVersion === 0) { + createSchema(db); + } + currentVersion++; + } + }; + + function createSchema(db) { db.createObjectStore(DOC_STORE, {keyPath : 'id'}) .createIndex('seq', 'seq', {unique: true}); db.createObjectStore(BY_SEQ_STORE, {autoIncrement : true}) @@ -88,7 +96,7 @@ var IdbPouch = function(opts, callback) { db.createObjectStore(ATTACH_STORE, {keyPath: 'digest'}); db.createObjectStore(META_STORE, {keyPath: 'id', autoIncrement: false}); db.createObjectStore(DETECT_BLOB_SUPPORT_STORE); - }; + } req.onsuccess = function(e) { From 22b5ac14a9b7ef64dfc91c97ff44a04504710e10 Mon Sep 17 00:00:00 2001 From: chunmun Date: Fri, 29 Mar 2013 23:16:43 +0800 Subject: [PATCH 22/22] (#451) - Conflict in views by default --- src/plugins/pouchdb.mapreduce.js | 6 ++++- tests/test.views.js | 39 +++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/plugins/pouchdb.mapreduce.js b/src/plugins/pouchdb.mapreduce.js index 8164c6a3cd..9a49c83438 100644 --- a/src/plugins/pouchdb.mapreduce.js +++ b/src/plugins/pouchdb.mapreduce.js @@ -25,6 +25,9 @@ var MapReduce = function(db) { options.reduce = false; } + // Including conflicts + options.conflicts = true; + function sum(values) { return values.reduce(function(a, b) { return a + b; }, 0); } @@ -39,11 +42,12 @@ var MapReduce = function(db) { id: current.doc._id, key: key, value: val - }; + }; if (options.startkey && Pouch.collate(key, options.startkey) < 0) return; if (options.endkey && Pouch.collate(key, options.endkey) > 0) return; if (options.key && Pouch.collate(key, options.key) !== 0) return; + num_started++; if (options.include_docs) { //in this special case, join on _id (issue #106) diff --git a/tests/test.views.js b/tests/test.views.js index 4fa42de7dd..73c7f11eb2 100644 --- a/tests/test.views.js +++ b/tests/test.views.js @@ -1,5 +1,5 @@ /*globals initTestDB: false, emit: true, generateAdapterUrl: false */ -/*globals PERSIST_DATABASES: false */ +/*globals PERSIST_DATABASES: false, initDBPair: false, utils: true */ "use strict"; @@ -25,10 +25,12 @@ adapters.map(function(adapter) { qunit('views: ' + adapter, { setup : function () { this.name = generateAdapterUrl(adapter); + this.remote = generateAdapterUrl('local-2'); }, teardown: function() { if (!PERSIST_DATABASES) { Pouch.destroy(this.name); + Pouch.destroy(this.remote); } } }); @@ -242,4 +244,39 @@ adapters.map(function(adapter) { }); }); + + asyncTest('Views should include _conflict by default', function() { + var self = this; + var doc1 = {_id: '1', foo: 'bar'}; + var doc2 = {_id: '1', foo: 'baz'}; + var queryFun = function(doc) { emit(doc._conflicts); }; + initDBPair(this.name, this.remote, function(db, remote) { + db.post(doc1, function(err, res) { + remote.post(doc2, function(err, res) { + db.replicate.from(remote, function(err, res) { + db.get(doc1._id, {conflicts: true}, function(err, res) { + ok(res._conflicts,'Conflict exists in db'); + + // Default behaviour + db.query(queryFun, function(err, res) { + equal(res.rows[0].key.length, 1, 'Conflicts included'); + + // conflicts: true + db.query(queryFun, {conflicts: true}, function(err, res) { + equal(res.rows[0].key.length, 1, 'Conflicts included'); + + // conflicts: false + db.query(queryFun, {conflicts: false}, function(err, res) { + equal(res.rows[0].key.length, 1,'Conflicts excluded'); + start(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + });