DiscofyAPI/node_modules/mariadb/lib/connection.js

1436 lines
39 KiB
JavaScript
Raw Normal View History

2021-03-25 17:23:36 +01:00
'use strict';
const EventEmitter = require('events');
const util = require('util');
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 moment = require('moment-timezone');
/*commands*/
const Handshake = require('./cmd/handshake/handshake');
const Quit = require('./cmd/quit');
const Ping = require('./cmd/ping');
const Reset = require('./cmd/reset');
const Query = require('./cmd/query');
const BatchRewrite = require('./cmd/batch-rewrite');
const BatchBulk = require('./cmd/batch-bulk');
const Stream = require('./cmd/stream');
const ChangeUser = require('./cmd/change-user');
const { Status } = require('./const/connection_status');
/**
* New Connection instance.
*
* @param options connection options
* @returns Connection instance
* @constructor
* @fires Connection#connect
* @fires Connection#end
* @fires Connection#error
*
*/
function Connection(options) {
//*****************************************************************
// public API functions
//*****************************************************************
/**
* Connect event
*
* @returns {Promise} promise
*/
this.connect = () => {
switch (_status) {
case Status.NOT_CONNECTED:
_status = Status.CONNECTING;
return new Promise(function (resolve, reject) {
_registerHandshakeCmd(resolve, reject);
});
case Status.CLOSING:
case Status.CLOSED:
return Promise.reject(
Errors.createError(
'Connection closed',
true,
info,
'08S01',
Errors.ER_CONNECTION_ALREADY_CLOSED
)
);
case Status.CONNECTING:
case Status.AUTHENTICATING:
return Promise.reject(
Errors.createError(
'Connection is already connecting',
true,
info,
'08S01',
Errors.ER_ALREADY_CONNECTING
)
);
}
//status Connected
return Promise.resolve(this);
};
/**
* Permit to change user during connection.
* All user variables will be reset, Prepare commands will be released.
* !!! mysql has a bug when CONNECT_ATTRS capability is set, that is default !!!!
*
* @param options connection options
* @returns {Promise} promise
*/
this.changeUser = (options) => {
if (!info.isMariaDB()) {
return Promise.reject(
Errors.createError(
'method changeUser not available for MySQL server due to Bug #83472',
false,
info,
'0A000',
Errors.ER_MYSQL_CHANGE_USER_BUG
)
);
}
return new Promise(function (resolve, reject) {
_addCommand(
new ChangeUser(
options,
(res) => {
if (options && options.collation) opts.collation = options.collation;
resolve(res);
},
_authFailHandler.bind(this, reject),
_addCommand.bind(this)
)
);
});
};
/**
* Start transaction
*
* @returns {Promise} promise
*/
this.beginTransaction = () => {
return this.query('START TRANSACTION');
};
/**
* Commit a transaction.
*
* @returns {Promise} command if commit was needed only
*/
this.commit = () => {
return _changeTransaction('COMMIT');
};
/**
* Roll back a transaction.
*
* @returns {Promise} promise
*/
this.rollback = () => {
return _changeTransaction('ROLLBACK');
};
/**
* Execute query using text protocol.
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @returns {Promise} promise
*/
this._queryPromise = (sql, values) => {
let _cmdOpt,
_sql,
_values = values;
if (typeof sql === 'object') {
_cmdOpt = sql;
_sql = _cmdOpt.sql;
if (_cmdOpt.values) _values = _cmdOpt.values;
} else {
_sql = sql;
}
return new Promise(function (resolve, reject) {
const cmd = new Query(resolve, reject, _cmdOpt, opts, _sql, _values);
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
});
};
/**
* Execute batch using text protocol.
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param initialValues object / array of placeholder values (not mandatory)
* @returns {Promise} promise
*/
this.batch = (sql, initialValues) => {
let _options,
_sql,
_values = initialValues;
if (typeof sql === 'object') {
_options = sql;
_sql = _options.sql;
if (_options.values) _values = _options.values;
} else {
_sql = sql;
}
if (!_values) {
return Promise.reject(
Errors.createError(
'Batch must have values set\nsql: ' + _sql + ' - parameters:[]',
false,
info,
'HY000',
Errors.ER_BATCH_WITH_NO_VALUES
)
);
}
const vals = Array.isArray(_values) ? _values : [_values];
return new Promise(function (resolve, reject) {
let useBulk = canUseBulk(vals);
const cmd = useBulk
? new BatchBulk(resolve, reject, _options, opts, _sql, vals)
: new BatchRewrite(resolve, reject, _options, opts, _sql, vals);
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
});
};
/**
* Execute query returning a Readable Object that will emit columns/data/end/error events
* to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @returns {Readable}
*/
this.queryStream = (sql, values) => {
let _cmdOpt,
_sql,
_values = values;
if (typeof sql === 'object') {
_cmdOpt = sql;
_sql = _cmdOpt.sql;
if (sql.values) _values = sql.values;
} else {
_sql = sql;
}
const cmd = new Stream(_cmdOpt, opts, _sql, _values, _socket);
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
return cmd.inStream;
};
/**
* Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
* @param timeout (optional) timeout value in ms. If reached, throw error and close connection
* @returns {Promise} promise
*/
this.ping = (timeout) => {
return new Promise(function (resolve, reject) {
if (timeout) {
if (timeout < 0) {
reject(
Errors.createError(
'Ping cannot have negative timeout value',
false,
info,
'0A000',
Errors.ER_BAD_PARAMETER_VALUE
)
);
return;
}
const tOut = setTimeout(() => {
reject(Errors.createError('Ping timeout', true, info, '0A000', Errors.ER_PING_TIMEOUT));
// close connection
_addCommand = _addCommandDisabled;
clearTimeout(_timeout);
if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
_sendQueue.clear();
_status = Status.CLOSED;
_socket.destroy();
}
_clear();
}, timeout);
return _addCommand(
new Ping(
() => {
clearTimeout(tOut);
resolve();
},
(err) => {
clearTimeout(tOut);
reject(err);
}
)
);
}
return _addCommand(new Ping(resolve, reject));
});
};
/**
* 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
*
* @returns {Promise} promise
*/
this.reset = () => {
if (
(info.isMariaDB() && info.hasMinVersion(10, 2, 4)) ||
(!info.isMariaDB() && info.hasMinVersion(5, 7, 3))
) {
return new Promise(function (resolve, reject) {
return _addCommand(new Reset(resolve, reject));
});
}
return Promise.reject(
new Error(
'Reset command not permitted for server ' +
this.info.serverVersion +
' (requires server MariaDB version 10.2.4+ or MySQL 5.7.3+)'
)
);
};
/**
* Indicates the state of the connection as the driver knows it
* @returns {boolean}
*/
this.isValid = () => {
return _status === Status.CONNECTED;
};
/**
* Terminate connection gracefully.
*
* @returns {Promise} promise
*/
this.end = () => {
_addCommand = _addCommandDisabled;
clearTimeout(_timeout);
if (
_status !== Status.CLOSING &&
_status !== Status.CLOSED &&
_status !== Status.NOT_CONNECTED
) {
_status = Status.CLOSING;
return new Promise(function (resolve, reject) {
const ended = () => {
_status = Status.CLOSED;
_socket.destroy();
_socket.unref();
_clear();
_receiveQueue.clear();
resolve();
};
const quitCmd = new Quit(ended, ended);
_sendQueue.push(quitCmd);
_receiveQueue.push(quitCmd);
if (_sendQueue.length === 1) {
process.nextTick(_nextSendCmd.bind(this));
}
});
}
return Promise.resolve();
};
/**
* Alias for destroy.
*/
this.close = function () {
this.destroy();
};
/**
* Force connection termination by closing the underlying socket and killing server process if any.
*/
this.destroy = () => {
_addCommand = _addCommandDisabled;
clearTimeout(_timeout);
if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
_status = Status.CLOSING;
_sendQueue.clear();
if (_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;
const killCon = new Connection(opts);
killCon
.connect()
.then(() => {
//*************************************************
//kill connection
//*************************************************
const killResHandler = () => {
const destroyError = Errors.createError(
'Connection destroyed, command was killed',
true,
info,
'08S01',
Errors.ER_CMD_NOT_EXECUTED_DESTROYED
);
socketErrorDispatchToQueries(destroyError);
process.nextTick(() => {
if (_socket) _socket.destroy();
});
_status = Status.CLOSED;
killCon.end().catch(() => {});
};
killCon
.query('KILL ' + info.threadId)
.then(killResHandler)
.catch(killResHandler);
})
.catch((err) => {
//*************************************************
//failing to create a kill connection, end normally
//*************************************************
const ended = () => {
let sock = _socket;
_clear();
_status = Status.CLOSED;
setImmediate(resolve);
sock.destroy();
_receiveQueue.clear();
};
const quitCmd = new Quit(ended, ended);
_sendQueue.push(quitCmd);
_receiveQueue.push(quitCmd);
if (_sendQueue.length === 1) {
process.nextTick(_nextSendCmd.bind(self));
}
});
} else {
_status = Status.CLOSED;
_socket.destroy();
}
}
_clear();
};
this.pause = () => {
_socket.pause();
};
this.resume = () => {
_socket.resume();
};
this.format = (sql, values) => {
throw Errors.createError(
'"Connection.format intentionally not implemented. please use Connection.query(sql, values), it will be more secure and faster',
false,
info,
'0A000',
Errors.ER_NOT_IMPLEMENTED_FORMAT
);
};
//*****************************************************************
// additional public methods
//*****************************************************************
/**
* return current connected server version information.
*
* @returns {*}
*/
this.serverVersion = () => {
if (!info.serverVersion)
throw new Error('cannot know if server information until connection is established');
return info.serverVersion.raw;
};
/**
* Change option "debug" during connection.
* @param val debug value
*/
this.debug = (val) => {
opts.debug = val;
opts.emit('debug', opts.logPackets, opts.debug);
};
this.debugCompress = (val) => {
opts.debugCompress = val;
};
//*****************************************************************
// internal public testing methods
//*****************************************************************
function TestMethods() {}
TestMethods.prototype.getCollation = () => {
return opts.collation;
};
TestMethods.prototype.getSocket = () => {
return _socket;
};
this.__tests = new TestMethods();
//*****************************************************************
// internal methods
//*****************************************************************
this._status = () => {
return _status;
};
/**
* Execute query using text protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @param cb callback
* @returns {Query} query
*/
this._queryCallback = (sql, values, cb) => {
let _cmdOpts,
_sql,
_values = values,
_cb = cb;
if (typeof values === 'function') {
_cb = values;
_values = undefined;
}
if (typeof sql === 'object') {
_cmdOpts = sql;
_sql = _cmdOpts.sql;
if (sql.values) _values = sql.values;
} else {
_sql = sql;
}
let cmd;
if (_cb) {
const resolve = (rows) => {
const meta = rows.meta;
delete rows.meta;
_cb(null, rows, meta);
};
cmd = new Query(resolve, _cb, _cmdOpts, opts, _sql, _values);
} else {
cmd = new Query(
() => {},
() => {},
_cmdOpts,
opts,
_sql,
_values
);
}
cmd.handleNewRows = (row) => {
cmd._rows[cmd._responseIndex].push(row);
cmd.emit('data', row);
};
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
return cmd;
};
/**
* Execute a batch using text protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @param cb callback
* @returns {Query} query
*/
this._batchCallback = (sql, values, cb) => {
let _cmdOpts,
_sql,
_values = values,
_cb = cb;
if (typeof values === 'function') {
_cb = values;
_values = undefined;
}
if (typeof sql === 'object') {
_cmdOpts = sql;
_sql = _cmdOpts.sql;
if (sql.values) _values = sql.values;
} else {
_sql = sql;
}
if (_values !== undefined) {
_values = Array.isArray(_values) ? _values : [_values];
}
let cmd;
if (!_values) {
if (_cb) {
_cb(
Errors.createError(
'Batch must have values set\nsql: ' + _sql + ' - parameters:[]',
false,
info,
'HY000',
Errors.ER_BATCH_WITH_NO_VALUES
)
);
}
return null;
}
let useBulk = canUseBulk(_values);
const fct = useBulk ? BatchBulk : BatchRewrite;
if (_cb) {
const resolve = (rows) => {
const meta = rows.meta;
delete rows.meta;
_cb(null, rows, meta);
};
cmd = new fct(resolve, _cb, _cmdOpts, opts, _sql, _values);
} else {
cmd = new fct(
() => {},
() => {},
_cmdOpts,
opts,
_sql,
_values
);
}
cmd.handleNewRows = (row) => {
cmd._rows[cmd._responseIndex].push(row);
cmd.emit('data', row);
};
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
return cmd;
};
/**
* Use Batch rewrite or MariaDB bulk protocol.
*
* @param values current batch values
* @return {boolean} indicating if must use rewrite or bulk
*/
const canUseBulk = (values) => {
// not using info.isMariaDB() directly in case of callback use,
// without connection beeing completly finished.
let useBulk =
info.serverVersion &&
info.serverVersion.mariaDb &&
info.hasMinVersion(10, 2, 7) &&
opts.bulk &&
(info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > BigInt(0);
if (useBulk) {
//ensure that there is no stream object
if (values !== undefined) {
if (!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;
}
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 useBulk;
};
/**
* Add handshake command to queue.
*
* @private
*/
const _registerHandshakeCmd = (resolve, rejected) => {
const _authFail = _authFailHandler.bind(this, rejected);
const _authSucceed = _authSucceedHandler.bind(this, resolve, _authFail);
const handshake = new Handshake(
_authSucceed,
_authFail,
_createSecureContext.bind(this, _authFail),
_addCommandEnable.bind(this),
_getSocket
);
Error.captureStackTrace(handshake);
handshake.once('end', () => {
process.nextTick(_nextSendCmd);
});
_receiveQueue.push(handshake);
_initSocket(_authFail);
};
const _executeSessionVariableQuery = () => {
if (opts.sessionVariables) {
const values = [];
let sessionQuery = 'set ';
let keys = Object.keys(opts.sessionVariables);
if (keys.length > 0) {
return new Promise(function (resolve, reject) {
for (let k = 0; k < keys.length; ++k) {
sessionQuery +=
(k !== 0 ? ',' : '') + '@@' + keys[k].replace(/[^a-z0-9_]/gi, '') + '=?';
values.push(opts.sessionVariables[keys[k]]);
}
const errorHandling = (initialErr) => {
reject(
Errors.createError(
'Error setting session variable (value ' +
JSON.stringify(opts.sessionVariables) +
'). Error: ' +
initialErr.message,
true,
info,
'08S01',
Errors.ER_SETTING_SESSION_ERROR,
null
)
);
};
const cmd = new Query(resolve, errorHandling, null, opts, sessionQuery, values);
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
});
}
}
return Promise.resolve();
};
/**
* Asking server timezone if not set in case of 'auto'
* @returns {Promise<void>}
* @private
*/
const _checkServerTimezone = () => {
if (opts.timezone === 'auto') {
return this._queryPromise('SELECT @@system_time_zone stz, @@time_zone tz').then((res) => {
const serverTimezone = res[0].tz === 'SYSTEM' ? res[0].stz : res[0].tz;
const serverZone = moment.tz.zone(serverTimezone);
if (serverZone) {
const localTz = moment.tz.guess();
if (serverTimezone === localTz) {
//db server and client use same timezone, avoid any conversion
opts.tz = null;
} else {
opts.localTz = localTz;
opts.tz = serverTimezone;
}
} else {
return Promise.reject(
Errors.createError(
"Automatic timezone setting fails. Server timezone '" +
serverTimezone +
"' does't have a corresponding IANA timezone. Option timezone must be set according to server timezone",
true,
info,
'08S01',
Errors.ER_WRONG_AUTO_TIMEZONE
)
);
}
return Promise.resolve();
});
}
if (opts.tz && !opts.skipSetTimezone) {
let tz = opts.tz;
if (opts.tz === 'Etc/UTC') {
tz = '+00:00';
} else if (opts.tz.startsWith('Etc/GMT')) {
let zone = moment.tz.zone(opts.tz);
tz = zone.abbrs[0] + ':00';
}
return this._queryPromise('SET time_zone=?', tz)
.then((res) => {
return Promise.resolve();
})
.catch((err) => {
console.log(
`warning: setting timezone '${opts.tz}' fails on server.\n look at https://mariadb.com/kb/en/mysql_tzinfo_to_sql/ to load IANA timezone.\nSetting timezone can be disabled with option \`skipSetTimezone\``
);
return Promise.resolve();
});
}
return Promise.resolve();
};
const _checkServerVersion = () => {
if (!opts.forceVersionCheck) {
return Promise.resolve();
}
return this._queryPromise('SELECT @@VERSION AS v').then((res) => {
info.serverVersion.raw = res[0].v;
info.serverVersion.mariaDb = info.serverVersion.raw.includes('MariaDB');
ConnectionInformation.parseVersionString(info);
return Promise.resolve();
});
};
const _executeInitQuery = () => {
if (opts.initSql) {
const initialArr = Array.isArray(opts.initSql) ? opts.initSql : [opts.initSql];
const initialPromises = [];
initialArr.forEach((sql) => {
initialPromises.push(this._queryPromise(sql));
});
return Promise.all(initialPromises).catch((initialErr) => {
return Promise.reject(
Errors.createError(
'Error executing initial sql command: ' + initialErr.message,
true,
info,
'08S01',
Errors.ER_INITIAL_SQL_ERROR,
null
)
);
});
}
return Promise.resolve();
};
const _executeSessionTimeout = () => {
if (opts.queryTimeout) {
if (info.isMariaDB() && info.hasMinVersion(10, 1, 2)) {
this._queryPromise('SET max_statement_time=' + opts.queryTimeout / 1000).catch(
(initialErr) => {
return Promise.reject(
Errors.createError(
'Error setting session queryTimeout: ' + initialErr.message,
true,
info,
'08S01',
Errors.ER_INITIAL_TIMEOUT_ERROR,
null
)
);
}
);
} else {
return Promise.reject(
Errors.createError(
'Can only use queryTimeout for MariaDB server after 10.1.1. queryTimeout value: ' +
opts.queryTimeout,
false,
info,
'HY000',
Errors.ER_TIMEOUT_NOT_SUPPORTED
)
);
}
}
return Promise.resolve();
};
const _getSocket = () => {
return _socket;
};
/**
* Initialize socket and associate events.
* @private
*/
const _initSocket = (authFailHandler) => {
if (opts.socketPath) {
_socket = Net.connect(opts.socketPath);
} else {
_socket = Net.connect(opts.port, opts.host);
}
if (opts.connectTimeout) {
_timeout = setTimeout(
_connectTimeoutReached,
opts.connectTimeout,
authFailHandler,
Date.now()
);
}
const _socketError = _socketErrorHandler.bind(this, authFailHandler);
_socket.on('data', _in.onData.bind(_in));
_socket.on('error', _socketError);
_socket.on('end', _socketError);
_socket.on(
'connect',
function () {
clearTimeout(_timeout);
if (_status === Status.CONNECTING) {
_status = Status.AUTHENTICATING;
_socketConnected = true;
_socket.setTimeout(opts.socketTimeout, _socketTimeoutReached.bind(this, authFailHandler));
_socket.setNoDelay(true);
// keep alive for socket. This won't reset server wait_timeout use pool option idleTimeout for that
if (opts.keepAliveDelay) {
_socket.setKeepAlive(true, opts.keepAliveDelay);
}
}
}.bind(this)
);
_socket.writeBuf = (buf) => _socket.write(buf);
_socket.flush = () => {};
_out.setStream(_socket);
};
/**
* Authentication success result handler.
*
* @private
*/
const _authSucceedHandler = (resolve, rejected) => {
//enable packet compression according to option
if (opts.logPackets) info.enableLogPacket();
if (opts.compress) {
if (info.serverCapabilities & Capabilities.COMPRESS) {
_out.setStream(new CompressionOutputStream(_socket, opts, info));
_in = new CompressionInputStream(_in, _receiveQueue, opts, info);
_socket.removeAllListeners('data');
_socket.on('data', _in.onData.bind(_in));
} else {
console.error(
"connection is configured to use packet compression, but the server doesn't have this capability"
);
}
}
_addCommand = opts.pipelining ? _addCommandEnablePipeline : _addCommandEnable;
const commands = _waitingAuthenticationQueue.toArray();
commands.forEach((cmd) => {
_addCommand(cmd);
});
const errorInitialQueries = (err) => {
if (!err.fatal) this.end().catch((err) => {});
process.nextTick(rejected, err);
};
_status = Status.INIT_CMD;
_executeSessionVariableQuery()
.then(() => {
return _checkServerTimezone();
})
.then(() => {
return _checkServerVersion();
})
.then(() => {
return _executeInitQuery();
})
.then(() => {
return _executeSessionTimeout();
})
.then(() => {
_status = Status.CONNECTED;
process.nextTick(resolve, this);
})
.catch(errorInitialQueries);
};
/**
* Authentication failed result handler.
*
* @private
*/
const _authFailHandler = (reject, err) => {
process.nextTick(reject, err);
//remove handshake command
_receiveQueue.shift();
_fatalError(err, true);
};
/**
* Create TLS socket and associate events.
*
* @param rejected rejected function when error
* @param callback callback function when done
* @private
*/
const _createSecureContext = (rejected, callback) => {
const _socketError = _socketErrorHandler.bind(this, rejected);
const sslOption = Object.assign({}, opts.ssl, {
servername: opts.host,
socket: _socket
});
try {
const secureSocket = tls.connect(sslOption, callback);
secureSocket.on('data', _in.onData.bind(_in));
secureSocket.on('error', _socketError);
secureSocket.on('end', _socketError);
secureSocket.writeBuf = (buf) => secureSocket.write(buf);
secureSocket.flush = () => {};
_socket.removeAllListeners('data');
_socket = secureSocket;
_out.setStream(secureSocket);
} catch (err) {
_socketError(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
*/
const _unexpectedPacket = function (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(info);
if (err.fatal && _status !== Status.CLOSING && _status !== Status.CLOSED) {
this.emit('error', err);
this.end();
}
} else if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
this.emit(
'error',
Errors.createError(
'receiving packet from server without active commands\n' +
'conn:' +
(info.threadId ? info.threadId : -1) +
'(' +
packet.pos +
',' +
packet.end +
')\n' +
Utils.log(opts, packet.buf, packet.pos, packet.end),
true,
info,
'08S01',
Errors.ER_UNEXPECTED_PACKET
)
);
this.destroy();
}
};
/**
* Change transaction state.
*
* @param sql sql
* @returns {Promise} promise
* @private
*/
const _changeTransaction = (sql) => {
//if command in progress, driver cannot rely on status and must execute query
if (_status === Status.CLOSING || _status === Status.CLOSED) {
return Promise.reject(
Errors.createError(
'Cannot execute new commands: connection closed\nsql: ' + sql,
true,
info,
'08S01',
Errors.ER_CMD_CONNECTION_CLOSED
)
);
}
//Command in progress => must execute query
//or if no command in progress, can rely on status to know if query is needed
if (_receiveQueue.peekFront() || info.status & ServerStatus.STATUS_IN_TRANS) {
return new Promise(function (resolve, reject) {
const cmd = new Query(resolve, reject, null, opts, sql, null);
if (opts.trace) Error.captureStackTrace(cmd);
_addCommand(cmd);
});
}
return Promise.resolve();
};
/**
* Handle connection timeout.
*
* @private
*/
const _connectTimeoutReached = function (authFailHandler, initialConnectionTime) {
_timeout = null;
const handshake = _receiveQueue.peekFront();
authFailHandler(
Errors.createError(
'Connection timeout: failed to create socket after ' +
(Date.now() - initialConnectionTime) +
'ms',
true,
info,
'08S01',
Errors.ER_CONNECTION_TIMEOUT,
handshake ? handshake.stack : null
)
);
};
/**
* Handle socket timeout.
*
* @private
*/
const _socketTimeoutReached = function () {
const err = Errors.createError('socket timeout', true, info, '08S01', Errors.ER_SOCKET_TIMEOUT);
const packetMsgs = info.getLastPackets();
if (packetMsgs !== '') {
err.message = err.message + '\nlast received packets:\n' + packetMsgs;
}
_fatalError(err, true);
};
/**
* Add command to waiting queue until authentication.
*
* @param cmd command
* @returns {*} current command
* @private
*/
const _addCommandQueue = (cmd) => {
_waitingAuthenticationQueue.push(cmd);
return cmd;
};
/**
* Add command to command sending and receiving queue.
*
* @param cmd command
* @returns {*} current command
* @private
*/
const _addCommandEnable = (cmd) => {
cmd.once('end', () => {
setImmediate(_nextSendCmd);
});
//send immediately only if no current active receiver
if (_sendQueue.isEmpty() && (_status === Status.INIT_CMD || _status === Status.CONNECTED)) {
if (_receiveQueue.peekFront()) {
_receiveQueue.push(cmd);
_sendQueue.push(cmd);
return cmd;
}
_receiveQueue.push(cmd);
cmd.start(_out, opts, info);
} else {
_receiveQueue.push(cmd);
_sendQueue.push(cmd);
}
return cmd;
};
/**
* Add command to command sending and receiving queue using pipelining
*
* @param cmd command
* @returns {*} current command
* @private
*/
const _addCommandEnablePipeline = (cmd) => {
cmd.once('send_end', () => {
setImmediate(_nextSendCmd);
});
_receiveQueue.push(cmd);
if (_sendQueue.isEmpty()) {
cmd.start(_out, opts, info);
if (cmd.sending) {
_sendQueue.push(cmd);
cmd.prependOnceListener('send_end', () => {
_sendQueue.shift();
});
}
} else {
_sendQueue.push(cmd);
}
return cmd;
};
/**
* Replacing command when connection is closing or closed to send a proper error message.
*
* @param cmd command
* @private
*/
const _addCommandDisabled = (cmd) => {
cmd.throwNewError(
'Cannot execute new commands: connection closed\n' + cmd.displaySql(),
true,
info,
'08S01',
Errors.ER_CMD_CONNECTION_CLOSED
);
};
/**
* Handle socket error.
*
* @param authFailHandler authentication handler
* @param err socket error
* @private
*/
const _socketErrorHandler = function (authFailHandler, err) {
if (_status === Status.CLOSING || _status === Status.CLOSED) return;
if (_socket) {
_socket.writeBuf = () => {};
_socket.flush = () => {};
}
//socket has been ended without error
if (!err) {
err = Errors.createError(
'socket has unexpectedly been closed',
true,
info,
'08S01',
Errors.ER_SOCKET_UNEXPECTED_CLOSE
);
} else {
err.fatal = true;
this.sqlState = 'HY000';
}
const packetMsgs = info.getLastPackets();
if (packetMsgs !== '') {
err.message += '\nlast received packets:\n' + packetMsgs;
}
switch (_status) {
case Status.CONNECTING:
case Status.AUTHENTICATING:
const currentCmd = _receiveQueue.peekFront();
if (currentCmd && currentCmd.stack && err) {
err.stack +=
'\n From event:\n' + currentCmd.stack.substring(currentCmd.stack.indexOf('\n') + 1);
}
authFailHandler(err);
break;
default:
_fatalError(err, false);
}
};
/**
* Fatal unexpected error : closing connection, and throw exception.
*
* @param self current connection
* @private
*/
const _fatalErrorHandler = function (self) {
return function (err, avoidThrowError) {
if (_status === Status.CLOSING || _status === Status.CLOSED) {
socketErrorDispatchToQueries(err);
return;
}
const mustThrowError = _status !== Status.CONNECTING;
_status = Status.CLOSING;
//prevent executing new commands
_addCommand = _addCommandDisabled;
if (_socket) {
_socket.removeAllListeners('error');
_socket.removeAllListeners('timeout');
_socket.removeAllListeners('close');
_socket.removeAllListeners('data');
if (!_socket.destroyed) _socket.destroy();
_socket = undefined;
}
_status = Status.CLOSED;
const errorThrownByCmd = socketErrorDispatchToQueries(err);
if (mustThrowError) {
if (self.listenerCount('error') > 0) {
self.emit('error', err);
self.emit('end');
_clear();
} else {
self.emit('end');
_clear();
//error will be thrown if no error listener and no command did throw the exception
if (!avoidThrowError && !errorThrownByCmd) throw err;
}
} else {
_clear();
}
};
};
/**
* Dispatch fatal error to current running queries.
*
* @param err the fatal error
* @return {boolean} return if error has been relayed to queries
*/
const socketErrorDispatchToQueries = (err) => {
let receiveCmd;
let errorThrownByCmd = false;
while ((receiveCmd = _receiveQueue.shift())) {
if (receiveCmd && receiveCmd.onPacketReceive) {
errorThrownByCmd = true;
setImmediate(receiveCmd.throwError.bind(receiveCmd), err, info);
}
}
return errorThrownByCmd;
};
/**
* Will send next command in queue if any.
*
* @private
*/
const _nextSendCmd = () => {
let sendCmd;
if ((sendCmd = _sendQueue.shift())) {
if (sendCmd.sending) {
_sendQueue.unshift(sendCmd);
} else {
sendCmd.start(_out, opts, info);
if (sendCmd.sending) {
sendCmd.prependOnceListener('send_end', () => {
_sendQueue.shift();
});
_sendQueue.unshift(sendCmd);
}
}
}
};
/**
* Clearing connection variables when ending.
*
* @private
*/
const _clear = () => {
_sendQueue.clear();
opts.removeAllListeners();
_out = undefined;
_socket = undefined;
};
//*****************************************************************
// internal variables
//*****************************************************************
EventEmitter.call(this);
const opts = Object.assign(new EventEmitter(), options);
const info = new ConnectionInformation();
const _sendQueue = new Queue();
const _receiveQueue = new Queue();
const _waitingAuthenticationQueue = new Queue();
let _status = Status.NOT_CONNECTED;
let _socketConnected = false;
let _socket = null;
let _timeout = null;
let _addCommand = _addCommandQueue;
const _fatalError = _fatalErrorHandler(this);
let _out = new PacketOutputStream(opts, info);
let _in = new PacketInputStream(_unexpectedPacket.bind(this), _receiveQueue, _out, opts, info);
this.query = this._queryPromise;
this.escape = Utils.escape.bind(this, opts, info);
this.escapeId = Utils.escapeId.bind(this, opts, info);
//add alias threadId for mysql/mysql2 compatibility
Object.defineProperty(this, 'threadId', {
get() {
return info ? info.threadId : undefined;
}
});
Object.defineProperty(this, 'info', {
get() {
return info;
}
});
}
util.inherits(Connection, EventEmitter);
module.exports = Connection;