1857 lines
		
	
	
		
			58 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1857 lines
		
	
	
		
			58 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| //  SPDX-License-Identifier: LGPL-2.1-or-later
 | |
| //  Copyright (c) 2015-2024 MariaDB Corporation Ab
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| const EventEmitter = require('events');
 | |
| const Queue = require('denque');
 | |
| const Net = require('net');
 | |
| const PacketInputStream = require('./io/packet-input-stream');
 | |
| const PacketOutputStream = require('./io/packet-output-stream');
 | |
| const CompressionInputStream = require('./io/compression-input-stream');
 | |
| const CompressionOutputStream = require('./io/compression-output-stream');
 | |
| const ServerStatus = require('./const/server-status');
 | |
| const ConnectionInformation = require('./misc/connection-information');
 | |
| 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');
 | |
| const Quit = require('./cmd/quit');
 | |
| const Ping = require('./cmd/ping');
 | |
| const Reset = require('./cmd/reset');
 | |
| const Query = require('./cmd/query');
 | |
| const Prepare = require('./cmd/prepare');
 | |
| const OkPacket = require('./cmd/class/ok-packet');
 | |
| const Execute = require('./cmd/execute');
 | |
| const ClosePrepare = require('./cmd/close-prepare');
 | |
| const BatchBulk = require('./cmd/batch-bulk');
 | |
| const ChangeUser = require('./cmd/change-user');
 | |
| const { Status } = require('./const/connection_status');
 | |
| 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') {
 | |
|     return '+00:00';
 | |
|   } else if (tz.startsWith('Etc/GMT') || tz.startsWith('GMT')) {
 | |
|     let tzdiff;
 | |
|     let negate;
 | |
| 
 | |
|     // strangely Etc/GMT+8 = GMT-08:00 = offset -8
 | |
|     if (tz.startsWith('Etc/GMT')) {
 | |
|       tzdiff = tz.substring(7);
 | |
|       negate = !tzdiff.startsWith('-');
 | |
|     } else {
 | |
|       tzdiff = tz.substring(3);
 | |
|       negate = tzdiff.startsWith('-');
 | |
|     }
 | |
|     let diff = parseInt(tzdiff.substring(1));
 | |
|     if (isNaN(diff)) {
 | |
|       throw Errors.createFatalError(
 | |
|         `Automatic timezone setting fails. wrong Server timezone '${tz}' conversion to +/-HH:00 conversion.`,
 | |
|         Errors.ER_WRONG_AUTO_TIMEZONE,
 | |
|         conn.info
 | |
|       );
 | |
|     }
 | |
|     return (negate ? '-' : '+') + (diff >= 10 ? diff : '0' + diff) + ':00';
 | |
|   }
 | |
|   return tz;
 | |
| };
 | |
| const redirectUrlFormat = /(mariadb|mysql):\/\/(([^/@:]+)?(:([^/]+))?@)?(([^/:]+)(:([0-9]+))?)(\/([^?]+)(\?(.*))?)?$/;
 | |
| 
 | |
| /**
 | |
|  * New Connection instance.
 | |
|  *
 | |
|  * @param options    connection options
 | |
|  * @returns Connection instance
 | |
|  * @constructor
 | |
|  * @fires Connection#connect
 | |
|  * @fires Connection#end
 | |
|  * @fires Connection#error
 | |
|  *
 | |
|  */
 | |
| class Connection extends EventEmitter {
 | |
|   opts;
 | |
|   sendQueue = new Queue();
 | |
|   receiveQueue = new Queue();
 | |
|   waitingAuthenticationQueue = new Queue();
 | |
|   status = Status.NOT_CONNECTED;
 | |
|   socket = null;
 | |
|   timeout = null;
 | |
|   addCommand;
 | |
|   streamOut;
 | |
|   streamIn;
 | |
|   info;
 | |
|   prepareCache;
 | |
| 
 | |
|   constructor(options) {
 | |
|     super();
 | |
| 
 | |
|     this.opts = Object.assign(new EventEmitter(), options);
 | |
|     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;
 | |
|     this.streamOut = new PacketOutputStream(this.opts, this.info);
 | |
|     this.streamIn = new PacketInputStream(
 | |
|       this.unexpectedPacket.bind(this),
 | |
|       this.receiveQueue,
 | |
|       this.streamOut,
 | |
|       this.opts,
 | |
|       this.info
 | |
|     );
 | |
| 
 | |
|     this.on('close_prepare', this._closePrepare.bind(this));
 | |
|     this.escape = Utils.escape.bind(this, this.opts, this.info);
 | |
|     this.escapeId = Utils.escapeId.bind(this, this.opts, this.info);
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // public methods
 | |
|   //*****************************************************************
 | |
| 
 | |
|   /**
 | |
|    * Connect event
 | |
|    *
 | |
|    * @returns {Promise} promise
 | |
|    */
 | |
|   connect() {
 | |
|     const conn = this;
 | |
|     this.status = Status.CONNECTING;
 | |
|     const authenticationParam = {
 | |
|       opts: this.opts
 | |
|     };
 | |
|     return new Promise(function (resolve, reject) {
 | |
|       conn.connectRejectFct = reject;
 | |
|       conn.connectResolveFct = resolve;
 | |
|       // add a handshake to msg queue
 | |
|       const authentication = new Authentication(
 | |
|         authenticationParam,
 | |
|         conn.authSucceedHandler.bind(conn),
 | |
|         conn.authFailHandler.bind(conn),
 | |
|         conn.createSecureContext.bind(conn),
 | |
|         conn.getSocket.bind(conn)
 | |
|       );
 | |
|       Error.captureStackTrace(authentication);
 | |
| 
 | |
|       authentication.once('end', () => {
 | |
|         conn.receiveQueue.shift();
 | |
|         // conn.info.collation might not be initialized
 | |
|         // in case of handshake throwing error
 | |
|         if (!conn.opts.collation && conn.info.collation) {
 | |
|           conn.opts.emit('collation', conn.info.collation);
 | |
|         }
 | |
|         process.nextTick(conn.nextSendCmd.bind(conn));
 | |
|       });
 | |
| 
 | |
|       conn.receiveQueue.push(authentication);
 | |
|       conn.streamInitSocket.call(conn);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   executePromise(cmdParam, prepare, resolve, reject) {
 | |
|     const cmd = new Execute(resolve, this._logAndReject.bind(this, reject), this.opts, cmdParam, prepare);
 | |
|     this.addCommand(cmd, true);
 | |
|   }
 | |
| 
 | |
|   batch(cmdParam) {
 | |
|     if (!cmdParam.sql) {
 | |
|       const err = Errors.createError(
 | |
|         'sql parameter is mandatory',
 | |
|         Errors.ER_UNDEFINED_SQL,
 | |
|         this.info,
 | |
|         'HY000',
 | |
|         null,
 | |
|         false,
 | |
|         cmdParam.stack
 | |
|       );
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       return Promise.reject(err);
 | |
|     }
 | |
|     if (!cmdParam.values) {
 | |
|       const err = Errors.createError(
 | |
|         'Batch must have values set',
 | |
|         Errors.ER_BATCH_WITH_NO_VALUES,
 | |
|         this.info,
 | |
|         'HY000',
 | |
|         cmdParam.sql.length > this.opts.debugLen ? cmdParam.sql.substring(0, this.opts.debugLen) + '...' : cmdParam.sql,
 | |
|         false,
 | |
|         cmdParam.stack
 | |
|       );
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       return Promise.reject(err);
 | |
|     }
 | |
| 
 | |
|     return new Promise(this.prepare.bind(this, cmdParam)).then((prepare) => {
 | |
|       const usePlaceHolder = (cmdParam.opts && cmdParam.opts.namedPlaceholders) || this.opts.namedPlaceholders;
 | |
|       let vals;
 | |
|       if (Array.isArray(cmdParam.values)) {
 | |
|         if (usePlaceHolder) {
 | |
|           vals = cmdParam.values;
 | |
|         } else if (Array.isArray(cmdParam.values[0])) {
 | |
|           vals = cmdParam.values;
 | |
|         } else if (prepare.parameterCount === 1) {
 | |
|           vals = [];
 | |
|           for (let i = 0; i < cmdParam.values.length; i++) {
 | |
|             vals.push([cmdParam.values[i]]);
 | |
|           }
 | |
|         } else {
 | |
|           vals = [cmdParam.values];
 | |
|         }
 | |
|       } else {
 | |
|         vals = [[cmdParam.values]];
 | |
|       }
 | |
|       cmdParam.values = vals;
 | |
|       let useBulk = this._canUseBulk(vals, cmdParam.opts);
 | |
|       if (useBulk) {
 | |
|         return new Promise(this.executeBulkPromise.bind(this, cmdParam, prepare, this.opts));
 | |
|       } else {
 | |
|         const executes = [];
 | |
|         const cmdOpt = Object.assign({}, this.opts, cmdParam.opts);
 | |
|         for (let i = 0; i < vals.length; i++) {
 | |
|           executes.push(prepare.execute(vals[i], cmdParam.opts, null, cmdParam.stack));
 | |
|         }
 | |
|         return Promise.all(executes)
 | |
|           .then(
 | |
|             function (res) {
 | |
|               if (cmdParam.opts && cmdParam.opts.fullResult) {
 | |
|                 return Promise.resolve(res);
 | |
|               } else {
 | |
|                 // aggregate results
 | |
|                 let firstResult = res[0];
 | |
|                 if (cmdOpt.metaAsArray) firstResult = firstResult[0];
 | |
|                 if (firstResult instanceof OkPacket) {
 | |
|                   let affectedRows = 0;
 | |
|                   const insertId = firstResult.insertId;
 | |
|                   const warningStatus = firstResult.warningStatus;
 | |
|                   if (cmdOpt.metaAsArray) {
 | |
|                     for (let i = 0; i < res.length; i++) {
 | |
|                       affectedRows += res[i][0].affectedRows;
 | |
|                     }
 | |
|                     return Promise.resolve([new OkPacket(affectedRows, insertId, warningStatus), []]);
 | |
|                   } else {
 | |
|                     for (let i = 0; i < res.length; i++) {
 | |
|                       affectedRows += res[i].affectedRows;
 | |
|                     }
 | |
|                     return Promise.resolve(new OkPacket(affectedRows, insertId, warningStatus));
 | |
|                   }
 | |
|                 } else {
 | |
|                   // results have result-set. example :'INSERT ... RETURNING'
 | |
|                   // aggregate results
 | |
|                   if (cmdOpt.metaAsArray) {
 | |
|                     const rs = [];
 | |
|                     res.forEach((row) => {
 | |
|                       rs.push(...row[0]);
 | |
|                     });
 | |
|                     return Promise.resolve([rs, res[0][1]]);
 | |
|                   } else {
 | |
|                     const rs = [];
 | |
|                     res.forEach((row) => {
 | |
|                       rs.push(...row);
 | |
|                     });
 | |
|                     Object.defineProperty(rs, 'meta', {
 | |
|                       value: res[0].meta,
 | |
|                       writable: true,
 | |
|                       enumerable: this.opts.metaEnumerable
 | |
|                     });
 | |
|                     return Promise.resolve(rs);
 | |
|                   }
 | |
|                 }
 | |
|               }
 | |
|             }.bind(this)
 | |
|           )
 | |
|           .finally(() => prepare.close());
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   executeBulkPromise(cmdParam, prepare, opts, resolve, reject) {
 | |
|     const cmd = new BatchBulk(
 | |
|       (res) => {
 | |
|         prepare.close();
 | |
|         return resolve(res);
 | |
|       },
 | |
|       function (err) {
 | |
|         prepare.close();
 | |
|         if (opts.logger.error) opts.logger.error(err);
 | |
|         reject(err);
 | |
|       },
 | |
|       opts,
 | |
|       prepare,
 | |
|       cmdParam
 | |
|     );
 | |
|     this.addCommand(cmd, true);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
 | |
|    * @param cmdParam command context
 | |
|    * @param resolve success function
 | |
|    * @param reject rejection function
 | |
|    */
 | |
|   ping(cmdParam, resolve, reject) {
 | |
|     if (cmdParam.opts && cmdParam.opts.timeout) {
 | |
|       if (cmdParam.opts.timeout < 0) {
 | |
|         const err = Errors.createError(
 | |
|           'Ping cannot have negative timeout value',
 | |
|           Errors.ER_BAD_PARAMETER_VALUE,
 | |
|           this.info,
 | |
|           '0A000'
 | |
|         );
 | |
|         if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|         reject(err);
 | |
|         return;
 | |
|       }
 | |
|       let tOut = setTimeout(
 | |
|         function () {
 | |
|           tOut = undefined;
 | |
|           const err = Errors.createFatalError('Ping timeout', Errors.ER_PING_TIMEOUT, this.info, '0A000');
 | |
|           if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|           // close connection
 | |
|           this.addCommand = this.addCommandDisabled;
 | |
|           clearTimeout(this.timeout);
 | |
|           if (this.status !== Status.CLOSING && this.status !== Status.CLOSED) {
 | |
|             this.sendQueue.clear();
 | |
|             this.status = Status.CLOSED;
 | |
|             this.socket.destroy();
 | |
|           }
 | |
|           this.clear();
 | |
|           reject(err);
 | |
|         }.bind(this),
 | |
|         cmdParam.opts.timeout
 | |
|       );
 | |
|       this.addCommand(
 | |
|         new Ping(
 | |
|           cmdParam,
 | |
|           () => {
 | |
|             if (tOut) {
 | |
|               clearTimeout(tOut);
 | |
|               resolve();
 | |
|             }
 | |
|           },
 | |
|           (err) => {
 | |
|             if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|             clearTimeout(tOut);
 | |
|             reject(err);
 | |
|           }
 | |
|         ),
 | |
|         true
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
|     this.addCommand(new Ping(cmdParam, resolve, reject), true);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Send a reset command that will
 | |
|    * - rollback any open transaction
 | |
|    * - reset transaction isolation level
 | |
|    * - reset session variables
 | |
|    * - delete user variables
 | |
|    * - remove temporary tables
 | |
|    * - remove all PREPARE statement
 | |
|    */
 | |
|   reset(cmdParam, resolve, reject) {
 | |
|     if (
 | |
|       (this.info.isMariaDB() && this.info.hasMinVersion(10, 2, 4)) ||
 | |
|       (!this.info.isMariaDB() && this.info.hasMinVersion(5, 7, 3))
 | |
|     ) {
 | |
|       const conn = this;
 | |
|       const resetCmd = new Reset(
 | |
|         cmdParam,
 | |
|         () => {
 | |
|           if (conn.prepareCache) conn.prepareCache.reset();
 | |
|           let prom = Promise.resolve();
 | |
|           // re-execute init query / session query timeout
 | |
|           prom
 | |
|             .then(conn.handleCharset.bind(conn))
 | |
|             .then(conn.handleTimezone.bind(conn))
 | |
|             .then(conn.executeInitQuery.bind(conn))
 | |
|             .then(conn.executeSessionTimeout.bind(conn))
 | |
|             .then(resolve)
 | |
|             .catch(reject);
 | |
|         },
 | |
|         reject
 | |
|       );
 | |
|       this.addCommand(resetCmd, true);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const err = new Error(
 | |
|       `Reset command not permitted for server ${this.info.serverVersion.raw} (requires server MariaDB version 10.2.4+ or MySQL 5.7.3+)`
 | |
|     );
 | |
|     err.stack = cmdParam.stack;
 | |
|     if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|     reject(err);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Indicates the state of the connection as the driver knows it
 | |
|    * @returns {boolean}
 | |
|    */
 | |
|   isValid() {
 | |
|     return this.status === Status.CONNECTED;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Terminate connection gracefully.
 | |
|    */
 | |
|   end(cmdParam, resolve, reject) {
 | |
|     this.addCommand = this.addCommandDisabled;
 | |
|     clearTimeout(this.timeout);
 | |
| 
 | |
|     if (this.status < Status.CLOSING && this.status !== Status.NOT_CONNECTED) {
 | |
|       this.status = Status.CLOSING;
 | |
|       const ended = () => {
 | |
|         this.status = Status.CLOSED;
 | |
|         this.socket.destroy();
 | |
|         this.socket.unref();
 | |
|         this.clear();
 | |
|         this.receiveQueue.clear();
 | |
|         resolve();
 | |
|       };
 | |
|       const quitCmd = new Quit(cmdParam, ended, ended);
 | |
|       this.sendQueue.push(quitCmd);
 | |
|       this.receiveQueue.push(quitCmd);
 | |
|       if (this.sendQueue.length === 1) {
 | |
|         process.nextTick(this.nextSendCmd.bind(this));
 | |
|       }
 | |
|     } else resolve();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Force connection termination by closing the underlying socket and killing server process if any.
 | |
|    */
 | |
|   destroy() {
 | |
|     this.addCommand = this.addCommandDisabled;
 | |
|     clearTimeout(this.timeout);
 | |
|     if (this.status < Status.CLOSING) {
 | |
|       this.status = Status.CLOSING;
 | |
|       this.sendQueue.clear();
 | |
|       if (this.receiveQueue.length > 0) {
 | |
|         //socket is closed, but server may still be processing a huge select
 | |
|         //only possibility is to kill process by another thread
 | |
|         //TODO reuse a pool connection to avoid connection creation
 | |
|         const self = this;
 | |
| 
 | |
|         // relying on IP in place of DNS to ensure using same server
 | |
|         const remoteAddress = this.socket.remoteAddress;
 | |
|         const connOption = remoteAddress ? Object.assign({}, this.opts, { host: remoteAddress }) : this.opts;
 | |
| 
 | |
|         const killCon = new Connection(connOption);
 | |
|         killCon
 | |
|           .connect()
 | |
|           .then(() => {
 | |
|             //*************************************************
 | |
|             //kill connection
 | |
|             //*************************************************
 | |
|             new Promise(killCon.query.bind(killCon, { sql: `KILL ${self.info.threadId}` })).finally((err) => {
 | |
|               const destroyError = Errors.createFatalError(
 | |
|                 'Connection destroyed, command was killed',
 | |
|                 Errors.ER_CMD_NOT_EXECUTED_DESTROYED,
 | |
|                 self.info
 | |
|               );
 | |
|               if (self.opts.logger.error) self.opts.logger.error(destroyError);
 | |
|               self.socketErrorDispatchToQueries(destroyError);
 | |
|               if (self.socket) {
 | |
|                 const sok = self.socket;
 | |
|                 process.nextTick(() => {
 | |
|                   sok.destroy();
 | |
|                 });
 | |
|               }
 | |
|               self.status = Status.CLOSED;
 | |
|               self.clear();
 | |
|               new Promise(killCon.end.bind(killCon)).catch(() => {});
 | |
|             });
 | |
|           })
 | |
|           .catch(() => {
 | |
|             //*************************************************
 | |
|             //failing to create a kill connection, end normally
 | |
|             //*************************************************
 | |
|             const ended = () => {
 | |
|               let sock = self.socket;
 | |
|               self.clear();
 | |
|               self.status = Status.CLOSED;
 | |
|               sock.destroy();
 | |
|               self.receiveQueue.clear();
 | |
|             };
 | |
|             const quitCmd = new Quit(ended, ended);
 | |
|             self.sendQueue.push(quitCmd);
 | |
|             self.receiveQueue.push(quitCmd);
 | |
|             if (self.sendQueue.length === 1) {
 | |
|               process.nextTick(self.nextSendCmd.bind(self));
 | |
|             }
 | |
|           });
 | |
|       } else {
 | |
|         this.status = Status.CLOSED;
 | |
|         this.socket.destroy();
 | |
|         this.clear();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   pause() {
 | |
|     this.socket.pause();
 | |
|   }
 | |
| 
 | |
|   resume() {
 | |
|     this.socket.resume();
 | |
|   }
 | |
| 
 | |
|   format(sql, values) {
 | |
|     const err = Errors.createError(
 | |
|       '"Connection.format intentionally not implemented. please use Connection.query(sql, values), it will be more secure and faster',
 | |
|       Errors.ER_NOT_IMPLEMENTED_FORMAT,
 | |
|       this.info,
 | |
|       '0A000'
 | |
|     );
 | |
|     if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|     throw err;
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // additional public methods
 | |
|   //*****************************************************************
 | |
| 
 | |
|   /**
 | |
|    * return current connected server version information.
 | |
|    *
 | |
|    * @returns {*}
 | |
|    */
 | |
|   serverVersion() {
 | |
|     if (!this.info.serverVersion) {
 | |
|       const err = new Error('cannot know if server information until connection is established');
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       throw err;
 | |
|     }
 | |
| 
 | |
|     return this.info.serverVersion.raw;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Change option "debug" during connection.
 | |
|    * @param val   debug value
 | |
|    */
 | |
|   debug(val) {
 | |
|     if (typeof val === 'boolean') {
 | |
|       if (val && !this.opts.logger.network) this.opts.logger.network = console.log;
 | |
|     } else if (typeof val === 'function') {
 | |
|       this.opts.logger.network = val;
 | |
|     }
 | |
|     this.opts.emit('debug', val);
 | |
|   }
 | |
| 
 | |
|   debugCompress(val) {
 | |
|     if (val) {
 | |
|       if (typeof val === 'boolean') {
 | |
|         this.opts.debugCompress = val;
 | |
|         if (val && !this.opts.logger.network) this.opts.logger.network = console.log;
 | |
|       } else if (typeof val === 'function') {
 | |
|         this.opts.debugCompress = true;
 | |
|         this.opts.logger.network = val;
 | |
|       }
 | |
|     } else this.opts.debugCompress = false;
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // internal public testing methods
 | |
|   //*****************************************************************
 | |
| 
 | |
|   get __tests() {
 | |
|     return new TestMethods(this.info.collation, this.socket);
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // internal methods
 | |
|   //*****************************************************************
 | |
| 
 | |
|   /**
 | |
|    * Use multiple COM_STMT_EXECUTE or COM_STMT_BULK_EXECUTE
 | |
|    *
 | |
|    * @param values current batch values
 | |
|    * @param _options batch option
 | |
|    * @return {boolean} indicating if can use bulk command
 | |
|    */
 | |
|   _canUseBulk(values, _options) {
 | |
|     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) &&
 | |
|       bulkEnable &&
 | |
|       (this.info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0n
 | |
|     ) {
 | |
|       //ensure that there is no stream object
 | |
|       if (values !== undefined) {
 | |
|         if (!this.opts.namedPlaceholders) {
 | |
|           //ensure that all parameters have same length
 | |
|           //single array is considered as an array of single element.
 | |
|           const paramLen = Array.isArray(values[0]) ? values[0].length : values[0] ? 1 : 0;
 | |
|           if (paramLen === 0) return false;
 | |
|           for (let r = 0; r < values.length; r++) {
 | |
|             let row = values[r];
 | |
|             if (!Array.isArray(row)) row = [row];
 | |
|             if (paramLen !== row.length) {
 | |
|               return false;
 | |
|             }
 | |
|             // streaming data not permitted
 | |
|             for (let j = 0; j < paramLen; j++) {
 | |
|               const val = row[j];
 | |
|               if (
 | |
|                 val != null &&
 | |
|                 typeof val === 'object' &&
 | |
|                 typeof val.pipe === 'function' &&
 | |
|                 typeof val.read === 'function'
 | |
|               ) {
 | |
|                 return false;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         } else {
 | |
|           for (let r = 0; r < values.length; r++) {
 | |
|             let row = values[r];
 | |
|             const keys = Object.keys(row);
 | |
|             for (let j = 0; j < keys.length; j++) {
 | |
|               const val = row[keys[j]];
 | |
|               if (
 | |
|                 val != null &&
 | |
|                 typeof val === 'object' &&
 | |
|                 typeof val.pipe === 'function' &&
 | |
|                 typeof val.read === 'function'
 | |
|               ) {
 | |
|                 return false;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   executeSessionVariableQuery() {
 | |
|     if (this.opts.sessionVariables) {
 | |
|       const values = [];
 | |
|       let sessionQuery = 'set ';
 | |
|       let keys = Object.keys(this.opts.sessionVariables);
 | |
|       if (keys.length > 0) {
 | |
|         for (let k = 0; k < keys.length; ++k) {
 | |
|           sessionQuery += (k !== 0 ? ',' : '') + '@@' + keys[k].replace(/[^a-z0-9_]/gi, '') + '=?';
 | |
|           values.push(this.opts.sessionVariables[keys[k]]);
 | |
|         }
 | |
| 
 | |
|         return new Promise(
 | |
|           this.query.bind(this, {
 | |
|             sql: sessionQuery,
 | |
|             values: values
 | |
|           })
 | |
|         ).catch((initialErr) => {
 | |
|           const err = Errors.createFatalError(
 | |
|             `Error setting session variable (value ${JSON.stringify(this.opts.sessionVariables)}). Error: ${
 | |
|               initialErr.message
 | |
|             }`,
 | |
|             Errors.ER_SETTING_SESSION_ERROR,
 | |
|             this.info,
 | |
|             '08S01',
 | |
|             sessionQuery
 | |
|           );
 | |
|           if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|           return Promise.reject(err);
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * set charset to charset/collation if set or utf8mb4 if not.
 | |
|    * @returns {Promise<void>}
 | |
|    * @private
 | |
|    */
 | |
|   handleCharset() {
 | |
|     if (this.opts.collation) {
 | |
|       // if index <= 255, skip command, since collation has already been set during handshake response.
 | |
|       if (this.opts.collation.index <= 255) return Promise.resolve();
 | |
|       const charset =
 | |
|         this.opts.collation.charset === 'utf8' && this.opts.collation.maxLength === 4
 | |
|           ? 'utf8mb4'
 | |
|           : this.opts.collation.charset;
 | |
|       return new Promise(
 | |
|         this.query.bind(this, {
 | |
|           sql: `SET NAMES ${charset} COLLATE ${this.opts.collation.name}`
 | |
|         })
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // MXS-4635: server can some information directly on first Ok_Packet, like not truncated collation
 | |
|     // in this case, avoid useless SET NAMES utf8mb4 command
 | |
|     if (
 | |
|       !this.opts.charset &&
 | |
|       this.info.collation &&
 | |
|       this.info.collation.charset === 'utf8' &&
 | |
|       this.info.collation.maxLength === 4
 | |
|     ) {
 | |
|       this.info.collation = Collations.fromCharset('utf8mb4');
 | |
|       return Promise.resolve();
 | |
|     }
 | |
|     const connCharset = this.opts.charset ? this.opts.charset : 'utf8mb4';
 | |
|     this.info.collation = Collations.fromCharset(connCharset);
 | |
|     return new Promise(
 | |
|       this.query.bind(this, {
 | |
|         sql: `SET NAMES ${connCharset}`
 | |
|       })
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Asking server timezone if not set in case of 'auto'
 | |
|    * @returns {Promise<void>}
 | |
|    * @private
 | |
|    */
 | |
|   handleTimezone() {
 | |
|     const conn = this;
 | |
|     if (this.opts.timezone === 'local') this.opts.timezone = undefined;
 | |
|     if (this.opts.timezone === 'auto') {
 | |
|       return new Promise(
 | |
|         this.query.bind(this, {
 | |
|           sql: 'SELECT @@system_time_zone stz, @@time_zone tz'
 | |
|         })
 | |
|       ).then((res) => {
 | |
|         const serverTimezone = res[0].tz === 'SYSTEM' ? res[0].stz : res[0].tz;
 | |
|         const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
 | |
|         if (serverTimezone === localTz || convertFixedTime(serverTimezone, conn) === convertFixedTime(localTz, conn)) {
 | |
|           //server timezone is identical to client tz, skipping setting
 | |
|           this.opts.timezone = localTz;
 | |
|           return Promise.resolve();
 | |
|         }
 | |
|         return this._setSessionTimezone(convertFixedTime(localTz, conn));
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     if (this.opts.timezone) {
 | |
|       return this._setSessionTimezone(convertFixedTime(this.opts.timezone, conn));
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   _setSessionTimezone(tz) {
 | |
|     return new Promise(
 | |
|       this.query.bind(this, {
 | |
|         sql: 'SET time_zone=?',
 | |
|         values: [tz]
 | |
|       })
 | |
|     ).catch((err) => {
 | |
|       const er = Errors.createFatalError(
 | |
|         `setting timezone '${tz}' fails on server.\n look at https://mariadb.com/kb/en/mysql_tzinfo_to_sql/ to load IANA timezone. `,
 | |
|         Errors.ER_WRONG_IANA_TIMEZONE,
 | |
|         this.info
 | |
|       );
 | |
|       if (this.opts.logger.error) this.opts.logger.error(er);
 | |
|       return Promise.reject(er);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   checkServerVersion() {
 | |
|     if (!this.opts.forceVersionCheck) {
 | |
|       return Promise.resolve();
 | |
|     }
 | |
|     return new Promise(
 | |
|       this.query.bind(this, {
 | |
|         sql: 'SELECT @@VERSION AS v'
 | |
|       })
 | |
|     ).then(
 | |
|       function (res) {
 | |
|         this.info.serverVersion.raw = res[0].v;
 | |
|         this.info.serverVersion.mariaDb = this.info.serverVersion.raw.includes('MariaDB');
 | |
|         ConnectionInformation.parseVersionString(this.info);
 | |
|         return Promise.resolve();
 | |
|       }.bind(this)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   executeInitQuery() {
 | |
|     if (this.opts.initSql) {
 | |
|       const initialArr = Array.isArray(this.opts.initSql) ? this.opts.initSql : [this.opts.initSql];
 | |
|       const initialPromises = [];
 | |
|       initialArr.forEach((sql) => {
 | |
|         initialPromises.push(
 | |
|           new Promise(
 | |
|             this.query.bind(this, {
 | |
|               sql: sql
 | |
|             })
 | |
|           )
 | |
|         );
 | |
|       });
 | |
| 
 | |
|       return Promise.all(initialPromises).catch((initialErr) => {
 | |
|         const err = Errors.createFatalError(
 | |
|           `Error executing initial sql command: ${initialErr.message}`,
 | |
|           Errors.ER_INITIAL_SQL_ERROR,
 | |
|           this.info
 | |
|         );
 | |
|         if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|         return Promise.reject(err);
 | |
|       });
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   executeSessionTimeout() {
 | |
|     if (this.opts.queryTimeout) {
 | |
|       if (this.info.isMariaDB() && this.info.hasMinVersion(10, 1, 2)) {
 | |
|         const query = `SET max_statement_time=${this.opts.queryTimeout / 1000}`;
 | |
|         new Promise(
 | |
|           this.query.bind(this, {
 | |
|             sql: query
 | |
|           })
 | |
|         ).catch(
 | |
|           function (initialErr) {
 | |
|             const err = Errors.createFatalError(
 | |
|               `Error setting session queryTimeout: ${initialErr.message}`,
 | |
|               Errors.ER_INITIAL_TIMEOUT_ERROR,
 | |
|               this.info,
 | |
|               '08S01',
 | |
|               query
 | |
|             );
 | |
|             if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|             return Promise.reject(err);
 | |
|           }.bind(this)
 | |
|         );
 | |
|       } else {
 | |
|         const err = Errors.createError(
 | |
|           `Can only use queryTimeout for MariaDB server after 10.1.1. queryTimeout value: ${this.opts.queryTimeout}`,
 | |
|           Errors.ER_TIMEOUT_NOT_SUPPORTED,
 | |
|           this.info,
 | |
|           'HY000',
 | |
|           this.opts.queryTimeout
 | |
|         );
 | |
|         if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|         return Promise.reject(err);
 | |
|       }
 | |
|     }
 | |
|     return Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   getSocket() {
 | |
|     return this.socket;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Initialize socket and associate events.
 | |
|    * @private
 | |
|    */
 | |
|   streamInitSocket() {
 | |
|     if (this.opts.connectTimeout) {
 | |
|       this.timeout = setTimeout(this.connectTimeoutReached.bind(this), this.opts.connectTimeout, Date.now());
 | |
|     }
 | |
|     if (this.opts.socketPath) {
 | |
|       this.socket = Net.connect(this.opts.socketPath);
 | |
|     } else if (this.opts.stream) {
 | |
|       if (typeof this.opts.stream === 'function') {
 | |
|         const tmpSocket = this.opts.stream(
 | |
|           function (err, stream) {
 | |
|             if (err) {
 | |
|               this.authFailHandler(err);
 | |
|               return;
 | |
|             }
 | |
|             this.socket = stream ? stream : Net.connect(this.opts.port, this.opts.host);
 | |
|             this.socketInit();
 | |
|           }.bind(this)
 | |
|         );
 | |
|         if (tmpSocket) {
 | |
|           this.socket = tmpSocket;
 | |
|           this.socketInit();
 | |
|         }
 | |
|       } else {
 | |
|         this.authFailHandler(
 | |
|           Errors.createError(
 | |
|             'stream option is not a function. stream must be a function with (error, callback) parameter',
 | |
|             Errors.ER_BAD_PARAMETER_VALUE,
 | |
|             this.info
 | |
|           )
 | |
|         );
 | |
|       }
 | |
|       return;
 | |
|     } else {
 | |
|       this.socket = Net.connect(this.opts.port, this.opts.host);
 | |
|       this.socket.setNoDelay(true);
 | |
|     }
 | |
|     this.socketInit();
 | |
|   }
 | |
| 
 | |
|   socketInit() {
 | |
|     this.socket.on('data', this.streamIn.onData.bind(this.streamIn));
 | |
|     this.socket.on('error', this.socketErrorHandler.bind(this));
 | |
|     this.socket.on('end', this.socketErrorHandler.bind(this));
 | |
|     this.socket.on(
 | |
|       'connect',
 | |
|       function () {
 | |
|         if (this.status === Status.CONNECTING) {
 | |
|           this.status = Status.AUTHENTICATING;
 | |
|           this.socket.setTimeout(this.opts.socketTimeout, this.socketTimeoutReached.bind(this));
 | |
|           this.socket.setNoDelay(true);
 | |
| 
 | |
|           // keep alive for socket. This won't reset server wait_timeout use pool option idleTimeout for that
 | |
|           if (this.opts.keepAliveDelay) {
 | |
|             this.socket.setKeepAlive(true, this.opts.keepAliveDelay);
 | |
|           }
 | |
|         }
 | |
|       }.bind(this)
 | |
|     );
 | |
| 
 | |
|     this.socket.writeBuf = (buf) => this.socket.write(buf);
 | |
|     this.socket.flush = () => {};
 | |
|     this.streamOut.setStream(this.socket);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Authentication success result handler.
 | |
|    *
 | |
|    * @private
 | |
|    */
 | |
|   authSucceedHandler() {
 | |
|     //enable packet compression according to option
 | |
|     if (this.opts.compress) {
 | |
|       if (this.info.serverCapabilities & Capabilities.COMPRESS) {
 | |
|         this.streamOut.setStream(new CompressionOutputStream(this.socket, this.opts, this.info));
 | |
|         this.streamIn = new CompressionInputStream(this.streamIn, this.receiveQueue, this.opts, this.info);
 | |
|         this.socket.removeAllListeners('data');
 | |
|         this.socket.on('data', this.streamIn.onData.bind(this.streamIn));
 | |
|       } else if (this.opts.logger.error) {
 | |
|         this.opts.logger.error(
 | |
|           Errors.createError(
 | |
|             "connection is configured to use packet compression, but the server doesn't have this capability",
 | |
|             Errors.ER_COMPRESSION_NOT_SUPPORTED,
 | |
|             this.info
 | |
|           )
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.addCommand = this.opts.pipelining ? this.addCommandEnablePipeline : this.addCommandEnable;
 | |
|     const conn = this;
 | |
|     this.status = Status.INIT_CMD;
 | |
|     this.executeSessionVariableQuery()
 | |
|       .then(conn.handleCharset.bind(conn))
 | |
|       .then(this.handleTimezone.bind(this))
 | |
|       .then(this.checkServerVersion.bind(this))
 | |
|       .then(this.executeInitQuery.bind(this))
 | |
|       .then(this.executeSessionTimeout.bind(this))
 | |
|       .then(() => {
 | |
|         clearTimeout(this.timeout);
 | |
|         conn.status = Status.CONNECTED;
 | |
|         process.nextTick(conn.connectResolveFct, conn);
 | |
| 
 | |
|         const commands = conn.waitingAuthenticationQueue.toArray();
 | |
|         commands.forEach((cmd) => {
 | |
|           conn.addCommand(cmd, true);
 | |
|         });
 | |
|         conn.waitingAuthenticationQueue = null;
 | |
| 
 | |
|         conn.connectRejectFct = null;
 | |
|         conn.connectResolveFct = null;
 | |
|       })
 | |
|       .catch((err) => {
 | |
|         if (!err.fatal) {
 | |
|           const res = () => {
 | |
|             conn.authFailHandler.call(conn, err);
 | |
|           };
 | |
|           conn.end(res, res);
 | |
|         } else {
 | |
|           conn.authFailHandler.call(conn, err);
 | |
|         }
 | |
|         return Promise.reject(err);
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Authentication failed result handler.
 | |
|    *
 | |
|    * @private
 | |
|    */
 | |
|   authFailHandler(err) {
 | |
|     clearTimeout(this.timeout);
 | |
|     if (this.connectRejectFct) {
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       //remove handshake command
 | |
|       this.receiveQueue.shift();
 | |
|       this.fatalError(err, true);
 | |
| 
 | |
|       process.nextTick(this.connectRejectFct, err);
 | |
|       this.connectRejectFct = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Create TLS socket and associate events.
 | |
|    *
 | |
|    * @param info current connection information
 | |
|    * @param callback  callback function when done
 | |
|    * @private
 | |
|    */
 | |
|   createSecureContext(info, callback) {
 | |
|     info.requireValidCert =
 | |
|       this.opts.ssl === true ||
 | |
|       this.opts.ssl.rejectUnauthorized === undefined ||
 | |
|       this.opts.ssl.rejectUnauthorized === true;
 | |
| 
 | |
|     const baseConf = {
 | |
|       servername: this.opts.host,
 | |
|       socket: this.socket,
 | |
|       rejectUnauthorized: false
 | |
|     };
 | |
|     const sslOption = this.opts.ssl === true ? baseConf : Object.assign({}, this.opts.ssl, baseConf);
 | |
| 
 | |
|     try {
 | |
|       const secureSocket = tls.connect(sslOption, callback);
 | |
|       secureSocket.on('data', this.streamIn.onData.bind(this.streamIn));
 | |
|       secureSocket.on('error', this.socketErrorHandler.bind(this));
 | |
|       secureSocket.on('end', this.socketErrorHandler.bind(this));
 | |
|       secureSocket.writeBuf = (buf) => secureSocket.write(buf);
 | |
|       secureSocket.flush = () => {};
 | |
| 
 | |
|       this.socket.removeAllListeners('data');
 | |
|       this.socket = secureSocket;
 | |
| 
 | |
|       this.streamOut.setStream(secureSocket);
 | |
|     } catch (err) {
 | |
|       this.socketErrorHandler(err);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handle packet when no packet is expected.
 | |
|    * (there can be an ERROR packet send by server/proxy to inform that connection is ending).
 | |
|    *
 | |
|    * @param packet  packet
 | |
|    * @private
 | |
|    */
 | |
|   unexpectedPacket(packet) {
 | |
|     if (packet && packet.peek() === 0xff) {
 | |
|       //can receive unexpected error packet from server/proxy
 | |
|       //to inform that connection is closed (usually by timeout)
 | |
|       let err = packet.readError(this.info);
 | |
|       if (err.fatal && this.status < Status.CLOSING) {
 | |
|         this.emit('error', err);
 | |
|         if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|         this.end(
 | |
|           () => {},
 | |
|           () => {}
 | |
|         );
 | |
|       }
 | |
|     } else if (this.status < Status.CLOSING) {
 | |
|       const err = Errors.createFatalError(
 | |
|         `receiving packet from server without active commands\nconn:${this.info.threadId ? this.info.threadId : -1}(${
 | |
|           packet.pos
 | |
|         },${packet.end})\n${Utils.log(this.opts, packet.buf, packet.pos, packet.end)}`,
 | |
|         Errors.ER_UNEXPECTED_PACKET,
 | |
|         this.info
 | |
|       );
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       this.emit('error', err);
 | |
|       this.destroy();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handle connection timeout.
 | |
|    *
 | |
|    * @private
 | |
|    */
 | |
|   connectTimeoutReached(initialConnectionTime) {
 | |
|     this.timeout = null;
 | |
|     const handshake = this.receiveQueue.peekFront();
 | |
|     const err = Errors.createFatalError(
 | |
|       `Connection timeout: failed to create socket after ${Date.now() - initialConnectionTime}ms`,
 | |
|       Errors.ER_CONNECTION_TIMEOUT,
 | |
|       this.info,
 | |
|       '08S01',
 | |
|       null,
 | |
|       handshake ? handshake.stack : null
 | |
|     );
 | |
|     if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|     this.authFailHandler(err);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handle socket timeout.
 | |
|    *
 | |
|    * @private
 | |
|    */
 | |
|   socketTimeoutReached() {
 | |
|     const err = Errors.createFatalError('socket timeout', Errors.ER_SOCKET_TIMEOUT, this.info);
 | |
|     if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|     this.fatalError(err, true);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add command to waiting queue until authentication.
 | |
|    *
 | |
|    * @param cmd         command
 | |
|    * @private
 | |
|    */
 | |
|   addCommandQueue(cmd) {
 | |
|     this.waitingAuthenticationQueue.push(cmd);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add command to command sending and receiving queue.
 | |
|    *
 | |
|    * @param cmd         command
 | |
|    * @param expectResponse queue command response
 | |
|    * @private
 | |
|    */
 | |
|   addCommandEnable(cmd, expectResponse) {
 | |
|     cmd.once('end', this._sendNextCmdImmediate.bind(this));
 | |
| 
 | |
|     //send immediately only if no current active receiver
 | |
|     if (this.sendQueue.isEmpty() && this.receiveQueue.isEmpty()) {
 | |
|       if (expectResponse) this.receiveQueue.push(cmd);
 | |
|       cmd.start(this.streamOut, this.opts, this.info);
 | |
|     } else {
 | |
|       if (expectResponse) this.receiveQueue.push(cmd);
 | |
|       this.sendQueue.push(cmd);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add command to command sending and receiving queue using pipelining
 | |
|    *
 | |
|    * @param cmd             command
 | |
|    * @param expectResponse queue command response
 | |
|    * @private
 | |
|    */
 | |
|   addCommandEnablePipeline(cmd, expectResponse) {
 | |
|     cmd.once('send_end', this._sendNextCmdImmediate.bind(this));
 | |
| 
 | |
|     if (expectResponse) this.receiveQueue.push(cmd);
 | |
|     if (this.sendQueue.isEmpty()) {
 | |
|       cmd.start(this.streamOut, this.opts, this.info);
 | |
|       if (cmd.sending) {
 | |
|         this.sendQueue.push(cmd);
 | |
|         cmd.prependOnceListener('send_end', this.sendQueue.shift.bind(this.sendQueue));
 | |
|       }
 | |
|     } else {
 | |
|       this.sendQueue.push(cmd);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Replacing command when connection is closing or closed to send a proper error message.
 | |
|    *
 | |
|    * @param cmd         command
 | |
|    * @private
 | |
|    */
 | |
|   addCommandDisabled(cmd) {
 | |
|     const err = cmd.throwNewError(
 | |
|       'Cannot execute new commands: connection closed',
 | |
|       true,
 | |
|       this.info,
 | |
|       '08S01',
 | |
|       Errors.ER_CMD_CONNECTION_CLOSED
 | |
|     );
 | |
|     if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handle socket error.
 | |
|    *
 | |
|    * @param err               socket error
 | |
|    * @private
 | |
|    */
 | |
|   socketErrorHandler(err) {
 | |
|     if (this.status >= Status.CLOSING) return;
 | |
|     if (this.socket) {
 | |
|       this.socket.writeBuf = () => {};
 | |
|       this.socket.flush = () => {};
 | |
|     }
 | |
| 
 | |
|     //socket has been ended without error
 | |
|     if (!err) {
 | |
|       err = Errors.createFatalError(
 | |
|         'socket has unexpectedly been closed',
 | |
|         Errors.ER_SOCKET_UNEXPECTED_CLOSE,
 | |
|         this.info
 | |
|       );
 | |
|     } else {
 | |
|       err.fatal = true;
 | |
|       err.sqlState = 'HY000';
 | |
|     }
 | |
| 
 | |
|     switch (this.status) {
 | |
|       case Status.CONNECTING:
 | |
|       case Status.AUTHENTICATING:
 | |
|         const currentCmd = this.receiveQueue.peekFront();
 | |
|         if (currentCmd && currentCmd.stack && err) {
 | |
|           err.stack += '\n From event:\n' + currentCmd.stack.substring(currentCmd.stack.indexOf('\n') + 1);
 | |
|         }
 | |
|         this.authFailHandler(err);
 | |
|         break;
 | |
| 
 | |
|       default:
 | |
|         this.fatalError(err, false);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Fatal unexpected error : closing connection, and throw exception.
 | |
|    */
 | |
|   fatalError(err, avoidThrowError) {
 | |
|     if (this.status >= Status.CLOSING) {
 | |
|       this.socketErrorDispatchToQueries(err);
 | |
|       return;
 | |
|     }
 | |
|     const mustThrowError = this.status !== Status.CONNECTING;
 | |
|     this.status = Status.CLOSING;
 | |
| 
 | |
|     //prevent executing new commands
 | |
|     this.addCommand = this.addCommandDisabled;
 | |
| 
 | |
|     if (this.socket) {
 | |
|       this.socket.removeAllListeners('error');
 | |
|       this.socket.removeAllListeners('timeout');
 | |
|       this.socket.removeAllListeners('close');
 | |
|       this.socket.removeAllListeners('data');
 | |
|       if (!this.socket.destroyed) this.socket.destroy();
 | |
|       this.socket = undefined;
 | |
|     }
 | |
|     this.status = Status.CLOSED;
 | |
| 
 | |
|     const errorThrownByCmd = this.socketErrorDispatchToQueries(err);
 | |
|     if (mustThrowError) {
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       if (this.listenerCount('error') > 0) {
 | |
|         this.emit('error', err);
 | |
|         this.emit('end');
 | |
|         this.clear();
 | |
|       } else {
 | |
|         this.emit('end');
 | |
|         this.clear();
 | |
|         //error will be thrown if no error listener and no command did throw the exception
 | |
|         if (!avoidThrowError && !errorThrownByCmd) throw err;
 | |
|       }
 | |
|     } else {
 | |
|       this.clear();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Dispatch fatal error to current running queries.
 | |
|    *
 | |
|    * @param err        the fatal error
 | |
|    * @return {boolean} return if error has been relayed to queries
 | |
|    */
 | |
|   socketErrorDispatchToQueries(err) {
 | |
|     let receiveCmd;
 | |
|     let errorThrownByCmd = false;
 | |
|     while ((receiveCmd = this.receiveQueue.shift())) {
 | |
|       if (receiveCmd && receiveCmd.onPacketReceive) {
 | |
|         errorThrownByCmd = true;
 | |
|         setImmediate(receiveCmd.throwError.bind(receiveCmd, err, this.info));
 | |
|       }
 | |
|     }
 | |
|     return errorThrownByCmd;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Will send next command in queue if any.
 | |
|    *
 | |
|    * @private
 | |
|    */
 | |
|   nextSendCmd() {
 | |
|     let sendCmd;
 | |
|     if ((sendCmd = this.sendQueue.shift())) {
 | |
|       if (sendCmd.sending) {
 | |
|         this.sendQueue.unshift(sendCmd);
 | |
|       } else {
 | |
|         sendCmd.start(this.streamOut, this.opts, this.info);
 | |
|         if (sendCmd.sending) {
 | |
|           this.sendQueue.unshift(sendCmd);
 | |
|           sendCmd.prependOnceListener('send_end', this.sendQueue.shift.bind(this.sendQueue));
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Change transaction state.
 | |
|    *
 | |
|    * @param cmdParam command parameter
 | |
|    * @param resolve success function to call
 | |
|    * @param reject error function to call
 | |
|    * @private
 | |
|    */
 | |
|   changeTransaction(cmdParam, resolve, reject) {
 | |
|     //if command in progress, driver cannot rely on status and must execute query
 | |
|     if (this.status >= Status.CLOSING) {
 | |
|       const err = Errors.createFatalError(
 | |
|         'Cannot execute new commands: connection closed',
 | |
|         Errors.ER_CMD_CONNECTION_CLOSED,
 | |
|         this.info,
 | |
|         '08S01',
 | |
|         cmdParam.sql
 | |
|       );
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       reject(err);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     //Command in progress => must execute query
 | |
|     //or if no command in progress, can rely on status to know if query is needed
 | |
|     if (this.receiveQueue.peekFront() || this.info.status & ServerStatus.STATUS_IN_TRANS) {
 | |
|       const cmd = new Query(
 | |
|         resolve,
 | |
|         (err) => {
 | |
|           if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|           reject(err);
 | |
|         },
 | |
|         this.opts,
 | |
|         cmdParam
 | |
|       );
 | |
|       this.addCommand(cmd, true);
 | |
|     } else resolve();
 | |
|   }
 | |
| 
 | |
|   changeUser(cmdParam, resolve, reject) {
 | |
|     if (!this.info.isMariaDB()) {
 | |
|       const err = Errors.createError(
 | |
|         'method changeUser not available for MySQL server due to Bug #83472',
 | |
|         Errors.ER_MYSQL_CHANGE_USER_BUG,
 | |
|         this.info,
 | |
|         '0A000'
 | |
|       );
 | |
|       if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|       reject(err);
 | |
|       return;
 | |
|     }
 | |
|     if (this.status < Status.CLOSING) {
 | |
|       this.addCommand = this.addCommandEnable;
 | |
|     }
 | |
|     let conn = this;
 | |
|     if (cmdParam.opts && cmdParam.opts.collation && typeof cmdParam.opts.collation === 'string') {
 | |
|       const val = cmdParam.opts.collation.toUpperCase();
 | |
|       cmdParam.opts.collation = Collations.fromName(cmdParam.opts.collation.toUpperCase());
 | |
|       if (cmdParam.opts.collation === undefined) return reject(new RangeError(`Unknown collation '${val}'`));
 | |
|     }
 | |
| 
 | |
|     this.addCommand(
 | |
|       new ChangeUser(
 | |
|         cmdParam,
 | |
|         this.opts,
 | |
|         (res) => {
 | |
|           if (conn.status < Status.CLOSING && conn.opts.pipelining) conn.addCommand = conn.addCommandEnablePipeline;
 | |
|           if (cmdParam.opts && cmdParam.opts.collation) conn.opts.collation = cmdParam.opts.collation;
 | |
|           conn
 | |
|             .handleCharset()
 | |
|             .then(() => {
 | |
|               if (cmdParam.opts && cmdParam.opts.collation) {
 | |
|                 conn.info.collation = cmdParam.opts.collation;
 | |
|                 conn.opts.emit('collation', cmdParam.opts.collation);
 | |
|               }
 | |
|               resolve(res);
 | |
|             })
 | |
|             .catch((err) => {
 | |
|               const res = () => conn.authFailHandler.call(conn, err);
 | |
|               if (!err.fatal) {
 | |
|                 conn.end(res, res);
 | |
|               } else {
 | |
|                 res();
 | |
|               }
 | |
|               reject(err);
 | |
|             });
 | |
|         },
 | |
|         this.authFailHandler.bind(this, reject),
 | |
|         this.getSocket.bind(this)
 | |
|       ),
 | |
|       true
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   query(cmdParam, resolve, reject) {
 | |
|     if (!cmdParam.sql)
 | |
|       return reject(
 | |
|         Errors.createError(
 | |
|           'sql parameter is mandatory',
 | |
|           Errors.ER_UNDEFINED_SQL,
 | |
|           this.info,
 | |
|           'HY000',
 | |
|           null,
 | |
|           false,
 | |
|           cmdParam.stack
 | |
|         )
 | |
|       );
 | |
|     const cmd = new Query(
 | |
|       resolve,
 | |
|       (err) => {
 | |
|         if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|         reject(err);
 | |
|       },
 | |
|       this.opts,
 | |
|       cmdParam
 | |
|     );
 | |
|     this.addCommand(cmd, true);
 | |
|   }
 | |
| 
 | |
|   prepare(cmdParam, resolve, reject) {
 | |
|     if (!cmdParam.sql)
 | |
|       return reject(Errors.createError('sql parameter is mandatory', Errors.ER_UNDEFINED_SQL, this.info, 'HY000'));
 | |
|     if (this.prepareCache && (this.sendQueue.isEmpty() || !this.receiveQueue.peekFront())) {
 | |
|       // no command in queue, database is then considered ok, and cache can be search right now
 | |
|       const cachedPrepare = this.prepareCache.get(cmdParam.sql);
 | |
|       if (cachedPrepare) {
 | |
|         return resolve(cachedPrepare);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const cmd = new Prepare(
 | |
|       resolve,
 | |
|       (err) => {
 | |
|         if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|         reject(err);
 | |
|       },
 | |
|       this.opts,
 | |
|       cmdParam,
 | |
|       this
 | |
|     );
 | |
|     this.addCommand(cmd, true);
 | |
|   }
 | |
| 
 | |
|   prepareExecute(cmdParam) {
 | |
|     if (!cmdParam.sql) {
 | |
|       return Promise.reject(
 | |
|         Errors.createError('sql parameter is mandatory', Errors.ER_UNDEFINED_SQL, this.info, 'HY000')
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (this.prepareCache && (this.sendQueue.isEmpty() || !this.receiveQueue.peekFront())) {
 | |
|       // no command in queue, current database is known, so cache can be search right now
 | |
|       const cachedPrepare = this.prepareCache.get(cmdParam.sql);
 | |
|       if (cachedPrepare) {
 | |
|         return new Promise(this.executePromise.bind(this, cmdParam, cachedPrepare)).finally(() =>
 | |
|           cachedPrepare.close()
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // permit pipelining PREPARE and EXECUTE if mariadb 10.2.4+ and has no streaming
 | |
|     const conn = this;
 | |
|     if (this.opts.pipelining && this.info.isMariaDB() && this.info.hasMinVersion(10, 2, 4)) {
 | |
|       let hasStreamingValue = false;
 | |
|       const vals = cmdParam.values ? (Array.isArray(cmdParam.values) ? cmdParam.values : [cmdParam.values]) : [];
 | |
|       for (let i = 0; i < vals.length; i++) {
 | |
|         const val = vals[i];
 | |
|         if (
 | |
|           val != null &&
 | |
|           typeof val === 'object' &&
 | |
|           typeof val.pipe === 'function' &&
 | |
|           typeof val.read === 'function'
 | |
|         ) {
 | |
|           hasStreamingValue = true;
 | |
|         }
 | |
|       }
 | |
|       if (!hasStreamingValue) {
 | |
|         return new Promise((resolve, reject) => {
 | |
|           let nbExecute = 0;
 | |
|           const executeCommand = new Execute(
 | |
|             (res) => {
 | |
|               if (nbExecute++ === 0) {
 | |
|                 executeCommand.prepare.close();
 | |
|                 resolve(res);
 | |
|               }
 | |
|             },
 | |
|             (err) => {
 | |
|               if (nbExecute++ === 0) {
 | |
|                 if (conn.opts.logger.error) conn.opts.logger.error(err);
 | |
|                 reject(err);
 | |
|                 if (executeCommand.prepare) {
 | |
|                   executeCommand.prepare.close();
 | |
|                 }
 | |
|               }
 | |
|             },
 | |
|             conn.opts,
 | |
|             cmdParam,
 | |
|             null
 | |
|           );
 | |
|           cmdParam.executeCommand = executeCommand;
 | |
|           const cmd = new Prepare(
 | |
|             (prep) => {
 | |
|               if (nbExecute > 0) prep.close();
 | |
|             },
 | |
|             (err) => {
 | |
|               if (nbExecute++ === 0) {
 | |
|                 if (conn.opts.logger.error) conn.opts.logger.error(err);
 | |
|                 reject(err);
 | |
|               }
 | |
|             },
 | |
|             conn.opts,
 | |
|             cmdParam,
 | |
|             conn
 | |
|           );
 | |
|           conn.addCommand(cmd, true);
 | |
|           conn.addCommand(executeCommand, true);
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     // execute PREPARE, then EXECUTE
 | |
|     return new Promise((resolve, reject) => {
 | |
|       const cmd = new Prepare(resolve, reject, this.opts, cmdParam, conn);
 | |
|       conn.addCommand(cmd, true);
 | |
|     })
 | |
|       .then((prepare) => {
 | |
|         return new Promise(function (resolve, reject) {
 | |
|           conn.executePromise.call(conn, cmdParam, prepare, resolve, reject);
 | |
|         }).finally(() => prepare.close());
 | |
|       })
 | |
|       .catch((err) => {
 | |
|         if (conn.opts.logger.error) conn.opts.logger.error(err);
 | |
|         throw err;
 | |
|       });
 | |
|   }
 | |
|   importFile(cmdParam, resolve, reject) {
 | |
|     const conn = this;
 | |
|     if (!cmdParam || !cmdParam.file) {
 | |
|       return reject(
 | |
|         Errors.createError(
 | |
|           'SQL file parameter is mandatory',
 | |
|           Errors.ER_MISSING_SQL_PARAMETER,
 | |
|           conn.info,
 | |
|           'HY000',
 | |
|           null,
 | |
|           false,
 | |
|           cmdParam.stack
 | |
|         )
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     const prevAddCommand = this.addCommand.bind(conn);
 | |
| 
 | |
|     this.waitingAuthenticationQueue = new Queue();
 | |
|     this.addCommand = this.addCommandQueue;
 | |
|     const tmpQuery = function (sql, resolve, reject) {
 | |
|       const cmd = new Query(
 | |
|         resolve,
 | |
|         (err) => {
 | |
|           if (conn.opts.logger.error) conn.opts.logger.error(err);
 | |
|           reject(err);
 | |
|         },
 | |
|         conn.opts,
 | |
|         {
 | |
|           sql: sql,
 | |
|           opts: {}
 | |
|         }
 | |
|       );
 | |
|       prevAddCommand(cmd, true);
 | |
|     };
 | |
| 
 | |
|     let prevDatabase = null;
 | |
|     return (
 | |
|       cmdParam.skipDbCheck ? Promise.resolve() : new Promise(tmpQuery.bind(conn, 'SELECT DATABASE() as db'))
 | |
|     ).then((res) => {
 | |
|       prevDatabase = res ? res[0].db : null;
 | |
|       if (
 | |
|         (cmdParam.skipDbCheck && !conn.opts.database) ||
 | |
|         (!cmdParam.skipDbCheck && !cmdParam.database && !prevDatabase)
 | |
|       ) {
 | |
|         return reject(
 | |
|           Errors.createError(
 | |
|             'Database parameter is not set and no database is selected',
 | |
|             Errors.ER_MISSING_DATABASE_PARAMETER,
 | |
|             conn.info,
 | |
|             'HY000',
 | |
|             null,
 | |
|             false,
 | |
|             cmdParam.stack
 | |
|           )
 | |
|         );
 | |
|       }
 | |
|       const searchDbPromise = cmdParam.database
 | |
|         ? new Promise(tmpQuery.bind(conn, `USE \`${cmdParam.database.replace(/`/gi, '``')}\``))
 | |
|         : Promise.resolve();
 | |
|       return searchDbPromise.then(() => {
 | |
|         const endingFunction = () => {
 | |
|           if (conn.status < Status.CLOSING) {
 | |
|             conn.addCommand = conn.addCommandEnable.bind(conn);
 | |
|             if (conn.status < Status.CLOSING && conn.opts.pipelining) {
 | |
|               conn.addCommand = conn.addCommandEnablePipeline.bind(conn);
 | |
|             }
 | |
|             const commands = conn.waitingAuthenticationQueue.toArray();
 | |
|             commands.forEach((cmd) => conn.addCommand(cmd, true));
 | |
|             conn.waitingAuthenticationQueue = null;
 | |
|           }
 | |
|         };
 | |
|         return fsPromises
 | |
|           .open(cmdParam.file, 'r')
 | |
|           .then(async (fd) => {
 | |
|             const buf = {
 | |
|               buffer: Buffer.allocUnsafe(16384),
 | |
|               offset: 0,
 | |
|               end: 0
 | |
|             };
 | |
| 
 | |
|             const queryPromises = [];
 | |
|             let cmdError = null;
 | |
|             while (!cmdError) {
 | |
|               try {
 | |
|                 const res = await fd.read(buf.buffer, buf.end, buf.buffer.length - buf.end, null);
 | |
|                 if (res.bytesRead === 0) {
 | |
|                   // end of file reached.
 | |
|                   fd.close().catch(() => {});
 | |
|                   if (cmdError) {
 | |
|                     endingFunction();
 | |
|                     reject(cmdError);
 | |
|                     return;
 | |
|                   }
 | |
|                   await Promise.allSettled(queryPromises)
 | |
|                     .then(() => {
 | |
|                       // reset connection to initial database if was set
 | |
|                       if (
 | |
|                         !cmdParam.skipDbCheck &&
 | |
|                         prevDatabase &&
 | |
|                         cmdParam.database &&
 | |
|                         cmdParam.database !== prevDatabase
 | |
|                       ) {
 | |
|                         return new Promise(tmpQuery.bind(conn, `USE \`${prevDatabase.replace(/`/gi, '``')}\``));
 | |
|                       }
 | |
|                       return Promise.resolve();
 | |
|                     })
 | |
|                     .then(() => {
 | |
|                       endingFunction();
 | |
|                       if (cmdError) {
 | |
|                         reject(cmdError);
 | |
|                       }
 | |
|                       resolve();
 | |
|                     })
 | |
|                     .catch((err) => {
 | |
|                       endingFunction();
 | |
|                       reject(err);
 | |
|                     });
 | |
|                   return;
 | |
|                 } else {
 | |
|                   buf.end += res.bytesRead;
 | |
|                   const queries = Parse.parseQueries(buf);
 | |
|                   const queryIntermediatePromise = queries.flatMap((element) => {
 | |
|                     return new Promise(tmpQuery.bind(conn, element)).catch((err) => {
 | |
|                       cmdError = err;
 | |
|                     });
 | |
|                   });
 | |
| 
 | |
|                   queryPromises.push(...queryIntermediatePromise);
 | |
|                   if (buf.offset === buf.end) {
 | |
|                     buf.offset = 0;
 | |
|                     buf.end = 0;
 | |
|                   } else {
 | |
|                     // ensure that buffer can at least read 8k bytes,
 | |
|                     // either by copying remaining data on used part or growing buffer
 | |
|                     if (buf.offset > 8192) {
 | |
|                       // reuse buffer, copying remaining data begin of buffer
 | |
|                       buf.buffer.copy(buf.buffer, 0, buf.offset, buf.end);
 | |
|                       buf.end -= buf.offset;
 | |
|                       buf.offset = 0;
 | |
|                     } else if (buf.buffer.length - buf.end < 8192) {
 | |
|                       // grow buffer
 | |
|                       const tmpBuf = Buffer.allocUnsafe(buf.buffer.length << 1);
 | |
|                       buf.buffer.copy(tmpBuf, 0, buf.offset, buf.end);
 | |
|                       buf.buffer = tmpBuf;
 | |
|                       buf.end -= buf.offset;
 | |
|                       buf.offset = 0;
 | |
|                     }
 | |
|                   }
 | |
|                 }
 | |
|               } catch (e) {
 | |
|                 fd.close().catch(() => {});
 | |
|                 endingFunction();
 | |
|                 Promise.allSettled(queryPromises).catch(() => {});
 | |
|                 return reject(
 | |
|                   Errors.createError(
 | |
|                     e.message,
 | |
|                     Errors.ER_SQL_FILE_ERROR,
 | |
|                     conn.info,
 | |
|                     'HY000',
 | |
|                     null,
 | |
|                     false,
 | |
|                     cmdParam.stack
 | |
|                   )
 | |
|                 );
 | |
|               }
 | |
|             }
 | |
|             if (cmdError) {
 | |
|               endingFunction();
 | |
|               reject(cmdError);
 | |
|             }
 | |
|           })
 | |
|           .catch((err) => {
 | |
|             endingFunction();
 | |
|             if (err.code === 'ENOENT') {
 | |
|               return reject(
 | |
|                 Errors.createError(
 | |
|                   `SQL file parameter '${cmdParam.file}' doesn't exists`,
 | |
|                   Errors.ER_MISSING_SQL_FILE,
 | |
|                   conn.info,
 | |
|                   'HY000',
 | |
|                   null,
 | |
|                   false,
 | |
|                   cmdParam.stack
 | |
|                 )
 | |
|               );
 | |
|             }
 | |
|             return reject(
 | |
|               Errors.createError(err.message, Errors.ER_SQL_FILE_ERROR, conn.info, 'HY000', null, false, cmdParam.stack)
 | |
|             );
 | |
|           });
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clearing connection variables when ending.
 | |
|    *
 | |
|    * @private
 | |
|    */
 | |
|   clear() {
 | |
|     this.sendQueue.clear();
 | |
|     this.opts.removeAllListeners();
 | |
|     this.streamOut = undefined;
 | |
|     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://[<user>[:<password>]@]<host>[:<port>]/[<db>[?<opt1>=<value1>[&<opt2>=<value2>]]]'`
 | |
|               )
 | |
|             );
 | |
|           return resolve();
 | |
|         }
 | |
| 
 | |
|         const options = {
 | |
|           host: matchResults[7] ? decodeURIComponent(matchResults[7]) : matchResults[6],
 | |
|           port: matchResults[9] ? parseInt(matchResults[9]) : 3306
 | |
|         };
 | |
| 
 | |
|         if (options.host === this.opts.host && options.port === this.opts.port) {
 | |
|           // redirection to the same host, skip loop redirection
 | |
|           return resolve();
 | |
|         }
 | |
| 
 | |
|         // 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 () {
 | |
|               await new Promise(this.end.bind(this, {}));
 | |
|               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;
 | |
|   }
 | |
| 
 | |
|   _sendNextCmdImmediate() {
 | |
|     if (!this.sendQueue.isEmpty()) {
 | |
|       setImmediate(this.nextSendCmd.bind(this));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _closePrepare(prepareResultPacket) {
 | |
|     this.addCommand(
 | |
|       new ClosePrepare(
 | |
|         {},
 | |
|         () => {},
 | |
|         () => {},
 | |
|         prepareResultPacket
 | |
|       ),
 | |
|       false
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _logAndReject(reject, err) {
 | |
|     if (this.opts.logger.error) this.opts.logger.error(err);
 | |
|     reject(err);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class TestMethods {
 | |
|   #collation;
 | |
|   #socket;
 | |
| 
 | |
|   constructor(collation, socket) {
 | |
|     this.#collation = collation;
 | |
|     this.#socket = socket;
 | |
|   }
 | |
| 
 | |
|   getCollation() {
 | |
|     return this.#collation;
 | |
|   }
 | |
| 
 | |
|   getSocket() {
 | |
|     return this.#socket;
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = Connection;
 |