From 5dab8cc59ddbf3d4af937ad029fdd70b5cd52b46 Mon Sep 17 00:00:00 2001 From: Chris Veness Date: Thu, 24 Sep 2020 11:41:49 +0100 Subject: [PATCH 01/31] Add 'promise.js' to exports Prior to 2.2.0, an ES6 import of the promise wrapper had to specify a file extension ("import mysql from 'mysql2/promise.js';"), which made PR#1100 a breaking change as only the extensionless version was included in the exports; this restores promise.js as a valid import. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7664577863..4d256c105d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ ], "exports": { ".": "./index.js", - "./promise": "./promise.js" + "./promise": "./promise.js", + "./promise.js": "./promise.js" }, "engines": { "node": ">= 8.0" From cf88141ba2aa878bedf22863946ffb355dc2250b Mon Sep 17 00:00:00 2001 From: Tomoaki Imai Date: Sat, 3 Oct 2020 11:24:30 -0700 Subject: [PATCH 02/31] mysql_clear_password can have string type - mysql_clear_password in authPlugins currently only supports Promise type. However AWS.RDS.Signer.getAuthToken returns string, which causes type error "Type 'string' is not assignable" - This fix will add support for string type as well --- index.d.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index fe3e066421..7c6635d0e5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -151,9 +151,10 @@ 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; export interface ConnectionOptions extends mysql.ConnectionOptions { charsetNumber?: number; @@ -175,7 +176,7 @@ export interface ConnectionOptions extends mysql.ConnectionOptions { queueLimit?: number; waitForConnections?: boolean; authPlugins?: { - [key: string]: authPlugins; + [key: string]: authPlugins; }; } From 3526352565e3e96610068f29f32ff956edb0592c Mon Sep 17 00:00:00 2001 From: Tomoaki Imai Date: Sat, 3 Oct 2020 21:54:15 -0700 Subject: [PATCH 03/31] add possible type for other authPlugins --- index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 7c6635d0e5..a465b43de7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -154,7 +154,9 @@ export interface Pool extends mysql.Connection { type authPlugins = (pluginMetadata: { connection: Connection; command: string; -}) => (pluginData: Buffer) => Promise | string; +}) => ( + pluginData: Buffer +) => Promise | string | Buffer | Promise | null; export interface ConnectionOptions extends mysql.ConnectionOptions { charsetNumber?: number; From a185fcb83a6300b97f38a18fb1d9f15c641067fc Mon Sep 17 00:00:00 2001 From: supleiades Date: Mon, 12 Oct 2020 12:09:07 +0900 Subject: [PATCH 04/31] fix small typo --- documentation/Examples.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(); }); ``` From ee3fd24fb924af9ce74b55201606821ed3bd44eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4=C3=A4kk=C3=B6nen?= Date: Wed, 25 Nov 2020 12:11:51 +0200 Subject: [PATCH 05/31] Allow unnamed placeholders even if the namedPlaceholders flag is enabled --- lib/connection.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/connection.js b/lib/connection.js index f99f53d6c4..c323519aa2 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -488,6 +488,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')(); } From af8bbffc0da0b92c645c05a4c8c75edf237e396b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4=C3=A4kk=C3=B6nen?= Date: Thu, 26 Nov 2020 11:37:25 +0200 Subject: [PATCH 06/31] Update named placeholder documentation to include unnamed fallback --- documentation/Extras.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 From b5ad7160faa28b5f2fcff628c0c810b52d185819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4=C3=A4kk=C3=B6nen?= Date: Thu, 26 Nov 2020 11:41:46 +0200 Subject: [PATCH 07/31] Add test for unnamed placeholder fallback --- ...paceholders.js => test-named-placeholders.js} | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) rename test/integration/connection/{test-named-paceholders.js => test-named-placeholders.js} (87%) 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; From 424a778258078c92cda4edc6549a7d951ad62d81 Mon Sep 17 00:00:00 2001 From: Chen WeiJian Date: Wed, 2 Dec 2020 09:50:24 +0800 Subject: [PATCH 08/31] Update Connection.d.ts --- typings/mysql/lib/Connection.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index b6d169f711..65f38b01bc 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -196,6 +196,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; From 759d440cafaa5265c175c893ad61a5b2cac0f0f6 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Mon, 15 Feb 2021 13:14:54 -0600 Subject: [PATCH 09/31] typing and readme docs for rowAsArray --- README.md | 30 +++++++++++++++++++ typings/mysql/lib/Connection.d.ts | 8 +++++ .../mysql/lib/protocol/sequences/Query.d.ts | 6 ++++ 3 files changed, 44 insertions(+) diff --git a/README.md b/README.md index 5a2f4423cc..df9f457347 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,36 @@ 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 +}); + +``` + + +### Query + + ## 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/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index 65f38b01bc..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 { 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 { From 9534c0bad7f9f72969ae3398130b45d985f59ae1 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Mon, 15 Feb 2021 13:17:09 -0600 Subject: [PATCH 10/31] oops, typo --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index df9f457347..b1de181a52 100644 --- a/README.md +++ b/README.md @@ -242,10 +242,6 @@ con.query({ sql: 'select 1 as foo, 2 as foo', rowsAsArray: true }, function(err, ``` - -### Query - - ## 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. From 0992eee47c01a970f9cb0e44b50ea1a72f7effc1 Mon Sep 17 00:00:00 2001 From: Adam Pancutt Date: Tue, 23 Feb 2021 08:47:30 +0100 Subject: [PATCH 11/31] Expose SQL in errors --- lib/commands/command.js | 1 + promise.js | 1 + test/integration/connection/test-errors.js | 2 ++ 3 files changed, 4 insertions(+) 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/promise.js b/promise.js index bcaf3f2f47..5822d858ea 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); 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; } From f939119c0bbfa073da01149844307e779f1bf523 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Thu, 21 Jan 2021 17:23:47 +0000 Subject: [PATCH 12/31] tests: decouple fake server from MYSQL_HOST Fake servers are being created on the host specified by the MYSQL_HOST environment variable. This can lead to issues when the real MySQL server instance used for the integration tests is not running on the same host, for instance, in Docker-based CI environments. This patch ensures fake servers are explicitely created in the host/container where the test suite is running. --- test/integration/connection/test-disconnects.js | 16 +++++++++++++++- .../connection/test-protocol-errors.js | 16 +++++++++++++++- test/integration/connection/test-quit.js | 16 +++++++++++++++- .../integration/connection/test-stream-errors.js | 16 +++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) 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-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-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; }); From 9cc56fb4e02f9776b111fec20b4930ad99e14999 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Tue, 1 Jun 2021 19:41:09 +0100 Subject: [PATCH 13/31] fix non-standard invalid date test NO_ZERO_DATE mode and NO_ZERO_IN_DATE mode are part of the strict SQL mode used by MySQL 8.0. Non-standard dates like '0000-00-00' are now stored and retrieved as NULL. This patch introduces changes to one of the integration tests to ensure this change in behavior is accounted for with the latest MySQL server versions. --- lib/packets/packet.js | 10 ++++++ .../connection/test-invalid-date-result.js | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) 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/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) { From ed73cc5610fa86f6a5f7f2340afc7f4b7f70c6b5 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Tue, 1 Jun 2021 11:13:01 +0100 Subject: [PATCH 14/31] handle connection close error message A custom ERR_PACKET is sent by the server on MySQL 8.0.24 (and higher) when the server disconnects inactive clients. This causes some integration tests that destroy the socket connection to fail due to packet number and order mismatch. The patch introduces logic to handle this potential new and unexpected packet and report the custom server message to the application. --- lib/connection.js | 31 ++++++++++++++++++---- lib/constants/errors.js | 7 +++++ lib/packets/index.js | 19 ++++++++++++++ test/integration/test-server-close.js | 38 +++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 test/integration/test-server-close.js diff --git a/lib/connection.js b/lib/connection.js index c323519aa2..6da01135b2 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,3 +1,8 @@ +// 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. + 'use strict'; const Net = require('net'); @@ -188,7 +193,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; @@ -368,6 +373,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 +428,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; } 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/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); +}); From cd61d7bf196c47faf8968e06939fb6ba1a645e8a Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Wed, 2 Jun 2021 16:25:20 +0100 Subject: [PATCH 15/31] avoid column length expectations for integers The column length returned as result set metadata from the server only makes sense for DECIMAL column data types. For any other type, the value is non-deterministic and this kind of 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 This patch updates some integration tests that are failing due to an incorrect assumption about the "columnLength" value, by avoiding to assert any expectation about it altogether. In this case, the tests are expecting a statement to yield a column length of a number of digits, which is the case on MySQL 8.0.24 or older versions. However, MySQL 8.0.25 reports an additional digit to accommodate the sign for signed integers. The breaking change was introduced by the server to address the issues described in the following bug report: https://bugs.mysql.com/bug.php?id=99567 --- .../test-binary-multiple-results.js | 15 +++++++++++--- .../connection/test-execute-nocolumndef.js | 20 ++++++++----------- .../connection/test-multiple-results.js | 15 +++++++++++--- 3 files changed, 32 insertions(+), 18 deletions(-) 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-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-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]); From 888e977585e0cbdc72173fd018acd2e2e2c4c057 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Mon, 14 Jun 2021 15:38:58 +0100 Subject: [PATCH 16/31] travis: save docker images on local cache The recently introduced rate limits on Docker Hub are constantly being reached by every TravisCI because each individual job that creates a MySQL container, tries to pull the base image from Docker Hub. With a not-so-small test matrix, this issue surfaces pretty fast. This patch introduces a local cache to save docker images and be able to re-use them on different jobs and avoid constantly pulling them from Docker Hub. --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 From e052e7ba147c1f6b3f21fd0fbbf5f88b038aaad1 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Thu, 17 Jun 2021 10:32:45 +0100 Subject: [PATCH 17/31] add handshake fatal error handling Errors that occur during the handshake stage should be fatal and should cause the connection to be closed. Currently, although unexpected errors such as ones created by unexpected packets sent by the server, are being handled, they are not considered fatal and do not result in closing the connection. On the other hand, errors reported by client-side authentication plugins are being ignored and are not reported to the application. Since errors reported by server-side authentication plugins are considered fatal and cause the connection to be closed, the fact that the same does not happen for errors generated on the client-side leads to some inconsistencies and forces custom authentication plugin implementations to forcibly close the connection on their own. This patch ensures that both this kind of errors are considered fatal and cause the connection to be closed. --- lib/commands/client_handshake.js | 17 +++- lib/connection.js | 16 +++- .../test-auth-switch-plugin-error.js | 87 +++++++++++++++++++ .../test-handshake-unknown-packet-error.js | 83 ++++++++++++++++++ 4 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 test/integration/test-auth-switch-plugin-error.js create mode 100644 test/integration/test-handshake-unknown-packet-error.js 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/connection.js b/lib/connection.js index 6da01135b2..47970e971c 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -3,6 +3,11 @@ // 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'); @@ -105,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; @@ -229,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) { 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); +}); From b027425f91c072759ae85ae96da9fd614090a78e Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Mon, 5 Jul 2021 18:01:46 +0100 Subject: [PATCH 18/31] add async authPlugin error handlers Errors reported by asynchronous authentication plugins are not properly handled, leading the client to become idle, at least until the server closes the connection because of inactivity. This patch introduces the changes to ensure all errors generated by asynchronous authentication plugins result in fatal 'error' events emitted at the command level (and subsequently bubbled up to the connection instance), allowing the client to immediately close the underlying socket connection and stop the process. --- lib/commands/auth_switch.js | 25 ++++-- .../test-auth-switch-plugin-async-error.js | 89 +++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 test/integration/test-auth-switch-plugin-async-error.js 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/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); +}); From 8c7b87f8e4c34263c462705ed081839a3ae128da Mon Sep 17 00:00:00 2001 From: Andrew Dibble Date: Tue, 20 Jul 2021 12:50:07 +0200 Subject: [PATCH 19/31] Remove calls to deprecated url.parse --- benchmarks/FB/hello.js | 4 ++-- benchmarks/http-select-and-render.js | 6 ++--- lib/connection_config.js | 36 ++++++++++++---------------- 3 files changed, 20 insertions(+), 26 deletions(-) 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/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; } } From e5af81ff067d899d3c6b5754eabead122c24923f Mon Sep 17 00:00:00 2001 From: Andrew Dibble Date: Tue, 20 Jul 2021 14:43:20 +0200 Subject: [PATCH 20/31] eslint fixes --- examples/promise-co-await/co.js | 6 +++--- examples/ssl/rds-ssl.js | 2 +- examples/ssl/select-over-ssl.js | 4 ++-- tools/generate-charset-mapping.js | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/promise-co-await/co.js b/examples/promise-co-await/co.js index fdae08b19b..fdc9d0a414 100644 --- a/examples/promise-co-await/co.js +++ b/examples/promise-co-await/co.js @@ -3,7 +3,7 @@ const mysql = require('mysql2/promise'); const co = require('co'); -co(function*() { +co(function* () { const c = yield mysql.createConnection({ port: 3306, user: 'root', @@ -14,10 +14,10 @@ co(function*() { console.log(yield c.execute('select 1+:toAdd as qqq', { toAdd: 10 })); yield c.end(); }) - .then(function() { + .then(() => { console.log('done'); }) - .catch(function(err) { + .catch((err) => { console.log(err); throw err; }); diff --git a/examples/ssl/rds-ssl.js b/examples/ssl/rds-ssl.js index 9138c53976..287467840d 100644 --- a/examples/ssl/rds-ssl.js +++ b/examples/ssl/rds-ssl.js @@ -11,7 +11,7 @@ const conn = mysql.createConnection({ ssl: 'Amazon RDS' }); -conn.query("show status like 'Ssl_cipher'", function(err, res) { +conn.query("show status like 'Ssl_cipher'", (err, res) => { console.log(err, res); conn.end(); }); diff --git a/examples/ssl/select-over-ssl.js b/examples/ssl/select-over-ssl.js index 13d8de3b32..d163891e0a 100644 --- a/examples/ssl/select-over-ssl.js +++ b/examples/ssl/select-over-ssl.js @@ -16,9 +16,9 @@ const conn = mysql.createConnection({ } }); -conn.query('select 1+1 as test', function(err, res) { +conn.query('select 1+1 as test', (err, res) => { console.log(res); - conn.query('select repeat("a", 100) as test', function(err, res) { + conn.query('select repeat("a", 100) as test', (err, res) => { console.log(res); }); }); diff --git a/tools/generate-charset-mapping.js b/tools/generate-charset-mapping.js index 5e2f82b18d..c5514dc226 100644 --- a/tools/generate-charset-mapping.js +++ b/tools/generate-charset-mapping.js @@ -30,9 +30,9 @@ const mysql2iconv = { const missing = {}; -conn.query('show collation', function(err, res) { +conn.query('show collation', (err, res) => { console.log(res); - res.forEach(r => { + res.forEach((r) => { const charset = r.Charset; const iconvCharset = mysql2iconv[charset] || charset; // if there is manuall mapping, override if (!iconv.encodingExists(iconvCharset)) { @@ -43,7 +43,7 @@ conn.query('show collation', function(err, res) { //console.log(JSON.stringify(missing, 4, null)); //console.log(JSON.stringify(charsets, 4, null)); for (let i = 0; i < charsets.length; i += 8) { - console.log(" '" + charsets.slice(i, i + 8).join("', '") + "',"); + console.log(` '${charsets.slice(i, i + 8).join("', '")}',`); } }); From 955db3bf8fec275501fb1da96d813eb2ef1afc4d Mon Sep 17 00:00:00 2001 From: Andrew Dibble Date: Thu, 22 Jul 2021 09:12:55 +0200 Subject: [PATCH 21/31] Add test --- test/unit/connection/test-connection_config.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/unit/connection/test-connection_config.js b/test/unit/connection/test-connection_config.js index 99824e8a26..b584d32d94 100644 --- a/test/unit/connection/test-connection_config.js +++ b/test/unit/connection/test-connection_config.js @@ -11,7 +11,7 @@ assert.throws( new ConnectionConfig({ ssl: true }), - err => err instanceof TypeError && err.message === expectedMessage, + (err) => err instanceof TypeError && err.message === expectedMessage, 'Error, the constructor accepts a boolean without throwing the right exception' ); @@ -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' +); From 0fdff1a4c3837a7303f3762bb644b56df041aed3 Mon Sep 17 00:00:00 2001 From: Andrew Dibble Date: Thu, 22 Jul 2021 11:14:13 +0200 Subject: [PATCH 22/31] revert formatting changes --- examples/promise-co-await/co.js | 6 +++--- examples/ssl/rds-ssl.js | 2 +- examples/ssl/select-over-ssl.js | 4 ++-- test/unit/connection/test-connection_config.js | 2 +- tools/generate-charset-mapping.js | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/promise-co-await/co.js b/examples/promise-co-await/co.js index fdc9d0a414..fdae08b19b 100644 --- a/examples/promise-co-await/co.js +++ b/examples/promise-co-await/co.js @@ -3,7 +3,7 @@ const mysql = require('mysql2/promise'); const co = require('co'); -co(function* () { +co(function*() { const c = yield mysql.createConnection({ port: 3306, user: 'root', @@ -14,10 +14,10 @@ co(function* () { console.log(yield c.execute('select 1+:toAdd as qqq', { toAdd: 10 })); yield c.end(); }) - .then(() => { + .then(function() { console.log('done'); }) - .catch((err) => { + .catch(function(err) { console.log(err); throw err; }); diff --git a/examples/ssl/rds-ssl.js b/examples/ssl/rds-ssl.js index 287467840d..9138c53976 100644 --- a/examples/ssl/rds-ssl.js +++ b/examples/ssl/rds-ssl.js @@ -11,7 +11,7 @@ const conn = mysql.createConnection({ ssl: 'Amazon RDS' }); -conn.query("show status like 'Ssl_cipher'", (err, res) => { +conn.query("show status like 'Ssl_cipher'", function(err, res) { console.log(err, res); conn.end(); }); diff --git a/examples/ssl/select-over-ssl.js b/examples/ssl/select-over-ssl.js index d163891e0a..13d8de3b32 100644 --- a/examples/ssl/select-over-ssl.js +++ b/examples/ssl/select-over-ssl.js @@ -16,9 +16,9 @@ const conn = mysql.createConnection({ } }); -conn.query('select 1+1 as test', (err, res) => { +conn.query('select 1+1 as test', function(err, res) { console.log(res); - conn.query('select repeat("a", 100) as test', (err, res) => { + conn.query('select repeat("a", 100) as test', function(err, res) { console.log(res); }); }); diff --git a/test/unit/connection/test-connection_config.js b/test/unit/connection/test-connection_config.js index b584d32d94..0e1c0b1ca6 100644 --- a/test/unit/connection/test-connection_config.js +++ b/test/unit/connection/test-connection_config.js @@ -11,7 +11,7 @@ assert.throws( new ConnectionConfig({ ssl: true }), - (err) => err instanceof TypeError && err.message === expectedMessage, + err => err instanceof TypeError && err.message === expectedMessage, 'Error, the constructor accepts a boolean without throwing the right exception' ); diff --git a/tools/generate-charset-mapping.js b/tools/generate-charset-mapping.js index c5514dc226..5e2f82b18d 100644 --- a/tools/generate-charset-mapping.js +++ b/tools/generate-charset-mapping.js @@ -30,9 +30,9 @@ const mysql2iconv = { const missing = {}; -conn.query('show collation', (err, res) => { +conn.query('show collation', function(err, res) { console.log(res); - res.forEach((r) => { + res.forEach(r => { const charset = r.Charset; const iconvCharset = mysql2iconv[charset] || charset; // if there is manuall mapping, override if (!iconv.encodingExists(iconvCharset)) { @@ -43,7 +43,7 @@ conn.query('show collation', (err, res) => { //console.log(JSON.stringify(missing, 4, null)); //console.log(JSON.stringify(charsets, 4, null)); for (let i = 0; i < charsets.length; i += 8) { - console.log(` '${charsets.slice(i, i + 8).join("', '")}',`); + console.log(" '" + charsets.slice(i, i + 8).join("', '") + "',"); } }); From b1fa0a3b3225ff711c7f35d0f0c394ba2db89f25 Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Sat, 24 Jul 2021 00:30:58 +0800 Subject: [PATCH 23/31] feat(poolCluster): add query method --- lib/pool_cluster.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/pool_cluster.js b/lib/pool_cluster.js index 47b044b966..9ee0991c36 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,43 @@ class PoolNamespace { }); } + /** + * pool cluster query + * @param {*} sql + * @param {*} values + * @param {*} cb + * @returns query + */ + query(sql, values, cb) { + const clusterNode = this._getClusterNode(); + const query = Connection.createQuery(sql, values, cb, {}); + if (clusterNode === null) { + return cb(new Error('Pool does Not exists.')); + } + this._cluster._getConnection(clusterNode, (err, conn) => { + if (err) { + if (typeof query.onResult === 'function') { + query.onResult(err); + } else { + query.emit('error', err); + } + return; + } + if (conn === 'retry') { + return this.query(sql, values, cb); + } + try { + conn.query(query).once('end', () => { + conn.release(); + }); + } catch (e) { + conn.release(); + throw e; + } + }); + return query; + } + _getClusterNode() { const foundNodeIds = this._cluster._findNodeIds(this._pattern); if (foundNodeIds.length === 0) { From 2cfecd9a5d48987ba98ce7a8de26d26399cda7f6 Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Mon, 26 Jul 2021 02:03:40 +0800 Subject: [PATCH 24/31] feat(poolCluster): add promise version --- index.js | 3 ++ lib/pool_cluster.js | 9 +---- promise.js | 98 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 8 deletions(-) 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/pool_cluster.js b/lib/pool_cluster.js index 9ee0991c36..6e65ce238f 100644 --- a/lib/pool_cluster.js +++ b/lib/pool_cluster.js @@ -55,12 +55,8 @@ class PoolNamespace { * @returns query */ query(sql, values, cb) { - const clusterNode = this._getClusterNode(); const query = Connection.createQuery(sql, values, cb, {}); - if (clusterNode === null) { - return cb(new Error('Pool does Not exists.')); - } - this._cluster._getConnection(clusterNode, (err, conn) => { + this.getConnection((err, conn) => { if (err) { if (typeof query.onResult === 'function') { query.onResult(err); @@ -69,9 +65,6 @@ class PoolNamespace { } return; } - if (conn === 'retry') { - return this.query(sql, values, cb); - } try { conn.query(query).once('end', () => { conn.release(); diff --git a/promise.js b/promise.js index 5822d858ea..06ef719733 100644 --- a/promise.js +++ b/promise.js @@ -428,8 +428,106 @@ 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); + }); + } + + 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; From 5fa053ade98ddd268dae4deef52aa117c44cd818 Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Wed, 28 Jul 2021 09:07:22 +0800 Subject: [PATCH 25/31] feat(query): add timeout option --- lib/commands/query.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/commands/query.js b/lib/commands/query.js index edf9bab366..4ebee2f203 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,15 @@ class Query extends Command { } this._connection = connection; this.options = Object.assign({}, connection.config, this._queryOptions); + + if (this.timeout) { + const timeoutHandler = this._handleTimeoutError.bind(this); + this.queryTimeout = Timers.setTimeout( + timeoutHandler, + this.timeout + ); + } + const cmdPacket = new Packets.Query( this.sql, connection.config.charsetNumber @@ -58,6 +70,10 @@ class Query extends Command { done() { this._unpipeStream(); + if (this.queryTimeout) { + Timers.clearTimeout(this.queryTimeout); + this.queryTimeout = null; + } if (this.onResult) { let rows, fields; if (this._resultIndex === 0) { @@ -272,6 +288,24 @@ class Query extends Command { }); return stream; } + + _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; From 6bc6d869cb4217039618eb4244ef67b9388685f4 Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Sat, 31 Jul 2021 10:10:41 +0800 Subject: [PATCH 26/31] feat(query): add timeout test --- lib/commands/query.js | 5 +++++ .../connection/test-query-timeout.js | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 test/integration/connection/test-query-timeout.js diff --git a/lib/commands/query.js b/lib/commands/query.js index 4ebee2f203..bbd163864d 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -70,6 +70,11 @@ 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; diff --git a/test/integration/connection/test-query-timeout.js b/test/integration/connection/test-query-timeout.js new file mode 100644 index 0000000000..d8df20dd88 --- /dev/null +++ b/test/integration/connection/test-query-timeout.js @@ -0,0 +1,21 @@ +'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.end(); +}); From 309703a3be99c88feca8c7a5dd1fd0c788ef4cb7 Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Sun, 1 Aug 2021 17:37:28 +0800 Subject: [PATCH 27/31] fix(timeout): execute add timeout option --- lib/commands/execute.js | 5 +++++ lib/commands/query.js | 19 +++++++++++-------- .../connection/test-query-timeout.js | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) 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 bbd163864d..344b4abc36 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -51,14 +51,7 @@ class Query extends Command { } this._connection = connection; this.options = Object.assign({}, connection.config, this._queryOptions); - - if (this.timeout) { - const timeoutHandler = this._handleTimeoutError.bind(this); - this.queryTimeout = Timers.setTimeout( - timeoutHandler, - this.timeout - ); - } + this._setTimeout(); const cmdPacket = new Packets.Query( this.sql, @@ -294,6 +287,16 @@ 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); diff --git a/test/integration/connection/test-query-timeout.js b/test/integration/connection/test-query-timeout.js index d8df20dd88..ca46373d9c 100644 --- a/test/integration/connection/test-query-timeout.js +++ b/test/integration/connection/test-query-timeout.js @@ -17,5 +17,20 @@ connection.query({ sql: 'SELECT sleep(1) as a', timeout: 5000 }, (err, res) => { 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(); }); From 1b542f828a06303cce46499e89e1ddb9782b1d10 Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Mon, 2 Aug 2021 00:35:58 +0800 Subject: [PATCH 28/31] test(timeout): add connectTimeout test --- test/common.js | 3 ++- .../connection/test-connect-timeout.js | 27 +++++++++++++++++++ .../connection/test-query-timeout.js | 14 ++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/integration/connection/test-connect-timeout.js 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-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-query-timeout.js b/test/integration/connection/test-query-timeout.js index ca46373d9c..4e117d8c6b 100644 --- a/test/integration/connection/test-query-timeout.js +++ b/test/integration/connection/test-query-timeout.js @@ -34,3 +34,17 @@ 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 From 703a9e32ad693a94391289236861de2e069caf4f Mon Sep 17 00:00:00 2001 From: yuxizhe Date: Wed, 4 Aug 2021 22:38:38 +0800 Subject: [PATCH 29/31] feat(poolCluster): add execute method --- lib/pool_cluster.js | 26 ++++++++++++++++++++++++++ promise.js | 14 ++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/pool_cluster.js b/lib/pool_cluster.js index 6e65ce238f..92f53de668 100644 --- a/lib/pool_cluster.js +++ b/lib/pool_cluster.js @@ -77,6 +77,32 @@ class PoolNamespace { 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/promise.js b/promise.js index 06ef719733..fc74cc0c3a 100644 --- a/promise.js +++ b/promise.js @@ -463,6 +463,20 @@ class PromisePoolCluster extends EventEmitter { }); } + 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), From b351d86d125ee71fac182d66526e3e8443065419 Mon Sep 17 00:00:00 2001 From: Andrey Sidorov Date: Thu, 5 Aug 2021 23:42:20 +1000 Subject: [PATCH 30/31] 2.3.0 changes --- Changelog.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 From 05e9e153a3c8530c957140b59a654a999e7c3c6e Mon Sep 17 00:00:00 2001 From: Andrey Sidorov Date: Thu, 5 Aug 2021 23:42:31 +1000 Subject: [PATCH 31/31] 2.3.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 4d256c105d..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": {