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

Commit 086255e5 by Harshith Kashyap Committed by Jan Aagaard Meier

Transactions for SQL Server (#6972)

* [MSSQL] Uses crypto randomBytes to generate 20 character trasaction identifier, enabled trasaction tests for mssql dialect

* Added missing break, changelog entry

* Added READ_COMMITTED_SNAPSHOT ON script for tests, fixes skipped tests, transaction savepoints, uses sequelize_test as default database on appveyor

* Added sqlcmd database query to appveyor-setup.ps1

* Added sequelize_test as default db in test/config/config for MSSQL dialect

* Moved READ_COMMITTED_SNAPSHOT ON query to appveyor ps script

* Replaced lodash includes with startsWith, review fixes, closes #6954
1 parent 4214143e
...@@ -20,7 +20,7 @@ $config = @{ ...@@ -20,7 +20,7 @@ $config = @{
username = "sa" username = "sa"
password = "Password12!" password = "Password12!"
port = $port port = $port
database = "master" database = "sequelize_test"
dialectOptions = @{ dialectOptions = @{
requestTimeout = 25000 requestTimeout = 25000
cryptoCredentialsDetails = @{ cryptoCredentialsDetails = @{
...@@ -35,5 +35,8 @@ $config = @{ ...@@ -35,5 +35,8 @@ $config = @{
$json = $config | ConvertTo-Json -Depth 3 $json = $config | ConvertTo-Json -Depth 3
# Create sequelize_test database
sqlcmd -S "(local)" -U "sa" -P "Password12!" -d "master" -Q "CREATE DATABASE [sequelize_test]; ALTER DATABASE [sequelize_test] SET READ_COMMITTED_SNAPSHOT ON;"
# cannot use Out-File because it outputs a BOM # cannot use Out-File because it outputs a BOM
[IO.File]::WriteAllLines((Join-Path $pwd "test\config\mssql.json"), $json) [IO.File]::WriteAllLines((Join-Path $pwd "test\config\mssql.json"), $json)
# Future # Future
- [FIXED] Transaction Name too long, transaction savepoints for SQL Server [#6972](https://github.com/sequelize/sequelize/pull/6972)
- [FIXED] Issue with sync hooks (before/afterInit, before/afterDefine) [#6680](https://github.com/sequelize/sequelize/issues/6680) - [FIXED] Issue with sync hooks (before/afterInit, before/afterDefine) [#6680](https://github.com/sequelize/sequelize/issues/6680)
- [FIXED] MSSQL handle large bulk inserts [#6866](https://github.com/sequelize/sequelize/issues/6866) - [FIXED] MSSQL handle large bulk inserts [#6866](https://github.com/sequelize/sequelize/issues/6866)
- [FIXED] describeTable returns a wrong value for primaryKey [#5756] (https://github.com/sequelize/sequelize/issues/5756) - [FIXED] describeTable returns a wrong value for primaryKey [#5756] (https://github.com/sequelize/sequelize/issues/5756)
......
...@@ -1577,6 +1577,10 @@ const QueryGenerator = { ...@@ -1577,6 +1577,10 @@ const QueryGenerator = {
return 'SET SESSION TRANSACTION ISOLATION LEVEL ' + value + ';'; return 'SET SESSION TRANSACTION ISOLATION LEVEL ' + value + ';';
}, },
generateTransactionId() {
return uuid.v4();
},
/** /**
* Returns a query that starts a transaction. * Returns a query that starts a transaction.
* *
......
...@@ -24,7 +24,7 @@ MssqlDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.support ...@@ -24,7 +24,7 @@ MssqlDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.support
'LIMIT ON UPDATE': true, 'LIMIT ON UPDATE': true,
'ORDER NULLS': false, 'ORDER NULLS': false,
lock: false, lock: false,
transactions: false, transactions: true,
migrations: false, migrations: false,
upserts: true, upserts: true,
returnValues: { returnValues: {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
var Utils = require('../../utils') var Utils = require('../../utils')
, DataTypes = require('../../data-types') , DataTypes = require('../../data-types')
, AbstractQueryGenerator = require('../abstract/query-generator') , AbstractQueryGenerator = require('../abstract/query-generator')
, randomBytes = require('crypto').randomBytes
, semver = require('semver'); , semver = require('semver');
/* istanbul ignore next */ /* istanbul ignore next */
...@@ -718,6 +719,10 @@ var QueryGenerator = { ...@@ -718,6 +719,10 @@ var QueryGenerator = {
return 'SET TRANSACTION ISOLATION LEVEL ' + value + ';'; return 'SET TRANSACTION ISOLATION LEVEL ' + value + ';';
}, },
generateTransactionId() {
return randomBytes(10).toString('hex');
},
startTransactionQuery(transaction, options) { startTransactionQuery(transaction, options) {
if (transaction.parent) { if (transaction.parent) {
return 'SAVE TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';'; return 'SAVE TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
......
...@@ -37,46 +37,53 @@ class Query extends AbstractQuery { ...@@ -37,46 +37,53 @@ class Query extends AbstractQuery {
if (benchmark) { if (benchmark) {
queryBegin = Date.now(); queryBegin = Date.now();
} else { } else {
this.sequelize.log('Executing (' + (connection.uuid || 'default') + '): ' + this.sql, this.options); this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
} }
debug(`executing(${connection.uuid || 'default'}) : ${this.sql}`); debug(`executing(${this.connection.uuid || 'default'}) : ${this.sql}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// TRANSACTION SUPPORT // TRANSACTION SUPPORT
if (_.includes(this.sql, 'BEGIN TRANSACTION')) { if (_.startsWith(this.sql, 'BEGIN TRANSACTION')) {
connection.beginTransaction(err => { connection.beginTransaction(err => {
if (!!err) { if (err) {
reject(this.formatError(err)); reject(this.formatError(err));
} else { } else {
resolve(this.formatResults()); resolve(this.formatResults());
} }
} /* name, isolation_level */); }, this.options.transaction.name, this.options.isolationLevel);
} else if (_.includes(this.sql, 'COMMIT TRANSACTION')) { } else if (_.startsWith(this.sql, 'COMMIT TRANSACTION')) {
connection.commitTransaction(err => { connection.commitTransaction(err => {
if (!!err) { if (err) {
reject(this.formatError(err)); reject(this.formatError(err));
} else { } else {
resolve(this.formatResults()); resolve(this.formatResults());
} }
}); });
} else if (_.includes(this.sql, 'ROLLBACK TRANSACTION')) { } else if (_.startsWith(this.sql, 'ROLLBACK TRANSACTION')) {
connection.rollbackTransaction(err => { connection.rollbackTransaction(err => {
if (!!err) { if (err) {
reject(this.formatError(err)); reject(this.formatError(err));
} else { } else {
resolve(this.formatResults()); resolve(this.formatResults());
} }
}); }, this.options.transaction.name);
} else if (_.startsWith(this.sql, 'SAVE TRANSACTION')) {
connection.saveTransaction(err => {
if (err) {
reject(this.formatError(err));
} else {
resolve(this.formatResults());
}
}, this.options.transaction.name);
} else { } else {
// QUERY SUPPORT
const results = []; const results = [];
const request = new connection.lib.Request(this.sql, (err, rowCount) => { const request = new connection.lib.Request(this.sql, (err, rowCount) => {
debug(`executed(${connection.uuid || 'default'}) : ${this.sql}`); debug(`executed(${this.connection.uuid || 'default'}) : ${this.sql}`);
if (benchmark) { if (benchmark) {
this.sequelize.log('Executed (' + (connection.uuid || 'default') + '): ' + this.sql, (Date.now() - queryBegin), this.options); this.sequelize.log('Executed (' + (this.connection.uuid || 'default') + '): ' + this.sql, (Date.now() - queryBegin), this.options);
} }
if (err) { if (err) {
......
...@@ -847,7 +847,7 @@ class QueryInterface { ...@@ -847,7 +847,7 @@ class QueryInterface {
options = _.assign({}, options, { options = _.assign({}, options, {
transaction: transaction.parent || transaction transaction: transaction.parent || transaction
}); });
options.transaction.name = transaction.parent ? transaction.name : undefined;
const sql = this.QueryGenerator.startTransactionQuery(transaction); const sql = this.QueryGenerator.startTransactionQuery(transaction);
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
...@@ -898,7 +898,7 @@ class QueryInterface { ...@@ -898,7 +898,7 @@ class QueryInterface {
transaction: transaction.parent || transaction, transaction: transaction.parent || transaction,
supportsSearchPath: false supportsSearchPath: false
}); });
options.transaction.name = transaction.parent ? transaction.name : undefined;
const sql = this.QueryGenerator.rollbackTransactionQuery(transaction); const sql = this.QueryGenerator.rollbackTransactionQuery(transaction);
const promise = this.sequelize.query(sql, options); const promise = this.sequelize.query(sql, options);
......
'use strict'; 'use strict';
const Utils = require('./utils'); const Utils = require('./utils');
const uuid = require('uuid');
/** /**
* The transaction object is used to identify a running transaction. It is created by calling `Sequelize.transaction()`. * The transaction object is used to identify a running transaction. It is created by calling `Sequelize.transaction()`.
...@@ -26,6 +25,7 @@ class Transaction { ...@@ -26,6 +25,7 @@ class Transaction {
// get dialect specific transaction options // get dialect specific transaction options
const transactionOptions = sequelize.dialect.supports.transactionOptions || {}; const transactionOptions = sequelize.dialect.supports.transactionOptions || {};
const generateTransactionId = this.sequelize.dialect.QueryGenerator.generateTransactionId;
this.options = Utils._.extend({ this.options = Utils._.extend({
autocommit: transactionOptions.autocommit || null, autocommit: transactionOptions.autocommit || null,
...@@ -34,14 +34,14 @@ class Transaction { ...@@ -34,14 +34,14 @@ class Transaction {
}, options || {}); }, options || {});
this.parent = this.options.transaction; this.parent = this.options.transaction;
this.id = this.parent ? this.parent.id : uuid.v4(); this.id = this.parent ? this.parent.id : generateTransactionId();
if (this.parent) { if (this.parent) {
this.id = this.parent.id; this.id = this.parent.id;
this.parent.savepoints.push(this); this.parent.savepoints.push(this);
this.name = this.id + '-savepoint-' + this.parent.savepoints.length; this.name = this.id + '-savepoint-' + this.parent.savepoints.length;
} else { } else {
this.id = this.name = uuid.v4(); this.id = this.name = generateTransactionId();
} }
delete this.options.transaction; delete this.options.transaction;
......
...@@ -23,17 +23,12 @@ module.exports = { ...@@ -23,17 +23,12 @@ module.exports = {
}, },
mssql: mssqlConfig || { mssql: mssqlConfig || {
database: process.env.SEQ_MSSQL_DB || process.env.SEQ_DB || (function () { database: process.env.SEQ_MSSQL_DB || process.env.SEQ_DB || 'sequelize_test',
var db = 'sequelize-test-' + ~~(Math.random() * 100);
console.log('Using database: ', db);
return db;
}()),
username: process.env.SEQ_MSSQL_USER || process.env.SEQ_USER || 'sequelize', username: process.env.SEQ_MSSQL_USER || process.env.SEQ_USER || 'sequelize',
password: process.env.SEQ_MSSQL_PW || process.env.SEQ_PW || 'nEGkLma26gXVHFUAHJxcmsrK', password: process.env.SEQ_MSSQL_PW || process.env.SEQ_PW || 'nEGkLma26gXVHFUAHJxcmsrK',
host: process.env.SEQ_MSSQL_HOST || process.env.SEQ_HOST || 'mssql.sequelizejs.com', host: process.env.SEQ_MSSQL_HOST || process.env.SEQ_HOST || 'mssql.sequelizejs.com',
port: process.env.SEQ_MSSQL_PORT || process.env.SEQ_PORT || 1433, port: process.env.SEQ_MSSQL_PORT || process.env.SEQ_PORT || 1433,
dialectOptions: { dialectOptions: {
instanceName: process.env.MSSQL_INSTANCE ? process.env.MSSQL_INSTANCE : 'SQLEXPRESS',
// big insert queries need a while // big insert queries need a while
requestTimeout: 60000 requestTimeout: 60000
}, },
......
...@@ -1244,6 +1244,28 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1244,6 +1244,28 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}); });
}); });
it('Works even when SQL query has a values of transaction keywords such as BEGIN TRANSACTION', function () {
const Task = this.sequelize.define('task', {
title: DataTypes.STRING
});
return Task.sync({ force: true })
.then(() => {
return Sequelize.Promise.all([
Task.create({ title: 'BEGIN TRANSACTION' }),
Task.create({ title: 'COMMIT TRANSACTION' }),
Task.create({ title: 'ROLLBACK TRANSACTION' }),
Task.create({ title: 'SAVE TRANSACTION' }),
]);
})
.then(newTasks => {
expect(newTasks).to.have.lengthOf(4);
expect(newTasks[0].title).to.equal('BEGIN TRANSACTION');
expect(newTasks[1].title).to.equal('COMMIT TRANSACTION');
expect(newTasks[2].title).to.equal('ROLLBACK TRANSACTION');
expect(newTasks[3].title).to.equal('SAVE TRANSACTION');
});
});
describe('enums', function() { describe('enums', function() {
it('correctly restores enum values', function() { it('correctly restores enum values', function() {
var self = this var self = this
...@@ -1366,19 +1388,25 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1366,19 +1388,25 @@ describe(Support.getTestDialectTeaser('Model'), function() {
describe('bulkCreate', function() { describe('bulkCreate', function() {
if (current.dialect.supports.transactions) { if (current.dialect.supports.transactions) {
it('supports transactions', function() { it('supports transactions', function() {
var self = this; const User = this.sequelize.define('User', {
return this.sequelize.transaction().then(function(t) { username: DataTypes.STRING
return self.User });
.bulkCreate([{ username: 'foo' }, { username: 'bar' }], { transaction: t }) let transaction, count1;
.then(function() { return User.sync({ force: true })
return self.User.count().then(function(count1) { .then(() => this.sequelize.transaction())
return self.User.count({ transaction: t }).then(function(count2) { .then(t => {
transaction = t;
return User.bulkCreate([{ username: 'foo' }, { username: 'bar' }], { transaction });
})
.then(() => User.count())
.then((count) => {
count1 = count;
return User.count({ transaction });
})
.then((count2) => {
expect(count1).to.equal(0); expect(count1).to.equal(0);
expect(count2).to.equal(2); expect(count2).to.equal(2);
return t.rollback(); return transaction.rollback();
});
});
});
}); });
}); });
} }
......
...@@ -71,6 +71,9 @@ describe(Support.getTestDialectTeaser('Sequelize#transaction'), function() { ...@@ -71,6 +71,9 @@ describe(Support.getTestDialectTeaser('Sequelize#transaction'), function() {
case 'sqlite': case 'sqlite':
query = 'select sqlite3_sleep(2000);'; query = 'select sqlite3_sleep(2000);';
break; break;
case 'mssql':
query = 'WAITFOR DELAY \'00:00:02\';';
break;
default: default:
break; break;
} }
......
...@@ -31,6 +31,14 @@ describe(Support.getTestDialectTeaser('Transaction'), function() { ...@@ -31,6 +31,14 @@ describe(Support.getTestDialectTeaser('Transaction'), function() {
var transaction = new Transaction(this.sequelize); var transaction = new Transaction(this.sequelize);
expect(transaction.id).to.exist; expect(transaction.id).to.exist;
}); });
it('should call dialect specific generateTransactionId method', function() {
const transaction = new Transaction(this.sequelize);
expect(transaction.id).to.exist;
if (dialect === 'mssql') {
expect(transaction.id).to.have.lengthOf(20);
}
});
}); });
describe('commit', function() { describe('commit', function() {
...@@ -72,7 +80,8 @@ describe(Support.getTestDialectTeaser('Transaction'), function() { ...@@ -72,7 +80,8 @@ describe(Support.getTestDialectTeaser('Transaction'), function() {
}); });
}); });
if (dialect === 'postgres' || dialect === 'mssql') { //Promise rejection test is specifc to postgres
if (dialect === 'postgres') {
it('do not rollback if already committed', function() { it('do not rollback if already committed', function() {
var SumSumSum = this.sequelize.define('transaction', { var SumSumSum = this.sequelize.define('transaction', {
value: { value: {
......
...@@ -218,6 +218,5 @@ if (typeof beforeEach !== 'undefined') { ...@@ -218,6 +218,5 @@ if (typeof beforeEach !== 'undefined') {
this.sequelize = Support.sequelize; this.sequelize = Support.sequelize;
}); });
} }
Support.sequelize = Support.createSequelizeInstance(); Support.sequelize = Support.createSequelizeInstance();
module.exports = Support; module.exports = Support;
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!