diff --git a/.travis.yml b/.travis.yml index 1691b47a70..d2c753f51a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +# This file was modified by Oracle on June 14, 2021. +# The changes involve the introduction of a local cache for Docker images. +# Modifications copyright (c) 2021, Oracle and/or its affiliates. + sudo: required dist: trusty @@ -9,9 +13,19 @@ language: node_js cache: yarn: true directories: + - docker_images - node_modules - $HOME/.yarn-cache +before_cache: + # save all docker images to a local cache in order to avoid the rate limit + # on Docker Hub + - docker save -o docker_images/images.tar $(docker images -a -q) + +before_install: + # load docker images from the local cache + - docker load -i docker_images/images.tar || true + # Node.js version: # we test only maintained LTS versions # and lastest dev version diff --git a/Changelog.md b/Changelog.md index 5e7499204e..51b84e91f9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,20 @@ +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 ) - typings: add ResultSetHeader #1213 diff --git a/README.md b/README.md index 5a2f4423cc..b1de181a52 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,32 @@ con.promise().query("SELECT 1") .then( () => con.end()); ``` +## 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 MySQL2 is mostly API compatible with [Node MySQL][node-mysql]. You should check their API documentation to see all available API options. diff --git a/benchmarks/FB/hello.js b/benchmarks/FB/hello.js index 22487266b7..c972626f75 100644 --- a/benchmarks/FB/hello.js +++ b/benchmarks/FB/hello.js @@ -61,8 +61,8 @@ function sequelizeQuery(callback) { } function handlePrepared(req, res) { - const values = url.parse(req.url, true); - const queries = values.query.queries || 1; + const values = new url.URL(req.url); + const queries = values.searchParams.get('queries') || 1; const results = []; for (let i = 0; i < queries; ++i) { mysql2conn.execute( diff --git a/benchmarks/http-select-and-render.js b/benchmarks/http-select-and-render.js index 91d847dda1..a6eee77d8e 100644 --- a/benchmarks/http-select-and-render.js +++ b/benchmarks/http-select-and-render.js @@ -10,10 +10,10 @@ const port = process.env.PORT; http .createServer((req, res) => { - const q = url.parse(req.url, true); + const q = new url.URL(req.url); if (q.pathname === '/render') { - const sql = q.query.q; - const n = q.query.n; + const sql = q.searchParams.get('q'); + const n = q.searchParams.get('n'); let rowsTotal = []; const doQueries = function(number) { if (number === 0) { diff --git a/documentation/Examples.md b/documentation/Examples.md index 77ebe1dc5f..8a1b138975 100644 --- a/documentation/Examples.md +++ b/documentation/Examples.md @@ -49,9 +49,9 @@ const connection = mysql.createConnection({ ssl: 'Amazon RDS' }); -conn.query('show status like \'Ssl_cipher\'', (err, res) => { +connection.query('show status like \'Ssl_cipher\'', (err, res) => { console.log(err, res); - conn.end(); + connection.end(); }); ``` diff --git a/documentation/Extras.md b/documentation/Extras.md index 0e5510c34f..a90f74c808 100644 --- a/documentation/Extras.md +++ b/documentation/Extras.md @@ -2,7 +2,7 @@ ## Named placeholders -You can use named placeholders for parameters by setting `namedPlaceholders` config value or query/execute time option. Named placeholders are converted to unnamed `?` on the client (mysql protocol does not support named parameters). If you reference parameter multiple times under the same name it is sent to server multiple times. +You can use named placeholders for parameters by setting `namedPlaceholders` config value or query/execute time option. Named placeholders are converted to unnamed `?` on the client (mysql protocol does not support named parameters). If you reference parameter multiple times under the same name it is sent to server multiple times. Unnamed placeholders can still be used by providing the values as an array instead of an object. ```js connection.config.namedPlaceholders = true; @@ -18,8 +18,14 @@ connection.execute('select :x + :x as z', { x: 1 }, (err, rows) => { connection.query('select :x + :x as z', { x: 1 }, (err, rows) => { // query select 1 + 1 as z }); + +// unnamed placeholders are still valid if the values are provided in an array +connection.query('select ? + ? as z', [1, 1], (err, rows) => { + // query select 1 + 1 as z +}); ``` + ## Receiving rows as array of columns instead of hash with column name as key: ```js diff --git a/index.d.ts b/index.d.ts index fe3e066421..a465b43de7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -151,9 +151,12 @@ export interface Pool extends mysql.Connection { promise(promiseImpl?: PromiseConstructor): PromisePool; } -type authPlugins = - (pluginMetadata: { connection: Connection; command: string }) => - (pluginData: Buffer) => Promise; +type authPlugins = (pluginMetadata: { + connection: Connection; + command: string; +}) => ( + pluginData: Buffer +) => Promise | string | Buffer | Promise | null; export interface ConnectionOptions extends mysql.ConnectionOptions { charsetNumber?: number; @@ -175,7 +178,7 @@ export interface ConnectionOptions extends mysql.ConnectionOptions { queueLimit?: number; waitForConnections?: boolean; authPlugins?: { - [key: string]: authPlugins; + [key: string]: authPlugins; }; } diff --git a/index.js b/index.js index 252b3d7c2a..cd9601e1c3 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ exports.connect = exports.createConnection; exports.Connection = Connection; const Pool = require('./lib/pool.js'); +const PoolCluster = require('./lib/pool_cluster.js'); exports.createPool = function(config) { const PoolConfig = require('./lib/pool_config.js'); @@ -29,6 +30,8 @@ exports.createQuery = Connection.createQuery; exports.Pool = Pool; +exports.PoolCluster = PoolCluster; + exports.createServer = function(handler) { const Server = require('./lib/server.js'); const s = new Server(); diff --git a/lib/commands/auth_switch.js b/lib/commands/auth_switch.js index 032ebde723..9e0b527aa1 100644 --- a/lib/commands/auth_switch.js +++ b/lib/commands/auth_switch.js @@ -1,3 +1,8 @@ +// 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'; const Packets = require('../packets/index.js'); @@ -17,6 +22,14 @@ function warnLegacyAuthSwitch() { ); } +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) { const { pluginName, pluginData } = Packets.AuthSwitchRequest.fromPacket( packet @@ -34,8 +47,7 @@ function authSwitchRequest(packet, connection, command) { warnLegacyAuthSwitch(); legacySwitchHandler({ pluginName, pluginData }, (err, data) => { if (err) { - connection.emit('error', err); - return; + return authSwitchPluginError(err, command); } connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket()); }); @@ -54,10 +66,12 @@ function authSwitchRequest(packet, connection, command) { if (data) { 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); if (connection.config.authSwitchHandler) { @@ -65,8 +79,7 @@ function authSwitchRequestMoreData(packet, connection) { warnLegacyAuthSwitch(); legacySwitchHandler({ pluginData: data }, (err, data) => { if (err) { - connection.emit('error', err); - return; + return authSwitchPluginError(err, command); } connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket()); }); @@ -82,6 +95,8 @@ function authSwitchRequestMoreData(packet, connection) { if (data) { connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket()); } + }).catch(err => { + authSwitchPluginError(err, command); }); } diff --git a/lib/commands/client_handshake.js b/lib/commands/client_handshake.js index db0b5e2a0a..c5e5de0c08 100644 --- a/lib/commands/client_handshake.js +++ b/lib/commands/client_handshake.js @@ -1,3 +1,8 @@ +// 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'; const Command = require('./command.js'); @@ -151,20 +156,28 @@ class ClientHandshake extends Command { } return ClientHandshake.prototype.handshakeResult; } 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); } return null; } } if (marker !== 0) { 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); } return null; } diff --git a/lib/commands/command.js b/lib/commands/command.js index 0a99b4e270..3e48564fdb 100644 --- a/lib/commands/command.js +++ b/lib/commands/command.js @@ -26,6 +26,7 @@ class Command extends EventEmitter { } if (packet && packet.isError()) { const err = packet.asError(connection.clientEncoding); + err.sql = this.sql || this.query; if (this.onResult) { this.onResult(err); this.emit('end'); diff --git a/lib/commands/execute.js b/lib/commands/execute.js index 9bc37843dd..d5c2d8ab79 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -15,6 +15,8 @@ class Execute extends Command { this.onResult = callback; this.parameters = options.values; this.insertId = 0; + this.timeout = options.timeout; + this.queryTimeout = null; this._rows = []; this._fields = []; this._result = []; @@ -35,6 +37,7 @@ class Execute extends Command { start(packet, connection) { this._connection = connection; this.options = Object.assign({}, connection.config, this._executeOptions); + this._setTimeout(); const executePacket = new Packets.Execute( this.statement.id, this.parameters, @@ -96,6 +99,8 @@ Execute.prototype.resultsetHeader = Query.prototype.resultsetHeader; Execute.prototype._findOrCreateReadStream = Query.prototype._findOrCreateReadStream; Execute.prototype._streamLocalInfile = Query.prototype._streamLocalInfile; +Execute.prototype._setTimeout = Query.prototype._setTimeout; +Execute.prototype._handleTimeoutError = Query.prototype._handleTimeoutError; Execute.prototype.row = Query.prototype.row; Execute.prototype.stream = Query.prototype.stream; diff --git a/lib/commands/query.js b/lib/commands/query.js index edf9bab366..344b4abc36 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -1,6 +1,7 @@ 'use strict'; const process = require('process'); +const Timers = require('timers'); const Readable = require('stream').Readable; @@ -21,6 +22,8 @@ class Query extends Command { this._queryOptions = options; this.namedPlaceholders = options.namedPlaceholders || false; this.onResult = callback; + this.timeout = options.timeout; + this.queryTimeout = null; this._fieldCount = 0; this._rowParser = null; this._fields = []; @@ -48,6 +51,8 @@ class Query extends Command { } this._connection = connection; this.options = Object.assign({}, connection.config, this._queryOptions); + this._setTimeout(); + const cmdPacket = new Packets.Query( this.sql, connection.config.charsetNumber @@ -58,6 +63,15 @@ class Query extends Command { done() { 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) { let rows, fields; if (this._resultIndex === 0) { @@ -272,6 +286,34 @@ class Query extends Command { }); return stream; } + + _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); + } + } } Query.prototype.catch = Query.prototype.then; diff --git a/lib/connection.js b/lib/connection.js index f99f53d6c4..47970e971c 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,3 +1,13 @@ +// 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'; const Net = require('net'); @@ -100,9 +110,10 @@ class Connection extends EventEmitter { if (!this.config.isServer) { handshakeCommand = new Commands.ClientHandshake(this.config.clientFlags); 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; } this._handshakePacket = handshakeCommand.handshake; @@ -188,7 +199,7 @@ class Connection extends EventEmitter { if (this.connectTimeout) { Timers.clearTimeout(this.connectTimeout); this.connectTimeout = null; - } + } // prevent from emitting 'PROTOCOL_CONNECTION_LOST' after EPIPE or ECONNRESET if (this._fatalError) { return; @@ -224,6 +235,10 @@ class Connection extends EventEmitter { if (bubbleErrorToConnection || this._pool) { this.emit('error', err); } + // close connection after emitting the event in case of a fatal error + if (err.fatal) { + this.close(); + } } write(buffer) { @@ -368,6 +383,14 @@ class Connection extends EventEmitter { } 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); err.fatal = true; err.code = code || 'PROTOCOL_ERROR'; @@ -415,10 +438,18 @@ class Connection extends EventEmitter { } } 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(); return; } @@ -488,6 +519,11 @@ class Connection extends EventEmitter { _resolveNamedPlaceholders(options) { let unnamed; 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) { convertNamedPlaceholders = require('named-placeholders')(); } diff --git a/lib/connection_config.js b/lib/connection_config.js index 64f7f81a29..11ad01b627 100644 --- a/lib/connection_config.js +++ b/lib/connection_config.js @@ -1,6 +1,6 @@ 'use strict'; -const urlParse = require('url').parse; +const { URL } = require('url'); const ClientConstants = require('./constants/client'); const Charsets = require('./constants/charsets'); let SSLProfiles = null; @@ -232,29 +232,23 @@ class ConnectionConfig { } 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; } } diff --git a/lib/constants/errors.js b/lib/constants/errors.js index 7d4d28f543..c4cf364843 100644 --- a/lib/constants/errors.js +++ b/lib/constants/errors.js @@ -1,3 +1,8 @@ +// This file was modified by Oracle on June 1, 2021. +// An entry was created for a new error reported by the MySQL server due to +// client inactivity. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; // copy from https://raw.githubusercontent.com/mysqljs/mysql/7770ee5bb13260c56a160b91fe480d9165dbeeba/lib/protocol/constants/errors.js @@ -994,6 +999,7 @@ exports.ER_INNODB_FT_AUX_NOT_HEX_ID = 1879; exports.ER_OLD_TEMPORALS_UPGRADED = 1880; exports.ER_INNODB_FORCED_RECOVERY = 1881; exports.ER_AES_INVALID_IV = 1882; +exports.ER_CLIENT_INTERACTION_TIMEOUT = 4031; // Lookup-by-number table exports[1] = 'EE_CANTCREATEFILE'; @@ -1982,3 +1988,4 @@ exports[1879] = 'ER_INNODB_FT_AUX_NOT_HEX_ID'; exports[1880] = 'ER_OLD_TEMPORALS_UPGRADED'; exports[1881] = 'ER_INNODB_FORCED_RECOVERY'; exports[1882] = 'ER_AES_INVALID_IV'; +exports[4031] = 'ER_CLIENT_INTERACTION_TIMEOUT'; diff --git a/lib/packets/index.js b/lib/packets/index.js index da1381e620..e5bb390e5c 100644 --- a/lib/packets/index.js +++ b/lib/packets/index.js @@ -1,3 +1,8 @@ +// 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'; const process = require('process'); @@ -122,6 +127,20 @@ class Error { packet._name = 'Error'; return packet; } + + 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; diff --git a/lib/packets/packet.js b/lib/packets/packet.js index 33cdfd489f..90233c919e 100644 --- a/lib/packets/packet.js +++ b/lib/packets/packet.js @@ -1,3 +1,8 @@ +// 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'; const ErrorCodeToName = require('../constants/errors.js'); @@ -274,6 +279,11 @@ class Packet { if (length > 10) { ms = this.readInt32() / 1000; } + // 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) { return INVALID_DATE; } diff --git a/lib/pool_cluster.js b/lib/pool_cluster.js index 47b044b966..92f53de668 100644 --- a/lib/pool_cluster.js +++ b/lib/pool_cluster.js @@ -4,6 +4,7 @@ const process = require('process'); const Pool = require('./pool.js'); const PoolConfig = require('./pool_config.js'); +const Connection = require('./connection.js'); const EventEmitter = require('events').EventEmitter; /** @@ -46,6 +47,62 @@ class PoolNamespace { }); } + /** + * 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() { const foundNodeIds = this._cluster._findNodeIds(this._pattern); if (foundNodeIds.length === 0) { diff --git a/package-lock.json b/package-lock.json index 870dfdb940..be3560dcc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mysql2", - "version": "2.2.5", + "version": "2.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7664577863..554561b24c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "main": "index.js", "directories": { @@ -44,7 +44,8 @@ ], "exports": { ".": "./index.js", - "./promise": "./promise.js" + "./promise": "./promise.js", + "./promise.js": "./promise.js" }, "engines": { "node": ">= 8.0" diff --git a/promise.js b/promise.js index bcaf3f2f47..fc74cc0c3a 100644 --- a/promise.js +++ b/promise.js @@ -9,6 +9,7 @@ function makeDoneCb(resolve, reject, localErr) { localErr.message = err.message; localErr.code = err.code; localErr.errno = err.errno; + localErr.sql = err.sql; localErr.sqlState = err.sqlState; localErr.sqlMessage = err.sqlMessage; reject(localErr); @@ -427,8 +428,120 @@ function createPool(opts) { 'format' ]); +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; exports.escapeId = core.escapeId; exports.format = core.format; diff --git a/test/common.js b/test/common.js index a0de4cd35e..8fb9e58f7d 100644 --- a/test/common.js +++ b/test/common.js @@ -121,7 +121,8 @@ exports.createConnection = function(args) { dateStrings: args && args.dateStrings, authSwitchHandler: args && args.authSwitchHandler, typeCast: args && args.typeCast, - namedPlaceholders: args && args.namedPlaceholders + namedPlaceholders: args && args.namedPlaceholders, + connectTimeout: args && args.connectTimeout, }; const conn = driver.createConnection(params); diff --git a/test/integration/connection/test-binary-multiple-results.js b/test/integration/connection/test-binary-multiple-results.js index 544956b690..bfe30db9a1 100644 --- a/test/integration/connection/test-binary-multiple-results.js +++ b/test/integration/connection/test-binary-multiple-results.js @@ -1,3 +1,8 @@ +// This file was modified by Oracle on June 2, 2021. +// The test has been updated to remove all expectations with regards to the +// "columnLength" metadata field. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const mysql = require('../../common.js').createConnection({ @@ -33,7 +38,6 @@ const fields1 = [ { catalog: 'def', characterSet: 63, - columnLength: 1, columnType: 8, decimals: 0, flags: 129, @@ -48,7 +52,6 @@ const nr_fields = [ { catalog: 'def', characterSet: 63, - columnLength: 11, columnType: 3, decimals: 0, flags: 0, @@ -140,7 +143,13 @@ function do_test(testIndex) { return void 0; } - return c.inspect(); + const column = c.inspect(); + // "columnLength" is non-deterministic and the display width for integer + // data types was deprecated on MySQL 8.0.17. + // https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html + delete column.columnLength; + + return column; }; assert.deepEqual(expectation[0], _rows); diff --git a/test/integration/connection/test-connect-timeout.js b/test/integration/connection/test-connect-timeout.js new file mode 100644 index 0000000000..5def005888 --- /dev/null +++ b/test/integration/connection/test-connect-timeout.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../../common'); +const connection = common.createConnection({ + host: '10.255.255.1', + debug: false, + connectTimeout: 100, +}); + +const assert = require('assert'); + +connection.query('SELECT sleep(3) as a', (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'ETIMEDOUT'); + assert.equal(err.message, 'connect ETIMEDOUT'); +}); + +connection.query({ sql: 'SELECT sleep(3) as a' , timeout: 50}, (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'ETIMEDOUT'); + assert.equal(err.message, 'connect ETIMEDOUT'); +}); + + + diff --git a/test/integration/connection/test-disconnects.js b/test/integration/connection/test-disconnects.js index 3acb4c685b..7f91a7ba35 100644 --- a/test/integration/connection/test-disconnects.js +++ b/test/integration/connection/test-disconnects.js @@ -1,3 +1,10 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const common = require('../../common'); @@ -10,7 +17,14 @@ const connections = []; const server = common.createServer( () => { - const connection = common.createConnection({ port: server._port }); + const connection = common.createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + port: server._port + }); connection.query('SELECT 123', (err, _rows, _fields) => { if (err) { throw err; diff --git a/test/integration/connection/test-errors.js b/test/integration/connection/test-errors.js index a51dec3089..213109f518 100644 --- a/test/integration/connection/test-errors.js +++ b/test/integration/connection/test-errors.js @@ -15,6 +15,7 @@ connection .execute('error in execute', [], err => { assert.equal(err.errno, 1064); assert.equal(err.code, 'ER_PARSE_ERROR'); + assert.equal(err.sql, 'error in execute'); if (err) { onExecuteResultError = true; } @@ -26,6 +27,7 @@ connection .query('error in query', [], err => { assert.equal(err.errno, 1064); assert.equal(err.code, 'ER_PARSE_ERROR'); + assert.equal(err.sql, 'error in query'); if (err) { onQueryResultError = true; } diff --git a/test/integration/connection/test-execute-nocolumndef.js b/test/integration/connection/test-execute-nocolumndef.js index 9987fd38b8..3553103288 100644 --- a/test/integration/connection/test-execute-nocolumndef.js +++ b/test/integration/connection/test-execute-nocolumndef.js @@ -1,3 +1,8 @@ +// This file was modified by Oracle on June 2, 2021. +// The test has been updated to remove all expectations with regards to the +// "columnLength" metadata field. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const common = require('../../common'); @@ -47,7 +52,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 63, - columnLength: 3, columnType: 8, flags: 161, decimals: 0 @@ -60,7 +64,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 76, columnType: 253, flags: 1, decimals: 31 @@ -73,7 +76,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 256, columnType: 253, flags: 0, decimals: 31 @@ -86,7 +88,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 25264128, columnType: 250, flags: 0, decimals: 31 @@ -99,7 +100,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 40, columnType: 253, flags: 0, decimals: 31 @@ -112,7 +112,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 16384, columnType: 253, flags: 0, decimals: 31 @@ -125,7 +124,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 256, columnType: 253, flags: 0, decimals: 31 @@ -138,7 +136,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 16384, columnType: 253, flags: 0, decimals: 31 @@ -151,7 +148,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 4096, columnType: 253, flags: 0, decimals: 31 @@ -164,7 +160,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 63, - columnLength: 10, columnType: 8, flags: 160, decimals: 0 @@ -177,7 +172,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 63, - columnLength: 4, columnType: 5, flags: 128, decimals: 2 @@ -190,7 +184,6 @@ const expectedFields = [ table: '', orgTable: '', characterSet: 224, - columnLength: 1020, columnType: 253, flags: 1, decimals: 31 @@ -201,6 +194,9 @@ process.on('exit', () => { assert.deepEqual(rows, expectedRows); fields.forEach((f, index) => { const fi = f.inspect(); + // "columnLength" is non-deterministic + delete fi.columnLength; + assert.deepEqual( Object.keys(fi).sort(), Object.keys(expectedFields[index]).sort() diff --git a/test/integration/connection/test-invalid-date-result.js b/test/integration/connection/test-invalid-date-result.js index 322fff3a5b..bd03bed076 100644 --- a/test/integration/connection/test-invalid-date-result.js +++ b/test/integration/connection/test-invalid-date-result.js @@ -1,3 +1,8 @@ +// This file was modified by Oracle on June 1, 2021. +// The test has been updated to be able to pass with different default +// strict modes used by different MySQL server versions. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const common = require('../../common'); @@ -6,12 +11,34 @@ const assert = require('assert'); let rows = undefined; -connection.execute('SELECT TIMESTAMP(0000-00-00) t', [], (err, _rows) => { +// Disable NO_ZERO_DATE mode and NO_ZERO_IN_DATE mode to ensure the old +// behaviour. +const strictModes = ['NO_ZERO_DATE', 'NO_ZERO_IN_DATE']; + +connection.query('SELECT variable_value as value FROM performance_schema.session_variables where variable_name = ?', ['sql_mode'], (err, _rows) => { if (err) { throw err; } - rows = _rows; - connection.end(); + + const deprecatedSqlMode = _rows[0].value + .split(',') + .filter(mode => strictModes.indexOf(mode) === -1) + .join(','); + + connection.query(`SET sql_mode=?`, [deprecatedSqlMode], err => { + if (err) { + throw err; + } + + connection.execute('SELECT TIMESTAMP(0000-00-00) t', [], (err, _rows) => { + if (err) { + throw err; + } + + rows = _rows; + connection.end(); + }); + }); }); function isInvalidTime(t) { diff --git a/test/integration/connection/test-multiple-results.js b/test/integration/connection/test-multiple-results.js index ce1462730b..ff48b89286 100644 --- a/test/integration/connection/test-multiple-results.js +++ b/test/integration/connection/test-multiple-results.js @@ -1,3 +1,8 @@ +// This file was modified by Oracle on June 2, 2021. +// The test has been updated to remove all expectations with regards to the +// "columnLength" metadata field. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const mysql = require('../../common.js').createConnection({ @@ -32,7 +37,6 @@ const fields1 = [ { catalog: 'def', characterSet: 63, - columnLength: 1, columnType: 8, decimals: 0, flags: 129, @@ -47,7 +51,6 @@ const nr_fields = [ { catalog: 'def', characterSet: 63, - columnLength: 11, columnType: 3, decimals: 0, flags: 0, @@ -137,7 +140,13 @@ function do_test(testIndex) { return void 0; } - return c.inspect(); + const column = c.inspect(); + // "columnLength" is non-deterministic and the display width for integer + // data types was deprecated on MySQL 8.0.17. + // https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html + delete column.columnLength; + + return column; }; assert.deepEqual(expectation, [_rows, arrOrColumn(_columns), _numResults]); diff --git a/test/integration/connection/test-named-paceholders.js b/test/integration/connection/test-named-placeholders.js similarity index 87% rename from test/integration/connection/test-named-paceholders.js rename to test/integration/connection/test-named-placeholders.js index 5c2483bca1..4b83953591 100644 --- a/test/integration/connection/test-named-paceholders.js +++ b/test/integration/connection/test-named-placeholders.js @@ -71,11 +71,23 @@ connection.query('SELECT :a + :a as sum', { a: 2 }, (err, rows) => { connection.end(); }); -const sql = connection.format( +const namedSql = connection.format( 'SELECT * from test_table where num1 < :numParam and num2 > :lParam', { lParam: 100, numParam: 2 } ); -assert.equal(sql, 'SELECT * from test_table where num1 < 2 and num2 > 100'); +assert.equal( + namedSql, + 'SELECT * from test_table where num1 < 2 and num2 > 100' +); + +const unnamedSql = connection.format( + 'SELECT * from test_table where num1 < ? and num2 > ?', + [2, 100] +); +assert.equal( + unnamedSql, + 'SELECT * from test_table where num1 < 2 and num2 > 100' +); const pool = common.createPool(); pool.config.connectionConfig.namedPlaceholders = true; diff --git a/test/integration/connection/test-protocol-errors.js b/test/integration/connection/test-protocol-errors.js index 23b5b3ed04..4ef3517383 100644 --- a/test/integration/connection/test-protocol-errors.js +++ b/test/integration/connection/test-protocol-errors.js @@ -1,3 +1,10 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const assert = require('assert'); @@ -9,7 +16,14 @@ let rows; const server = common.createServer( () => { - const connection = common.createConnection({ port: server._port }); + const connection = common.createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + port: server._port + }); connection.query(query, (err, _rows, _fields) => { if (err) { throw err; diff --git a/test/integration/connection/test-query-timeout.js b/test/integration/connection/test-query-timeout.js new file mode 100644 index 0000000000..4e117d8c6b --- /dev/null +++ b/test/integration/connection/test-query-timeout.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../../common'); +const connection = common.createConnection({ debug: false }); +const assert = require('assert'); + +connection.query({ sql: 'SELECT sleep(3) as a', timeout: 500 }, (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'PROTOCOL_SEQUENCE_TIMEOUT'); + assert.equal(err.message, 'Query inactivity timeout'); +}); + +connection.query({ sql: 'SELECT sleep(1) as a', timeout: 5000 }, (err, res) => { + assert.deepEqual(res, [{ a: 0 }]); +}); + +connection.query('SELECT sleep(1) as a', (err, res) => { + assert.deepEqual(res, [{ a: 0 }]); +}); + +connection.execute({ sql: 'SELECT sleep(3) as a', timeout: 500 }, (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'PROTOCOL_SEQUENCE_TIMEOUT'); + assert.equal(err.message, 'Query inactivity timeout'); +}); + +connection.execute({ sql: 'SELECT sleep(1) as a', timeout: 5000 }, (err, res) => { + assert.deepEqual(res, [{ a: 0 }]); +}); + +connection.execute('SELECT sleep(1) as a', (err, res) => { + assert.deepEqual(res, [{ a: 0 }]); + connection.end(); +}); + +const connectionTimeout = common.createConnection({ + host: '10.255.255.1', + debug: false, + connectTimeout: 100, +}); + +// return connect timeout error first +connectionTimeout.query({ sql: 'SELECT sleep(3) as a', timeout: 50 }, (err, res) => { + assert.equal(res, null); + assert.ok(err); + assert.equal(err.code, 'ETIMEDOUT'); + assert.equal(err.message, 'connect ETIMEDOUT'); +}); \ No newline at end of file diff --git a/test/integration/connection/test-quit.js b/test/integration/connection/test-quit.js index 44d1c9baa3..aa79e304e9 100644 --- a/test/integration/connection/test-quit.js +++ b/test/integration/connection/test-quit.js @@ -1,3 +1,10 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const assert = require('assert'); @@ -9,7 +16,14 @@ let rows; let fields; const server = common.createServer( () => { - const connection = common.createConnection({ port: server._port }); + const connection = common.createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + port: server._port + }); connection.query(queryCli, (err, _rows, _fields) => { if (err) { diff --git a/test/integration/connection/test-stream-errors.js b/test/integration/connection/test-stream-errors.js index bf821fc883..fd054b2c15 100644 --- a/test/integration/connection/test-stream-errors.js +++ b/test/integration/connection/test-stream-errors.js @@ -1,3 +1,10 @@ +// This file was modified by Oracle on January 21, 2021. +// The connection with the mock server needs to happen in the same host where +// the tests are running in order to avoid connecting a potential MySQL server +// instance running in the host identified by the MYSQL_HOST environment +// variable. +// Modifications copyright (c) 2021, Oracle and/or its affiliates. + 'use strict'; const assert = require('assert'); @@ -12,7 +19,14 @@ const query = 'SELECT 1'; const server = common.createServer( () => { - clientConnection = common.createConnection({ port: server._port }); + clientConnection = common.createConnection({ + // The mock server is running on the same host machine. + // We need to explicitly define the host to avoid connecting to a potential + // different host provided via MYSQL_HOST that identifies a real MySQL + // server instance. + host: 'localhost', + port: server._port + }); clientConnection.query(query, err => { receivedError1 = err; }); diff --git a/test/integration/test-auth-switch-plugin-async-error.js b/test/integration/test-auth-switch-plugin-async-error.js new file mode 100644 index 0000000000..53d99ebc99 --- /dev/null +++ b/test/integration/test-auth-switch-plugin-async-error.js @@ -0,0 +1,89 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +'use strict'; + +const mysql = require('../../index.js'); +const Command = require('../../lib/commands/command.js'); +const Packets = require('../../lib/packets/index.js'); + +const assert = require('assert'); + +class TestAuthSwitchPluginError extends Command { + constructor(args) { + super(); + this.args = args; + } + + start(_, connection) { + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + protocolVersion: 10, + serverVersion: 'node.js rocks' + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestAuthSwitchPluginError.prototype.sendAuthSwitchRequest; + } + + sendAuthSwitchRequest(_, connection) { + const asr = new Packets.AuthSwitchRequest(this.args); + connection.writePacket(asr.toPacket()); + return TestAuthSwitchPluginError.prototype.finish; + } + + finish(_, connection) { + connection.end(); + return TestAuthSwitchPluginError.prototype.finish; + } +} + +const server = mysql.createServer(conn => { + conn.addCommand( + new TestAuthSwitchPluginError({ + pluginName: 'auth_test_plugin', + pluginData: Buffer.allocUnsafe(0) + }) + ); +}); + +let error; +let uncaughtExceptions = 0; + +const portfinder = require('portfinder'); +portfinder.getPort((_, port) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + authPlugins: { + auth_test_plugin () { + return function () { + return Promise.reject(Error('boom')); + } + } + } + }); + + conn.on('error', err => { + error = err; + + conn.end(); + server.close(); + }); +}); + +process.on('uncaughtException', err => { + // The plugin reports a fatal error + assert.equal(error.code, 'AUTH_SWITCH_PLUGIN_ERROR'); + assert.equal(error.message, 'boom'); + assert.equal(error.fatal, true); + // The server must close the connection + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); + + uncaughtExceptions += 1; +}); + +process.on('exit', () => { + assert.equal(uncaughtExceptions, 1); +}); diff --git a/test/integration/test-auth-switch-plugin-error.js b/test/integration/test-auth-switch-plugin-error.js new file mode 100644 index 0000000000..24a0b8d4e3 --- /dev/null +++ b/test/integration/test-auth-switch-plugin-error.js @@ -0,0 +1,87 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +'use strict'; + +const mysql = require('../../index.js'); +const Command = require('../../lib/commands/command.js'); +const Packets = require('../../lib/packets/index.js'); + +const assert = require('assert'); + +class TestAuthSwitchPluginError extends Command { + constructor(args) { + super(); + this.args = args; + } + + start(_, connection) { + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + protocolVersion: 10, + serverVersion: 'node.js rocks' + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestAuthSwitchPluginError.prototype.sendAuthSwitchRequest; + } + + sendAuthSwitchRequest(_, connection) { + const asr = new Packets.AuthSwitchRequest(this.args); + connection.writePacket(asr.toPacket()); + return TestAuthSwitchPluginError.prototype.finish; + } + + finish(_, connection) { + connection.end(); + return TestAuthSwitchPluginError.prototype.finish; + } +} + +const server = mysql.createServer(conn => { + conn.addCommand( + new TestAuthSwitchPluginError({ + pluginName: 'auth_test_plugin', + pluginData: Buffer.allocUnsafe(0) + }) + ); +}); + +let error; +let uncaughtExceptions = 0; + +const portfinder = require('portfinder'); +portfinder.getPort((_, port) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port, + authPlugins: { + auth_test_plugin: () => { + throw new Error('boom'); + } + } + }); + + conn.on('error', err => { + error = err; + + conn.end(); + server.close(); + }); +}); + +process.on('uncaughtException', err => { + // The plugin reports a fatal error + assert.equal(error.code, 'AUTH_SWITCH_PLUGIN_ERROR'); + assert.equal(error.message, 'boom'); + assert.equal(error.fatal, true); + // The server must close the connection + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); + + uncaughtExceptions += 1; +}); + +process.on('exit', () => { + assert.equal(uncaughtExceptions, 1); +}); diff --git a/test/integration/test-handshake-unknown-packet-error.js b/test/integration/test-handshake-unknown-packet-error.js new file mode 100644 index 0000000000..3b275ba54a --- /dev/null +++ b/test/integration/test-handshake-unknown-packet-error.js @@ -0,0 +1,83 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +'use strict'; + +const mysql = require('../../index.js'); +const Command = require('../../lib/commands/command.js'); +const Packet = require('../../lib/packets/packet.js'); +const Packets = require('../../lib/packets/index.js'); + +const assert = require('assert'); + +class TestUnknownHandshakePacket extends Command { + constructor(args) { + super(); + this.args = args; + } + + start(_, connection) { + const serverHelloPacket = new Packets.Handshake({ + // "required" properties + protocolVersion: 10, + serverVersion: 'node.js rocks' + }); + this.serverHello = serverHelloPacket; + serverHelloPacket.setScrambleData(() => { + connection.writePacket(serverHelloPacket.toPacket(0)); + }); + return TestUnknownHandshakePacket.prototype.writeUnexpectedPacket; + } + + writeUnexpectedPacket(_, connection) { + const length = 6 + this.args.length; + const buffer = Buffer.allocUnsafe(length); + const up = new Packet(0, buffer, 0, length); + up.offset = 4; + up.writeInt8(0xfd); + up.writeBuffer(this.args); + connection.writePacket(up); + return TestUnknownHandshakePacket.prototype.finish; + } + + finish(_, connection) { + connection.end(); + return TestUnknownHandshakePacket.prototype.finish; + } +} + +const server = mysql.createServer(conn => { + conn.addCommand(new TestUnknownHandshakePacket(Buffer.alloc(0))); +}); + +let error; +let uncaughtExceptions = 0; + +const portfinder = require('portfinder'); +portfinder.getPort((_, port) => { + server.listen(port); + const conn = mysql.createConnection({ + port: port + }); + + conn.on('error', err => { + error = err; + + conn.end(); + server.close(); + }); +}); + +process.on('uncaughtException', err => { + // The plugin reports a fatal error + assert.equal(error.code, 'HANDSHAKE_UNKNOWN_ERROR'); + assert.equal(error.message, 'Unexpected packet during handshake phase'); + assert.equal(error.fatal, true); + // The server must close the connection + assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST'); + + uncaughtExceptions += 1; +}); + +process.on('exit', () => { + assert.equal(uncaughtExceptions, 1); +}); diff --git a/test/integration/test-server-close.js b/test/integration/test-server-close.js new file mode 100644 index 0000000000..5c893a9621 --- /dev/null +++ b/test/integration/test-server-close.js @@ -0,0 +1,38 @@ +// Copyright (c) 2021, Oracle and/or its affiliates. + +'use strict'; + +const errors = require('../../lib/constants/errors'); +const common = require('../common'); +const connection = common.createConnection(); +const assert = require('assert'); + +const customWaitTimeout = 1; // seconds + +let error; + +connection.on('error', err => { + error = err; + + connection.close(); +}); + +connection.query(`set wait_timeout=${customWaitTimeout}`, () => { + setTimeout(() => {}, customWaitTimeout * 1000 * 2); +}); + +process.on('uncaughtException', err => { + // The ERR Packet is only sent by MySQL server 8.0.24 or higher, so we + // need to account for the fact it is not sent by older server versions. + if (err.code !== 'ERR_ASSERTION') { + throw err; + } + + assert.equal(error.message, 'Connection lost: The server closed the connection.'); + assert.equal(error.code, 'PROTOCOL_CONNECTION_LOST'); +}); + +process.on('exit', () => { + assert.equal(error.message, 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.'); + assert.equal(error.code, errors.ER_CLIENT_INTERACTION_TIMEOUT); +}); diff --git a/test/unit/connection/test-connection_config.js b/test/unit/connection/test-connection_config.js index 99824e8a26..0e1c0b1ca6 100644 --- a/test/unit/connection/test-connection_config.js +++ b/test/unit/connection/test-connection_config.js @@ -42,3 +42,10 @@ assert.doesNotThrow(() => { flags: ['-FOUND_ROWS'] }); }, 'Error, the constructor threw an exception due to a flags array'); + +assert.strictEqual( + ConnectionConfig.parseUrl( + String.raw`fml://test:pass!@$%^&*()\word:@www.example.com/database` + ).password, + 'pass!%40$%%5E&*()%5Cword%3A' +); diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index b6d169f711..4fb126e9b6 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -147,6 +147,14 @@ declare namespace Connection { * object with ssl parameters or a string containing name of ssl profile */ 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 } export interface SslOptions { @@ -196,6 +204,7 @@ declare class Connection extends EventEmitter { config: Connection.ConnectionOptions; threadId: number; + authorized: boolean; static createQuery(sql: string, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; static createQuery(sql: string, values: any | any[] | { [param: string]: any }, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; diff --git a/typings/mysql/lib/protocol/sequences/Query.d.ts b/typings/mysql/lib/protocol/sequences/Query.d.ts index 9329d3b556..5eaa7491fd 100644 --- a/typings/mysql/lib/protocol/sequences/Query.d.ts +++ b/typings/mysql/lib/protocol/sequences/Query.d.ts @@ -51,6 +51,12 @@ declare namespace Query { * You can find which field function you need to use by looking at: RowDataPacket.prototype._typeCast */ typeCast?: any; + + /** + * This overrides the same option set at the connection level. + * + */ + rowsAsArray?: boolean } export interface StreamOptions {