Comparing version 2.2.5 to 2.3.0
@@ -0,1 +1,18 @@ | ||
2.3.0 | ||
- Add PoolCluster promise wrappers #1369, #1363 | ||
- support for connect and query timeouts #1364 | ||
- add missing query() method on PoolCluster #1362 | ||
- fix incorrect parsing of passwords | ||
containing ":" #1357 | ||
- handle errors generated by asynchronous | ||
authentication plugins #1354 | ||
- add proper handshake fatal error handling #1352 | ||
- fix tests to work with the latest MySQL | ||
server versions (up to 8.0.25) #1338 | ||
- expose SQL query in errors #1295 | ||
- typing and readme docs for rowAsArray #1288 | ||
- allow unnamed placeholders even if the | ||
namedPlaceholders flag is enabled #1251 | ||
- better ESM support #1217 | ||
2.2.5 ( 21/09/2020 ) | ||
@@ -2,0 +19,0 @@ - typings: add ResultSetHeader #1213 |
@@ -154,5 +154,8 @@ import { | ||
type authPlugins = | ||
(pluginMetadata: { connection: Connection; command: string }) => | ||
(pluginData: Buffer) => Promise<string>; | ||
type authPlugins = (pluginMetadata: { | ||
connection: Connection; | ||
command: string; | ||
}) => ( | ||
pluginData: Buffer | ||
) => Promise<string> | string | Buffer | Promise<Buffer> | null; | ||
@@ -179,3 +182,3 @@ export interface ConnectionOptions extends mysql.ConnectionOptions { | ||
authPlugins?: { | ||
[key: string]: authPlugins; | ||
[key: string]: authPlugins; | ||
}; | ||
@@ -182,0 +185,0 @@ } |
@@ -17,2 +17,3 @@ 'use strict'; | ||
const Pool = require('./lib/pool.js'); | ||
const PoolCluster = require('./lib/pool_cluster.js'); | ||
@@ -33,2 +34,4 @@ exports.createPool = function(config) { | ||
exports.PoolCluster = PoolCluster; | ||
exports.createServer = function(handler) { | ||
@@ -35,0 +38,0 @@ const Server = require('./lib/server.js'); |
@@ -0,1 +1,6 @@ | ||
// This file was modified by Oracle on July 5, 2021. | ||
// Errors generated by asynchronous authentication plugins are now being | ||
// handled and subsequently emitted at the command level. | ||
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | ||
'use strict'; | ||
@@ -20,2 +25,10 @@ | ||
function authSwitchPluginError(error, command) { | ||
// Authentication errors are fatal | ||
error.code = 'AUTH_SWITCH_PLUGIN_ERROR'; | ||
error.fatal = true; | ||
command.emit('error', error); | ||
} | ||
function authSwitchRequest(packet, connection, command) { | ||
@@ -38,4 +51,3 @@ const { pluginName, pluginData } = Packets.AuthSwitchRequest.fromPacket( | ||
if (err) { | ||
connection.emit('error', err); | ||
return; | ||
return authSwitchPluginError(err, command); | ||
} | ||
@@ -59,6 +71,8 @@ connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket()); | ||
} | ||
}).catch(err => { | ||
authSwitchPluginError(err, command); | ||
}); | ||
} | ||
function authSwitchRequestMoreData(packet, connection) { | ||
function authSwitchRequestMoreData(packet, connection, command) { | ||
const { data } = Packets.AuthSwitchRequestMoreData.fromPacket(packet); | ||
@@ -71,4 +85,3 @@ | ||
if (err) { | ||
connection.emit('error', err); | ||
return; | ||
return authSwitchPluginError(err, command); | ||
} | ||
@@ -89,2 +102,4 @@ connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket()); | ||
} | ||
}).catch(err => { | ||
authSwitchPluginError(err, command); | ||
}); | ||
@@ -91,0 +106,0 @@ } |
@@ -0,1 +1,6 @@ | ||
// This file was modified by Oracle on June 17, 2021. | ||
// Handshake errors are now maked as fatal and the corresponding events are | ||
// emitted in the command instance itself. | ||
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | ||
'use strict'; | ||
@@ -154,6 +159,10 @@ | ||
} catch (err) { | ||
// Authentication errors are fatal | ||
err.code = 'AUTH_SWITCH_PLUGIN_ERROR'; | ||
err.fatal = true; | ||
if (this.onResult) { | ||
this.onResult(err); | ||
} else { | ||
connection.emit('error', err); | ||
this.emit('error', err); | ||
} | ||
@@ -165,6 +174,10 @@ return null; | ||
const err = new Error('Unexpected packet during handshake phase'); | ||
// Unknown handshake errors are fatal | ||
err.code = 'HANDSHAKE_UNKNOWN_ERROR'; | ||
err.fatal = true; | ||
if (this.onResult) { | ||
this.onResult(err); | ||
} else { | ||
connection.emit('error', err); | ||
this.emit('error', err); | ||
} | ||
@@ -171,0 +184,0 @@ return null; |
@@ -29,2 +29,3 @@ 'use strict'; | ||
const err = packet.asError(connection.clientEncoding); | ||
err.sql = this.sql || this.query; | ||
if (this.onResult) { | ||
@@ -31,0 +32,0 @@ this.onResult(err); |
@@ -18,2 +18,4 @@ 'use strict'; | ||
this.insertId = 0; | ||
this.timeout = options.timeout; | ||
this.queryTimeout = null; | ||
this._rows = []; | ||
@@ -39,2 +41,3 @@ this._fields = []; | ||
this.options = Object.assign({}, connection.config, this._executeOptions); | ||
this._setTimeout(); | ||
const executePacket = new Packets.Execute( | ||
@@ -101,2 +104,4 @@ this.statement.id, | ||
Execute.prototype._streamLocalInfile = Query.prototype._streamLocalInfile; | ||
Execute.prototype._setTimeout = Query.prototype._setTimeout; | ||
Execute.prototype._handleTimeoutError = Query.prototype._handleTimeoutError; | ||
Execute.prototype.row = Query.prototype.row; | ||
@@ -103,0 +108,0 @@ Execute.prototype.stream = Query.prototype.stream; |
'use strict'; | ||
const process = require('process'); | ||
const Timers = require('timers'); | ||
@@ -24,2 +25,4 @@ const Readable = require('stream').Readable; | ||
this.onResult = callback; | ||
this.timeout = options.timeout; | ||
this.queryTimeout = null; | ||
this._fieldCount = 0; | ||
@@ -52,2 +55,4 @@ this._rowParser = null; | ||
this.options = Object.assign({}, connection.config, this._queryOptions); | ||
this._setTimeout(); | ||
const cmdPacket = new Packets.Query( | ||
@@ -63,2 +68,11 @@ this.sql, | ||
this._unpipeStream(); | ||
// if all ready timeout, return null directly | ||
if (this.timeout && !this.queryTimeout) { | ||
return null; | ||
} | ||
// else clear timer | ||
if (this.queryTimeout) { | ||
Timers.clearTimeout(this.queryTimeout); | ||
this.queryTimeout = null; | ||
} | ||
if (this.onResult) { | ||
@@ -278,2 +292,30 @@ let rows, fields; | ||
} | ||
_setTimeout() { | ||
if (this.timeout) { | ||
const timeoutHandler = this._handleTimeoutError.bind(this); | ||
this.queryTimeout = Timers.setTimeout( | ||
timeoutHandler, | ||
this.timeout | ||
); | ||
} | ||
} | ||
_handleTimeoutError() { | ||
if (this.queryTimeout) { | ||
Timers.clearTimeout(this.queryTimeout); | ||
this.queryTimeout = null; | ||
} | ||
const err = new Error('Query inactivity timeout'); | ||
err.errorno = 'PROTOCOL_SEQUENCE_TIMEOUT'; | ||
err.code = 'PROTOCOL_SEQUENCE_TIMEOUT'; | ||
err.syscall = 'query'; | ||
if (this.onResult) { | ||
this.onResult(err); | ||
} else { | ||
this.emit('error', err); | ||
} | ||
} | ||
} | ||
@@ -280,0 +322,0 @@ |
'use strict'; | ||
const urlParse = require('url').parse; | ||
const { URL } = require('url'); | ||
const ClientConstants = require('./constants/client'); | ||
@@ -235,25 +235,19 @@ const Charsets = require('./constants/charsets'); | ||
static parseUrl(url) { | ||
url = urlParse(url, true); | ||
const parsedUrl = new URL(url); | ||
const options = { | ||
host: url.hostname, | ||
port: url.port, | ||
database: url.pathname.substr(1) | ||
host: parsedUrl.hostname, | ||
port: parsedUrl.port, | ||
database: parsedUrl.pathname.substr(1), | ||
user: parsedUrl.username, | ||
password: parsedUrl.password | ||
}; | ||
if (url.auth) { | ||
const auth = url.auth.split(':'); | ||
options.user = auth[0]; | ||
options.password = auth[1]; | ||
} | ||
if (url.query) { | ||
for (const key in url.query) { | ||
const value = url.query[key]; | ||
try { | ||
// Try to parse this as a JSON expression first | ||
options[key] = JSON.parse(value); | ||
} catch (err) { | ||
// Otherwise assume it is a plain string | ||
options[key] = value; | ||
} | ||
parsedUrl.searchParams.forEach((value, key) => { | ||
try { | ||
// Try to parse this as a JSON expression first | ||
options[key] = JSON.parse(value); | ||
} catch (err) { | ||
// Otherwise assume it is a plain string | ||
options[key] = value; | ||
} | ||
} | ||
}); | ||
return options; | ||
@@ -260,0 +254,0 @@ } |
@@ -0,1 +1,11 @@ | ||
// This file was modified by Oracle on June 1, 2021. | ||
// The changes involve new logic to handle an additional ERR Packet sent by | ||
// the MySQL server when the connection is closed unexpectedly. | ||
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | ||
// This file was modified by Oracle on June 17, 2021. | ||
// The changes involve logic to ensure the socket connection is closed when | ||
// there is a fatal error. | ||
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | ||
'use strict'; | ||
@@ -103,5 +113,6 @@ | ||
handshakeCommand.on('end', () => { | ||
// this happens when handshake finishes early and first packet is error | ||
// and not server hello ( for example, 'Too many connactions' error) | ||
if (!handshakeCommand.handshake) { | ||
// this happens when handshake finishes early either because there was | ||
// some fatal error or the server sent an error packet instead of | ||
// an hello packet (for example, 'Too many connactions' error) | ||
if (!handshakeCommand.handshake || this._fatalError || this._protocolError) { | ||
return; | ||
@@ -192,3 +203,3 @@ } | ||
this.connectTimeout = null; | ||
} | ||
} | ||
// prevent from emitting 'PROTOCOL_CONNECTION_LOST' after EPIPE or ECONNRESET | ||
@@ -229,2 +240,6 @@ if (this._fatalError) { | ||
} | ||
// close connection after emitting the event in case of a fatal error | ||
if (err.fatal) { | ||
this.close(); | ||
} | ||
} | ||
@@ -374,2 +389,10 @@ | ||
protocolError(message, code) { | ||
// Starting with MySQL 8.0.24, if the client closes the connection | ||
// unexpectedly, the server will send a last ERR Packet, which we can | ||
// safely ignore. | ||
// https://dev.mysql.com/worklog/task/?id=12999 | ||
if (this._closing) { | ||
return; | ||
} | ||
const err = new Error(message); | ||
@@ -422,6 +445,14 @@ err.fatal = true; | ||
if (!this._command) { | ||
this.protocolError( | ||
'Unexpected packet while no commands in the queue', | ||
'PROTOCOL_UNEXPECTED_PACKET' | ||
); | ||
const marker = packet.peekByte(); | ||
// If it's an Err Packet, we should use it. | ||
if (marker === 0xff) { | ||
const error = Packets.Error.fromPacket(packet); | ||
this.protocolError(error.message, error.code); | ||
} else { | ||
// Otherwise, it means it's some other unexpected packet. | ||
this.protocolError( | ||
'Unexpected packet while no commands in the queue', | ||
'PROTOCOL_UNEXPECTED_PACKET' | ||
); | ||
} | ||
this.close(); | ||
@@ -496,2 +527,7 @@ return; | ||
if (this.config.namedPlaceholders || options.namedPlaceholders) { | ||
if (Array.isArray(options.values)) { | ||
// if an array is provided as the values, assume the conversion is not necessary. | ||
// this allows the usage of unnamed placeholders even if the namedPlaceholders flag is enabled. | ||
return | ||
} | ||
if (convertNamedPlaceholders === null) { | ||
@@ -498,0 +534,0 @@ convertNamedPlaceholders = require('named-placeholders')(); |
@@ -0,1 +1,6 @@ | ||
// This file was modified by Oracle on June 1, 2021. | ||
// A utility method was introduced to generate an Error instance from a | ||
// binary server packet. | ||
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | ||
'use strict'; | ||
@@ -125,4 +130,18 @@ | ||
} | ||
static fromPacket(packet) { | ||
packet.readInt8(); // marker | ||
const code = packet.readInt16(); | ||
packet.readString(1, 'ascii'); // sql state marker | ||
// The SQL state of the ERR_Packet which is always 5 bytes long. | ||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.11/page_protocol_basic_dt_strings.html#sect_protocol_basic_dt_string_fix | ||
packet.readString(5, 'ascii'); // sql state (ignore for now) | ||
const message = packet.readNullTerminatedString('utf8'); | ||
const error = new Error(); | ||
error.message = message; | ||
error.code = code; | ||
return error; | ||
} | ||
} | ||
exports.Error = Error; |
@@ -0,1 +1,6 @@ | ||
// This file was modified by Oracle on June 1, 2021. | ||
// A comment describing some changes in the strict default SQL mode regarding | ||
// non-standard dates was introduced. | ||
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | ||
'use strict'; | ||
@@ -277,2 +282,7 @@ | ||
} | ||
// NO_ZERO_DATE mode and NO_ZERO_IN_DATE mode are part of the strict | ||
// default SQL mode used by MySQL 8.0. This means that non-standard | ||
// dates like '0000-00-00' become NULL. For older versions and other | ||
// possible MySQL flavours we still need to account for the | ||
// non-standard behaviour. | ||
if (y + m + d + H + M + S + ms === 0) { | ||
@@ -279,0 +289,0 @@ return INVALID_DATE; |
@@ -7,2 +7,3 @@ 'use strict'; | ||
const PoolConfig = require('./pool_config.js'); | ||
const Connection = require('./connection.js'); | ||
const EventEmitter = require('events').EventEmitter; | ||
@@ -50,2 +51,58 @@ | ||
/** | ||
* pool cluster query | ||
* @param {*} sql | ||
* @param {*} values | ||
* @param {*} cb | ||
* @returns query | ||
*/ | ||
query(sql, values, cb) { | ||
const query = Connection.createQuery(sql, values, cb, {}); | ||
this.getConnection((err, conn) => { | ||
if (err) { | ||
if (typeof query.onResult === 'function') { | ||
query.onResult(err); | ||
} else { | ||
query.emit('error', err); | ||
} | ||
return; | ||
} | ||
try { | ||
conn.query(query).once('end', () => { | ||
conn.release(); | ||
}); | ||
} catch (e) { | ||
conn.release(); | ||
throw e; | ||
} | ||
}); | ||
return query; | ||
} | ||
/** | ||
* pool cluster execute | ||
* @param {*} sql | ||
* @param {*} values | ||
* @param {*} cb | ||
*/ | ||
execute(sql, values, cb) { | ||
if (typeof values === 'function') { | ||
cb = values; | ||
values = []; | ||
} | ||
this.getConnection((err, conn) => { | ||
if (err) { | ||
return cb(err); | ||
} | ||
try { | ||
conn.execute(sql, values, cb).once('end', () => { | ||
conn.release(); | ||
}); | ||
} catch (e) { | ||
conn.release(); | ||
throw e; | ||
} | ||
}); | ||
} | ||
_getClusterNode() { | ||
@@ -52,0 +109,0 @@ const foundNodeIds = this._cluster._findNodeIds(this._pattern); |
{ | ||
"name": "mysql2", | ||
"version": "2.2.5", | ||
"version": "2.3.0", | ||
"description": "fast mysql driver. Implements core protocol, prepared statements, ssl and compression in native JS", | ||
@@ -47,3 +47,4 @@ "main": "index.js", | ||
".": "./index.js", | ||
"./promise": "./promise.js" | ||
"./promise": "./promise.js", | ||
"./promise.js": "./promise.js" | ||
}, | ||
@@ -50,0 +51,0 @@ "engines": { |
113
promise.js
@@ -12,2 +12,3 @@ 'use strict'; | ||
localErr.errno = err.errno; | ||
localErr.sql = err.sql; | ||
localErr.sqlState = err.sqlState; | ||
@@ -431,4 +432,116 @@ localErr.sqlMessage = err.sqlMessage; | ||
class PromisePoolCluster extends EventEmitter { | ||
constructor(poolCluster, thePromise) { | ||
super(); | ||
this.poolCluster = poolCluster; | ||
this.Promise = thePromise || Promise; | ||
inheritEvents(poolCluster, this, ['acquire', 'connection', 'enqueue', 'release']); | ||
} | ||
getConnection() { | ||
const corePoolCluster = this.poolCluster; | ||
return new this.Promise((resolve, reject) => { | ||
corePoolCluster.getConnection((err, coreConnection) => { | ||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(new PromisePoolConnection(coreConnection, this.Promise)); | ||
} | ||
}); | ||
}); | ||
} | ||
query(sql, args) { | ||
const corePoolCluster = this.poolCluster; | ||
const localErr = new Error(); | ||
if (typeof args === 'function') { | ||
throw new Error( | ||
'Callback function is not available with promise clients.' | ||
); | ||
} | ||
return new this.Promise((resolve, reject) => { | ||
const done = makeDoneCb(resolve, reject, localErr); | ||
corePoolCluster.query(sql, args, done); | ||
}); | ||
} | ||
execute(sql, args) { | ||
const corePoolCluster = this.poolCluster; | ||
const localErr = new Error(); | ||
if (typeof args === 'function') { | ||
throw new Error( | ||
'Callback function is not available with promise clients.' | ||
); | ||
} | ||
return new this.Promise((resolve, reject) => { | ||
const done = makeDoneCb(resolve, reject, localErr); | ||
corePoolCluster.execute(sql, args, done); | ||
}); | ||
} | ||
of(pattern, selector) { | ||
return new PromisePoolCluster( | ||
this.poolCluster.of(pattern, selector), | ||
this.Promise | ||
); | ||
} | ||
end() { | ||
const corePoolCluster = this.poolCluster; | ||
const localErr = new Error(); | ||
return new this.Promise((resolve, reject) => { | ||
corePoolCluster.end(err => { | ||
if (err) { | ||
localErr.message = err.message; | ||
localErr.code = err.code; | ||
localErr.errno = err.errno; | ||
localErr.sqlState = err.sqlState; | ||
localErr.sqlMessage = err.sqlMessage; | ||
reject(localErr); | ||
} else { | ||
resolve(); | ||
} | ||
}); | ||
}); | ||
} | ||
} | ||
/** | ||
* proxy poolCluster synchronous functions | ||
*/ | ||
(function (functionsToWrap) { | ||
for (let i = 0; functionsToWrap && i < functionsToWrap.length; i++) { | ||
const func = functionsToWrap[i]; | ||
if ( | ||
typeof core.PoolCluster.prototype[func] === 'function' && | ||
PromisePoolCluster.prototype[func] === undefined | ||
) { | ||
PromisePoolCluster.prototype[func] = (function factory(funcName) { | ||
return function () { | ||
return core.PoolCluster.prototype[funcName].apply(this.poolCluster, arguments); | ||
}; | ||
})(func); | ||
} | ||
} | ||
})([ | ||
'add' | ||
]); | ||
function createPoolCluster(opts) { | ||
const corePoolCluster = core.createPoolCluster(opts); | ||
const thePromise = (opts && opts.Promise) || Promise; | ||
if (!thePromise) { | ||
throw new Error( | ||
'no Promise implementation available.' + | ||
'Use promise-enabled node version or pass userland Promise' + | ||
" implementation as parameter, for example: { Promise: require('bluebird') }" | ||
); | ||
} | ||
return new PromisePoolCluster(corePoolCluster, thePromise); | ||
} | ||
exports.createConnection = createConnection; | ||
exports.createPool = createPool; | ||
exports.createPoolCluster = createPoolCluster; | ||
exports.escape = core.escape; | ||
@@ -435,0 +548,0 @@ exports.escapeId = core.escapeId; |
@@ -219,2 +219,28 @@ ## Node MySQL 2 | ||
## Array results | ||
If you have two columns with the same name, you might want to get results as an array rather than an object to prevent them from clashing. This is a deviation from the [Node MySQL][node-mysql] library. | ||
For example: `select 1 as foo, 2 as foo`. | ||
You can enable this setting at either the connection level (applies to all queries), or at the query level (applies only to that specific query). | ||
### Connection Option | ||
```js | ||
const con = mysql.createConnection( | ||
{ host: 'localhost', database: 'test', user: 'root', rowsAsArray: true } | ||
); | ||
``` | ||
### Query Option | ||
```js | ||
con.query({ sql: 'select 1 as foo, 2 as foo', rowsAsArray: true }, function(err, results, fields) { | ||
console.log(results) // will be an array of arrays rather than an array of objects | ||
console.log(fields) // these are unchanged | ||
}); | ||
``` | ||
## API and Configuration | ||
@@ -221,0 +247,0 @@ |
@@ -150,2 +150,10 @@ | ||
ssl?: string | SslOptions; | ||
/** | ||
* Return each row as an array, not as an object. | ||
* This is useful when you have duplicate column names. | ||
* This can also be set in the `QueryOption` object to be applied per-query. | ||
*/ | ||
rowsAsArray?: boolean | ||
} | ||
@@ -200,2 +208,3 @@ | ||
threadId: number; | ||
authorized: boolean; | ||
@@ -202,0 +211,0 @@ static createQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader>(sql: string, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; |
@@ -54,2 +54,8 @@ | ||
typeCast?: any; | ||
/** | ||
* This overrides the same option set at the connection level. | ||
* | ||
*/ | ||
rowsAsArray?: boolean | ||
} | ||
@@ -56,0 +62,0 @@ |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
473325
11365
282