From 41247305db798cab408f85b57e88a1efd254fefa Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Tue, 24 Sep 2013 17:59:59 -0700 Subject: [PATCH] version 0 of optimistic upsert simulation --- packages/mongo-livedata/collection.js | 14 ++++ packages/mongo-livedata/mongo_driver.js | 80 ++++++++++++++++++- .../mongo-livedata/mongo_livedata_tests.js | 36 ++++++++- 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 01f7401cfb..ffb33545d3 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -374,6 +374,20 @@ _.each(["insert", "update", "remove"], function (name) { } } else { args[0] = Meteor.Collection._rewriteSelector(args[0]); + + if (name === "update") { + var options = args[2]; + if (options && options.upsert) { + // set `insertedId` if absent. `insertedId` is a Meteor extension. + if (options.insertedId) { + if (!(typeof options.insertedId === 'string' + || options.insertedId instanceof Meteor.Collection.ObjectID)) + throw new Error("insertedId must be string or ObjectID"); + } else { + options.insertedId = self._makeNewID(); + } + } + } } var wrappedCallback; diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index bfaf7e4d2c..0e2a5ce9d6 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -335,15 +335,89 @@ MongoConnection.prototype._update = function (collection_name, selector, mod, // explictly enumerate options that minimongo supports if (options.upsert) mongoOpts.upsert = true; if (options.multi) mongoOpts.multi = true; - collection.update(replaceTypes(selector, replaceMeteorAtomWithMongo), - replaceTypes(mod, replaceMeteorAtomWithMongo), - mongoOpts, numberAffectedCallback(callback)); + + var mongoSelector = replaceTypes(selector, replaceMeteorAtomWithMongo); + var mongoMod = replaceTypes(mod, replaceMeteorAtomWithMongo); + + var isModify = isModificationMod(mongoMod); + + if (options.upsert && + (isModify ? (! mongoSelector._id) : (! mongoMod._id)) && + options.insertedId) { + mongoOpts.insertedId = options.insertedId; + simulateUpsertWithInsertedId(collection, mongoSelector, mongoMod, + isModify, mongoOpts, callback); + } else { + collection.update(mongoSelector, mongoMod, mongoOpts, + numberAffectedCallback(callback)); + } } catch (e) { write.committed(); throw e; } }; +var isModificationMod = function (mod) { + for (var k in mod) + if (k.substr(0, 1) === '$') + return true; + return false; +}; + +var simulateUpsertWithInsertedId = function (collection, selector, mod, + isModify, options, callback) { + var insertedId = options.insertedId; // must exist + + var mongoOpts = _.extend({}, options); + delete mongoOpts.insertedId; + delete mongoOpts.upsert; + + var doUpdate = function () { + mongoOpts.upsert = false; + collection.update(selector, mod, mongoOpts, + numberAffectedCallback(function (err, result) { + if (err) { + callback(err); + } else if (result.numberAffected) { + callback(null, result); + } else { + doConditionalInsert(); + } + })); + }; + + var doConditionalInsert = function () { + mongoOpts.upsert = true; + var replacementWithId = _.extend( + replaceTypes({_id: insertedId}, replaceMeteorAtomWithMongo), + mod); + collection.update(selector, replacementWithId, mongoOpts, + numberAffectedCallback(function (err, result) { + if (err) { + // XXX figure out if this is a + // "cannot change _id of document" error, and + // if so, try doUpdate() again, up to 3 times. + // Otherwise, pass err to callback. + Meteor._debug(err); + } else { + callback(null, _.extend(result, + { insertedId: insertedId })); + } + })); + }; + + if (isModify) { + // XXX TODO + } else { + doUpdate(); + } +}; + +var modifyDocument = function (doc, mod) { + // XXX use LocalCollection._modify + return mod; +}; + _.each(["insert", "update", "remove"], function (method) { MongoConnection.prototype[method] = function (/* arguments */) { var self = this; diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 9e96ed8f71..06c4a9439e 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -901,8 +901,42 @@ if (Meteor.isServer) { _.each(handlesToStop, function (h) {h.stop();}); onComplete(); }); -} + Tinytest.addAsync("mongo-livedata - upsert, " + idGeneration, function (test, onComplete) { + var run = test.runId(); + var coll = new Meteor.Collection("livedata_upsert_collection_"+run, collectionOptions); + + var result1 = coll.update({foo: 'bar'}, {foo: 'bar'}, {upsert: true}); + test.equal(result1.numberAffected, 1); + test.isTrue(result1.insertedId); + test.equal(coll.find().fetch(), [{foo: 'bar', _id: result1.insertedId}]); + + var result2 = coll.update({foo: 'bar'}, {foo: 'baz'}, {upsert: true}); + test.equal(result2.numberAffected, 1); + test.isFalse(result2.insertedId); + test.equal(coll.find().fetch(), [{foo: 'baz', _id: result1.insertedId}]); + + coll.remove({}); + + // Test values that require transformation to go into Mongo: + + var t1 = new Meteor.Collection.ObjectID(); + var t2 = new Meteor.Collection.ObjectID(); + var result3 = coll.update({foo: t1}, {foo: t1}, {upsert: true}); + test.equal(result3.numberAffected, 1); + test.isTrue(result3.insertedId); + test.equal(coll.find().fetch(), [{foo: t1, _id: result3.insertedId}]); + + var result4 = coll.update({foo: t1}, {foo: t2}, {upsert: true}); + test.equal(result2.numberAffected, 1); + test.isFalse(result2.insertedId); + test.equal(coll.find().fetch(), [{foo: t2, _id: result3.insertedId}]); + + + onComplete(); + }); + +} // end Meteor.isServer }); // end idGeneration parametrization