2014-09-01 17:18:45 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
2013-09-13 16:58:38 -04:00
|
|
|
// Transaction
|
|
|
|
|
// -------
|
2015-04-22 10:34:14 -04:00
|
|
|
var Promise = require('./promise')
|
|
|
|
|
var EventEmitter = require('events').EventEmitter
|
|
|
|
|
var inherits = require('inherits')
|
|
|
|
|
|
|
|
|
|
var makeKnex = require('./util/make-knex')
|
|
|
|
|
|
|
|
|
|
var assign = require('lodash/object/assign')
|
|
|
|
|
var uniqueId = require('lodash/utility/uniqueId');
|
|
|
|
|
|
|
|
|
|
var debug = require('debug')
|
|
|
|
|
var debugTx = debug('knex:tx')
|
|
|
|
|
var debugQuery = debug('knex:query')
|
|
|
|
|
|
|
|
|
|
// Container for a Promise
|
|
|
|
|
function Transaction(client, outerTx) {
|
|
|
|
|
|
|
|
|
|
this.txid = uniqueId('trx')
|
|
|
|
|
this.client = client
|
|
|
|
|
this._outerTx = outerTx
|
|
|
|
|
|
|
|
|
|
debugTx('%s: Starting %s transaction', this.txid, outerTx ? 'nested' : 'top level')
|
|
|
|
|
|
|
|
|
|
this._dfd = new Promise(function(resolver, rejecter) {
|
|
|
|
|
this._resolver = resolver
|
|
|
|
|
this._rejecter = rejecter
|
|
|
|
|
}.bind(this))
|
|
|
|
|
|
|
|
|
|
this._completed = false
|
|
|
|
|
|
|
|
|
|
// If there is more than one child transaction,
|
|
|
|
|
// we queue them, executing each when the previous completes.
|
|
|
|
|
this._trxQueue = []
|
|
|
|
|
|
|
|
|
|
if (outerTx) {
|
|
|
|
|
var len = outerTx._trxQueue.length
|
|
|
|
|
if (len > 0) {
|
|
|
|
|
debugTx('%s: Queueing transaction in %s index: %d', this.txid, outerTx.txid, len)
|
|
|
|
|
this._queue = outerTx._trxQueue[len - 1].finally(function() {
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
outerTx._trxQueue.push(this._dfd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._queue = this._queue || Promise.resolve(true)
|
2014-04-08 16:25:57 -04:00
|
|
|
}
|
2015-04-22 10:34:14 -04:00
|
|
|
inherits(Transaction, EventEmitter)
|
|
|
|
|
|
|
|
|
|
assign(Transaction.prototype, {
|
|
|
|
|
|
|
|
|
|
isCancelled: function() {
|
|
|
|
|
return this._cancelled || this._outerTx && this._outerTx.isCancelled() || false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
run: function(container, config) {
|
|
|
|
|
config = config || {}
|
|
|
|
|
var t = this
|
|
|
|
|
var client = this.client
|
|
|
|
|
|
|
|
|
|
Promise.using(this.acquireConnection(config), function(connection) {
|
|
|
|
|
|
|
|
|
|
var trxClient = t.makeClient(connection)
|
|
|
|
|
var init = client.transacting ? t.savepoint(connection) : t.begin(connection)
|
|
|
|
|
|
|
|
|
|
return init.then(function() {
|
|
|
|
|
return t.makeTransactor(connection, trxClient)
|
|
|
|
|
})
|
|
|
|
|
.tap(function(transactor) {
|
|
|
|
|
if (client.transacting) {
|
|
|
|
|
return t.savepoint(transactor)
|
|
|
|
|
}
|
|
|
|
|
return transactor.client
|
|
|
|
|
})
|
|
|
|
|
.then(function(transactor) {
|
|
|
|
|
|
|
|
|
|
var result = container(transactor)
|
|
|
|
|
|
|
|
|
|
// If we've returned a "thenable" from the transaction container,
|
|
|
|
|
// and it's got the transaction object we're running for this, assume
|
|
|
|
|
// the rollback and commit are chained to this object's success / failure.
|
|
|
|
|
if (result && result.then && typeof result.then === 'function') {
|
|
|
|
|
result.then(function(val) {
|
|
|
|
|
debugTx('%s: promise-resolved', t.txid)
|
|
|
|
|
transactor.commit(val)
|
|
|
|
|
}).catch(function(err) {
|
|
|
|
|
debugTx('%s: catch-rollback', t.txid)
|
|
|
|
|
transactor.rollback(err)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2015-04-19 16:31:52 -04:00
|
|
|
})
|
2013-09-04 17:32:32 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
return this;
|
|
|
|
|
},
|
2014-03-14 16:56:55 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
acquireConnection: function(config) {
|
|
|
|
|
var t = this
|
|
|
|
|
return Promise.try(function() {
|
|
|
|
|
return config.connection || t.client.acquireConnection()
|
|
|
|
|
}).disposer(function(connection) {
|
|
|
|
|
if (!config.connection) {
|
|
|
|
|
t.client.releaseConnection(connection)
|
|
|
|
|
} else {
|
|
|
|
|
debugTx('%s: not releasing external connection', t.txid)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
2013-09-04 17:32:32 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
begin: function(conn) {
|
|
|
|
|
return this.query(conn, 'begin;')
|
|
|
|
|
},
|
2014-05-08 17:58:07 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
savepoint: function(conn) {
|
|
|
|
|
return this.query(conn, 'savepoint ' + this.txid + ';')
|
|
|
|
|
},
|
2013-09-04 17:32:32 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
commit: function(conn, value) {
|
|
|
|
|
return this.query(conn, 'commit;', 1, value)
|
|
|
|
|
},
|
2013-09-05 16:36:49 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
release: function(conn, value) {
|
|
|
|
|
return this.query(conn, 'release ' + this.txid + ';', 1, value)
|
|
|
|
|
},
|
2014-04-16 01:23:50 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
rollback: function(conn, error) {
|
|
|
|
|
return this.query(conn, 'rollback;', 2, error)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
rollbackTo: function(conn, error) {
|
|
|
|
|
return this.query(conn, 'rollback to ' + this.txid + ';', 2, error)
|
|
|
|
|
},
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
query: function(conn, sql, status, value) {
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
if (this.isCancelled()) {
|
|
|
|
|
return this._skipping(sql)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 1 || status === 2) {
|
|
|
|
|
this._completed = true
|
|
|
|
|
}
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
if (typeof sql === 'string') sql = {sql: sql}
|
|
|
|
|
|
|
|
|
|
debugQuery('%s: query %s', this.txid, sql.sql.slice(0, 300))
|
|
|
|
|
|
|
|
|
|
this.emit('query', assign({__knexUid: conn.__knexUid}, sql))
|
|
|
|
|
|
|
|
|
|
var t = this
|
|
|
|
|
return this.client._query(conn, sql)
|
|
|
|
|
.tap(function() {
|
|
|
|
|
if (status === 1) t._resolver(value)
|
|
|
|
|
if (status === 2) t._rejecter(value)
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
stream: function(conn, sql, stream, options) {
|
|
|
|
|
|
|
|
|
|
debugQuery('%s: streaming', this.txid)
|
|
|
|
|
|
|
|
|
|
if (this.isCancelled()) {
|
|
|
|
|
return this._skipping(sql)
|
2015-04-19 16:31:52 -04:00
|
|
|
}
|
|
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
if (typeof sql === 'string') sql = {sql: sql}
|
|
|
|
|
this.emit('query:stream', assign({__knexUid: conn.__knexUid}, sql))
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
return this.client.stream(conn, sql, stream, options)
|
|
|
|
|
},
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
_skipping: function() {
|
|
|
|
|
return Promise.reject(new Error('Transaction ' + this.txid + ' has already been released skipping: ' + sql))
|
|
|
|
|
},
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
// The transactor is a full featured knex object, with a "commit",
|
|
|
|
|
// a "rollback" and a "savepoint" function. The "savepoint" is just
|
|
|
|
|
// sugar for creating a new transaction. If the rollback is run
|
|
|
|
|
// inside a savepoint, it rolls back to the last savepoint - otherwise
|
|
|
|
|
// it rolls back the transaction.
|
|
|
|
|
makeTransactor: function(connection, trxClient) {
|
|
|
|
|
var t = this
|
|
|
|
|
var transactor = makeKnex(trxClient)
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
transactor.transaction = function(container, options) {
|
|
|
|
|
return trxClient.transaction(t).run(container, options)
|
|
|
|
|
}
|
|
|
|
|
transactor.savepoint = function(container, options) {
|
|
|
|
|
return transactor.transaction(container, options)
|
|
|
|
|
}
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
if (this.client.transacting) {
|
|
|
|
|
transactor.commit = function(value) {
|
|
|
|
|
debugTx('%s: releasing savepoint', t.txid)
|
|
|
|
|
return t.release(connection, value)
|
|
|
|
|
}
|
|
|
|
|
transactor.rollback = function(error) {
|
|
|
|
|
debugTx('%s: rolling back savepoint', t.txid)
|
|
|
|
|
return t.rollbackTo(connection, error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
transactor.commit = function(value) {
|
|
|
|
|
debugTx('%s: committing', t.txid)
|
|
|
|
|
return t.commit(connection, value)
|
|
|
|
|
}
|
|
|
|
|
transactor.rollback = function(error) {
|
|
|
|
|
debugTx('%s: rolling back', t.txid)
|
|
|
|
|
return t.rollback(connection, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return transactor
|
|
|
|
|
},
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
// We need to make a client object which always acquires the same
|
|
|
|
|
// connection and does not release back into the pool.
|
|
|
|
|
makeClient: function(connection) {
|
|
|
|
|
var t = this
|
|
|
|
|
var trxClient = Object.create(this.client.constructor.prototype)
|
|
|
|
|
trxClient.config = this.client.config
|
|
|
|
|
trxClient.transacting = true;
|
2015-04-19 16:31:52 -04:00
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
trxClient.query = function(conn, obj) {
|
|
|
|
|
return Promise.try(function() {
|
|
|
|
|
if (conn !== connection) throw new Error('Invalid connection for transaction query.')
|
|
|
|
|
return t.query(conn, obj)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
trxClient.stream = function(conn, obj, stream, options) {
|
|
|
|
|
return Promise.try(function() {
|
|
|
|
|
if (conn !== connection) throw new Error('Invalid connection for transaction query.')
|
|
|
|
|
return t.stream(conn, obj, stream, options)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trxClient.acquireConnection = function() {
|
|
|
|
|
return t._queue.then(function() {
|
|
|
|
|
return connection
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
trxClient.releaseConnection = function() {
|
|
|
|
|
return Promise.resolve()
|
2015-04-19 16:31:52 -04:00
|
|
|
}
|
|
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
return trxClient
|
2015-04-19 16:31:52 -04:00
|
|
|
}
|
2015-04-22 10:34:14 -04:00
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Allow the `Transaction` object to be utilized with
|
|
|
|
|
// full access to the relevant promise API.
|
|
|
|
|
require('./interface')(Transaction)
|
|
|
|
|
|
|
|
|
|
Transaction.prototype.transacting = undefined
|
|
|
|
|
|
|
|
|
|
// Passed a `container` function, this method runs the current
|
|
|
|
|
// transaction, returning a promise.
|
|
|
|
|
Transaction.prototype.then = function(/* onFulfilled, onRejected */) {
|
|
|
|
|
return this._dfd.then.apply(this._dfd, arguments)
|
2015-04-19 16:31:52 -04:00
|
|
|
}
|
|
|
|
|
|
2015-04-22 10:34:14 -04:00
|
|
|
// Passed a `container` function, this method runs the current
|
|
|
|
|
// transaction, returning a promise.
|
|
|
|
|
Transaction.prototype.catch = function(/* onFulfilled, onRejected */) {
|
|
|
|
|
return this._dfd.catch.apply(this._dfd, arguments)
|
|
|
|
|
}
|
2013-09-05 16:36:49 -04:00
|
|
|
|
2015-02-07 12:20:24 -05:00
|
|
|
module.exports = Transaction;
|