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

This PR adds support for .transaction() #814

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,25 @@ async function main() {
);
```

### Using managed transaction

When using a promise pool, MySQL2 offers a .transaction() function to allow you to execute code within a transaction, with pre-defined behavior to roll back the transaction on error, and which automatically releases the connection after you are finished working with it:
```js
async function main() {
// get the client
const mysql = require('mysql2/promise');
// create the pool
const pool = mysql.createPool({host:'localhost', user: 'root', database: 'test'});
// using the pool, execute a chain of queries within a transaction
pool.transaction({ autoCommit: false, readWrite: true, consistentSnapshot: false, isolationLevel: mysql.ISOLATION_LEVEL.READ_COMMITTED}, async function(con) {
const [rows, fields] = await con.query('SELECT * FROM `table` WHERE `name` = ? AND `age` > ?', ['Morty', 14]);
await con.execute('INSERT INTO `table` (name,age) VALUES(?,?)', ['Bob',rows[0].age]); // Bob and Morty are the same age
});
// If the promise chain passed to .transaction() resolves, .transaction() commits the changes, and releases the connection.
// If the promise chain passed to .transaction() rejects, or if a connection or SQL error occures, .transaction() rolls back the changes, and releases the connection.
}
```

## API and Configuration

MySQL2 is mostly API compatible with [Node MySQL][node-mysql]. You should check their API documentation to see all available API options.
Expand Down
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,10 @@ exports.setMaxParserCache = function(max) {
exports.clearParserCache = function() {
parserCache.clearCache();
};

exports.ISOLATION_LEVEL = {
READ_UNCOMMITTED: 'READ UNCOMMITTED',
READ_COMMITTED: 'READ COMMITTED',
REPEATABLE_READ: 'REPEATABLE READ',
SERIALIZABLE: 'SERIALIZABLE'
};
23 changes: 21 additions & 2 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -679,8 +679,27 @@ class Connection extends EventEmitter {
}

// transaction helpers
beginTransaction(cb) {
return this.query('START TRANSACTION', cb);
beginTransaction(options,cb) {
if (cb === undefined) {
cb = options;
options = {};
}

const startTransactionSQL =
"START TRANSACTION" +
( options.consistentSnapshot === true ? " WITH CONSISTENT SNAPSHOT," : "" ) +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent quotes, apparently lint isn't enforcing this.

( options.readWrite !== false ? " READ WRITE" : " READ ONLY" )
;


if (options.isolationLevel) {
this.query("SET TRANSACTION ISOLATION LEVEL " + options.isolationLevel, err => {
if (err) return cb(err);
this.query(startTransactionSQL,cb);
});
} else {
this.query(startTransactionSQL,cb);
}
}

commit(cb) {
Expand Down
8 changes: 4 additions & 4 deletions lib/packets/packet.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,7 @@ class Packet {
result.push({ x: x, y: y });
}
break;
case 3: // WKBPolygon
// eslint-disable-next-line no-case-declarations
case 3: { // WKBPolygon
const numRings = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
Expand All @@ -555,11 +554,11 @@ class Packet {
result.push(line);
}
break;
}
case 4: // WKBMultiPoint
case 5: // WKBMultiLineString
case 6: // WKBMultiPolygon
case 7: // WKBGeometryCollection
// eslint-disable-next-line no-case-declarations
case 7: { // WKBGeometryCollection
const num = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
Expand All @@ -569,6 +568,7 @@ class Packet {
result.push(parseGeometry());
}
break;
}
}
return result;
}
Expand Down
52 changes: 50 additions & 2 deletions promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,47 @@ class PromiseConnection extends EventEmitter {
});
}

beginTransaction() {
beginTransaction(options) {
const c = this.connection;
const localErr = new Error();
return new this.Promise((resolve, reject) => {
const done = makeDoneCb(resolve, reject, localErr);
c.beginTransaction(done);
c.beginTransaction(options,done);
});
}

transaction(options,promiseCallback) {
if (! promiseCallback) {
promiseCallback = options;
options = {};
}
options = options || {};

let promiseChain = this.Promise.resolve();

if (options.autoCommit !== true) {
promiseChain = promiseChain.then(() => this.beginTransaction(options));
}

promiseChain = promiseChain.then(() => promiseCallback(this));

if (options.autoCommit !== true) {
promiseChain = promiseChain
.then( res =>
this.commit()
.then(
() => res
)
)
.catch( err =>
this.rollback()
.catch(() => null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably emit an error or even override the previous error, it certainly shouldn't be swallowed.

.then(() => { throw err })
)
}
return promiseChain;
}

commit() {
const c = this.connection;
const localErr = new Error();
Expand Down Expand Up @@ -330,6 +362,21 @@ class PromisePool extends EventEmitter {
});
}

transaction(options,promiseCallback) {
return this.getConnection()
.then(con =>
con.transaction(options,promiseCallback)
.then(res => {
con.release();
return res;
})
.catch(err => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, you probably want to merge the then and catch into one then(ok, err) chain instead, otherwise the catch handler would potentially catch .release() errors.

con.release();
throw err;
})
);
}

execute(sql, values) {
const corePool = this.pool;
const localErr = new Error();
Expand Down Expand Up @@ -403,3 +450,4 @@ module.exports.raw = core.raw;
module.exports.PromisePool = PromisePool;
module.exports.PromiseConnection = PromiseConnection;
module.exports.PromisePoolConnection = PromisePoolConnection;
module.exports.ISOLATION_LEVEL = core.ISOLATION_LEVEL;