不要怂,就是干,撸起袖子干!

Commit a64fe80d by Mick Hansen

Merge pull request #1780 from seegno/add-savepoint-support

Add savepoint support for postgres transactions
2 parents 532c05e5 50cc79de
...@@ -1008,7 +1008,18 @@ module.exports = (function() { ...@@ -1008,7 +1008,18 @@ module.exports = (function() {
return 'SET autocommit = ' + (!!value ? 1 : 0) + ';'; return 'SET autocommit = ' + (!!value ? 1 : 0) + ';';
}, },
setIsolationLevelQuery: function(value) { /**
* Returns a query that sets the transaction isolation level.
*
* @param {String} value The isolation level.
* @param {Object} options An object with options.
* @return {String} The generated sql query.
*/
setIsolationLevelQuery: function(value, options) {
if (options.parent) {
return;
}
return 'SET SESSION TRANSACTION ISOLATION LEVEL ' + value + ';'; return 'SET SESSION TRANSACTION ISOLATION LEVEL ' + value + ';';
}, },
...@@ -1019,6 +1030,10 @@ module.exports = (function() { ...@@ -1019,6 +1030,10 @@ module.exports = (function() {
* @return {String} The generated sql query. * @return {String} The generated sql query.
*/ */
startTransactionQuery: function(options) { startTransactionQuery: function(options) {
if (options.parent) {
return 'SAVEPOINT ' + this.quoteIdentifier(options.parent.id) + ';';
}
return 'START TRANSACTION;'; return 'START TRANSACTION;';
}, },
...@@ -1029,6 +1044,10 @@ module.exports = (function() { ...@@ -1029,6 +1044,10 @@ module.exports = (function() {
* @return {String} The generated sql query. * @return {String} The generated sql query.
*/ */
commitTransactionQuery: function(options) { commitTransactionQuery: function(options) {
if (options.parent) {
return;
}
return 'COMMIT;'; return 'COMMIT;';
}, },
...@@ -1039,6 +1058,10 @@ module.exports = (function() { ...@@ -1039,6 +1058,10 @@ module.exports = (function() {
* @return {String} The generated sql query. * @return {String} The generated sql query.
*/ */
rollbackTransactionQuery: function(options) { rollbackTransactionQuery: function(options) {
if (options.parent) {
return 'ROLLBACK TO SAVEPOINT ' + this.quoteIdentifier(options.parent.id) + ';';
}
return 'ROLLBACK;'; return 'ROLLBACK;';
}, },
......
...@@ -27,7 +27,7 @@ module.exports = (function() { ...@@ -27,7 +27,7 @@ module.exports = (function() {
this.sql = sql; this.sql = sql;
if (this.options.logging !== false) { if (this.options.logging !== false) {
this.sequelize.log('Executing (' + this.options.uuid + '): ' + this.sql); this.sequelize.log('Executing (' + this.client.uuid + '): ' + this.sql);
} }
var resultSet = [], var resultSet = [],
......
...@@ -32,7 +32,7 @@ module.exports = (function() { ...@@ -32,7 +32,7 @@ module.exports = (function() {
, rows = []; , rows = [];
if (this.options.logging !== false) { if (this.options.logging !== false) {
this.sequelize.log('Executing (' + this.options.uuid + '): ' + this.sql); this.sequelize.log('Executing (' + this.client.uuid + '): ' + this.sql);
} }
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
...@@ -45,7 +45,7 @@ module.exports = (function() { ...@@ -45,7 +45,7 @@ module.exports = (function() {
query.on('error', function(err) { query.on('error', function(err) {
receivedError = true; receivedError = true;
err.sql = sql; err.sql = sql;
promise.emit('sql', sql, self.options.uuid); promise.emit('sql', sql, self.client.uuid);
reject(err); reject(err);
}); });
...@@ -54,7 +54,7 @@ module.exports = (function() { ...@@ -54,7 +54,7 @@ module.exports = (function() {
return; return;
} }
promise.emit('sql', self.sql, self.options.uuid); promise.emit('sql', self.sql, self.client.uuid);
resolve([rows, sql, result]); resolve([rows, sql, result]);
}); });
}).spread(function(rows, sql, result) { }).spread(function(rows, sql, result) {
......
...@@ -372,7 +372,11 @@ module.exports = (function() { ...@@ -372,7 +372,11 @@ module.exports = (function() {
}); });
}, },
startTransactionQuery: function() { startTransactionQuery: function(options) {
if (options.parent) {
return 'SAVEPOINT ' + this.quoteIdentifier(options.parent.id) + ';';
}
return "BEGIN TRANSACTION;"; return "BEGIN TRANSACTION;";
}, },
......
...@@ -30,7 +30,7 @@ module.exports = (function() { ...@@ -30,7 +30,7 @@ module.exports = (function() {
this.sql = sql; this.sql = sql;
if (this.options.logging !== false) { if (this.options.logging !== false) {
this.sequelize.log('Executing (' + this.options.uuid + '): ' + this.sql); this.sequelize.log('Executing (' + this.database.uuid + '): ' + this.sql);
} }
return new Utils.Promise(function(resolve) { return new Utils.Promise(function(resolve) {
......
...@@ -699,13 +699,22 @@ module.exports = (function() { ...@@ -699,13 +699,22 @@ module.exports = (function() {
return this.sequelize.query(sql, null, { transaction: transaction}); return this.sequelize.query(sql, null, { transaction: transaction});
}; };
QueryInterface.prototype.setIsolationLevel = function(transaction, value) { QueryInterface.prototype.setIsolationLevel = function(transaction, value, options) {
if (!transaction || !(transaction instanceof Transaction)) { if (!transaction || !(transaction instanceof Transaction)) {
throw new Error('Unable to set isolation level for a transaction without transaction object!'); throw new Error('Unable to set isolation level for a transaction without transaction object!');
} }
var sql = this.QueryGenerator.setIsolationLevelQuery(value); options = Utils._.extend({
parent: options.transaction
}, options || {});
var sql = this.QueryGenerator.setIsolationLevelQuery(value, options);
if (sql) {
return this.sequelize.query(sql, null, { transaction: transaction }); return this.sequelize.query(sql, null, { transaction: transaction });
} else {
return Utils.Promise.resolve();
}
}; };
QueryInterface.prototype.startTransaction = function(transaction, options) { QueryInterface.prototype.startTransaction = function(transaction, options) {
...@@ -714,7 +723,8 @@ module.exports = (function() { ...@@ -714,7 +723,8 @@ module.exports = (function() {
} }
options = Utils._.extend({ options = Utils._.extend({
transaction: transaction transaction: transaction,
parent: options.transaction
}, options || {}); }, options || {});
var sql = this.QueryGenerator.startTransactionQuery(options); var sql = this.QueryGenerator.startTransactionQuery(options);
...@@ -727,11 +737,17 @@ module.exports = (function() { ...@@ -727,11 +737,17 @@ module.exports = (function() {
} }
options = Utils._.extend({ options = Utils._.extend({
transaction: transaction transaction: transaction,
parent: options.transaction
}, options || {}); }, options || {});
var sql = this.QueryGenerator.commitTransactionQuery(options); var sql = this.QueryGenerator.commitTransactionQuery(options);
if (sql) {
return this.sequelize.query(sql, null, options); return this.sequelize.query(sql, null, options);
} else {
return Utils.Promise.resolve();
}
}; };
QueryInterface.prototype.rollbackTransaction = function(transaction, options) { QueryInterface.prototype.rollbackTransaction = function(transaction, options) {
...@@ -740,7 +756,8 @@ module.exports = (function() { ...@@ -740,7 +756,8 @@ module.exports = (function() {
} }
options = Utils._.extend({ options = Utils._.extend({
transaction: transaction transaction: transaction,
parent: options.transaction
}, options || {}); }, options || {});
var sql = this.QueryGenerator.rollbackTransactionQuery(options); var sql = this.QueryGenerator.rollbackTransactionQuery(options);
......
...@@ -11,11 +11,11 @@ var Utils = require('./utils') ...@@ -11,11 +11,11 @@ var Utils = require('./utils')
*/ */
var Transaction = module.exports = function(sequelize, options) { var Transaction = module.exports = function(sequelize, options) {
this.sequelize = sequelize; this.sequelize = sequelize;
this.id = Utils.generateUUID();
this.options = Utils._.extend({ this.options = Utils._.extend({
autocommit: true, autocommit: true,
isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ
}, options || {}); }, options || {});
this.id = this.options.transaction ? this.options.transaction.id : Utils.generateUUID();
}; };
/** /**
...@@ -65,11 +65,17 @@ Transaction.LOCK = Transaction.prototype.LOCK = { ...@@ -65,11 +65,17 @@ Transaction.LOCK = Transaction.prototype.LOCK = {
* @return {this} * @return {this}
*/ */
Transaction.prototype.commit = function() { Transaction.prototype.commit = function() {
var self = this;
return this return this
.sequelize .sequelize
.getQueryInterface() .getQueryInterface()
.commitTransaction(this, {}) .commitTransaction(this, this.options)
.finally(this.cleanup.bind(this)); .finally(function() {
if (!self.options.transaction) {
self.cleanup();
}
});
}; };
...@@ -79,19 +85,25 @@ Transaction.prototype.commit = function() { ...@@ -79,19 +85,25 @@ Transaction.prototype.commit = function() {
* @return {this} * @return {this}
*/ */
Transaction.prototype.rollback = function() { Transaction.prototype.rollback = function() {
var self = this;
return this return this
.sequelize .sequelize
.getQueryInterface() .getQueryInterface()
.rollbackTransaction(this, {}) .rollbackTransaction(this, this.options)
.finally(this.cleanup.bind(this)); .finally(function() {
if (!self.options.transaction) {
self.cleanup();
}
});
}; };
Transaction.prototype.prepareEnvironment = function() { Transaction.prototype.prepareEnvironment = function() {
var self = this; var self = this;
return this.sequelize.connectionManager.getConnection({ return Utils.Promise.resolve(
uuid: self.id self.options.transaction ? self.options.transaction.connection : self.sequelize.connectionManager.getConnection({ uuid: self.id })
}).then(function (connection) { ).then(function (connection) {
self.connection = connection; self.connection = connection;
self.connection.uuid = self.id; self.connection.uuid = self.id;
}).then(function () { }).then(function () {
...@@ -102,11 +114,12 @@ Transaction.prototype.prepareEnvironment = function() { ...@@ -102,11 +114,12 @@ Transaction.prototype.prepareEnvironment = function() {
return self.setAutocommit(); return self.setAutocommit();
}); });
}; };
Transaction.prototype.begin = function() { Transaction.prototype.begin = function() {
return this return this
.sequelize .sequelize
.getQueryInterface() .getQueryInterface()
.startTransaction(this, {}); .startTransaction(this, this.options);
}; };
Transaction.prototype.setAutocommit = function() { Transaction.prototype.setAutocommit = function() {
...@@ -120,7 +133,7 @@ Transaction.prototype.setIsolationLevel = function() { ...@@ -120,7 +133,7 @@ Transaction.prototype.setIsolationLevel = function() {
return this return this
.sequelize .sequelize
.getQueryInterface() .getQueryInterface()
.setIsolationLevel(this, this.options.isolationLevel); .setIsolationLevel(this, this.options.isolationLevel, this.options);
}; };
Transaction.prototype.cleanup = function() { Transaction.prototype.cleanup = function() {
......
...@@ -888,6 +888,77 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { ...@@ -888,6 +888,77 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
}) })
} }
it('supports nested transactions using savepoints', function(done) {
var self = this
var User = this.sequelize.define('Users', { username: DataTypes.STRING })
User.sync({ force: true }).success(function() {
self.sequelizeWithTransaction.transaction(function(t1) {
User.create({ username: 'foo' }, { transaction: t1 }).success(function(user) {
self.sequelizeWithTransaction.transaction({ transaction: t1 }, function(t2) {
user.updateAttributes({ username: 'bar' }, { transaction: t2 }).success(function() {
t2.commit().then(function() {
user.reload({ transaction: t1 }).success(function(newUser) {
expect(newUser.username).to.equal('bar')
t1.commit().then(function() {
done()
});
})
})
})
})
})
})
})
})
it('supports rolling back a nested transaction', function(done) {
var self = this
var User = this.sequelize.define('Users', { username: DataTypes.STRING })
User.sync({ force: true }).success(function() {
self.sequelizeWithTransaction.transaction(function(t1) {
User.create({ username: 'foo' }, { transaction: t1 }).success(function(user) {
self.sequelizeWithTransaction.transaction({ transaction: t1 }, function(t2) {
user.updateAttributes({ username: 'bar' }, { transaction: t2 }).success(function() {
t2.rollback().then(function() {
user.reload({ transaction: t2 }).success(function(newUser) {
expect(newUser.username).to.equal('foo')
t1.commit().then(function() {
done()
});
})
})
})
})
})
})
})
})
it('supports rolling back outermost transaction', function(done) {
var self = this
var User = this.sequelize.define('Users', { username: DataTypes.STRING })
User.sync({ force: true }).success(function() {
self.sequelizeWithTransaction.transaction(function(t1) {
User.create({ username: 'foo' }, { transaction: t1 }).success(function(user) {
self.sequelizeWithTransaction.transaction({ transaction: t1 }, function(t2) {
user.updateAttributes({ username: 'bar' }, { transaction: t2 }).success(function() {
t1.rollback().then(function() {
User.findAll().success(function(users) {
expect(users.length).to.equal(0);
done()
})
})
})
})
})
})
})
})
}) })
}) })
}) })
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!