mariadb
Version:
fast mariadb or mysql connector.
1,594 lines (1,452 loc) • 68.5 kB
JavaScript
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 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)
);
// Capture stack trace for better error reporting
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);
});
}
/**
* Execute a prepared statement with the given parameters
*
* @param {Object} cmdParam - Command parameters
* @param {Object} prepare - Prepared statement
* @param {Function} resolve - Promise resolve function
* @param {Function} reject - Promise reject function
*/
executePromise(cmdParam, prepare, resolve, reject) {
const cmd = new Execute(resolve, this._logAndReject.bind(this, reject), this.opts, cmdParam, prepare);
this.addCommand(cmd, true);
}
/**
* Execute a batch of the same SQL statement with different parameter sets
*
* @param {Object|String} cmdParam - SQL statement or options object
* @param {function} resolve - promise resolve function
* @param {function} reject - promise reject function
*/
batch(cmdParam, resolve, reject) {
// Validate SQL parameter
if (!cmdParam.sql) {
return this.handleMissingSqlError(reject);
}
// Validate values parameter
if (!cmdParam.values) {
return this.handleMissingValuesError(cmdParam, reject);
}
// Execute the batch operation
this.prepare(
cmdParam,
(prepare) => this.executeBatch(cmdParam, prepare, resolve, reject),
(err) => this._logAndReject(reject, err)
);
}
/**
* Handle missing SQL parameter error
*
* @param {Function} reject - Promise reject function
* @private
*/
handleMissingSqlError(reject) {
const err = Errors.createError(
'sql parameter is mandatory',
Errors.ER_UNDEFINED_SQL,
this.info,
'HY000',
null,
false
);
// Add stack trace for better debugging
Error.captureStackTrace(err, this.handleMissingSqlError);
this._logAndReject(reject, err);
}
/**
* Handle missing values parameter error
*
* @param {Object} cmdParam - Command parameters
* @param {Function} reject - Promise reject function
* @private
*/
handleMissingValuesError(cmdParam, reject) {
const sql = cmdParam.sql;
// Truncate SQL for debug output if it's too long
const debugSql = sql.length > this.opts.debugLen ? sql.substring(0, this.opts.debugLen) + '...' : sql;
const err = Errors.createError(
'Batch must have values set',
Errors.ER_BATCH_WITH_NO_VALUES,
this.info,
'HY000',
debugSql,
false,
cmdParam.stack
);
this._logAndReject(reject, err);
}
/**
* Execute batch operation with prepared statement
*
* @param {Object} cmdParam - Command parameters
* @param {Object} prepare - Prepared statement
* @param {Function} resolve - Promise resolve function
* @param {Function} reject - Promise reject function
* @private
*/
executeBatch(cmdParam, prepare, resolve, reject) {
const usePlaceHolder = (cmdParam.opts && cmdParam.opts.namedPlaceholders) || this.opts.namedPlaceholders;
let values = this.formatBatchValues(cmdParam.values, usePlaceHolder, prepare.parameterCount);
cmdParam.values = values;
// Determine if bulk protocol can be used
const useBulk = this._canUseBulk(values, cmdParam.opts);
if (useBulk) {
this.executeBulkPromise(cmdParam, prepare, this.opts, resolve, reject);
} else {
this.executeIndividualBatches(cmdParam, prepare, resolve, reject);
}
}
/**
* Execute bulk operation using specialized bulk protocol
*
* @param {Object} cmdParam - Command parameters
* @param {Object} prepare - Prepared statement
* @param {Object} opts - Options
* @param {Function} resolve - Promise resolve function
* @param {Function} reject - Promise reject function
* @private
*/
executeBulkPromise(cmdParam, prepare, opts, resolve, reject) {
const cmd = new BatchBulk(
(res) => {
prepare.close();
return resolve(res);
},
(err) => {
prepare.close();
if (opts.logger.error) opts.logger.error(err);
reject(err);
},
opts,
prepare,
cmdParam
);
this.addCommand(cmd, true);
}
/**
* Format batch values into the correct structure
*
* @param {Array} values - Original values array
* @param {Boolean} usePlaceHolder - Whether named placeholders are used
* @param {Number} parameterCount - Number of parameters in prepared statement
* @returns {Array} Formatted values array
* @private
*/
formatBatchValues(values, usePlaceHolder, parameterCount) {
// If values is not an array, wrap it
if (!Array.isArray(values)) {
return [[values]];
}
// For named placeholders, return as is
if (usePlaceHolder) {
return values;
}
// If already in correct format (array of arrays), return as is
if (Array.isArray(values[0])) {
return values;
}
// If only one parameter expected, convert flat array to array of single-item arrays
if (parameterCount === 1) {
// Pre-allocate result array for better performance
const result = new Array(values.length);
for (let i = 0; i < values.length; i++) {
result[i] = [values[i]];
}
return result;
}
// Single set of parameters for multiple placeholders
return [values];
}
/**
* Execute individual batch operations when bulk protocol can't be used
*
* @param {Object} cmdParam - Command parameters
* @param {Object} prepare - Prepared statement
* @param {Function} resolve - Promise resolve function
* @param {Function} reject - Promise reject function
* @private
*/
executeIndividualBatches(cmdParam, prepare, resolve, reject) {
const results = [];
const batchSize = 1000; // Process in chunks to avoid memory issues
const totalBatches = Math.ceil(cmdParam.values.length / batchSize);
// Execute by chunks to avoid excessive memory usage
this.executeBatchChunk(cmdParam, prepare, 0, batchSize, totalBatches, results, resolve, reject);
}
/**
* Execute a chunk of the batch operations
*
* @param {Object} cmdParam - Command parameters
* @param {Object} prepare - Prepared statement
* @param {Number} chunkIndex - Current chunk index
* @param {Number} batchSize - Size of each batch chunk
* @param {Number} totalBatches - Total number of chunks
* @param {Array} results - Accumulated results
* @param {Function} resolve - Promise resolve function
* @param {Function} reject - Promise reject function
* @private
*/
executeBatchChunk(cmdParam, prepare, chunkIndex, batchSize, totalBatches, results, resolve, reject) {
const values = cmdParam.values;
const startIdx = chunkIndex * batchSize;
const endIdx = Math.min(startIdx + batchSize, values.length);
const executes = [];
// Create execute promises for this chunk
for (let i = startIdx; i < endIdx; i++) {
executes.push(prepare.execute(values[i], cmdParam.opts, null, cmdParam.stack));
}
// Execute all promises in this chunk
Promise.all(executes)
.then(
(chunkResults) => {
// Add results from this chunk to accumulated results
results.push(...chunkResults);
// If this was the last chunk, process results
if (chunkIndex === totalBatches - 1) {
const cmdOpt = Object.assign({}, this.opts, cmdParam.opts);
this.processBatchResults(results, cmdOpt, cmdParam, resolve);
prepare.close();
} else {
// Process next chunk
setImmediate(() => {
this.executeBatchChunk(
cmdParam,
prepare,
chunkIndex + 1,
batchSize,
totalBatches,
results,
resolve,
reject
);
});
}
},
(err) => {
prepare.close();
reject(err);
}
)
.catch((err) => {
prepare.close();
reject(err);
});
}
/**
* Process batch results from individual executions
*
* @param {Array} results - Array of individual results
* @param {Object} cmdOpt - Command options
* @param {Object} cmdParam - Command parameters
* @param {Function} resolve - Promise resolve function
* @private
*/
processBatchResults(results, cmdOpt, cmdParam, resolve) {
// Handle empty results case
if (!results.length) {
resolve(cmdOpt.metaAsArray ? [[], []] : []);
return;
}
// Return full results when requested
const fullResult = cmdOpt.fullResult === undefined || cmdOpt.fullResult;
if (fullResult) {
if (cmdOpt.metaAsArray) {
const aggregateResults = results.reduce((accumulator, currentValue) => {
if (Array.isArray(currentValue[0])) {
accumulator.push(...currentValue[0]);
} else if (currentValue[0] instanceof OkPacket) {
accumulator.push(currentValue[0]);
} else {
accumulator.push([currentValue[0]]);
}
return accumulator;
}, []);
const meta = results[0][1];
resolve([aggregateResults, meta]);
} else {
const aggregateResults = results.reduce((accumulator, currentValue) => {
if (currentValue instanceof OkPacket) {
accumulator.push(currentValue);
} else if (!cmdOpt.rowsAsArray && Array.isArray(currentValue[0])) {
accumulator.push(...currentValue[0]);
} else {
accumulator.push(currentValue[0]);
}
return accumulator;
}, []);
const meta = results[0].meta;
Object.defineProperty(aggregateResults, 'meta', {
value: meta,
writable: true,
enumerable: cmdOpt.metaEnumerable
});
resolve(aggregateResults);
}
return;
}
// Get first result to determine result type
const firstResult = cmdOpt.metaAsArray ? results[0][0] : results[0];
// Process based on result type
if (firstResult instanceof OkPacket) {
this.aggregateOkPackets(results, cmdOpt, resolve);
} else {
this.aggregateResultSets(results, cmdOpt, resolve);
}
}
/**
* Aggregate OK packets from multiple executions
*
* @param {Array} results - Array of individual results
* @param {Object} cmdOpt - Command options
* @param {Function} resolve - Promise resolve function
* @private
*/
aggregateOkPackets(results, cmdOpt, resolve) {
// Get first packet's insertId and last packet's warning status
const insertId = results[0].insertId;
const warningStatus = results[results.length - 1].warningStatus;
let affectedRows = 0;
if (cmdOpt.metaAsArray) {
// Use reduce for better performance with large result sets
affectedRows = results.reduce((sum, result) => sum + result[0].affectedRows, 0);
resolve([new OkPacket(affectedRows, insertId, warningStatus), []]);
} else {
affectedRows = results.reduce((sum, result) => sum + result.affectedRows, 0);
resolve(new OkPacket(affectedRows, insertId, warningStatus));
}
}
/**
* Aggregate result sets from multiple executions
*
* @param {Array} results - Array of individual results
* @param {Object} cmdOpt - Command options
* @param {Function} resolve - Promise resolve function
* @private
*/
aggregateResultSets(results, cmdOpt, resolve) {
if (cmdOpt.metaAsArray) {
// Calculate total length to avoid resizing
const totalLength = results.reduce((sum, row) => sum + (row[0]?.length || 0), 0);
const rs = new Array(totalLength);
// Efficiently copy all results into a single array
let index = 0;
for (const row of results) {
if (row[0] && row[0].length) {
const rowData = row[0];
for (let i = 0; i < rowData.length; i++) {
rs[index++] = rowData[i];
}
}
}
resolve([rs.slice(0, index), results[0][1]]);
} else {
// Calculate total length to avoid resizing
const totalLength = results.reduce((sum, row) => sum + (Array.isArray(row) ? row.length : 0), 0);
const rs = new Array(totalLength);
// Efficiently copy all results into a single array
let index = 0;
for (const row of results) {
if (Array.isArray(row) && row.length) {
for (let i = 0; i < row.length; i++) {
rs[index++] = row[i];
}
}
}
// Create final result array and add metadata
const finalResult = rs.slice(0, index);
// Add metadata as non-enumerable property
if (results[0] && results[0].meta) {
Object.defineProperty(finalResult, 'meta', {
value: results[0].meta,
writable: true,
enumerable: cmdOpt.metaEnumerable
});
}
resolve(finalResult);
}
}
/**
* Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
* @param {Object} cmdParam - command context
* @param {Function} resolve - success function
* @param {Function} reject - rejection function
*/
ping(cmdParam, resolve, reject) {
// Handle custom timeout if provided
if (cmdParam.opts && cmdParam.opts.timeout !== undefined) {
// Validate timeout value
if (cmdParam.opts.timeout < 0) {
const err = Errors.createError(
'Ping cannot have negative timeout value',
Errors.ER_BAD_PARAMETER_VALUE,
this.info,
'0A000'
);
this._logAndReject(reject, err);
return;
}
let timeoutRef = setTimeout(() => {
// If timeout occurs, mark variable as cleared to avoid double resolving
timeoutRef = undefined;
// Create error with proper details
const err = Errors.createFatalError('Ping timeout', Errors.ER_PING_TIMEOUT, this.info, '0A000');
// Close connection properly
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();
this._logAndReject(reject, err);
}, cmdParam.opts.timeout);
// Create ping command with wrapped callbacks to handle timeout
this.addCommand(
new Ping(
cmdParam,
() => {
// Successful ping response - clear timeout if it hasn't fired yet
if (timeoutRef) {
clearTimeout(timeoutRef);
resolve();
}
},
(err) => {
// Error during ping - clear timeout if it hasn't fired yet
if (timeoutRef) {
clearTimeout(timeoutRef);
this._logAndReject(reject, err);
}
}
),
true
);
return;
}
// Simple ping without custom timeout
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;
this._logAndReject(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
//*****************************************************************
/**
* Determine if the bulk protocol can be used for batch operations
*
* @param {Array} values - Batch values array
* @param {Object} options - Batch options
* @return {boolean} Whether bulk protocol can be used
* @private
*/
_canUseBulk(values, options) {
// 1. Check compatibility with fullResult option
if (options && options.fullResult && (this.info.clientCapabilities & Capabilities.BULK_UNIT_RESULTS) === 0n) {
return false;
}
// 2. Determine if bulk operations are enabled
const bulkEnable =
options === undefined || options === null
? this.opts.bulk
: options.bulk !== undefined && options.bulk !== null
? options.bulk
: this.opts.bulk;
// 3. Check if server supports bulk operations
const serverSupportsBulk =
this.info.serverVersion &&
this.info.serverVersion.mariaDb &&
this.info.hasMinVersion(10, 2, 7) &&
(this.info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0n;
// If server doesn't support bulk or it's disabled, return false
if (!serverSupportsBulk || !bulkEnable) {
return false;
}
// 4. No need to validate values if none provided
if (values === undefined) {
return true;
}
// 5. Validate values based on placeholder type
if (!this.opts.namedPlaceholders) {
// For positional parameters
return this._validatePositionalParameters(values);
} else {
// For named parameters
return this._validateNamedParameters(values);
}
}
/**
* Validate batch values for positional parameters
*
* @param {Array} values - Batch values array
* @return {boolean} Whether values are valid for bulk protocol
* @private
*/
_validatePositionalParameters(values) {
// Determine expected parameter length
const paramLen = Array.isArray(values[0]) ? values[0].length : values[0] ? 1 : 0;
// If no parameters, can't use bulk
if (paramLen === 0) {
return false;
}
// Check parameter consistency and streaming
for (const row of values) {
const rowArray = Array.isArray(row) ? row : [row];
// All parameter sets must have same length
if (paramLen !== rowArray.length) {
return false;
}
// Check for streaming data (not permitted)
for (const val of rowArray) {
if (this._isStreamingValue(val)) {
return false;
}
}
}
return true;
}
/**
* Validate batch values for named parameters
*
* @param {Array} values - Batch values array
* @return {boolean} Whether values are valid for bulk protocol
* @private
*/
_validateNamedParameters(values) {
// Check each row for streaming values
for (const row of values) {
for (const val of Object.values(row)) {
if (this._isStreamingValue(val)) {
return false;
}
}
}
return true;
}
/**
* Check if a value is a streaming value
*
* @param {*} val - Value to check
* @return {boolean} Whether value is a streaming value
* @private
*/
_isStreamingValue(val) {
return val != null && typeof val === 'object' && typeof val.pipe === 'function' && typeof val.read === 'function';
}
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.setNoDelay(true);
this.socket.setTimeout(this.opts.socketTimeout, this.socketTimeoutReached.bind(this));
// keep alive for socket. This won't reset server wait_timeout use pool option idleTimeout for that
if (this.opts.keepAliveDelay >= 0) {
this.socket.setKeepAlive(true, this.opts.keepAliveDelay);
} else {
this.socket.setKeepAlive(true);
}
}
}.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 = { socket: this.socket };
if (info.isMariaDB()) {
// for MariaDB servers, permit self-signed certificated
// this will be replaced by fingerprint validation with ending OK_PACKET
baseConf['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() {
clearTimeout(this.timeout);
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();
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;