diff --git a/.travis.yml b/.travis.yml index 1bf2a82d..a4b5ecab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,113 +1,45 @@ -os: linux -dist: jammy language: node_js -services: docker node_js: 20 -addons: - hosts: - - mariadb.example.com - +version: ~> 1.0 before_install: - - git clone https://github.com/mariadb-corporation/connector-test-machine.git - - -install: - |- case $TRAVIS_OS_NAME in windows) powershell -Command Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe - ;; - linux) - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ls -lrt - ;; - esac - - |- - case $TRAVIS_OS_NAME in - windows) choco install -y --force nodejs-lts # force refresh path export PATH=$(cmd.exe //c "refreshenv > nul & C:\Progra~1\Git\bin\bash -c 'echo \$PATH' ") - connector-test-machine/launch.bat -t "$srv" -v "$v" -d testn ;; linux) - source connector-test-machine/launch.sh -t "$srv" -v "$v" -d testn -l "$local" -c "$CLEAR_TEXT" -s "$DISABLE_SSL" + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ls -lrt ;; esac env: - global: - - RUN_LONG_TEST=1 - - CLEAR_TEXT=0 + global: RUN_LONG_TEST=1 CLEAR_TEXT=0 DB=testn -stages: - - Minimal - - name: Enterprise - if: type = push AND fork = false - - Community +import: mariadb-corporation/connector-test-machine:common-build.yml@master jobs: - fast_finish: true - allow_failures: - - env: srv=build - - env: srv=xpand RUN_LONG_TEST=0 include: - - stage: Minimal - env: srv=mariadb v=10.6 local=1 packet=40 - name: "CS 10.6" - - env: srv=mariadb-es v=10.6 - name: "ES 10.6" - if: type = push AND fork = false - - - stage: Enterprise - env: srv=mariadb-es v=10.4 - name: "ES 10.4" - if: type = push AND fork = false - - env: srv=mariadb-es v=10.5 - name: "ES 10.5" - if: type = push AND fork = false - - env: srv=mariadb-es-test v=23.08 - name: "ES 23.08" - if: type = push AND fork = false - - env: srv=maxscale - name: "Maxscale" - - env: srv=xpand RUN_LONG_TEST=0 - name: "Xpand" - - - stage: Community - env: srv=mariadb v=10.6 - os: windows - language: shell - name: "CS 10.6 - Windows" - - env: srv=mariadb v=10.4 local=1 - dist: bionic - node_js: 16 - name: "CS 10.4" - - env: srv=mariadb v=10.5 local=1 DISABLE_SSL=1 - dist: bionic - name: "CS 10.5 - node.js 14" - node_js: 14 - - env: srv=mariadb v=10.11 local=1 + - stage: Language + env: srv=mariadb v=10.11 local=1 name: "CS 10.11 - node.js 16" node_js: 16 - - env: srv=mariadb v=11.0 local=1 CLEAR_TEXT=1 + - stage: Language + env: srv=mariadb v=10.11 local=1 CLEAR_TEXT=1 node_js: 18 - name: "CS 11.0 - node.js 18" - - env: srv=mariadb v=11.1 local=1 - name: "CS 11.1 - node.js 20" + name: "CS 10.11 - node.js 18" + - stage: Language + env: srv=mariadb v=10.11 local=1 DISABLE_SSL=1 + name: "CS 10.11 - node.js 20" node_js: 20 - - env: srv=mysql v=5.7 - name: "MySQL 5.7" - - env: srv=mysql v=8.0 - name: "MySQL 8.0" - - env: srv=mariadb v=10.6 BENCH=1 local=1 + - stage: Benchmarks + env: srv=mariadb v=10.11 BENCH=1 local=1 name: "Benchmarks" - node_js: 18 - dist: focal - - env: srv=build - name: "CS build" script: - npm install @@ -131,6 +63,3 @@ script: after_success: - if [ -z "$BENCH" ] ; then npm run coverage:report; fi - -after_failure: - - if [ "$srv" == "maxscale" ] ; then docker-compose -f ${COMPOSE_FILE} exec -u root maxscale tail -500 /var/log/maxscale/maxscale.log; fi \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a7700b..5e5da692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.2.3](https://github.com/mariadb-corporation/mariadb-connector-nodejs/tree/3.2.3) (Dec 2023) +[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-nodejs/compare/3.2.2...3.2.3) + +* CONJS-207 Add support for connection redirection +* CONJS-271 wrong binary decoding of 00:00:00 TIME values +* CONJS-272 Error doesn't always have parameters according to option +* CONJS-273 Bulk insert error when last bunch of parameters is reaching max_allowed_packet +* CONJS-274 permit disabling BULK insert for one batch +* CONJS-207 Add support for connection redirection + + ## [3.2.2](https://github.com/mariadb-corporation/mariadb-connector-nodejs/tree/3.2.2) (Oct 2023) [Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-nodejs/compare/3.2.1...3.2.2) diff --git a/README.md b/README.md index 5834a34f..7f9c6b89 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ **Non-blocking MariaDB and MySQL client for Node.js.** -MariaDB and MySQL client, 100% JavaScript, with TypeScript definition, with the Promise API. +MariaDB and MySQL client, 100% JavaScript, with TypeScript definition, with the Promise API, distributed under the LGPL license version 2.1 or later (LGPL-2.1-or-later) ## Documentation diff --git a/documentation/callback-api.md b/documentation/callback-api.md index c4bee3b1..6f6cb11b 100644 --- a/documentation/callback-api.md +++ b/documentation/callback-api.md @@ -523,9 +523,9 @@ https.get("https://node.green/#ES2018-features-Promise-prototype-finally-basic-s Queries issued from the Connector return two different kinds of results: a JSON object and an array, depending on the type of query you issue. Queries that write to the database, such as `INSERT`, `DELETE` and `UPDATE` commands return a JSON object with the following properties: -* `affectedRows`: Indicates the number of rows affected by the query. -* `insertId`: Shows the last auto-increment value from an `INSERT`. -* `warningStatus`: Indicates whether the query ended with a warning. +* `affectedRows`: An integer listing the number of affected rows. +* `insertId`: An integer noting the auto-increment ID. In case multiple rows have been inserted, this corresponds to the FIRST auto-increment value. +* `warningStatus`: An integer indicating whether the query ended with a warning. ```js connection.query( diff --git a/documentation/promise-api.md b/documentation/promise-api.md index a6e3ec0b..de7e1640 100644 --- a/documentation/promise-api.md +++ b/documentation/promise-api.md @@ -600,7 +600,7 @@ https.get( Queries return two different kinds of results, depending on the type of query you execute. When you execute write statements, (such as `INSERT`, `DELETE` and `UPDATE`), the method returns a JSON object with the following properties: * `affectedRows`: An integer listing the number of affected rows. -* `insertId`: An integer noting the auto-increment ID of the last row written to the table. +* `insertId`: An integer noting the auto-increment ID. In case multiple rows have been inserted, this corresponds to the FIRST auto-increment value. * `warningStatus`: An integer indicating whether the query ended with a warning. ```js diff --git a/lib/cmd/batch-bulk.js b/lib/cmd/batch-bulk.js index e1d2101a..3a433b4f 100644 --- a/lib/cmd/batch-bulk.js +++ b/lib/cmd/batch-bulk.js @@ -213,7 +213,7 @@ class BatchBulk extends Parser { */ sendComStmtBulkExecute(out, opts, info) { if (opts.logger.query) - opts.logger.query(`BULK: (${this.prepare.id}) sql: ${opts.logger.logParam ? this.displaySql() : this.sql}`); + opts.logger.query(`BULK: (${this.prepare.id}) sql: ${opts.logParam ? this.displaySql() : this.sql}`); const parameterCount = this.prepare.parameterCount; this.rowIdx = 0; this.vals = this.values[this.rowIdx++]; @@ -250,7 +250,7 @@ class BatchBulk extends Parser { out.writeBuffer(lastCmdData, 0, lastCmdData.length); out.mark(); lastCmdData = null; - if (!this.rowIdx >= this.values.length) { + if (this.rowIdx >= this.values.length) { break; } this.vals = this.values[this.rowIdx++]; @@ -353,10 +353,9 @@ class BatchBulk extends Parser { for (let i = 0; i < this.initialValues.length; i++) { if (i !== 0) sqlMsg += ','; let param = this.initialValues[i]; - sqlMsg = this.logParameters(sqlMsg, param); + sqlMsg = Parser.logParameters(this.opts, sqlMsg, param); if (sqlMsg.length > this.opts.debugLen) { - sqlMsg = sqlMsg.substring(0, this.opts.debugLen) + '...'; - break; + return sqlMsg.substring(0, this.opts.debugLen) + '...'; } } sqlMsg += ']'; @@ -482,9 +481,9 @@ class BatchBulk extends Parser { if (this.values[r].length < nbParameter) { this.emit('send_end'); this.throwNewError( - `Expect ${nbParameter} parameters, but at index ${r}, parameters only contains ${ - this.values[r].length - }\n ${this.displaySql()}`, + `Expect ${nbParameter} parameters, but at index ${r}, parameters only contains ${this.values[r].length}\n ${ + this.opts.logParam ? this.displaySql() : this.sql + }`, false, info, 'HY000', diff --git a/lib/cmd/class/prepare-result-packet.js b/lib/cmd/class/prepare-result-packet.js index 20594a89..98f02be3 100644 --- a/lib/cmd/class/prepare-result-packet.js +++ b/lib/cmd/class/prepare-result-packet.js @@ -5,6 +5,7 @@ const CommandParameter = require('../../command-parameter'); const Errors = require('../../misc/errors'); const ExecuteStream = require('../execute-stream'); +const Parser = require('../parser'); /** * Prepare result @@ -37,12 +38,22 @@ class PrepareResultPacket { } if (this.isClose()) { + let sql = this.query; + if (this.conn.opts.logParam) { + if (this.query.length > this.conn.opts.debugLen) { + sql = this.query.substring(0, this.conn.opts.debugLen) + '...'; + } else { + let sqlMsg = this.query + ' - parameters:'; + sql = Parser.logParameters(this.conn.opts, sqlMsg, values); + } + } + const error = Errors.createError( `Execute fails, prepare command as already been closed`, Errors.ER_PREPARE_CLOSED, null, '22000', - this.query + sql ); if (!_cb) { diff --git a/lib/cmd/command.js b/lib/cmd/command.js index e6602309..21e6f552 100644 --- a/lib/cmd/command.js +++ b/lib/cmd/command.js @@ -41,7 +41,7 @@ class Command extends EventEmitter { errno, info, sqlState, - this.displaySql(), + this.opts && this.opts.logParam ? this.displaySql() : this.sql, fatal, this.cmdParam ? this.cmdParam.stack : null, false @@ -80,7 +80,7 @@ class Command extends EventEmitter { * @param info connection information */ sendCancelled(msg, errno, info) { - const err = Errors.createError(msg, errno, info, 'HY000', this.displaySql()); + const err = Errors.createError(msg, errno, info, 'HY000', this.opts.logParam ? this.displaySql() : this.sql); this.emit('send_end'); this.throwError(err, info); } diff --git a/lib/cmd/execute.js b/lib/cmd/execute.js index 9d0ba573..200a5897 100644 --- a/lib/cmd/execute.js +++ b/lib/cmd/execute.js @@ -57,9 +57,7 @@ class Execute extends Parser { Buffer.isBuffer(value)) ) { if (opts.logger.query) - opts.logger.query( - `EXECUTE: (${this.prepare.id}) sql: ${opts.logger.logParam ? this.displaySql() : this.sql}` - ); + opts.logger.query(`EXECUTE: (${this.prepare.id}) sql: ${opts.logParam ? this.displaySql() : this.sql}`); if (!this.longDataStep) { this.longDataStep = true; this.registerStreamSendEvent(out, info); @@ -73,7 +71,7 @@ class Execute extends Parser { if (!this.longDataStep) { // no stream parameter, so can send directly if (opts.logger.query) - opts.logger.query(`EXECUTE: (${this.prepare.id}) sql: ${opts.logger.logParam ? this.displaySql() : this.sql}`); + opts.logger.query(`EXECUTE: (${this.prepare.id}) sql: ${opts.logParam ? this.displaySql() : this.sql}`); this.sendComStmtExecute(out, info); } } @@ -88,7 +86,9 @@ class Execute extends Parser { //validate parameter size. if (this.prepare.parameterCount > this.values.length) { this.sendCancelled( - `Parameter at position ${this.values.length} is not set\\nsql: ${this.displaySql()}`, + `Parameter at position ${this.values.length} is not set\\nsql: ${ + this.opts.logParam ? this.displaySql() : this.sql + }`, Errors.ER_MISSING_PARAMETER, info ); diff --git a/lib/cmd/handshake/authentication.js b/lib/cmd/handshake/authentication.js index c25eb3ba..e80edb9e 100644 --- a/lib/cmd/handshake/authentication.js +++ b/lib/cmd/handshake/authentication.js @@ -69,7 +69,7 @@ class Authentication extends Command { packet.skipLengthCodedNumber(); //skip affected rows packet.skipLengthCodedNumber(); //skip last insert id info.status = packet.readUInt16(); - + let mustRedirect = false; if (info.status & ServerStatus.SESSION_STATE_CHANGED) { packet.skip(2); //skip warning count packet.skipLengthCodedNumber(); @@ -97,6 +97,11 @@ class Authentication extends Command { opts.emit('collation', info.collation); break; + case 'redirect_url': + mustRedirect = true; + info.redirect(value, this.successEnd); + break; + case 'connection_id': info.threadId = parseInt(value); break; @@ -116,7 +121,8 @@ class Authentication extends Command { } } } - return this.successEnd(); + if (!mustRedirect) this.successEnd(); + return; //********************************************************************************************************* //* ERR_Packet diff --git a/lib/cmd/parser.js b/lib/cmd/parser.js index 3fba2194..29ab7e92 100644 --- a/lib/cmd/parser.js +++ b/lib/cmd/parser.js @@ -55,7 +55,11 @@ class Parser extends Command { //* ERROR response //********************************************************************************************************* case 0xff: - const err = packet.readError(info, this.displaySql(), this.stack); + // in case of timeout, free accumulated rows + this._columns = null; + this._rows = []; + + const err = packet.readError(info, opts.logParam ? this.displaySql() : this.sql, this.stack); //force in transaction status, since query will have created a transaction if autocommit is off //goal is to avoid unnecessary COMMIT/ROLLBACK. info.status |= ServerStatus.STATUS_IN_TRANS; @@ -135,7 +139,7 @@ class Parser extends Command { } const okPacket = new OkPacket(affectedRows, insertId, packet.readUInt16()); - + let mustRedirect = false; if (info.status & ServerStatus.SESSION_STATE_CHANGED) { packet.skipLengthCodedNumber(); while (packet.remaining()) { @@ -162,6 +166,11 @@ class Parser extends Command { opts.emit('collation', info.collation); break; + case 'redirect_url': + mustRedirect = true; + info.redirect(value, this.okPacketSuccess.bind(this, okPacket, info)); + break; + case 'connection_id': info.threadId = parseInt(value); break; @@ -181,7 +190,20 @@ class Parser extends Command { } } } + if (!mustRedirect) { + if ( + info.redirectRequest && + (info.status & ServerStatus.STATUS_IN_TRANS) === 0 && + (info.status & ServerStatus.MORE_RESULTS_EXISTS) === 0 + ) { + info.redirect(info.redirectRequest, this.okPacketSuccess.bind(this, okPacket, info)); + } else { + this.okPacketSuccess(okPacket, info); + } + } + } + okPacketSuccess(okPacket, info) { if (this._responseIndex === 0) { // fast path for standard single result if (info.status & ServerStatus.MORE_RESULTS_EXISTS) { @@ -213,7 +235,7 @@ class Parser extends Command { success(val) { this.successEnd(val); this._columns = null; - this._rows = null; + this._rows = []; } /** @@ -366,7 +388,10 @@ class Parser extends Command { //force in transaction status, since query will have created a transaction if autocommit is off //goal is to avoid unnecessary COMMIT/ROLLBACK. info.status |= ServerStatus.STATUS_IN_TRANS; - return this.throwError(packet.readError(info, this.displaySql(), this.stack), info); + return this.throwError( + packet.readError(info, this.opts.logParam ? this.displaySql() : this.sql, this.stack), + info + ); } if ((!info.eofDeprecated && packet.length() < 13) || (info.eofDeprecated && packet.length() < 0xffffff)) { @@ -380,47 +405,15 @@ class Parser extends Command { info.status = packet.readUInt16(); } - if (this.opts.metaAsArray) { - //return promise object as array : - // example for SELECT 1 => - // [ - // [ {"1": 1} ], //rows - // [ColumnDefinition] //meta - // ] - - if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) { - if (!this._meta) this._meta = []; - this._meta[this._responseIndex] = this._columns; - this._responseIndex++; - return (this.onPacketReceive = this.readResponsePacket); - } - if (this._responseIndex === 0) { - this.success([this._rows[0], this._columns]); - } else { - if (!this._meta) this._meta = []; - this._meta[this._responseIndex] = this._columns; - this.success([this._rows, this._meta]); - } + if ( + info.redirectRequest && + (info.status & ServerStatus.STATUS_IN_TRANS) === 0 && + (info.status & ServerStatus.MORE_RESULTS_EXISTS) === 0 + ) { + info.redirect(info.redirectRequest, this.resultSetEndingPacketResult.bind(this, info)); } else { - //return promise object as rows that have meta property : - // example for SELECT 1 => - // [ - // {"1": 1}, - // meta: [ColumnDefinition] - // ] - Object.defineProperty(this._rows[this._responseIndex], 'meta', { - value: this._columns, - writable: true, - enumerable: this.opts.metaEnumerable - }); - - if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) { - this._responseIndex++; - return (this.onPacketReceive = this.readResponsePacket); - } - this.success(this._responseIndex === 0 ? this._rows[0] : this._rows); + this.resultSetEndingPacketResult(info); } - return; } } @@ -428,6 +421,49 @@ class Parser extends Command { this.handleNewRows(this.parseRow(packet)); } + resultSetEndingPacketResult(info) { + if (this.opts.metaAsArray) { + //return promise object as array : + // example for SELECT 1 => + // [ + // [ {"1": 1} ], //rows + // [ColumnDefinition] //meta + // ] + + if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) { + if (!this._meta) this._meta = []; + this._meta[this._responseIndex] = this._columns; + this._responseIndex++; + return (this.onPacketReceive = this.readResponsePacket); + } + if (this._responseIndex === 0) { + this.success([this._rows[0], this._columns]); + } else { + if (!this._meta) this._meta = []; + this._meta[this._responseIndex] = this._columns; + this.success([this._rows, this._meta]); + } + } else { + //return promise object as rows that have meta property : + // example for SELECT 1 => + // [ + // {"1": 1}, + // meta: [ColumnDefinition] + // ] + Object.defineProperty(this._rows[this._responseIndex], 'meta', { + value: this._columns, + writable: true, + enumerable: this.opts.metaEnumerable + }); + + if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) { + this._responseIndex++; + return (this.onPacketReceive = this.readResponsePacket); + } + this.success(this._responseIndex === 0 ? this._rows[0] : this._rows); + } + } + /** * Display current SQL with parameters (truncated if too big) * @@ -440,7 +476,7 @@ class Parser extends Command { } let sqlMsg = this.sql + ' - parameters:'; - return this.logParameters(sqlMsg, this.initialValues); + return Parser.logParameters(this.opts, sqlMsg, this.initialValues); } if (this.sql.length > this.opts.debugLen) { return this.sql.substring(0, this.opts.debugLen) + '... - parameters:[]'; @@ -448,8 +484,8 @@ class Parser extends Command { return this.sql + ' - parameters:[]'; } - logParameters(sqlMsg, values) { - if (this.opts.namedPlaceholders) { + static logParameters(opts, sqlMsg, values) { + if (opts.namedPlaceholders) { sqlMsg += '{'; let first = true; for (let key in values) { @@ -461,9 +497,8 @@ class Parser extends Command { sqlMsg += "'" + key + "':"; let param = values[key]; sqlMsg = Parser.logParam(sqlMsg, param); - if (sqlMsg.length > this.opts.debugLen) { - sqlMsg = sqlMsg.substring(0, this.opts.debugLen) + '...'; - break; + if (sqlMsg.length > opts.debugLen) { + return sqlMsg.substring(0, opts.debugLen) + '...'; } } sqlMsg += '}'; @@ -474,15 +509,14 @@ class Parser extends Command { if (i !== 0) sqlMsg += ','; let param = values[i]; sqlMsg = Parser.logParam(sqlMsg, param); - if (sqlMsg.length > this.opts.debugLen) { - sqlMsg = sqlMsg.substring(0, this.opts.debugLen) + '...'; - break; + if (sqlMsg.length > opts.debugLen) { + return sqlMsg.substring(0, opts.debugLen) + '...'; } } } else { sqlMsg = Parser.logParam(sqlMsg, values); - if (sqlMsg.length > this.opts.debugLen) { - sqlMsg = sqlMsg.substring(0, this.opts.debugLen) + '...'; + if (sqlMsg.length > opts.debugLen) { + return sqlMsg.substring(0, opts.debugLen) + '...'; } } sqlMsg += ']'; @@ -579,7 +613,7 @@ class Parser extends Command { Errors.ER_LOCAL_INFILE_NOT_READABLE, info, '22000', - this.sql + this.opts.logParam ? this.displaySql() : this.sql ); error.cause = e; process.nextTick(this.reject, error); @@ -588,19 +622,22 @@ class Parser extends Command { return (this.onPacketReceive = this.readResponsePacket); } - stream.on('error', (err) => { - out.writeEmptyPacket(); - const error = Errors.createError( - `LOCAL INFILE command failed: ${err.message}`, - Errors.ER_LOCAL_INFILE_NOT_READABLE, - info, - '22000', - this.sql - ); - process.nextTick(this.reject, error); - this.reject = null; - this.resolve = null; - }); + stream.on( + 'error', + function (err) { + out.writeEmptyPacket(); + const error = Errors.createError( + `LOCAL INFILE command failed: ${err.message}`, + Errors.ER_LOCAL_INFILE_NOT_READABLE, + info, + '22000', + this.sql + ); + process.nextTick(this.reject, error); + this.reject = null; + this.resolve = null; + }.bind(this) + ); stream.on('data', (chunk) => { out.writeBuffer(chunk, 0, chunk.length); }); diff --git a/lib/cmd/ping.js b/lib/cmd/ping.js index 0e3be024..ca92cedc 100644 --- a/lib/cmd/ping.js +++ b/lib/cmd/ping.js @@ -4,6 +4,7 @@ 'use strict'; const Command = require('./command'); +const ServerStatus = require('../const/server-status'); const PING_COMMAND = new Uint8Array([1, 0, 0, 0, 0x0e]); @@ -39,7 +40,11 @@ class Ping extends Command { packet.skipLengthCodedNumber(); //affected rows packet.skipLengthCodedNumber(); //insert ids info.status = packet.readUInt16(); - this.successEnd(null); + if (info.redirectRequest && (info.status & ServerStatus.STATUS_IN_TRANS) === 0) { + info.redirect(info.redirectRequest, this.successEnd.bind(this, null)); + } else { + this.successEnd(null); + } } } diff --git a/lib/cmd/query.js b/lib/cmd/query.js index 3ad04dd7..80df3a44 100644 --- a/lib/cmd/query.js +++ b/lib/cmd/query.js @@ -29,7 +29,7 @@ class Query extends Parser { * @param info connection information */ start(out, opts, info) { - if (opts.logger.query) opts.logger.query(`QUERY: ${opts.logger.logParam ? this.displaySql() : this.sql}`); + if (opts.logger.query) opts.logger.query(`QUERY: ${opts.logParam ? this.displaySql() : this.sql}`); this.onPacketReceive = this.readResponsePacket; if (this.initialValues === undefined) { //shortcut if no parameters @@ -50,7 +50,7 @@ class Query extends Parser { this.encodedSql, info, this.initialValues, - this.displaySql.bind(this) + this.opts.logParam ? this.displaySql.bind(this) : () => this.sql ); this.paramPositions = parsed.paramPositions; this.values = parsed.values; diff --git a/lib/cmd/reset.js b/lib/cmd/reset.js index e68a769f..f381ff86 100644 --- a/lib/cmd/reset.js +++ b/lib/cmd/reset.js @@ -4,6 +4,7 @@ 'use strict'; const Command = require('./command'); +const ServerStatus = require('../const/server-status'); const RESET_COMMAND = new Uint8Array([1, 0, 0, 0, 0x1f]); /** * send a COM_RESET_CONNECTION: permits to reset a connection without re-authentication. @@ -38,7 +39,11 @@ class Reset extends Command { packet.skipLengthCodedNumber(); //insert ids info.status = packet.readUInt16(); - this.successEnd(); + if (info.redirectRequest && (info.status & ServerStatus.STATUS_IN_TRANS) === 0) { + info.redirect(info.redirectRequest, this.successEnd.bind(this)); + } else { + this.successEnd(); + } } } diff --git a/lib/config/connection-options.js b/lib/config/connection-options.js index 767ad39b..01a5f0b1 100644 --- a/lib/config/connection-options.js +++ b/lib/config/connection-options.js @@ -36,27 +36,26 @@ class ConnectionOptions { this.debug = opts.debug || false; this.debugCompress = opts.debugCompress || false; this.debugLen = opts.debugLen || 256; - + this.logParam = opts.logParam === undefined ? true : opts.logParam === true; if (opts.logger) { if (typeof opts.logger === 'function') { this.logger = { network: opts.logger, query: opts.logger, error: opts.logger, - warning: opts.logger, - logParam: true + warning: opts.logger }; } else { this.logger = { network: opts.logger.network, query: opts.logger.query, error: opts.logger.error, - warning: opts.logger.warning || console.log, - logParam: opts.logger.logParam == null ? true : opts.logger.logParam + warning: opts.logger.warning || console.log }; + if (opts.logger.logParam !== undefined) this.logParam = opts.logger.logParam; } } else { - this.logger = { network: null, query: null, error: null, warning: console.log, logParam: false }; + this.logger = { network: null, query: null, error: null, warning: console.log }; if ((this.debug || this.debugCompress) && !this.logger.network) { this.logger.network = console.log; } @@ -87,6 +86,7 @@ class ConnectionOptions { } // connection options + this.permitRedirect = opts.permitRedirect === undefined ? true : opts.permitRedirect; this.initSql = opts.initSql; this.connectTimeout = opts.connectTimeout === undefined ? 1000 : opts.connectTimeout; this.connectAttributes = opts.connectAttributes || false; @@ -186,6 +186,8 @@ class ConnectionOptions { if (opts.charsetNumber && !isNaN(Number.parseInt(opts.charsetNumber))) { opts.charsetNumber = Number.parseInt(opts.charsetNumber); } + if (opts.permitRedirect) opts.permitRedirect = opts.permitRedirect === 'true'; + if (opts.logParam) opts.logParam = opts.logParam === 'true'; if (opts.compress) opts.compress = opts.compress === 'true'; if (opts.connectAttributes) opts.connectAttributes = JSON.parse(opts.connectAttributes); if (opts.connectTimeout) opts.connectTimeout = parseInt(opts.connectTimeout); diff --git a/lib/connection.js b/lib/connection.js index a14c219e..90699d55 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -16,6 +16,7 @@ const tls = require('tls'); const Errors = require('./misc/errors'); const Utils = require('./misc/utils'); const Capabilities = require('./const/capabilities'); +const ConnectionOptions = require('./config/connection-options'); /*commands*/ const Authentication = require('./cmd/handshake/authentication'); @@ -35,6 +36,7 @@ const LruPrepareCache = require('./lru-prepare-cache'); const fsPromises = require('fs').promises; const Parse = require('./misc/parse'); const Collations = require('./const/collations'); +const ConnOptions = require('./config/connection-options'); const convertFixedTime = function (tz, conn) { if (tz === 'UTC' || tz === 'Etc/UTC' || tz === 'Z' || tz === 'Etc/GMT') { @@ -63,6 +65,7 @@ const convertFixedTime = function (tz, conn) { } return tz; }; +const redirectUrlFormat = /(mariadb|mysql):\/\/(([^/@:]+)?(:([^/]+))?@)?(([^/:]+)(:([0-9]+))?)(\/([^?]+)(\?(.*))?)?$/; /** * New Connection instance. @@ -93,7 +96,7 @@ class Connection extends EventEmitter { super(); this.opts = Object.assign(new EventEmitter(), options); - this.info = new ConnectionInformation(this.opts); + this.info = new ConnectionInformation(this.opts, this.redirect.bind(this)); this.prepareCache = this.opts.prepareCacheLength > 0 ? new LruPrepareCache(this.info, this.opts.prepareCacheLength) : null; this.addCommand = this.addCommandQueue; @@ -177,7 +180,7 @@ class Connection extends EventEmitter { Errors.ER_BATCH_WITH_NO_VALUES, this.info, 'HY000', - cmdParam.sql, + cmdParam.sql.length > this.opts.debugLen ? cmdParam.sql.substring(0, this.opts.debugLen) + '...' : cmdParam.sql, false, cmdParam.stack ); @@ -578,11 +581,17 @@ class Connection extends EventEmitter { if (_options && _options.fullResult) return false; // not using info.isMariaDB() directly in case of callback use, // without connection being completely finished. + const bulkEnable = + _options === undefined || _options === null + ? this.opts.bulk + : _options.bulk !== undefined && _options.bulk !== null + ? _options.bulk + : this.opts.bulk; if ( this.info.serverVersion && this.info.serverVersion.mariaDb && this.info.hasMinVersion(10, 2, 7) && - this.opts.bulk && + bulkEnable && (this.info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0n ) { //ensure that there is no stream object @@ -1571,6 +1580,79 @@ class Connection extends EventEmitter { this.socket = undefined; } + /** + * Redirecting connection to server indicated value. + * @param value server host string + * @param resolve promise result when done + */ + redirect(value, resolve) { + if (this.opts.permitRedirect && value) { + // redirect only if : + // * when pipelining, having received all waiting responses. + // * not in a transaction + if (this.receiveQueue.length <= 1 && (this.info.status & ServerStatus.STATUS_IN_TRANS) === 0) { + this.info.redirectRequest = null; + const matchResults = value.match(redirectUrlFormat); + if (!matchResults) { + if (this.opts.logger.error) + this.opts.logger.error( + new Error( + `error parsing redirection string '${value}'. format must be 'mariadb/mysql://[[:]@][:]/[[?=[&=]]]'` + ) + ); + return resolve(); + } + + const options = { + host: matchResults[7] ? decodeURIComponent(matchResults[7]) : matchResults[6], + port: matchResults[9] ? parseInt(matchResults[9]) : 3306 + }; + + // actually only options accepted are user and password + // there might be additional possible options in the future + if (matchResults[3]) options.user = matchResults[3]; + if (matchResults[5]) options.password = matchResults[5]; + + const redirectOpts = ConnectionOptions.parseOptionDataType(options); + + const finalRedirectOptions = new ConnOptions(Object.assign({}, this.opts, redirectOpts)); + const conn = new Connection(finalRedirectOptions); + conn + .connect() + .then( + async function () { + const cmdParam = new CommandParameter(); + await new Promise(this.end.bind(this, cmdParam)); + this.status = Status.CONNECTED; + this.info = conn.info; + this.opts = conn.opts; + this.socket = conn.socket; + if (this.prepareCache) this.prepareCache.reset(); + this.streamOut = conn.streamOut; + this.streamIn = conn.streamIn; + resolve(); + }.bind(this) + ) + .catch( + function (e) { + if (this.opts.logger.error) { + const err = new Error(`fail to redirect to '${value}'`); + err.cause = e; + this.opts.logger.error(err); + } + resolve(); + }.bind(this) + ); + } else { + this.info.redirectRequest = value; + resolve(); + } + } else { + this.info.redirectRequest = null; + resolve(); + } + } + get threadId() { return this.info ? this.info.threadId : null; } diff --git a/lib/io/packet.js b/lib/io/packet.js index 6d0f6f17..464ece93 100644 --- a/lib/io/packet.js +++ b/lib/io/packet.js @@ -455,13 +455,20 @@ class Packet { readBinaryTime() { const len = this.buf[this.pos++]; - const negate = this.buf[this.pos++] === 1; - const hour = this.readUInt32() * 24 + this.readUInt8(); - const min = this.readUInt8(); - const sec = this.readUInt8(); + let negate = false; + let hour = 0; + let min = 0; + let sec = 0; let microSec = 0; - if (len > 8) { - microSec = this.readUInt32(); + + if (len > 0) { + negate = this.buf[this.pos++] === 1; + hour = this.readUInt32() * 24 + this.readUInt8(); + min = this.readUInt8(); + sec = this.readUInt8(); + if (len > 8) { + microSec = this.readUInt32(); + } } let val = appendZero(hour, 2) + ':' + appendZero(min, 2) + ':' + appendZero(sec, 2); if (microSec > 0) { diff --git a/lib/misc/connection-information.js b/lib/misc/connection-information.js index 8a8ffe60..b84325d2 100644 --- a/lib/misc/connection-information.js +++ b/lib/misc/connection-information.js @@ -4,12 +4,16 @@ 'use strict'; class ConnectionInformation { - constructor(opts) { + #redirectFct; + constructor(opts, redirectFct) { this.threadId = -1; this.status = null; this.serverVersion = null; this.serverCapabilities = null; this.database = opts.database; + this.port = opts.port; + this.#redirectFct = redirectFct; + this.redirectRequest = null; } hasMinVersion(major, minor, patch) { @@ -28,6 +32,10 @@ class ConnectionInformation { ); } + redirect(value, resolve) { + return this.#redirectFct(value, resolve); + } + isMariaDB() { if (!this.serverVersion) throw new Error('cannot know if server is MariaDB until connection is established'); return this.serverVersion.mariaDb; diff --git a/lib/pool.js b/lib/pool.js index c6335403..3352363d 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -522,7 +522,15 @@ class Pool extends EventEmitter { getConnection(cmdParam) { if (this.#closed) { return Promise.reject( - Errors.createError('pool is closed', Errors.ER_POOL_ALREADY_CLOSED, null, 'HY000', null, false, cmdParam.stack) + Errors.createError( + 'pool is closed', + Errors.ER_POOL_ALREADY_CLOSED, + null, + 'HY000', + cmdParam === null ? null : cmdParam.sql, + false, + cmdParam.stack + ) ); } return this._doAcquire().then( @@ -538,7 +546,7 @@ class Pool extends EventEmitter { Errors.ER_POOL_ALREADY_CLOSED, null, 'HY000', - null, + cmdParam === null ? null : cmdParam.sql, false, cmdParam.stack ); diff --git a/package.json b/package.json index 66c11a04..662c541a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mariadb", - "version": "3.2.2", + "version": "3.2.3", "description": "fast mariadb or mysql connector.", "main": "promise.js", "types": "types/index.d.ts", diff --git a/test/integration/datatype/test-time.js b/test/integration/datatype/test-time.js index ece02f65..e4426a30 100644 --- a/test/integration/datatype/test-time.js +++ b/test/integration/datatype/test-time.js @@ -30,4 +30,24 @@ describe('time', () => { assert.equal(results[1].t2, '25:00:00'); await shareConn.commit(); }); + + it('prepare time data', async function () { + // skipping test for mysql since TIME doesn't have microseconds + if (!shareConn.info.isMariaDB()) this.skip(); + + await shareConn.query('DROP TABLE IF EXISTS time_data'); + await shareConn.query('CREATE TABLE time_data(t1 time(6), t2 time(6))'); + await shareConn.beginTransaction(); + await shareConn.execute('INSERT INTO time_data VALUES (?, ?)', ['-838:59:58', '-838:59:59.999999']); + await shareConn.execute('INSERT INTO time_data VALUES (?, ?)', ['00:00:00', '-838:59:59.999999']); + await shareConn.execute('INSERT INTO time_data VALUES (?, ?)', ['-1:00:00', '25:00:00']); + let results = await shareConn.execute('SELECT * FROM time_data'); + assert.equal(results[0].t1, '-838:59:58'); + assert.equal(results[0].t2, isXpand() ? '-838:59:59.000000' : '-838:59:59.999999'); + assert.equal(results[1].t1, '00:00:00'); + assert.equal(results[1].t2, isXpand() ? '-838:59:59.000000' : '-838:59:59.999999'); + assert.equal(results[2].t1, '-01:00:00'); + assert.equal(results[2].t2, '25:00:00'); + await shareConn.commit(); + }); }); diff --git a/test/integration/test-batch.js b/test/integration/test-batch.js index 980d1fa7..bc940f93 100644 --- a/test/integration/test-batch.js +++ b/test/integration/test-batch.js @@ -808,6 +808,11 @@ describe('batch', function () { err.message.includes('This command is not supported in the prepared statement protocol yet'), err.message ); + // ensure option is taken in account + await conn.batch({ bulk: false, sql: 'SELECT ? as id, ? as t' }, [ + [1, 'john'], + [2, 'jack'] + ]); } } await conn.end(); @@ -848,6 +853,42 @@ describe('batch', function () { await conn.end(); }; + const bigBatchWith16mMaxAllowedPacketBig = async (useCompression, useBulk) => { + const conn = await base.createConnection({ + compress: useCompression, + maxAllowedPacket: 16 * 1024 * 1024, + bulk: useBulk + }); + conn.query('DROP TABLE IF EXISTS bigBatchWith16mMaxAllowedPacketBig'); + conn.query('CREATE TABLE bigBatchWith16mMaxAllowedPacketBig(id int, t LONGTEXT) CHARSET utf8mb4'); + await conn.query('FLUSH TABLES'); + await conn.query('START TRANSACTION'); + const testSize = 15 * 1024 * 1024; + const buf = Buffer.alloc(testSize); + for (let i = 0; i < testSize; i++) { + buf[i] = 97 + (i % 10); + } + const str = buf.toString(); + const values = []; + for (let i = 0; i < 5; i++) { + values.push([i, str]); + } + let res = await conn.batch('INSERT INTO `bigBatchWith16mMaxAllowedPacketBig` values (?, ?)', values); + assert.equal(res.affectedRows, 5); + + res = await conn.query('select * from `bigBatchWith16mMaxAllowedPacketBig`'); + assert.deepEqual(res, [ + { id: 0, t: str }, + { id: 1, t: str }, + { id: 2, t: str }, + { id: 3, t: str }, + { id: 4, t: str } + ]); + + await conn.query('ROLLBACK'); + await conn.end(); + }; + const bigBatchWith4mMaxAllowedPacket = async (useCompression, useBulk) => { const conn = await base.createConnection({ compress: useCompression, @@ -1403,6 +1444,14 @@ describe('batch', function () { await bigBatchWith16mMaxAllowedPacket(useCompression, true); }); + it('16M+ batch with 16M max_allowed_packet big insert', async function () { + // // skipping in maxscale due to a bug: https://jira.mariadb.org/browse/MXS-3588 + if (process.env.srv === 'maxscale' || process.env.srv === 'skysql-ha') this.skip(); + if (!RUN_LONG_TEST || maxAllowedSize <= testSize) return this.skip(); + this.timeout(320000); + await bigBatchWith16mMaxAllowedPacketBig(useCompression, true); + }); + it('16M+ batch with max_allowed_packet set to 4M', async function () { if (!RUN_LONG_TEST || maxAllowedSize <= 4 * 1024 * 1024) { this.skip(); diff --git a/test/integration/test-big-query.js b/test/integration/test-big-query.js index e0cfe7f4..fe2b9701 100644 --- a/test/integration/test-big-query.js +++ b/test/integration/test-big-query.js @@ -65,10 +65,10 @@ describe('Big query', function () { } catch (e) { assert.isTrue( e.sql.includes( - "insert into bigParameterBigParam(b) values(?) - parameters:[['test'],[0x6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162...]" + "insert into bigParameterBigParam(b) values(?) - parameters:[['test'],[0x6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162..." ) || e.sql.includes( - 'insert into bigParameterBigParam(b) values(?) - parameters:[0x6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a61626364656667...]' + 'insert into bigParameterBigParam(b) values(?) - parameters:[0x6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a6162636465666768696a61626364656667...' ) ); } diff --git a/test/integration/test-connection-opts.js b/test/integration/test-connection-opts.js index 114f412c..1291c835 100644 --- a/test/integration/test-connection-opts.js +++ b/test/integration/test-connection-opts.js @@ -360,7 +360,7 @@ describe('connection option', () => { conn .query({ timeout: 1000, - sql: 'select c1.COLUMN_NAME from information_schema.columns as c1, information_schema.tables, information_schema.tables as t2' + sql: 'select c1.* from information_schema.columns as c1, information_schema.tables, information_schema.tables as t2' }) .then(() => { conn.end(); @@ -381,12 +381,12 @@ describe('connection option', () => { .catch(done); } else { base - .createConnection({ multipleStatements: true, queryTimeout: 10000 }) + .createConnection({ multipleStatements: true, queryTimeout: 10000000 }) .then((conn) => { conn .query({ timeout: 1000, - sql: 'select c1.COLUMN_NAME from information_schema.columns as c1, information_schema.tables, information_schema.tables as t2' + sql: 'select c1.* from information_schema.columns as c1, information_schema.tables, information_schema.tables as t2' }) .then(() => { conn.end(); diff --git a/test/integration/test-connection.js b/test/integration/test-connection.js index c66038e9..047b2dde 100644 --- a/test/integration/test-connection.js +++ b/test/integration/test-connection.js @@ -427,8 +427,8 @@ describe('connection', () => { it('connection timeout connect (wrong url) with callback', (done) => { const initTime = Date.now(); - dns.resolve4('www.google.fr', (err, res) => { - if (err) done(err); + dns.resolve4('www.google.com', (err, res) => { + if (err) done(); else if (res.length > 0) { const host = res[0]; const conn = base.createCallbackConnection({ @@ -531,8 +531,8 @@ describe('connection', () => { }); it('connection timeout connect (wrong url) with callback no function', (done) => { - dns.resolve4('www.google.fr', (err, res) => { - if (err) done(err); + dns.resolve4('www.google.com', (err, res) => { + if (err) done(); else if (res.length > 0) { const host = res[0]; const conn = base.createCallbackConnection({ @@ -566,7 +566,7 @@ describe('connection', () => { it('connection timeout connect (wrong url) with promise', (done) => { const initTime = Date.now(); - dns.resolve4('www.google.fr', (err, res) => { + dns.resolve4('www.google.com', (err, res) => { if (err) done(err); else if (res.length > 0) { const host = res[0]; @@ -601,8 +601,8 @@ describe('connection', () => { it('connection timeout error (wrong url)', function (done) { const initTime = Date.now(); - dns.resolve4('www.google.fr', (err, res) => { - if (err) done(err); + dns.resolve4('www.google.com', (err, res) => { + if (err) done(); else if (res.length > 0) { const host = res[0]; base.createConnection({ host: host, connectTimeout: 1000 }).catch((err) => { diff --git a/test/integration/test-error.js b/test/integration/test-error.js index 0ced1361..ed4ebfcf 100644 --- a/test/integration/test-error.js +++ b/test/integration/test-error.js @@ -154,8 +154,8 @@ describe('Error', () => { } else { if (!isXpand()) { assert.isTrue(err.message.includes('You have an error in your SQL syntax')); - assert.isTrue(err.message.includes("sql: wrong query ?, ? - parameters:[123456789,'long paramete...]")); - assert.equal(err.sql, "wrong query ?, ? - parameters:[123456789,'long paramete...]"); + assert.isTrue(err.message.includes("sql: wrong query ?, ? - parameters:[123456789,'long paramete...")); + assert.equal(err.sql, "wrong query ?, ? - parameters:[123456789,'long paramete..."); assert.equal(err.sqlState, 42000); } assert.equal(err.errno, 1064); @@ -257,8 +257,8 @@ describe('Error', () => { assert.equal(err.errno, 1064); assert.equal(err.code, 'ER_PARSE_ERROR'); } - assert.isTrue(err.message.includes("sql: wrong query :par1, :par2 - parameters:{'par1':'some par...}")); - assert.equal(err.sql, "wrong query :par1, :par2 - parameters:{'par1':'some par...}"); + assert.isTrue(err.message.includes("sql: wrong query :par1, :par2 - parameters:{'par1':'some par...")); + assert.equal(err.sql, "wrong query :par1, :par2 - parameters:{'par1':'some par..."); conn.end(); done(); diff --git a/test/integration/test-execute.js b/test/integration/test-execute.js index b9edcbca..266a91a8 100644 --- a/test/integration/test-execute.js +++ b/test/integration/test-execute.js @@ -98,6 +98,51 @@ describe('prepare and execute', () => { conn.end(); }); + it('logger error', async () => { + let errorLogged = ''; + const conn = await base.createConnection({ + logger: { + error: (msg) => { + errorLogged += msg + '\n'; + } + } + }); + try { + await conn.query('SELECT * FROM nonexistant WHERE a = ? AND b= ?', ['a', true]); + } catch (e) { + // eat + } + console.log(errorLogged); + assert.isTrue( + errorLogged.includes( + "Table 'testn.nonexistant' doesn't exist\n" + + "sql: SELECT * FROM nonexistant WHERE a = ? AND b= ? - parameters:['a',true]" + ), + errorLogged + ); + conn.end(); + }); + + it('logger error without parameters', async () => { + let errorLogged = ''; + const conn = await base.createConnection({ + logger: { + error: (msg) => { + errorLogged += msg + '\n'; + } + }, + logParam: false + }); + try { + await conn.query('SELECT * FROM NONEXISTANT WHERE a = ? AND b= ?', ['a', true]); + } catch (e) { + // eat + } + console.log(errorLogged); + assert.isFalse(errorLogged.includes(" - parameters:['a',true]")); + conn.end(); + }); + it('prepare close with cache', async () => { const conn = await base.createConnection({ prepareCacheLength: 2 }); for (let i = 0; i < 10; i++) { @@ -166,11 +211,64 @@ describe('prepare and execute', () => { const prepare = await conn.prepare('select ?'); await prepare.execute('1'); await prepare.close(); + try { + await prepare.execute('1'); + throw new Error('must have thrown error'); + } catch (e) { + assert.equal(e.sql, "select ? - parameters:['1']"); + assert.isTrue(e.message.includes('Execute fails, prepare command as already been closed')); + } + try { + await prepare.execute([1, 2]); + throw new Error('must have thrown error'); + } catch (e) { + assert.equal(e.sql, 'select ? - parameters:[1,2]'); + assert.isTrue(e.message.includes('Execute fails, prepare command as already been closed')); + } + const prepare2 = await conn.prepare('select ?'); + await prepare2.execute('2'); + await prepare2.close(); + + conn.end(); + }); + + it('prepare after prepare close - no cache - error trunk', async () => { + const conn = await base.createConnection({ prepareCacheLength: 0, debugLen: 8 }); + const prepare = await conn.prepare('select ?'); + await prepare.execute('1'); + await prepare.close(); + try { + await prepare.execute('1'); + throw new Error('must have thrown error'); + } catch (e) { + assert.equal(e.sql, 'select ?...'); + assert.isTrue(e.message.includes('Execute fails, prepare command as already been closed')); + } + try { + await prepare.execute([1, 2]); + throw new Error('must have thrown error'); + } catch (e) { + assert.equal(e.sql, 'select ?...'); + assert.isTrue(e.message.includes('Execute fails, prepare command as already been closed')); + } + const prepare2 = await conn.prepare('select ?'); + await prepare2.execute('2'); + await prepare2.close(); + + conn.end(); + }); + + it('prepare after prepare close - no cache - parameter logged', async () => { + const conn = await base.createConnection({ prepareCacheLength: 0, logParam: false }); + const prepare = await conn.prepare('select ?'); + await prepare.execute('1'); + await prepare.close(); try { await prepare.execute('1'); throw new Error('must have thrown error'); } catch (e) { assert.isTrue(e.message.includes('Execute fails, prepare command as already been closed')); + assert.equal(e.sql, 'select ?'); } const prepare2 = await conn.prepare('select ?'); diff --git a/test/integration/test-redirection.js b/test/integration/test-redirection.js new file mode 100644 index 00000000..eedf620f --- /dev/null +++ b/test/integration/test-redirection.js @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2015-2023 MariaDB Corporation Ab + +'use strict'; + +require('../base.js'); +const base = require('../base.js'); +const Proxy = require('../tools/proxy'); +const Conf = require('../conf'); +const { assert } = require('chai'); +describe('redirection', () => { + it('basic redirection', async function () { + if (process.env.srv === 'skysql' || process.env.srv === 'skysql-ha') this.skip(); + const proxy = new Proxy({ + port: Conf.baseConfig.port, + host: Conf.baseConfig.host, + resetAfterUse: false + }); + await proxy.start(); + let conn = await base.createConnection({ port: proxy.port() }); + try { + assert.equal(proxy.port(), conn.info.port); + let permitRedirection = true; + try { + await conn.query('set @@session.redirect_url="mariadb://localhost:' + Conf.baseConfig.port + '"'); + } catch (e) { + // if server doesn't support redirection + permitRedirection = false; + } + if (permitRedirection) { + assert.equal(Conf.baseConfig.port, conn.info.port); + } + } finally { + conn.end(); + proxy.close(); + } + }); + + it('redirection during pipelining', async function () { + if (process.env.srv === 'skysql' || process.env.srv === 'skysql-ha') this.skip(); + const proxy = new Proxy({ + port: Conf.baseConfig.port, + host: Conf.baseConfig.host, + resetAfterUse: false + }); + await proxy.start(); + let conn = await base.createConnection({ port: proxy.port() }); + try { + assert.equal(proxy.port(), conn.info.port); + let permitRedirection = true; + conn.query('SELECT 1'); + conn.query('set @@session.redirect_url="mariadb://localhost:' + Conf.baseConfig.port + '"').catch((e) => { + permitRedirection = false; + }); + conn.query('SELECT 2'); + assert.equal(proxy.port(), conn.info.port); + await conn.query('SELECT 3'); + if (permitRedirection) { + assert.equal(Conf.baseConfig.port, conn.info.port); + } + } finally { + conn.end(); + proxy.close(); + } + }); + + it('redirection during transaction', async function () { + if (process.env.srv === 'skysql' || process.env.srv === 'skysql-ha') this.skip(); + const proxy = new Proxy({ + port: Conf.baseConfig.port, + host: Conf.baseConfig.host, + resetAfterUse: false + }); + await proxy.start(); + let conn = await base.createConnection({ port: proxy.port() }); + try { + assert.equal(proxy.port(), conn.info.port); + let permitRedirection = true; + try { + await conn.beginTransaction(); + await conn.query('set @@session.redirect_url="mariadb://localhost:' + Conf.baseConfig.port + '"'); + } catch (e) { + // if server doesn't support redirection + permitRedirection = false; + } + assert.equal(proxy.port(), conn.info.port); + if (permitRedirection) { + await conn.commit(); + assert.equal(Conf.baseConfig.port, conn.info.port); + } + } finally { + conn.end(); + proxy.close(); + } + }); +}); diff --git a/test/unit/config/test-options.js b/test/unit/config/test-options.js index e90f2bca..caa50a22 100644 --- a/test/unit/config/test-options.js +++ b/test/unit/config/test-options.js @@ -54,9 +54,9 @@ describe('test options', () => { error: null, network: null, query: null, - logParam: false, warning: console.log }, + logParam: true, metaAsArray: false, metaEnumerable: false, multipleStatements: false, @@ -75,7 +75,8 @@ describe('test options', () => { keepEof: false, permitLocalInfile: false, bigNumberStrings: false, - supportBigNumbers: false + supportBigNumbers: false, + permitRedirect: true }; assert.deepEqual(expected, defaultOpts); assert.deepEqual(expected, defaultOptsCall);