From 9feeb26c4fe176f8b3a2b70b930aa9eacbe37310 Mon Sep 17 00:00:00 2001 From: Louis Chatriot Date: Fri, 31 May 2013 18:39:12 +0200 Subject: [PATCH] Can batch-update an index, no change if an error was thrown --- lib/indexes.js | 47 ++++++++++++++++++++-- test/db.test.js | 2 +- test/indexes.test.js | 92 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/lib/indexes.js b/lib/indexes.js index d2507c4..ca5ed29 100644 --- a/lib/indexes.js +++ b/lib/indexes.js @@ -71,8 +71,8 @@ Index.prototype.insert = function (doc) { /** - * When inserting an array of documents, we need to rollback all insertions - * if an error is thrown + * Insert an array of documents in the index + * If a constraint is violated, an error should be thrown and the changes rolled back */ Index.prototype.insertMultipleDocs = function (docs) { var i, error, failingI; @@ -100,6 +100,7 @@ Index.prototype.insertMultipleDocs = function (docs) { /** * Remove a document from the index * If an array is passed, we remove all its elements + * The remove operation is safe with regards to the 'unique' constraint * O(log(n)) */ Index.prototype.remove = function (doc) { @@ -120,14 +121,54 @@ Index.prototype.remove = function (doc) { /** * Update a document in the index - * O(log(n)) + * Naive implementation, still in O(log(n)) */ Index.prototype.update = function (oldDoc, newDoc) { + if (util.isArray(oldDoc)) { this.updateMultipleDocs(oldDoc); } + this.remove(oldDoc); this.insert(newDoc); }; +/** + * Update multiple documents in the index + * If a constraint is violated, the changes need to be rolled back + * and an error thrown + * @param {Array of oldDoc, newDoc pairs} pairs + */ +Index.prototype.updateMultipleDocs = function (pairs) { + var i, failingI, error; + + for (i = 0; i < pairs.length; i += 1) { + this.remove(pairs[i].oldDoc); + } + + for (i = 0; i < pairs.length; i += 1) { + try { + this.insert(pairs[i].newDoc); + } catch (e) { + error = e; + failingI = i; + break; + } + } + + // If an error was raised, roll back changes in the inverse order + if (error) { + for (i = 0; i < failingI; i += 1) { + this.remove(pairs[i].newDoc); + } + + for (i = 0; i < pairs.length; i += 1) { + this.insert(pairs[i].oldDoc); + } + + throw error; + } +}; + + /** * Get all documents in index that match the query on fieldName * For now only works with field equality (i.e. can't use the index for $lt query for example) diff --git a/test/db.test.js b/test/db.test.js index 4c0c92f..ffaef22 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -1491,7 +1491,7 @@ describe('Database', function () { }); // ==== End of 'Indexing newly inserted documents' ==== // - describe.only('Updating indexes upon document update', function () { + describe.skip('Updating indexes upon document update', function () { it('Updating docs still works as before with an index', function (done) { d.ensureIndex({ fieldName: 'a' }); diff --git a/test/indexes.test.js b/test/indexes.test.js index 11705fb..baa99a1 100644 --- a/test/indexes.test.js +++ b/test/indexes.test.js @@ -239,6 +239,98 @@ describe('Indexes', function () { assert.deepEqual(idx.tree.search('changed'), [doc5]); }); + it('Can update an array of documents', function () { + var idx = new Index({ fieldName: 'tf' }) + , doc1 = { a: 5, tf: 'hello' } + , doc2 = { a: 8, tf: 'world' } + , doc3 = { a: 2, tf: 'bloup' } + , doc1b = { a: 23, tf: 'world' } + , doc2b = { a: 1, tf: 'changed' } + , doc3b = { a: 44, tf: 'bloup' } + ; + + idx.insert(doc1); + idx.insert(doc2); + idx.insert(doc3); + idx.tree.getNumberOfKeys().should.equal(3); + + idx.update([{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]); + + idx.tree.getNumberOfKeys().should.equal(3); + idx.getMatching('world').length.should.equal(1); + idx.getMatching('world')[0].should.equal(doc1b); + idx.getMatching('changed').length.should.equal(1); + idx.getMatching('changed')[0].should.equal(doc2b); + idx.getMatching('bloup').length.should.equal(1); + idx.getMatching('bloup')[0].should.equal(doc3b); + }); + + it('If a unique constraint is violated during an array-update, all changes are rolled back and an error thrown', function () { + var idx = new Index({ fieldName: 'tf', unique: true }) + , doc0 = { a: 432, tf: 'notthistoo' } + , doc1 = { a: 5, tf: 'hello' } + , doc2 = { a: 8, tf: 'world' } + , doc3 = { a: 2, tf: 'bloup' } + , doc1b = { a: 23, tf: 'changed' } + , doc2b = { a: 1, tf: 'changed' } // Will violate the constraint (first try) + , doc2c = { a: 1, tf: 'notthistoo' } // Will violate the constraint (second try) + , doc3b = { a: 44, tf: 'alsochanged' } + ; + + idx.insert(doc1); + idx.insert(doc2); + idx.insert(doc3); + idx.tree.getNumberOfKeys().should.equal(3); + + try { + idx.update([{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]); + } catch (e) { + e.errorType.should.equal('uniqueViolated'); + } + + idx.tree.getNumberOfKeys().should.equal(3); + idx.getMatching('hello').length.should.equal(1); + idx.getMatching('hello')[0].should.equal(doc1); + idx.getMatching('world').length.should.equal(1); + idx.getMatching('world')[0].should.equal(doc2); + idx.getMatching('bloup').length.should.equal(1); + idx.getMatching('bloup')[0].should.equal(doc3); + + try { + idx.update([{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]); + } catch (e) { + e.errorType.should.equal('uniqueViolated'); + } + + idx.tree.getNumberOfKeys().should.equal(3); + idx.getMatching('hello').length.should.equal(1); + idx.getMatching('hello')[0].should.equal(doc1); + idx.getMatching('world').length.should.equal(1); + idx.getMatching('world')[0].should.equal(doc2); + idx.getMatching('bloup').length.should.equal(1); + idx.getMatching('bloup')[0].should.equal(doc3); + }); + + it('If an update doesnt change a document, the unique constraint is not violated', function () { + var idx = new Index({ fieldName: 'tf', unique: true }) + , doc1 = { a: 5, tf: 'hello' } + , doc2 = { a: 8, tf: 'world' } + , doc3 = { a: 2, tf: 'bloup' } + , noChange = { a: 8, tf: 'world' } + ; + + idx.insert(doc1); + idx.insert(doc2); + idx.insert(doc3); + idx.tree.getNumberOfKeys().should.equal(3); + assert.deepEqual(idx.tree.search('world'), [doc2]); + + idx.update(doc2, noChange); // No error thrown + idx.tree.getNumberOfKeys().should.equal(3); + assert.deepEqual(idx.tree.search('world'), [noChange]); + }); + + }); // ==== End of 'Update' ==== //