Skip to content

Commit

Permalink
Support upsert
Browse files Browse the repository at this point in the history
  • Loading branch information
1602 committed Mar 22, 2012
1 parent ff65e3a commit c06f28f
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 11 deletions.
30 changes: 26 additions & 4 deletions lib/abstract-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,30 @@ AbstractClass.create = function (data, callback) {
}
};

AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
var Model = this;
if (!data.id) return this.create(data, callback);
if (this.schema.adapter.updateOrCreate) {
this.schema.adapter.updateOrCreate(Model.modelName, data, function (err, data) {
var obj = data ? new Model(data) : null;
if (obj) {
addToCache(Model, obj);
}
callback(err, obj);
});
} else {
this.find(data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
inst.updateAttributes(data, callback);
} else {
var obj = new Model(data);
obj.save(data, callback);
}
});
}
};

AbstractClass.exists = function exists(id, cb) {
if (id) {
this.schema.adapter.exists(this.modelName, id, cb);
Expand Down Expand Up @@ -250,9 +274,7 @@ function substractDirtyAttributes(object, data) {

AbstractClass.destroyAll = function destroyAll(cb) {
this.schema.adapter.destroyAll(this.modelName, function (err) {
if (!err) {
clearCache(this);
}
clearCache(this);
cb(err);
}.bind(this));
};
Expand Down Expand Up @@ -424,7 +446,7 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
}
done.call(inst, function () {
saveDone.call(inst, function () {
cb(err);
cb(err, inst);
});
});
});
Expand Down
19 changes: 17 additions & 2 deletions lib/adapters/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,29 @@ Memory.prototype.define = function defineModel(descr) {
};

Memory.prototype.create = function create(model, data, callback) {
var id = this.ids[model]++;
var id = data.id || this.ids[model]++;
data.id = id;
this.cache[model][id] = data;
callback(null, id);
};

Memory.prototype.updateOrCreate = function (model, data, callback) {
var mem = this;
this.exists(model, data.id, function (err, exists) {
if (exists) {
mem.save(model, data, callback);
} else {
mem.create(model, data, function (err, id) {
data.id = id;
callback(err, data);
});
}
});
};

Memory.prototype.save = function save(model, data, callback) {
this.cache[model][data.id] = data;
callback();
callback(null, data);
};

Memory.prototype.exists = function exists(model, id, callback) {
Expand Down Expand Up @@ -151,6 +165,7 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
};

function merge(base, update) {
if (!base) return update;
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
Expand Down
23 changes: 23 additions & 0 deletions lib/adapters/mongodb.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ MongoDB.prototype.find = function find(model, id, callback) {
});
};

MongoDB.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
var adapter = this;
if (!data.id) return this.create(data, callback);
this.find(model, data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
adapter.updateAttributes(model, data.id, data, callback);
} else {
delete data.id;
adapter.create(model, data, function (err, id) {
if (err) return callback(err);
if (id) {
data.id = id;
delete data._id;
callback(null, data);
} else{
callback(null, null); // wtf?
}
});
}
});
};

MongoDB.prototype.destroy = function destroy(model, id, callback) {
this.collection(model).remove({_id: new ObjectID(id)}, callback);
};
Expand Down
66 changes: 64 additions & 2 deletions lib/adapters/mysql.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,24 @@ exports.initialize = function initializeSchema(schema, callback) {
port: s.port || 3306,
user: s.username,
password: s.password,
database: s.database,
debug: s.debug
});

schema.adapter = new MySQL(schema.client);
schema.adapter.schema = schema;
// schema.client.query('SET TIME_ZONE = "+04:00"', callback);
process.nextTick(callback);
schema.client.query('USE ' + s.database, function (err) {
if (err && err.message.match(/^unknown database/i)) {
var dbName = s.database;
schema.client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
callback();
} else {
throw error;
}
});
} else callback();
});
};

function MySQL(client) {
Expand All @@ -32,10 +43,27 @@ function MySQL(client) {
require('util').inherits(MySQL, BaseSQL);

MySQL.prototype.query = function (sql, callback) {
if (!this.schema.connected) {
return this.schema.on('connected', function () {
this.query(sql, callback);
}.bind(this));
}
var client = this.client;
var time = Date.now();
var log = this.log;
if (typeof callback !== 'function') throw new Error('callback should be a function');
this.client.query(sql, function (err, data) {
if (err && err.message.match(/^unknown database/i)) {
var dbName = err.message.match(/^unknown database '(.*?)'/i)[1];
client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
client.query(sql, callback);
} else {
callback(err);
}
});
return;
}
if (log) log(sql, time);
callback(err, data);
});
Expand All @@ -57,6 +85,40 @@ MySQL.prototype.create = function (model, data, callback) {
});
};

MySQL.prototype.updateOrCreate = function (model, data, callback) {
var mysql = this;
var fieldsNames = [];
var fieldValues = [];
var combined = [];
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '`' + key + '`';
var v;
if (key !== 'id') {
v = mysql.toDatabase(props[key], data[key]);
} else {
v = data[key];
}
fieldsNames.push(k);
fieldValues.push(v);
if (key !== 'id') combined.push(k + ' = ' + v);
}
});

var sql = 'INSERT INTO ' + this.tableEscaped(model);
sql += ' (' + fieldsNames.join(', ') + ')';
sql += ' VALUES (' + fieldValues.join(', ') + ')';
sql += ' ON DUPLICATE KEY UPDATE ' + combined.join(', ');

this.query(sql, function (err, info) {
if (!err && info && info.insertId) {
data.id = info.insertId;
}
callback(err, data);
});
};

MySQL.prototype.toFields = function (model, data) {
var fields = [];
var props = this._models[model].properties;
Expand Down
2 changes: 1 addition & 1 deletion lib/adapters/neo4j.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Neo4j.prototype.save = function save(model, data, callback) {
if (err) return callback(err);
self.updateIndexes(model, node, function (err) {
if (err) return console.log(err);
callback(null);
callback(null, node.data);
});
});
});
Expand Down
39 changes: 38 additions & 1 deletion lib/adapters/postgres.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ PG.prototype.query = function (sql, callback) {
* Must invoke callback(err, id)
*/
PG.prototype.create = function (model, data, callback) {
var fields = this.toFields(model, data,true);
var fields = this.toFields(model, data, true);
var sql = 'INSERT INTO ' + this.tableEscaped(model) + '';
if (fields) {
sql += ' ' + fields;
Expand All @@ -69,6 +69,43 @@ PG.prototype.create = function (model, data, callback) {
});
};

PG.prototype.updateOrCreate = function (model, data, callback) {
var pg = this;
var fieldsNames = [];
var fieldValues = [];
var combined = [];
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '"' + key + '"';
var v;
if (key !== 'id') {
v = pg.toDatabase(props[key], data[key]);
} else {
v = data[key];
}
fieldsNames.push(k);
fieldValues.push(v);
if (key !== 'id') combined.push(k + ' = ' + v);
}
});

var sql = 'UPDATE ' + this.tableEscaped(model);
sql += ' SET ' + combined + ' WHERE id = ' + data.id + ';';
sql += ' INSERT INTO ' + this.tableEscaped(model);
sql += ' (' + fieldsNames.join(', ') + ')';
sql += ' SELECT ' + fieldValues.join(', ')
sql += ' WHERE NOT EXISTS (SELECT 1 FROM ' + this.tableEscaped(model);
sql += ' WHERE id = ' + data.id + ') RETURNING id';

this.query(sql, function (err, info) {
if (!err && info && info[0] && info[0].id) {
data.id = info[0].id;
}
callback(err, data);
});
};

PG.prototype.toFields = function (model, data, forCreate) {
var fields = [];
var props = this._models[model].properties;
Expand Down
18 changes: 18 additions & 0 deletions lib/adapters/sqlite3.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ SQLite3.prototype.create = function (model, data, callback) {
});
};

SQLite3.prototype.updateOrCreate = function (model, data, callback) {
data = data || {};
var questions = [];
var values = Object.keys(data).map(function (key) {
questions.push('?');
return data[key];
});
var sql = 'INSERT OR REPLACE INTO ' + this.tableEscaped(model) + ' (' + Object.keys(data).join(',') + ') VALUES ('
sql += questions.join(',');
sql += ')';
this.command(sql, values, function (err) {
if (!err && this) {
data.id = this.lastID;
}
callback(err, data);
});
};

SQLite3.prototype.toFields = function (model, data) {
var fields = [];
var props = this._models[model].properties;
Expand Down
36 changes: 35 additions & 1 deletion test/common_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Object.keys(schemas).forEach(function (schemaName) {
});

schema.log = function (a) {
// console.log(a);
// console.log(a);
};
testOrm(schema);
if (specificTest[schemaName]) specificTest[schemaName](schema);
Expand Down Expand Up @@ -716,6 +716,40 @@ function testOrm(schema) {
});
});

if (schema.name !== 'mongoose' && schema.name !== 'neo4j')
it('should update or create record', function (test) {
var newData = {
id: 1,
title: 'New title (really new)',
content: 'Some example content (updated)'
};
Post.updateOrCreate(newData, function (err, updatedPost) {
if (err) throw err;
test.ok(updatedPost);
if (!updatedPost) throw Error('No post!');

test.equal(newData.id, updatedPost.toObject().id);
test.equal(newData.title, updatedPost.toObject().title);
test.equal(newData.content, updatedPost.toObject().content);

Post.find(updatedPost.id, function (err, post) {
if (err) throw err;
if (!post) throw Error('No post!');
test.equal(newData.id, post.toObject().id);
test.equal(newData.title, post.toObject().title);
test.equal(newData.content, post.toObject().content);
Post.updateOrCreate({id: 100001, title: 'hey'}, function (err, post) {
if (schema.name !== 'mongodb') test.equal(post.id, 100001);
test.equal(post.title, 'hey');
Post.find(post.id, function (err, post) {
if (!post) throw Error('No post!');
test.done();
});
});
});
});
});

it('all tests done', function (test) {
test.done();
process.nextTick(allTestsDone);
Expand Down

0 comments on commit c06f28f

Please sign in to comment.