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

Commit 9f0b5d1a by Sushant Committed by Jan Aagaard Meier

Migrate to Node-MySQL2 (#6354)

* node-mysql2 install

* fixed geom, buffer and connection error handling

* disconnect connections gracefully

* switch work

* typeCast is global now, removed comment

* use mysql promise wrapper

* use sequelize's Promise instance

* used domains to catch errors properly

* parse named timezones for mysql

* return null when BLOB is empty

* use connection event emitter errors, why I didnt think about it earlier :(

* using connect fix introduced in mysql2@rc9

* handshake phase event cleanup

* need refer defined before using them

* use latedef flag with const

* [ci skip] Changelog entry
1 parent 32ec4e27
# Future # Future
- [INTERNAL] Migrated to `node-mysql2` for prepared statements [#6354](https://github.com/sequelize/sequelize/issues/6354)
- [ADDED] SQLCipher support via the SQLite connection manager - [ADDED] SQLCipher support via the SQLite connection manager
- [CHANGED] Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive) [#5990](https://github.com/sequelize/sequelize/issues/5990) - [CHANGED] Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive) [#5990](https://github.com/sequelize/sequelize/issues/5990)
- [ADDED] Support for range operators [#5990](https://github.com/sequelize/sequelize/issues/5990) - [ADDED] Support for range operators [#5990](https://github.com/sequelize/sequelize/issues/5990)
......
...@@ -33,7 +33,7 @@ Once done, you can install Sequelize and the connector for your database of choi ...@@ -33,7 +33,7 @@ Once done, you can install Sequelize and the connector for your database of choi
```bash ```bash
$ npm install --save sequelize $ npm install --save sequelize
$ npm install --save pg # for postgres $ npm install --save pg # for postgres
$ npm install --save mysql # for mysql $ npm install --save mysql2 # for mysql
$ npm install --save sqlite3 # for sqlite $ npm install --save sqlite3 # for sqlite
``` ```
......
...@@ -7,7 +7,7 @@ $ npm install --save sequelize ...@@ -7,7 +7,7 @@ $ npm install --save sequelize
# And one of the following: # And one of the following:
$ npm install --save pg pg-hstore $ npm install --save pg pg-hstore
$ npm install --save mysql $ npm install --save mysql2
$ npm install --save sqlite3 $ npm install --save sqlite3
$ npm install --save tedious // MSSQL $ npm install --save tedious // MSSQL
``` ```
......
...@@ -52,7 +52,6 @@ var sequelize = new Sequelize('database', 'username', 'password', { ...@@ -52,7 +52,6 @@ var sequelize = new Sequelize('database', 'username', 'password', {
logging: false, logging: false,
   
// the sql dialect of the database // the sql dialect of the database
// - default is 'mysql'
// - currently supported: 'mysql', 'sqlite', 'postgres', 'mssql' // - currently supported: 'mysql', 'sqlite', 'postgres', 'mssql'
dialect: 'mysql', dialect: 'mysql',
   
...@@ -166,13 +165,10 @@ With the release of Sequelize`1.6.0`, the library got independent from specific ...@@ -166,13 +165,10 @@ With the release of Sequelize`1.6.0`, the library got independent from specific
### MySQL ### MySQL
In order to get Sequelize working nicely together with MySQL, you'll need to install`mysql@~2.5.0`or higher. Once that's done you can use it like this: In order to get Sequelize working nicely together with MySQL, you'll need to install`mysql2@^1.0.0-rc.10`or higher. Once that's done you can use it like this:
```js ```js
var sequelize = new Sequelize('database', 'username', 'password', { var sequelize = new Sequelize('database', 'username', 'password', {
// mysql is the default dialect, but you know...
// for demo purposes we are defining it nevertheless :)
// so: we want mysql!
dialect: 'mysql' dialect: 'mysql'
}) })
``` ```
......
'use strict'; 'use strict';
const AbstractConnectionManager = require('../abstract/connection-manager'); const AbstractConnectionManager = require('../abstract/connection-manager');
const SequelizeErrors = require('../../errors');
const Utils = require('../../utils'); const Utils = require('../../utils');
const DataTypes = require('../../data-types').mysql;
const momentTz = require('moment-timezone');
const debug = Utils.getLogger().debugContext('connection:mysql'); const debug = Utils.getLogger().debugContext('connection:mysql');
const Promise = require('../../promise');
const sequelizeErrors = require('../../errors');
const dataTypes = require('../../data-types').mysql;
const parserMap = new Map(); const parserMap = new Map();
/**
* MySQL Connection Managger
*
* Get connections, validate and disconnect them.
* AbstractConnectionManager pooling use it to handle MySQL specific connections
* Use https://github.com/sidorares/node-mysql2 to connect with MySQL server
*
* @extends AbstractConnectionManager
* @return Class<ConnectionManager>
*/
class ConnectionManager extends AbstractConnectionManager { class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) { constructor(dialect, sequelize) {
super(dialect, sequelize); super(dialect, sequelize);
...@@ -18,19 +29,19 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -18,19 +29,19 @@ class ConnectionManager extends AbstractConnectionManager {
if (sequelize.config.dialectModulePath) { if (sequelize.config.dialectModulePath) {
this.lib = require(sequelize.config.dialectModulePath); this.lib = require(sequelize.config.dialectModulePath);
} else { } else {
this.lib = require('mysql'); this.lib = require('mysql2');
} }
} catch (err) { } catch (err) {
if (err.code === 'MODULE_NOT_FOUND') { if (err.code === 'MODULE_NOT_FOUND') {
throw new Error('Please install mysql package manually'); throw new Error('Please install mysql2 package manually');
} }
throw err; throw err;
} }
this.refreshTypeParser(dataTypes); this.refreshTypeParser(DataTypes);
} }
// Expose this as a method so that the parsing may be updated when the user has added additional, custom types // Update parsing when the user has added additional, custom types
_refreshTypeParser(dataType) { _refreshTypeParser(dataType) {
for (const type of dataType.types.mysql) { for (const type of dataType.types.mysql) {
parserMap.set(type, dataType.parse); parserMap.set(type, dataType.parse);
...@@ -43,14 +54,19 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -43,14 +54,19 @@ class ConnectionManager extends AbstractConnectionManager {
static _typecast(field, next) { static _typecast(field, next) {
if (parserMap.has(field.type)) { if (parserMap.has(field.type)) {
return parserMap.get(field.type)(field, this.sequelize.options); return parserMap.get(field.type)(field, this.sequelize.options, next);
} }
return next(); return next();
} }
/**
* Connect with MySQL database based on config, Handle any errors in connection
* Set the pool handlers on connection.error
* Also set proper timezone once conection is connected
*
* @return Promise<Connection>
*/
connect(config) { connect(config) {
return new Promise((resolve, reject) => {
const connectionConfig = { const connectionConfig = {
host: config.host, host: config.host,
port: config.port, port: config.port,
...@@ -69,46 +85,36 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -69,46 +85,36 @@ class ConnectionManager extends AbstractConnectionManager {
} }
} }
const connection = this.lib.createConnection(connectionConfig); return new Utils.Promise((resolve, reject) => {
const connection = this.lib.createConnection(connectionConfig);
connection.connect(err => { /*jshint latedef:false*/
if (err) { const errorHandler = (e) => {
if (err.code) { // clean up connect event if there is error
switch (err.code) { connection.removeListener('connect', connectHandler);
case 'ECONNREFUSED': reject(e);
reject(new sequelizeErrors.ConnectionRefusedError(err)); };
break;
case 'ER_ACCESS_DENIED_ERROR':
reject(new sequelizeErrors.AccessDeniedError(err));
break;
case 'ENOTFOUND':
reject(new sequelizeErrors.HostNotFoundError(err));
break;
case 'EHOSTUNREACH':
reject(new sequelizeErrors.HostNotReachableError(err));
break;
case 'EINVAL':
reject(new sequelizeErrors.InvalidConnectionError(err));
break;
default:
reject(new sequelizeErrors.ConnectionError(err));
break;
}
} else {
reject(new sequelizeErrors.ConnectionError(err));
}
return; const connectHandler = () => {
} // clean up error event if connected
connection.removeListener('error', errorHandler);
resolve(connection);
};
/*jshint latedef:true*/
connection.once('error', errorHandler);
connection.once('connect', connectHandler);
})
.then((connection) => {
if (config.pool.handleDisconnects) { if (config.pool.handleDisconnects) {
// Connection to the MySQL server is usually // Connection to the MySQL server is usually
// lost due to either server restart, or a // lost due to either server restart, or a
// connnection idle timeout (the wait_timeout // connection idle timeout (the wait_timeout
// server variable configures this) // server variable configures this)
// //
// See [stackoverflow answer](http://stackoverflow.com/questions/20210522/nodejs-mysql-error-connection-lost-the-server-closed-the-connection) // See [stackoverflow answer](http://stackoverflow.com/questions/20210522/nodejs-mysql-error-connection-lost-the-server-closed-the-connection)
connection.on('error', err => { connection.on('error', (err) => {
if (err.code === 'PROTOCOL_CONNECTION_LOST') { if (err.code === 'PROTOCOL_CONNECTION_LOST') {
// Remove it from read/write pool // Remove it from read/write pool
this.pool.destroy(connection); this.pool.destroy(connection);
...@@ -116,35 +122,66 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -116,35 +122,66 @@ class ConnectionManager extends AbstractConnectionManager {
debug(`connection error ${err.code}`); debug(`connection error ${err.code}`);
}); });
} }
debug(`connection acquired`); debug(`connection acquired`);
resolve(connection); return connection;
})
.then((connection) => {
return new Utils.Promise((resolve, reject) => {
// set timezone for this connection
// but named timezone are not directly supported in mysql, so get its offset first
let tzOffset = this.sequelize.options.timezone;
tzOffset = /\//.test(tzOffset) ? momentTz.tz(tzOffset).format('Z') : tzOffset;
connection.query(`SET time_zone = '${tzOffset}'`, (err) => {
if (err) { reject(err); } else { resolve(connection); }
});
});
})
.catch((err) => {
if (err.code) {
switch (err.code) {
case 'ECONNREFUSED':
throw new SequelizeErrors.ConnectionRefusedError(err);
case 'ER_ACCESS_DENIED_ERROR':
throw new SequelizeErrors.AccessDeniedError(err);
case 'ENOTFOUND':
throw new SequelizeErrors.HostNotFoundError(err);
case 'EHOSTUNREACH':
throw new SequelizeErrors.HostNotReachableError(err);
case 'EINVAL':
throw new SequelizeErrors.InvalidConnectionError(err);
default:
throw new SequelizeErrors.ConnectionError(err);
}
} else {
throw new SequelizeErrors.ConnectionError(err);
}
}); });
}).tap(connection => {
connection.query("SET time_zone = '" + this.sequelize.options.timezone + "'"); /* jshint ignore: line */
});
} }
disconnect(connection) { disconnect(connection) {
// Dont disconnect connections with an ended protocol // Dont disconnect connections with CLOSED state
// That wil trigger a connection error if (connection._closing) {
if (connection._protocol._ended) { debug(`connection tried to disconnect but was already at CLOSED state`);
debug(`connection tried to disconnect but was already at ENDED state`); return Utils.Promise.resolve();
return Promise.resolve();
} }
return new Promise((resolve, reject) => { return new Utils.Promise((resolve, reject) => {
connection.end(err => { connection.end((err) => {
if (err) return reject(new sequelizeErrors.ConnectionError(err)); if (err) {
debug(`connection disconnected`); reject(new SequelizeErrors.ConnectionError(err));
resolve(); } else {
debug(`connection disconnected`);
resolve();
}
}); });
}); });
} }
validate(connection) { validate(connection) {
return connection && ['disconnected', 'protocol_error'].indexOf(connection.state) === -1; return connection && connection._fatalError === null && connection._protocolError === null && !connection._closing;
} }
} }
......
...@@ -25,6 +25,22 @@ module.exports = BaseTypes => { ...@@ -25,6 +25,22 @@ module.exports = BaseTypes => {
BaseTypes.REAL.types.mysql = ['DOUBLE']; BaseTypes.REAL.types.mysql = ['DOUBLE'];
BaseTypes.DOUBLE.types.mysql = ['DOUBLE']; BaseTypes.DOUBLE.types.mysql = ['DOUBLE'];
function BLOB(length) {
if (!(this instanceof BLOB)) return new BLOB(length);
BaseTypes.BLOB.apply(this, arguments);
}
inherits(BLOB, BaseTypes.BLOB);
BLOB.parse = function (value, options, next) {
let data = next();
if (Buffer.isBuffer(data) && data.length === 0) {
return null;
}
return data;
};
function DECIMAL(precision, scale) { function DECIMAL(precision, scale) {
if (!(this instanceof DECIMAL)) return new DECIMAL(precision, scale); if (!(this instanceof DECIMAL)) return new DECIMAL(precision, scale);
BaseTypes.DECIMAL.apply(this, arguments); BaseTypes.DECIMAL.apply(this, arguments);
...@@ -91,6 +107,7 @@ module.exports = BaseTypes => { ...@@ -91,6 +107,7 @@ module.exports = BaseTypes => {
return 'CHAR(36) BINARY'; return 'CHAR(36) BINARY';
}; };
const SUPPORTED_GEOMETRY_TYPES = ['POINT', 'LINESTRING', 'POLYGON']; const SUPPORTED_GEOMETRY_TYPES = ['POINT', 'LINESTRING', 'POLYGON'];
function GEOMETRY(type, srid) { function GEOMETRY(type, srid) {
...@@ -110,8 +127,9 @@ module.exports = BaseTypes => { ...@@ -110,8 +127,9 @@ module.exports = BaseTypes => {
GEOMETRY.parse = GEOMETRY.prototype.parse = function parse(value) { GEOMETRY.parse = GEOMETRY.prototype.parse = function parse(value) {
value = value.buffer(); value = value.buffer();
//MySQL doesn't support POINT EMPTY, https://dev.mysql.com/worklog/task/?id=2381 // Empty buffer, MySQL doesn't support POINT EMPTY
if (value === null) { // check, https://dev.mysql.com/worklog/task/?id=2381
if (value.length === 0) {
return null; return null;
} }
...@@ -146,7 +164,8 @@ module.exports = BaseTypes => { ...@@ -146,7 +164,8 @@ module.exports = BaseTypes => {
DATE, DATE,
UUID, UUID,
GEOMETRY, GEOMETRY,
DECIMAL DECIMAL,
BLOB
}; };
_.forIn(exports, (DataType, key) => { _.forIn(exports, (DataType, key) => {
......
...@@ -42,8 +42,7 @@ class Query extends AbstractQuery { ...@@ -42,8 +42,7 @@ class Query extends AbstractQuery {
debug(`executing(${this.connection.uuid || 'default'}) : ${this.sql}`); debug(`executing(${this.connection.uuid || 'default'}) : ${this.sql}`);
return new Utils.Promise((resolve, reject) => { return new Utils.Promise((resolve, reject) => {
this.connection.query(this.sql, (err, results) => { this.connection.query({ sql: this.sql }, (err, results) => {
debug(`executed(${this.connection.uuid || 'default'}) : ${this.sql}`); debug(`executed(${this.connection.uuid || 'default'}) : ${this.sql}`);
if (benchmark) { if (benchmark) {
...@@ -61,7 +60,7 @@ class Query extends AbstractQuery { ...@@ -61,7 +60,7 @@ class Query extends AbstractQuery {
}) })
// Log warnings if we've got them. // Log warnings if we've got them.
.then(results => { .then(results => {
if (showWarnings && results && results.warningCount > 0) { if (showWarnings && results && results.warningStatus > 0) {
return this.logWarnings(results); return this.logWarnings(results);
} }
return results; return results;
...@@ -136,7 +135,6 @@ class Query extends AbstractQuery { ...@@ -136,7 +135,6 @@ class Query extends AbstractQuery {
return this.run('SHOW WARNINGS').then(warningResults => { return this.run('SHOW WARNINGS').then(warningResults => {
const warningMessage = 'MySQL Warnings (' + (this.connection.uuid||'default') + '): '; const warningMessage = 'MySQL Warnings (' + (this.connection.uuid||'default') + '): ';
const messages = []; const messages = [];
for (const _warningRow of warningResults) { for (const _warningRow of warningResults) {
for (const _warningResult of _warningRow) { for (const _warningResult of _warningRow) {
if (_warningResult.hasOwnProperty('Message')) { if (_warningResult.hasOwnProperty('Message')) {
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
"jshint": "^2.9.2", "jshint": "^2.9.2",
"lcov-result-merger": "^1.2.0", "lcov-result-merger": "^1.2.0",
"mocha": "^3.0.2", "mocha": "^3.0.2",
"mysql": "~2.11.1", "mysql2": "^1.0.0-rc.10",
"pg": "^6.0.0", "pg": "^6.0.0",
"pg-hstore": "^2.3.1", "pg-hstore": "^2.3.1",
"pg-native": "^1.8.0", "pg-native": "^1.8.0",
......
...@@ -396,7 +396,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), function() { ...@@ -396,7 +396,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), function() {
return Model.sync({ force: true }).then(function () { return Model.sync({ force: true }).then(function () {
return Model.create(sampleData); return Model.create(sampleData);
}).then(function () { }).then(function () {
return Model.find({id: 1}); return Model.findById(1);
}).then(function (user) { }).then(function (user) {
expect(user.get('jewelPurity')).to.be.eql(sampleData.jewelPurity); expect(user.get('jewelPurity')).to.be.eql(sampleData.jewelPurity);
expect(user.get('jewelPurity')).to.be.string; expect(user.get('jewelPurity')).to.be.string;
......
...@@ -100,7 +100,7 @@ if (dialect === 'mysql') { ...@@ -100,7 +100,7 @@ if (dialect === 'mysql') {
conn = connection; conn = connection;
// simulate a unexpected end // simulate a unexpected end
connection._protocol.end(); connection.close();
}) })
.then(function() { .then(function() {
return cm.releaseConnection(conn); return cm.releaseConnection(conn);
......
...@@ -415,8 +415,7 @@ if (current.dialect.supports.groupedLimit) { ...@@ -415,8 +415,7 @@ if (current.dialect.supports.groupedLimit) {
include: [{ model: Task, limit: 2, as: 'tasks', order:[['id', 'ASC']] }], include: [{ model: Task, limit: 2, as: 'tasks', order:[['id', 'ASC']] }],
order: [ order: [
['id', 'ASC'] ['id', 'ASC']
], ]
logging: console.log
}).then((result) => { }).then((result) => {
expect(result[0].tasks.length).to.equal(2); expect(result[0].tasks.length).to.equal(2);
expect(result[0].tasks[0].title).to.equal('b'); expect(result[0].tasks[0].title).to.equal('b');
......
...@@ -2383,7 +2383,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -2383,7 +2383,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return; return;
}).catch (function(err) { }).catch (function(err) {
if (dialect === 'mysql') { if (dialect === 'mysql') {
expect(err.message).to.match(/ER_CANNOT_ADD_FOREIGN|ER_CANT_CREATE_TABLE/); expect(err.message).to.match(/Can\'t create table/);
} else if (dialect === 'sqlite') { } else if (dialect === 'sqlite') {
// the parser should not end up here ... see above // the parser should not end up here ... see above
expect(1).to.equal(2); expect(1).to.equal(2);
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!