Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not reject promise on transaction rollback #3235

Merged
merged 9 commits into from
Jun 21, 2019
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Master (Unreleased)

### Bug fixes:

- Do not reject promise on transaction rollback (only for new, non-callback, style of transactions for now to avoid breaking old code) #3235

### New features:

- Use extension from knexfile for generating migrations unless overriden #3282
Expand Down
4 changes: 4 additions & 0 deletions src/dialects/mssql/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ module.exports = class Transaction_MSSQL extends Transaction {
() => {
let err = error;
if (isUndefined(error)) {
if (this.doNotRejectOnRollback) {
this._resolver();
return;
}
err = new Error(`Transaction rejected with non-error: ${error}`);
}
this._rejecter(err);
Expand Down
8 changes: 8 additions & 0 deletions src/dialects/mysql/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ assign(Transaction_MySQL.prototype, {
if (status === 1) t._resolver(value);
if (status === 2) {
if (isUndefined(value)) {
if (
sql &&
sql.toUpperCase() === 'ROLLBACK' &&
t.doNotRejectOnRollback
) {
t._resolver();
return;
}
value = new Error(`Transaction rejected with non-error: ${value}`);
}
t._rejecter(value);
Expand Down
8 changes: 8 additions & 0 deletions src/dialects/mysql2/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ assign(Transaction_MySQL2.prototype, {
if (status === 1) t._resolver(value);
if (status === 2) {
if (isUndefined(value)) {
if (
sql &&
sql.toUpperCase() === 'ROLLBACK' &&
t.doNotRejectOnRollback
) {
t._resolver();
return;
}
value = new Error(`Transaction rejected with non-error: ${value}`);
}
t._rejecter(value);
Expand Down
4 changes: 4 additions & 0 deletions src/dialects/oracle/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ module.exports = class Oracle_Transaction extends Transaction {
.throw(err)
.catch((error) => {
if (isUndefined(error)) {
if (this.doNotRejectOnRollback) {
this._resolver();
return;
}
error = new Error(`Transaction rejected with non-error: ${error}`);
}

Expand Down
4 changes: 4 additions & 0 deletions src/dialects/oracledb/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ module.exports = class Oracle_Transaction extends Transaction {
})
.then(function() {
if (isUndefined(err)) {
if (self.doNotRejectOnRollback) {
self._resolver();
return;
}
err = new Error(`Transaction rejected with non-error: ${err}`);
}
self._rejecter(err);
Expand Down
24 changes: 23 additions & 1 deletion src/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const makeKnex = require('./util/make-knex');

const debug = Debug('knex:tx');

const { uniqueId, isUndefined } = require('lodash');
const { uniqueId, isUndefined, get } = require('lodash');

// Acts as a facade for a Promise, keeping the internal state
// and managing any child transactions.
Expand All @@ -20,12 +20,26 @@ class Transaction extends EventEmitter {

// If there is no container provided, assume user wants to get instance of transaction and use it directly
if (!container) {
// Default behaviour for new style of transactions is not to reject on rollback
if (!config || isUndefined(config.doNotRejectOnRollback)) {
this.doNotRejectOnRollback = true;
} else {
this.doNotRejectOnRollback = config.doNotRejectOnRollback;
}

this.initPromise = new Promise((resolve, reject) => {
this.initRejectFn = reject;
container = (transactor) => {
resolve(transactor);
};
});
} else {
// Default behaviour for old style of transactions is to reject on rollback
if (!config || isUndefined(config.doNotRejectOnRollback)) {
this.doNotRejectOnRollback = false;
} else {
this.doNotRejectOnRollback = config.doNotRejectOnRollback;
}
}

this.client = client;
Expand Down Expand Up @@ -173,6 +187,14 @@ class Transaction extends EventEmitter {
}
if (status === 2) {
if (isUndefined(value)) {
if (
get(res, 'context.sql', '').toUpperCase() === 'ROLLBACK' &&
this.doNotRejectOnRollback
) {
this._resolver();
return;
}

value = new Error(`Transaction rejected with non-error: ${value}`);
}
this._rejecter(value);
Expand Down
9 changes: 9 additions & 0 deletions test/integration/builder/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,15 @@ module.exports = function(knex) {
});
});

it('does not reject promise when rolling back a transaction', async () => {
const trxProvider = knex.transactionProvider();
const trxPromise = trxProvider();

await trxPromise.then((trx) => {
return trx.rollback();
});
});

it('should allow for nested transactions', function() {
if (/redshift/i.test(knex.client.driverName)) {
return Promise.resolve();
Expand Down
31 changes: 29 additions & 2 deletions test/unit/knex.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ describe('knex', () => {
expect(result).to.be.undefined;
});

it('rejects execution promise if there was a rollback', async () => {
it('resolves execution promise if there was a manual rollback and transaction is set not to reject', async () => {
const knex = Knex(sqliteConfig);

const trx = await knex.transaction();
Expand All @@ -373,6 +373,23 @@ describe('knex', () => {
expect(rows[0].result).to.equal(1);
await trx.rollback();

const result = await executionPromise;
expect(result).to.be.undefined;
});

it('rejects execution promise if there was a manual rollback and transaction is set to reject', async () => {
const knex = Knex(sqliteConfig);

const trx = await knex.transaction(undefined, {
doNotRejectOnRollback: false,
});
const executionPromise = trx.executionPromise;

expect(trx.client.transacting).to.equal(true);
const rows = await knex.transacting(trx).select(knex.raw('1 as result'));
expect(rows[0].result).to.equal(1);
await trx.rollback();

let errorWasThrown;
try {
await executionPromise;
Expand All @@ -382,10 +399,19 @@ describe('knex', () => {
'Transaction rejected with non-error: undefined'
);
}

expect(errorWasThrown).to.be.true;
});

it('does not reject promise when rolling back a transaction', async () => {
const knex = Knex(sqliteConfig);
const trxProvider = knex.transactionProvider();
const trxPromise = trxProvider();

await trxPromise.then((trx) => {
return trx.rollback();
});
});

it('creating transaction copy with user params should throw an error', () => {
if (!sqliteConfig) {
return;
Expand All @@ -409,6 +435,7 @@ describe('knex', () => {
function ClientFoobar(config) {
SqliteClient.call(this, config);
}

inherits(ClientFoobar, SqliteClient);

ClientFoobar.prototype._driver = () => {
Expand Down