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 4d811cdcd5..694a602ee7 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', @@ -215,7 +215,7 @@ module.exports = function(grunt) { } }, 'publish-results': { - server: 'http://pouchdb.iriscouch.com', + server: 'http://couchdb.pouchdb.com', db: 'test_results' } }); @@ -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", "node-qunit"]); + grunt.registerTask("test-travis", ["jshint", "build", "connect", "cors-server", + "node-qunit", "saucelabs-qunit", + "publish-results"]); + grunt.registerTask('default', 'build'); }; 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]) 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" } } diff --git a/src/adapters/pouch.http.js b/src/adapters/pouch.http.js index 291229faf4..26e2cc6726 100644 --- a/src/adapters/pouch.http.js +++ b/src/adapters/pouch.http.js @@ -209,16 +209,34 @@ 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); + }, function() { + function ping() { + api.info(function(err, res) { + if (!res.compact_running) { + call(callback, null); + } else { + setTimeout(ping, 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 diff --git a/src/adapters/pouch.idb.js b/src/adapters/pouch.idb.js index 57c776b45f..bfa145d4be 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, filterChange: false */ +/*globals isLocalId: false, isDeleted: false, Changes: false, filterChange: false */ 'use strict'; @@ -78,18 +77,26 @@ 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}) - .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); - }; + } req.onsuccess = function(e) { @@ -160,20 +167,13 @@ 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) { 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; }); @@ -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) { @@ -370,12 +371,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)); @@ -404,28 +403,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 +427,6 @@ var IdbPouch = function(opts, callback) { processDocs(); }); - }; function sortByBulkSeq(a, b) { @@ -467,16 +455,19 @@ 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; } 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; @@ -622,8 +613,12 @@ 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); + doc.doc._conflicts = Pouch.merge.collectConflicts(metadata) + .map(function(x) { return x.id; }); } } if ('keys' in opts) { @@ -645,9 +640,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); }; } @@ -687,8 +683,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; } @@ -758,9 +752,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; @@ -775,13 +771,14 @@ 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') { - // 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,11 +786,13 @@ var IdbPouch = function(opts, callback) { changes: changeList, doc: doc }; + if (isDeleted(metadata, mainRev)) { 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 @@ -846,9 +845,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.leveldb.js b/src/adapters/pouch.leveldb.js index 05fd276395..c54eb99411 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,20 +256,13 @@ 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; 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,24 +621,24 @@ 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 }; change.doc._rev = Pouch.merge.winningRev(metadata); - if (isDeleted(metadata)) { + if (isDeleted(metadata)) { 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?) - 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) { @@ -848,4 +839,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 15746a3e87..4147f37ac7 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, filterChange: false */ +/*globals isLocalId: false, isDeleted: false, Changes: false, filterChange: false */ 'use strict'; @@ -55,7 +54,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); @@ -108,18 +107,12 @@ 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) { 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; }); @@ -241,8 +234,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 +291,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); @@ -310,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'); @@ -410,13 +405,14 @@ 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; } 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; @@ -450,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 = {}; @@ -491,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) { @@ -536,8 +539,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; } @@ -583,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); @@ -591,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); } @@ -676,8 +677,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/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/src/pouch.adapter.js b/src/pouch.adapter.js index 3a575df43b..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 */ +/*globals Pouch: true, yankError: false, extend: false, call: false, parseDocId: false, traverseRevTree: false */ +/*globals arrayFirst: false, rootToLeaf: false, computeHeight: false */ "use strict"; @@ -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); @@ -244,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(); @@ -256,16 +288,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 @@ -286,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); @@ -336,7 +370,7 @@ var PouchAdapter = function(opts, callback) { } else { finish(); } - + }); }; @@ -363,15 +397,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 +418,7 @@ var PouchAdapter = function(opts, callback) { api.taskqueue.addTask('changes', arguments); return; } + opts = extend(true, {}, opts); return customApi._changes(opts); }; @@ -422,6 +457,8 @@ var PouchAdapter = function(opts, callback) { } if (!opts) { opts = {}; + } else { + opts = extend(true, {}, opts); } if (!req || !req.docs || req.docs.length < 1) { @@ -432,11 +469,12 @@ 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; } - return customApi._bulkDocs(req, opts, callback); + return customApi._bulkDocs(req, opts, autoCompact(callback)); }; /* End Wrappers */ 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(); } 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.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/src/pouch.utils.js b/src/pouch.utils.js index 104b4ad8b9..a87f2f00d7 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,67 +173,32 @@ 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) { +// for every node in a revision tree computes its distance from the closest +// leaf +var computeHeight = function(revs) { + var height = {}; + var edges = []; + Pouch.merge.traverseRevTree(revs, function(isLeaf, pos, id, prnt) { + var rev = pos + "-" + id; if (isLeaf) { - leaves.unshift({rev: pos + "-" + id, pos: pos}); + height[rev] = 0; } + if (prnt !== undefined) { + edges.push({from: prnt, to: rev}); + } + return rev; }); - 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); + 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 conflicts; + return height; }; // returns first element of arr satisfying callback predicate @@ -277,9 +229,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}); @@ -289,6 +241,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"); }; @@ -318,9 +290,7 @@ if (typeof module !== 'undefined' && module.exports) { parseDoc: parseDoc, isDeleted: isDeleted, compareRevs: compareRevs, - collectRevs: collectRevs, - collectLeaves: collectLeaves, - collectConflicts: collectConflicts, + computeHeight: computeHeight, arrayFirst: arrayFirst, filterChange: filterChange, atob: function(str) { @@ -331,7 +301,6 @@ if (typeof module !== 'undefined' && module.exports) { }, extend: extend, ajax: ajax, - traverseRevTree: traverseRevTree, rootToLeaf: rootToLeaf, isChromeApp: isChromeApp }; 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; } }); diff --git a/tests/test.changes.js b/tests/test.changes.js index c5cc08d403..4d64d40727 100644 --- a/tests/test.changes.js +++ b/tests/test.changes.js @@ -365,7 +365,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; @@ -373,7 +372,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) { diff --git a/tests/test.compaction.js b/tests/test.compaction.js index e13a94d774..9f7ebd1d04 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.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) { diff --git a/tests/test.issue221.js b/tests/test.issue221.js index 843329bbf6..715ddbce0b 100644 --- a/tests/test.issue221.js +++ b/tests/test.issue221.js @@ -1,11 +1,14 @@ -/*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 */ "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]); @@ -40,50 +43,28 @@ 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. - 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(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(); - }); - }); - } + // 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) { + // 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(); }); - }; - interval = setInterval(checkDoc, 100); + }); }); }); }); @@ -93,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. - remote.put(doc, {}, function(err, results) { + // 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) { - ok(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(); }); }); }); 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.replication.js b/tests/test.replication.js index dfa634c1a8..60afffbce9 100644 --- a/tests/test.replication.js +++ b/tests/test.replication.js @@ -667,6 +667,43 @@ 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(); + }); + }); + }); + }); + }); + + 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 diff --git a/tests/test.utils.js b/tests/test.utils.js index 369b7720bd..22500e6922 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); }); } @@ -111,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); @@ -144,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{ 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(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + });