@tachybase/module-multi-app 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/externalVersion.js +5 -5
  2. package/dist/node_modules/mariadb/callback.js +43 -8
  3. package/dist/node_modules/mariadb/check-node.js +30 -0
  4. package/dist/node_modules/mariadb/lib/cluster-callback.js +84 -0
  5. package/dist/node_modules/mariadb/lib/cluster.js +446 -0
  6. package/dist/node_modules/mariadb/lib/cmd/batch-bulk.js +576 -177
  7. package/dist/node_modules/mariadb/lib/cmd/change-user.js +54 -44
  8. package/dist/node_modules/mariadb/lib/cmd/class/ok-packet.js +3 -2
  9. package/dist/node_modules/mariadb/lib/cmd/class/prepare-cache-wrapper.js +46 -0
  10. package/dist/node_modules/mariadb/lib/cmd/class/prepare-result-packet.js +141 -0
  11. package/dist/node_modules/mariadb/lib/cmd/class/prepare-wrapper.js +70 -0
  12. package/dist/node_modules/mariadb/lib/cmd/close-prepare.js +38 -0
  13. package/dist/node_modules/mariadb/lib/cmd/column-definition.js +145 -47
  14. package/dist/node_modules/mariadb/lib/cmd/command.js +41 -75
  15. package/dist/node_modules/mariadb/lib/cmd/decoder/binary-decoder.js +282 -0
  16. package/dist/node_modules/mariadb/lib/cmd/decoder/text-decoder.js +210 -0
  17. package/dist/node_modules/mariadb/lib/cmd/{common-binary-cmd.js → encoder/binary-encoder.js} +34 -77
  18. package/dist/node_modules/mariadb/lib/cmd/encoder/text-encoder.js +311 -0
  19. package/dist/node_modules/mariadb/lib/cmd/execute-stream.js +61 -0
  20. package/dist/node_modules/mariadb/lib/cmd/execute.js +338 -0
  21. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/caching-sha2-password-auth.js +25 -62
  22. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/clear-password-auth.js +39 -6
  23. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/ed25519-password-auth.js +48 -16
  24. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/handshake.js +198 -0
  25. package/dist/node_modules/mariadb/lib/cmd/handshake/{initial-handshake.js → auth/initial-handshake.js} +10 -8
  26. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/native-password-auth.js +22 -9
  27. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/pam-password-auth.js +9 -4
  28. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/parsec-auth.js +115 -0
  29. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/plugin-auth.js +12 -5
  30. package/dist/node_modules/mariadb/lib/cmd/handshake/auth/sha256-password-auth.js +44 -33
  31. package/dist/node_modules/mariadb/lib/cmd/handshake/authentication.js +335 -0
  32. package/dist/node_modules/mariadb/lib/cmd/handshake/client-capabilities.js +20 -19
  33. package/dist/node_modules/mariadb/lib/cmd/handshake/ssl-request.js +6 -3
  34. package/dist/node_modules/mariadb/lib/cmd/parser.js +861 -0
  35. package/dist/node_modules/mariadb/lib/cmd/ping.js +17 -18
  36. package/dist/node_modules/mariadb/lib/cmd/prepare.js +170 -0
  37. package/dist/node_modules/mariadb/lib/cmd/query.js +281 -144
  38. package/dist/node_modules/mariadb/lib/cmd/quit.js +9 -6
  39. package/dist/node_modules/mariadb/lib/cmd/reset.js +15 -19
  40. package/dist/node_modules/mariadb/lib/cmd/stream.js +21 -6
  41. package/dist/node_modules/mariadb/lib/config/cluster-options.js +23 -0
  42. package/dist/node_modules/mariadb/lib/config/connection-options.js +196 -132
  43. package/dist/node_modules/mariadb/lib/config/pool-options.js +27 -19
  44. package/dist/node_modules/mariadb/lib/connection-callback.js +492 -120
  45. package/dist/node_modules/mariadb/lib/connection-promise.js +372 -0
  46. package/dist/node_modules/mariadb/lib/connection.js +1739 -1016
  47. package/dist/node_modules/mariadb/lib/const/capabilities.js +36 -30
  48. package/dist/node_modules/mariadb/lib/const/collations.js +972 -36
  49. package/dist/node_modules/mariadb/lib/const/connection_status.js +3 -0
  50. package/dist/node_modules/mariadb/lib/const/error-code.js +35 -11
  51. package/dist/node_modules/mariadb/lib/const/field-detail.js +3 -0
  52. package/dist/node_modules/mariadb/lib/const/field-type.js +7 -4
  53. package/dist/node_modules/mariadb/lib/const/server-status.js +4 -1
  54. package/dist/node_modules/mariadb/lib/const/state-change.js +3 -0
  55. package/dist/node_modules/mariadb/lib/filtered-cluster-callback.js +136 -0
  56. package/dist/node_modules/mariadb/lib/filtered-cluster.js +118 -0
  57. package/dist/node_modules/mariadb/lib/io/compression-input-stream.js +14 -13
  58. package/dist/node_modules/mariadb/lib/io/compression-output-stream.js +21 -18
  59. package/dist/node_modules/mariadb/lib/io/packet-input-stream.js +75 -64
  60. package/dist/node_modules/mariadb/lib/io/packet-node-encoded.js +13 -9
  61. package/dist/node_modules/mariadb/lib/io/packet-node-iconv.js +12 -10
  62. package/dist/node_modules/mariadb/lib/io/packet-output-stream.js +402 -134
  63. package/dist/node_modules/mariadb/lib/io/packet.js +287 -202
  64. package/dist/node_modules/mariadb/lib/lru-prepare-cache.js +84 -0
  65. package/dist/node_modules/mariadb/lib/misc/connection-information.js +15 -32
  66. package/dist/node_modules/mariadb/lib/misc/errors.js +68 -25
  67. package/dist/node_modules/mariadb/lib/misc/parse.js +207 -711
  68. package/dist/node_modules/mariadb/lib/misc/utils.js +34 -62
  69. package/dist/node_modules/mariadb/lib/pool-callback.js +213 -174
  70. package/dist/node_modules/mariadb/lib/pool-promise.js +228 -94
  71. package/dist/node_modules/mariadb/lib/pool.js +951 -0
  72. package/dist/node_modules/mariadb/package.json +1 -1
  73. package/dist/node_modules/mariadb/promise.js +1 -34
  74. package/dist/node_modules/mariadb/types/callback.d.ts +207 -0
  75. package/dist/node_modules/mariadb/types/index.d.ts +94 -674
  76. package/dist/node_modules/mariadb/types/share.d.ts +804 -0
  77. package/dist/node_modules/qs/package.json +1 -1
  78. package/dist/server/actions/apps.js +2 -2
  79. package/dist/server/app-lifecycle.d.ts +1 -1
  80. package/dist/server/app-lifecycle.js +4 -4
  81. package/dist/server/models/application.d.ts +1 -1
  82. package/package.json +7 -7
  83. package/dist/node_modules/mariadb/lib/cmd/batch-rewrite.js +0 -372
  84. package/dist/node_modules/mariadb/lib/cmd/common-text-cmd.js +0 -427
  85. package/dist/node_modules/mariadb/lib/cmd/handshake/client-handshake-response.js +0 -126
  86. package/dist/node_modules/mariadb/lib/cmd/handshake/handshake.js +0 -292
  87. package/dist/node_modules/mariadb/lib/cmd/resultset.js +0 -607
  88. package/dist/node_modules/mariadb/lib/config/pool-cluster-options.js +0 -19
  89. package/dist/node_modules/mariadb/lib/filtered-pool-cluster.js +0 -81
  90. package/dist/node_modules/mariadb/lib/io/bulk-packet.js +0 -590
  91. package/dist/node_modules/mariadb/lib/io/rewrite-packet.js +0 -481
  92. package/dist/node_modules/mariadb/lib/pool-base.js +0 -611
  93. package/dist/node_modules/mariadb/lib/pool-cluster-callback.js +0 -66
  94. package/dist/node_modules/mariadb/lib/pool-cluster.js +0 -407
@@ -1,7 +1,9 @@
1
+ // SPDX-License-Identifier: LGPL-2.1-or-later
2
+ // Copyright (c) 2015-2025 MariaDB Corporation Ab
3
+
1
4
  'use strict';
2
5
 
3
6
  const EventEmitter = require('events');
4
- const util = require('util');
5
7
  const Queue = require('denque');
6
8
  const Net = require('net');
7
9
  const PacketInputStream = require('./io/packet-input-stream');
@@ -14,19 +16,55 @@ const tls = require('tls');
14
16
  const Errors = require('./misc/errors');
15
17
  const Utils = require('./misc/utils');
16
18
  const Capabilities = require('./const/capabilities');
17
- const moment = require('moment-timezone');
19
+ const ConnectionOptions = require('./config/connection-options');
18
20
 
19
21
  /*commands*/
20
- const Handshake = require('./cmd/handshake/handshake');
22
+ const Authentication = require('./cmd/handshake/authentication');
21
23
  const Quit = require('./cmd/quit');
22
24
  const Ping = require('./cmd/ping');
23
25
  const Reset = require('./cmd/reset');
24
26
  const Query = require('./cmd/query');
25
- const BatchRewrite = require('./cmd/batch-rewrite');
27
+ const Prepare = require('./cmd/prepare');
28
+ const OkPacket = require('./cmd/class/ok-packet');
29
+ const Execute = require('./cmd/execute');
30
+ const ClosePrepare = require('./cmd/close-prepare');
26
31
  const BatchBulk = require('./cmd/batch-bulk');
27
- const Stream = require('./cmd/stream');
28
32
  const ChangeUser = require('./cmd/change-user');
29
33
  const { Status } = require('./const/connection_status');
34
+ const LruPrepareCache = require('./lru-prepare-cache');
35
+ const fsPromises = require('fs').promises;
36
+ const Parse = require('./misc/parse');
37
+ const Collations = require('./const/collations');
38
+ const ConnOptions = require('./config/connection-options');
39
+
40
+ const convertFixedTime = function (tz, conn) {
41
+ if (tz === 'UTC' || tz === 'Etc/UTC' || tz === 'Z' || tz === 'Etc/GMT') {
42
+ return '+00:00';
43
+ } else if (tz.startsWith('Etc/GMT') || tz.startsWith('GMT')) {
44
+ let tzdiff;
45
+ let negate;
46
+
47
+ // strangely Etc/GMT+8 = GMT-08:00 = offset -8
48
+ if (tz.startsWith('Etc/GMT')) {
49
+ tzdiff = tz.substring(7);
50
+ negate = !tzdiff.startsWith('-');
51
+ } else {
52
+ tzdiff = tz.substring(3);
53
+ negate = tzdiff.startsWith('-');
54
+ }
55
+ let diff = parseInt(tzdiff.substring(1));
56
+ if (isNaN(diff)) {
57
+ throw Errors.createFatalError(
58
+ `Automatic timezone setting fails. wrong Server timezone '${tz}' conversion to +/-HH:00 conversion.`,
59
+ Errors.ER_WRONG_AUTO_TIMEZONE,
60
+ conn.info
61
+ );
62
+ }
63
+ return (negate ? '-' : '+') + (diff >= 10 ? diff : '0' + diff) + ':00';
64
+ }
65
+ return tz;
66
+ };
67
+ const redirectUrlFormat = /(mariadb|mysql):\/\/(([^/@:]+)?(:([^/]+))?@)?(([^/:]+)(:([0-9]+))?)(\/([^?]+)(\?(.*))?)?$/;
30
68
 
31
69
  /**
32
70
  * New Connection instance.
@@ -39,9 +77,44 @@ const { Status } = require('./const/connection_status');
39
77
  * @fires Connection#error
40
78
  *
41
79
  */
42
- function Connection(options) {
80
+ class Connection extends EventEmitter {
81
+ opts;
82
+ sendQueue = new Queue();
83
+ receiveQueue = new Queue();
84
+ waitingAuthenticationQueue = new Queue();
85
+ status = Status.NOT_CONNECTED;
86
+ socket = null;
87
+ timeout = null;
88
+ addCommand;
89
+ streamOut;
90
+ streamIn;
91
+ info;
92
+ prepareCache;
93
+
94
+ constructor(options) {
95
+ super();
96
+
97
+ this.opts = Object.assign(new EventEmitter(), options);
98
+ this.info = new ConnectionInformation(this.opts, this.redirect.bind(this));
99
+ this.prepareCache =
100
+ this.opts.prepareCacheLength > 0 ? new LruPrepareCache(this.info, this.opts.prepareCacheLength) : null;
101
+ this.addCommand = this.addCommandQueue;
102
+ this.streamOut = new PacketOutputStream(this.opts, this.info);
103
+ this.streamIn = new PacketInputStream(
104
+ this.unexpectedPacket.bind(this),
105
+ this.receiveQueue,
106
+ this.streamOut,
107
+ this.opts,
108
+ this.info
109
+ );
110
+
111
+ this.on('close_prepare', this._closePrepare.bind(this));
112
+ this.escape = Utils.escape.bind(this, this.opts, this.info);
113
+ this.escapeId = Utils.escapeId.bind(this, this.opts, this.info);
114
+ }
115
+
43
116
  //*****************************************************************
44
- // public API functions
117
+ // public methods
45
118
  //*****************************************************************
46
119
 
47
120
  /**
@@ -49,259 +122,518 @@ function Connection(options) {
49
122
  *
50
123
  * @returns {Promise} promise
51
124
  */
52
- this.connect = () => {
53
- switch (_status) {
54
- case Status.NOT_CONNECTED:
55
- _status = Status.CONNECTING;
56
- return new Promise(function (resolve, reject) {
57
- _registerHandshakeCmd(resolve, reject);
58
- });
125
+ connect() {
126
+ const conn = this;
127
+ this.status = Status.CONNECTING;
128
+ const authenticationParam = {
129
+ opts: this.opts
130
+ };
59
131
 
60
- case Status.CLOSING:
61
- case Status.CLOSED:
62
- return Promise.reject(
63
- Errors.createError(
64
- 'Connection closed',
65
- null,
66
- true,
67
- info,
68
- '08S01',
69
- Errors.ER_CONNECTION_ALREADY_CLOSED
70
- )
71
- );
132
+ return new Promise(function (resolve, reject) {
133
+ conn.connectRejectFct = reject;
134
+ conn.connectResolveFct = resolve;
135
+
136
+ // Add a handshake to msg queue
137
+ const authentication = new Authentication(
138
+ authenticationParam,
139
+ conn.authSucceedHandler.bind(conn),
140
+ conn.authFailHandler.bind(conn),
141
+ conn.createSecureContext.bind(conn),
142
+ conn.getSocket.bind(conn)
143
+ );
72
144
 
73
- case Status.CONNECTING:
74
- case Status.AUTHENTICATING:
75
- return Promise.reject(
76
- Errors.createError(
77
- 'Connection is already connecting',
78
- null,
79
- true,
80
- info,
81
- '08S01',
82
- Errors.ER_ALREADY_CONNECTING
83
- )
84
- );
85
- }
86
- //status Connected
87
- return Promise.resolve(this);
88
- };
145
+ // Capture stack trace for better error reporting
146
+ Error.captureStackTrace(authentication);
147
+
148
+ authentication.once('end', () => {
149
+ conn.receiveQueue.shift();
150
+
151
+ // conn.info.collation might not be initialized
152
+ // in case of handshake throwing error
153
+ if (!conn.opts.collation && conn.info.collation) {
154
+ conn.opts.emit('collation', conn.info.collation);
155
+ }
156
+
157
+ process.nextTick(conn.nextSendCmd.bind(conn));
158
+ });
159
+
160
+ conn.receiveQueue.push(authentication);
161
+ conn.streamInitSocket.call(conn);
162
+ });
163
+ }
89
164
 
90
165
  /**
91
- * Permit to change user during connection.
92
- * All user variables will be reset, Prepare commands will be released.
93
- * !!! mysql has a bug when CONNECT_ATTRS capability is set, that is default !!!!
166
+ * Execute a prepared statement with the given parameters
94
167
  *
95
- * @param options connection options
96
- * @returns {Promise} promise
168
+ * @param {Object} cmdParam - Command parameters
169
+ * @param {Object} prepare - Prepared statement
170
+ * @param {Function} resolve - Promise resolve function
171
+ * @param {Function} reject - Promise reject function
97
172
  */
98
- this.changeUser = (options) => {
99
- if (!info.isMariaDB()) {
100
- return Promise.reject(
101
- Errors.createError(
102
- 'method changeUser not available for MySQL server due to Bug #83472',
103
- null,
104
- false,
105
- info,
106
- '0A000',
107
- Errors.ER_MYSQL_CHANGE_USER_BUG
108
- )
109
- );
173
+ executePromise(cmdParam, prepare, resolve, reject) {
174
+ const cmd = new Execute(resolve, this._logAndReject.bind(this, reject), this.opts, cmdParam, prepare);
175
+ this.addCommand(cmd, true);
176
+ }
177
+
178
+ /**
179
+ * Execute a batch of the same SQL statement with different parameter sets
180
+ *
181
+ * @param {Object|String} cmdParam - SQL statement or options object
182
+ * @param {function} resolve - promise resolve function
183
+ * @param {function} reject - promise reject function
184
+ */
185
+ batch(cmdParam, resolve, reject) {
186
+ // Validate SQL parameter
187
+ if (!cmdParam.sql) {
188
+ return this.handleMissingSqlError(reject);
110
189
  }
111
190
 
112
- return new Promise(function (resolve, reject) {
113
- _addCommand(
114
- new ChangeUser(
115
- options,
116
- (res) => {
117
- if (options && options.collation) opts.collation = options.collation;
118
- resolve(res);
119
- },
120
- _authFailHandler.bind(this, reject),
121
- _addCommand.bind(this)
122
- )
123
- );
124
- });
125
- };
191
+ // Validate values parameter
192
+ if (!cmdParam.values) {
193
+ return this.handleMissingValuesError(cmdParam, reject);
194
+ }
195
+
196
+ // Execute the batch operation
197
+ this.prepare(
198
+ cmdParam,
199
+ (prepare) => this.executeBatch(cmdParam, prepare, resolve, reject),
200
+ (err) => this._logAndReject(reject, err)
201
+ );
202
+ }
126
203
 
127
204
  /**
128
- * Start transaction
205
+ * Handle missing SQL parameter error
129
206
  *
130
- * @returns {Promise} promise
207
+ * @param {Function} reject - Promise reject function
208
+ * @private
131
209
  */
132
- this.beginTransaction = () => {
133
- return this.query('START TRANSACTION');
134
- };
210
+ handleMissingSqlError(reject) {
211
+ const err = Errors.createError(
212
+ 'sql parameter is mandatory',
213
+ Errors.ER_UNDEFINED_SQL,
214
+ this.info,
215
+ 'HY000',
216
+ null,
217
+ false
218
+ );
219
+
220
+ // Add stack trace for better debugging
221
+ Error.captureStackTrace(err, this.handleMissingSqlError);
222
+ this._logAndReject(reject, err);
223
+ }
135
224
 
136
225
  /**
137
- * Commit a transaction.
226
+ * Handle missing values parameter error
138
227
  *
139
- * @returns {Promise} command if commit was needed only
228
+ * @param {Object} cmdParam - Command parameters
229
+ * @param {Function} reject - Promise reject function
230
+ * @private
140
231
  */
141
- this.commit = () => {
142
- return _changeTransaction('COMMIT');
143
- };
232
+ handleMissingValuesError(cmdParam, reject) {
233
+ const sql = cmdParam.sql;
234
+ // Truncate SQL for debug output if it's too long
235
+ const debugSql = sql.length > this.opts.debugLen ? sql.substring(0, this.opts.debugLen) + '...' : sql;
236
+
237
+ const err = Errors.createError(
238
+ 'Batch must have values set',
239
+ Errors.ER_BATCH_WITH_NO_VALUES,
240
+ this.info,
241
+ 'HY000',
242
+ debugSql,
243
+ false,
244
+ cmdParam.stack
245
+ );
246
+ this._logAndReject(reject, err);
247
+ }
144
248
 
145
249
  /**
146
- * Roll back a transaction.
250
+ * Execute batch operation with prepared statement
147
251
  *
148
- * @returns {Promise} promise
252
+ * @param {Object} cmdParam - Command parameters
253
+ * @param {Object} prepare - Prepared statement
254
+ * @param {Function} resolve - Promise resolve function
255
+ * @param {Function} reject - Promise reject function
256
+ * @private
149
257
  */
150
- this.rollback = () => {
151
- return _changeTransaction('ROLLBACK');
152
- };
258
+ executeBatch(cmdParam, prepare, resolve, reject) {
259
+ const usePlaceHolder = (cmdParam.opts && cmdParam.opts.namedPlaceholders) || this.opts.namedPlaceholders;
260
+ let values = this.formatBatchValues(cmdParam.values, usePlaceHolder, prepare.parameterCount);
261
+ cmdParam.values = values;
262
+
263
+ // Determine if bulk protocol can be used
264
+ const useBulk = this._canUseBulk(values, cmdParam.opts);
265
+
266
+ if (useBulk) {
267
+ this.executeBulkPromise(cmdParam, prepare, this.opts, resolve, reject);
268
+ } else {
269
+ this.executeIndividualBatches(cmdParam, prepare, resolve, reject);
270
+ }
271
+ }
153
272
 
154
273
  /**
155
- * Execute query using text protocol.
274
+ * Execute bulk operation using specialized bulk protocol
156
275
  *
157
- * @param sql sql parameter Object can be used to supersede default option.
158
- * Object must then have sql property.
159
- * @param values object / array of placeholder values (not mandatory)
160
- * @returns {Promise} promise
276
+ * @param {Object} cmdParam - Command parameters
277
+ * @param {Object} prepare - Prepared statement
278
+ * @param {Object} opts - Options
279
+ * @param {Function} resolve - Promise resolve function
280
+ * @param {Function} reject - Promise reject function
281
+ * @private
161
282
  */
162
- this._queryPromise = (sql, values) => {
163
- let _cmdOpt,
164
- _sql,
165
- _values = values;
166
- if (typeof sql === 'object') {
167
- _cmdOpt = sql;
168
- _sql = _cmdOpt.sql;
169
- if (_cmdOpt.values) _values = _cmdOpt.values;
170
- } else {
171
- _sql = sql;
283
+ executeBulkPromise(cmdParam, prepare, opts, resolve, reject) {
284
+ const cmd = new BatchBulk(
285
+ (res) => {
286
+ prepare.close();
287
+ return resolve(res);
288
+ },
289
+ (err) => {
290
+ prepare.close();
291
+ if (opts.logger.error) opts.logger.error(err);
292
+ reject(err);
293
+ },
294
+ opts,
295
+ prepare,
296
+ cmdParam
297
+ );
298
+ this.addCommand(cmd, true);
299
+ }
300
+
301
+ /**
302
+ * Format batch values into the correct structure
303
+ *
304
+ * @param {Array} values - Original values array
305
+ * @param {Boolean} usePlaceHolder - Whether named placeholders are used
306
+ * @param {Number} parameterCount - Number of parameters in prepared statement
307
+ * @returns {Array} Formatted values array
308
+ * @private
309
+ */
310
+ formatBatchValues(values, usePlaceHolder, parameterCount) {
311
+ // If values is not an array, wrap it
312
+ if (!Array.isArray(values)) {
313
+ return [[values]];
172
314
  }
173
315
 
174
- return new Promise(function (resolve, reject) {
175
- const cmd = new Query(resolve, reject, _cmdOpt, opts, _sql, _values);
176
- if (opts.trace) Error.captureStackTrace(cmd);
177
- _addCommand(cmd);
178
- });
179
- };
316
+ // For named placeholders, return as is
317
+ if (usePlaceHolder) {
318
+ return values;
319
+ }
320
+
321
+ // If already in correct format (array of arrays), return as is
322
+ if (Array.isArray(values[0])) {
323
+ return values;
324
+ }
325
+
326
+ // If only one parameter expected, convert flat array to array of single-item arrays
327
+ if (parameterCount === 1) {
328
+ // Pre-allocate result array for better performance
329
+ const result = new Array(values.length);
330
+ for (let i = 0; i < values.length; i++) {
331
+ result[i] = [values[i]];
332
+ }
333
+ return result;
334
+ }
335
+
336
+ // Single set of parameters for multiple placeholders
337
+ return [values];
338
+ }
180
339
 
181
340
  /**
182
- * Execute batch using text protocol.
341
+ * Execute individual batch operations when bulk protocol can't be used
183
342
  *
184
- * @param sql sql parameter Object can be used to supersede default option.
185
- * Object must then have sql property.
186
- * @param initialValues object / array of placeholder values (not mandatory)
187
- * @returns {Promise} promise
343
+ * @param {Object} cmdParam - Command parameters
344
+ * @param {Object} prepare - Prepared statement
345
+ * @param {Function} resolve - Promise resolve function
346
+ * @param {Function} reject - Promise reject function
347
+ * @private
188
348
  */
189
- this.batch = (sql, initialValues) => {
190
- let _options,
191
- _sql,
192
- _values = initialValues;
193
- if (typeof sql === 'object') {
194
- _options = sql;
195
- _sql = _options.sql;
196
- if (_options.values) _values = _options.values;
197
- } else {
198
- _sql = sql;
349
+ executeIndividualBatches(cmdParam, prepare, resolve, reject) {
350
+ const results = [];
351
+ const batchSize = 1000; // Process in chunks to avoid memory issues
352
+ const totalBatches = Math.ceil(cmdParam.values.length / batchSize);
353
+
354
+ // Execute by chunks to avoid excessive memory usage
355
+ this.executeBatchChunk(cmdParam, prepare, 0, batchSize, totalBatches, results, resolve, reject);
356
+ }
357
+
358
+ /**
359
+ * Execute a chunk of the batch operations
360
+ *
361
+ * @param {Object} cmdParam - Command parameters
362
+ * @param {Object} prepare - Prepared statement
363
+ * @param {Number} chunkIndex - Current chunk index
364
+ * @param {Number} batchSize - Size of each batch chunk
365
+ * @param {Number} totalBatches - Total number of chunks
366
+ * @param {Array} results - Accumulated results
367
+ * @param {Function} resolve - Promise resolve function
368
+ * @param {Function} reject - Promise reject function
369
+ * @private
370
+ */
371
+ executeBatchChunk(cmdParam, prepare, chunkIndex, batchSize, totalBatches, results, resolve, reject) {
372
+ const values = cmdParam.values;
373
+ const startIdx = chunkIndex * batchSize;
374
+ const endIdx = Math.min(startIdx + batchSize, values.length);
375
+ const executes = [];
376
+
377
+ // Create execute promises for this chunk
378
+ for (let i = startIdx; i < endIdx; i++) {
379
+ executes.push(prepare.execute(values[i], cmdParam.opts, null, cmdParam.stack));
199
380
  }
200
381
 
201
- if (!_values) {
202
- return Promise.reject(
203
- Errors.createError(
204
- 'Batch must have values set',
205
- _sql,
206
- false,
207
- info,
208
- 'HY000',
209
- Errors.ER_BATCH_WITH_NO_VALUES
210
- )
211
- );
382
+ // Execute all promises in this chunk
383
+ Promise.all(executes)
384
+ .then(
385
+ (chunkResults) => {
386
+ // Add results from this chunk to accumulated results
387
+ results.push(...chunkResults);
388
+
389
+ // If this was the last chunk, process results
390
+ if (chunkIndex === totalBatches - 1) {
391
+ const cmdOpt = Object.assign({}, this.opts, cmdParam.opts);
392
+ this.processBatchResults(results, cmdOpt, cmdParam, resolve);
393
+ prepare.close();
394
+ } else {
395
+ // Process next chunk
396
+ setImmediate(() => {
397
+ this.executeBatchChunk(
398
+ cmdParam,
399
+ prepare,
400
+ chunkIndex + 1,
401
+ batchSize,
402
+ totalBatches,
403
+ results,
404
+ resolve,
405
+ reject
406
+ );
407
+ });
408
+ }
409
+ },
410
+ (err) => {
411
+ prepare.close();
412
+ reject(err);
413
+ }
414
+ )
415
+ .catch((err) => {
416
+ prepare.close();
417
+ reject(err);
418
+ });
419
+ }
420
+
421
+ /**
422
+ * Process batch results from individual executions
423
+ *
424
+ * @param {Array} results - Array of individual results
425
+ * @param {Object} cmdOpt - Command options
426
+ * @param {Object} cmdParam - Command parameters
427
+ * @param {Function} resolve - Promise resolve function
428
+ * @private
429
+ */
430
+ processBatchResults(results, cmdOpt, cmdParam, resolve) {
431
+ // Handle empty results case
432
+ if (!results.length) {
433
+ resolve(cmdOpt.metaAsArray ? [[], []] : []);
434
+ return;
212
435
  }
213
436
 
214
- const vals = Array.isArray(_values) ? _values : [_values];
437
+ // Return full results when requested
438
+ const fullResult = cmdOpt.fullResult === undefined || cmdOpt.fullResult;
439
+ if (fullResult) {
440
+ if (cmdOpt.metaAsArray) {
441
+ const aggregateResults = results.reduce((accumulator, currentValue) => {
442
+ if (Array.isArray(currentValue[0])) {
443
+ accumulator.push(...currentValue[0]);
444
+ } else if (currentValue[0] instanceof OkPacket) {
445
+ accumulator.push(currentValue[0]);
446
+ } else {
447
+ accumulator.push([currentValue[0]]);
448
+ }
449
+ return accumulator;
450
+ }, []);
451
+ const meta = results[0][1];
452
+ resolve([aggregateResults, meta]);
453
+ } else {
454
+ const aggregateResults = results.reduce((accumulator, currentValue) => {
455
+ if (currentValue instanceof OkPacket) {
456
+ accumulator.push(currentValue);
457
+ } else if (!cmdOpt.rowsAsArray && Array.isArray(currentValue[0])) {
458
+ accumulator.push(...currentValue[0]);
459
+ } else {
460
+ accumulator.push(currentValue[0]);
461
+ }
462
+ return accumulator;
463
+ }, []);
464
+ const meta = results[0].meta;
465
+ Object.defineProperty(aggregateResults, 'meta', {
466
+ value: meta,
467
+ writable: true,
468
+ enumerable: cmdOpt.metaEnumerable
469
+ });
470
+ resolve(aggregateResults);
471
+ }
472
+ return;
473
+ }
215
474
 
216
- return new Promise(function (resolve, reject) {
217
- let useBulk = canUseBulk(vals);
475
+ // Get first result to determine result type
476
+ const firstResult = cmdOpt.metaAsArray ? results[0][0] : results[0];
218
477
 
219
- const cmd = useBulk
220
- ? new BatchBulk(resolve, reject, _options, opts, _sql, vals)
221
- : new BatchRewrite(resolve, reject, _options, opts, _sql, vals);
222
- if (opts.trace) Error.captureStackTrace(cmd);
223
- _addCommand(cmd);
224
- });
225
- };
478
+ // Process based on result type
479
+ if (firstResult instanceof OkPacket) {
480
+ this.aggregateOkPackets(results, cmdOpt, resolve);
481
+ } else {
482
+ this.aggregateResultSets(results, cmdOpt, resolve);
483
+ }
484
+ }
226
485
 
227
486
  /**
228
- * Execute query returning a Readable Object that will emit columns/data/end/error events
229
- * to permit streaming big result-set
487
+ * Aggregate OK packets from multiple executions
230
488
  *
231
- * @param sql sql parameter Object can be used to supersede default option.
232
- * Object must then have sql property.
233
- * @param values object / array of placeholder values (not mandatory)
234
- * @returns {Readable}
489
+ * @param {Array} results - Array of individual results
490
+ * @param {Object} cmdOpt - Command options
491
+ * @param {Function} resolve - Promise resolve function
492
+ * @private
235
493
  */
236
- this.queryStream = (sql, values) => {
237
- let _cmdOpt,
238
- _sql,
239
- _values = values;
240
- if (typeof sql === 'object') {
241
- _cmdOpt = sql;
242
- _sql = _cmdOpt.sql;
243
- if (sql.values) _values = sql.values;
494
+ aggregateOkPackets(results, cmdOpt, resolve) {
495
+ // Get first packet's insertId and last packet's warning status
496
+ const insertId = results[0].insertId;
497
+ const warningStatus = results[results.length - 1].warningStatus;
498
+ let affectedRows = 0;
499
+
500
+ if (cmdOpt.metaAsArray) {
501
+ // Use reduce for better performance with large result sets
502
+ affectedRows = results.reduce((sum, result) => sum + result[0].affectedRows, 0);
503
+ resolve([new OkPacket(affectedRows, insertId, warningStatus), []]);
244
504
  } else {
245
- _sql = sql;
505
+ affectedRows = results.reduce((sum, result) => sum + result.affectedRows, 0);
506
+ resolve(new OkPacket(affectedRows, insertId, warningStatus));
246
507
  }
508
+ }
509
+
510
+ /**
511
+ * Aggregate result sets from multiple executions
512
+ *
513
+ * @param {Array} results - Array of individual results
514
+ * @param {Object} cmdOpt - Command options
515
+ * @param {Function} resolve - Promise resolve function
516
+ * @private
517
+ */
518
+ aggregateResultSets(results, cmdOpt, resolve) {
519
+ if (cmdOpt.metaAsArray) {
520
+ // Calculate total length to avoid resizing
521
+ const totalLength = results.reduce((sum, row) => sum + (row[0]?.length || 0), 0);
522
+ const rs = new Array(totalLength);
523
+
524
+ // Efficiently copy all results into a single array
525
+ let index = 0;
526
+ for (const row of results) {
527
+ if (row[0] && row[0].length) {
528
+ const rowData = row[0];
529
+ for (let i = 0; i < rowData.length; i++) {
530
+ rs[index++] = rowData[i];
531
+ }
532
+ }
533
+ }
534
+
535
+ resolve([rs.slice(0, index), results[0][1]]);
536
+ } else {
537
+ // Calculate total length to avoid resizing
538
+ const totalLength = results.reduce((sum, row) => sum + (Array.isArray(row) ? row.length : 0), 0);
539
+ const rs = new Array(totalLength);
540
+
541
+ // Efficiently copy all results into a single array
542
+ let index = 0;
543
+ for (const row of results) {
544
+ if (Array.isArray(row) && row.length) {
545
+ for (let i = 0; i < row.length; i++) {
546
+ rs[index++] = row[i];
547
+ }
548
+ }
549
+ }
550
+
551
+ // Create final result array and add metadata
552
+ const finalResult = rs.slice(0, index);
247
553
 
248
- const cmd = new Stream(_cmdOpt, opts, _sql, _values, _socket);
249
- if (opts.trace) Error.captureStackTrace(cmd);
250
- _addCommand(cmd);
251
- return cmd.inStream;
252
- };
554
+ // Add metadata as non-enumerable property
555
+ if (results[0] && results[0].meta) {
556
+ Object.defineProperty(finalResult, 'meta', {
557
+ value: results[0].meta,
558
+ writable: true,
559
+ enumerable: cmdOpt.metaEnumerable
560
+ });
561
+ }
562
+
563
+ resolve(finalResult);
564
+ }
565
+ }
253
566
 
254
567
  /**
255
568
  * Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
256
- * @param timeout (optional) timeout value in ms. If reached, throw error and close connection
257
- * @returns {Promise} promise
569
+ * @param {Object} cmdParam - command context
570
+ * @param {Function} resolve - success function
571
+ * @param {Function} reject - rejection function
258
572
  */
259
- this.ping = (timeout) => {
260
- return new Promise(function (resolve, reject) {
261
- if (timeout) {
262
- if (timeout < 0) {
263
- reject(
264
- Errors.createError(
265
- 'Ping cannot have negative timeout value',
266
- null,
267
- false,
268
- info,
269
- '0A000',
270
- Errors.ER_BAD_PARAMETER_VALUE
271
- )
272
- );
273
- return;
573
+ ping(cmdParam, resolve, reject) {
574
+ // Handle custom timeout if provided
575
+ if (cmdParam.opts && cmdParam.opts.timeout !== undefined) {
576
+ // Validate timeout value
577
+ if (cmdParam.opts.timeout < 0) {
578
+ const err = Errors.createError(
579
+ 'Ping cannot have negative timeout value',
580
+ Errors.ER_BAD_PARAMETER_VALUE,
581
+ this.info,
582
+ '0A000'
583
+ );
584
+ this._logAndReject(reject, err);
585
+ return;
586
+ }
587
+
588
+ let timeoutRef = setTimeout(() => {
589
+ // If timeout occurs, mark variable as cleared to avoid double resolving
590
+ timeoutRef = undefined;
591
+
592
+ // Create error with proper details
593
+ const err = Errors.createFatalError('Ping timeout', Errors.ER_PING_TIMEOUT, this.info, '0A000');
594
+
595
+ // Close connection properly
596
+ this.addCommand = this.addCommandDisabled;
597
+ clearTimeout(this.timeout);
598
+
599
+ if (this.status !== Status.CLOSING && this.status !== Status.CLOSED) {
600
+ this.sendQueue.clear();
601
+ this.status = Status.CLOSED;
602
+ this.socket.destroy();
274
603
  }
275
- const tOut = setTimeout(() => {
276
- reject(
277
- Errors.createError('Ping timeout', null, true, info, '0A000', Errors.ER_PING_TIMEOUT)
278
- );
279
- // close connection
280
- _addCommand = _addCommandDisabled;
281
- clearTimeout(_timeout);
282
- if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
283
- _sendQueue.clear();
284
- _status = Status.CLOSED;
285
- _socket.destroy();
286
- }
287
- _clear();
288
- }, timeout);
289
- return _addCommand(
290
- new Ping(
291
- () => {
292
- clearTimeout(tOut);
604
+
605
+ this.clear();
606
+ this._logAndReject(reject, err);
607
+ }, cmdParam.opts.timeout);
608
+
609
+ // Create ping command with wrapped callbacks to handle timeout
610
+ this.addCommand(
611
+ new Ping(
612
+ cmdParam,
613
+ () => {
614
+ // Successful ping response - clear timeout if it hasn't fired yet
615
+ if (timeoutRef) {
616
+ clearTimeout(timeoutRef);
293
617
  resolve();
294
- },
295
- (err) => {
296
- clearTimeout(tOut);
297
- reject(err);
298
618
  }
299
- )
300
- );
301
- }
302
- return _addCommand(new Ping(resolve, reject));
303
- });
304
- };
619
+ },
620
+ (err) => {
621
+ // Error during ping - clear timeout if it hasn't fired yet
622
+ if (timeoutRef) {
623
+ clearTimeout(timeoutRef);
624
+ this._logAndReject(reject, err);
625
+ }
626
+ }
627
+ ),
628
+ true
629
+ );
630
+
631
+ return;
632
+ }
633
+
634
+ // Simple ping without custom timeout
635
+ this.addCommand(new Ping(cmdParam, resolve, reject), true);
636
+ }
305
637
 
306
638
  /**
307
639
  * Send a reset command that will
@@ -311,165 +643,163 @@ function Connection(options) {
311
643
  * - delete user variables
312
644
  * - remove temporary tables
313
645
  * - remove all PREPARE statement
314
- *
315
- * @returns {Promise} promise
316
646
  */
317
- this.reset = () => {
647
+ reset(cmdParam, resolve, reject) {
318
648
  if (
319
- (info.isMariaDB() && info.hasMinVersion(10, 2, 4)) ||
320
- (!info.isMariaDB() && info.hasMinVersion(5, 7, 3))
649
+ (this.info.isMariaDB() && this.info.hasMinVersion(10, 2, 4)) ||
650
+ (!this.info.isMariaDB() && this.info.hasMinVersion(5, 7, 3))
321
651
  ) {
322
- return new Promise(function (resolve, reject) {
323
- return _addCommand(new Reset(resolve, reject));
324
- });
652
+ const conn = this;
653
+ const resetCmd = new Reset(
654
+ cmdParam,
655
+ () => {
656
+ if (conn.prepareCache) conn.prepareCache.reset();
657
+ let prom = Promise.resolve();
658
+ // re-execute init query / session query timeout
659
+ prom
660
+ .then(conn.handleCharset.bind(conn))
661
+ .then(conn.handleTimezone.bind(conn))
662
+ .then(conn.executeInitQuery.bind(conn))
663
+ .then(conn.executeSessionTimeout.bind(conn))
664
+ .then(resolve)
665
+ .catch(reject);
666
+ },
667
+ reject
668
+ );
669
+ this.addCommand(resetCmd, true);
670
+ return;
325
671
  }
326
- return Promise.reject(
327
- new Error(
328
- 'Reset command not permitted for server ' +
329
- this.info.serverVersion +
330
- ' (requires server MariaDB version 10.2.4+ or MySQL 5.7.3+)'
331
- )
672
+
673
+ const err = new Error(
674
+ `Reset command not permitted for server ${this.info.serverVersion.raw} (requires server MariaDB version 10.2.4+ or MySQL 5.7.3+)`
332
675
  );
333
- };
676
+ err.stack = cmdParam.stack;
677
+ this._logAndReject(reject, err);
678
+ }
334
679
 
335
680
  /**
336
681
  * Indicates the state of the connection as the driver knows it
337
682
  * @returns {boolean}
338
683
  */
339
- this.isValid = () => {
340
- return _status === Status.CONNECTED;
341
- };
684
+ isValid() {
685
+ return this.status === Status.CONNECTED;
686
+ }
342
687
 
343
688
  /**
344
689
  * Terminate connection gracefully.
345
- *
346
- * @returns {Promise} promise
347
- */
348
- this.end = () => {
349
- _addCommand = _addCommandDisabled;
350
- clearTimeout(_timeout);
351
-
352
- if (
353
- _status !== Status.CLOSING &&
354
- _status !== Status.CLOSED &&
355
- _status !== Status.NOT_CONNECTED
356
- ) {
357
- _status = Status.CLOSING;
358
- return new Promise(function (resolve, reject) {
359
- const ended = () => {
360
- _status = Status.CLOSED;
361
- _socket.destroy();
362
- _socket.unref();
363
- _clear();
364
- _receiveQueue.clear();
365
- resolve();
366
- };
367
- const quitCmd = new Quit(ended, ended);
368
- _sendQueue.push(quitCmd);
369
- _receiveQueue.push(quitCmd);
370
- if (_sendQueue.length === 1) {
371
- process.nextTick(_nextSendCmd.bind(this));
372
- }
373
- });
374
- }
375
- return Promise.resolve();
376
- };
377
-
378
- /**
379
- * Alias for destroy.
380
690
  */
381
- this.close = function () {
382
- this.destroy();
383
- };
691
+ end(cmdParam, resolve, reject) {
692
+ this.addCommand = this.addCommandDisabled;
693
+ clearTimeout(this.timeout);
694
+
695
+ if (this.status < Status.CLOSING && this.status !== Status.NOT_CONNECTED) {
696
+ this.status = Status.CLOSING;
697
+ const ended = () => {
698
+ this.status = Status.CLOSED;
699
+ this.socket.destroy();
700
+ this.socket.unref();
701
+ this.clear();
702
+ this.receiveQueue.clear();
703
+ resolve();
704
+ };
705
+ const quitCmd = new Quit(cmdParam, ended, ended);
706
+ this.sendQueue.push(quitCmd);
707
+ this.receiveQueue.push(quitCmd);
708
+ if (this.sendQueue.length === 1) {
709
+ process.nextTick(this.nextSendCmd.bind(this));
710
+ }
711
+ } else resolve();
712
+ }
384
713
 
385
714
  /**
386
715
  * Force connection termination by closing the underlying socket and killing server process if any.
387
716
  */
388
- this.destroy = () => {
389
- _addCommand = _addCommandDisabled;
390
- clearTimeout(_timeout);
391
- if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
392
- _status = Status.CLOSING;
393
- _sendQueue.clear();
394
- if (_receiveQueue.length > 0) {
717
+ destroy() {
718
+ this.addCommand = this.addCommandDisabled;
719
+ clearTimeout(this.timeout);
720
+ if (this.status < Status.CLOSING) {
721
+ this.status = Status.CLOSING;
722
+ this.sendQueue.clear();
723
+ if (this.receiveQueue.length > 0) {
395
724
  //socket is closed, but server may still be processing a huge select
396
725
  //only possibility is to kill process by another thread
397
726
  //TODO reuse a pool connection to avoid connection creation
398
727
  const self = this;
399
- const killCon = new Connection(opts);
728
+
729
+ // relying on IP in place of DNS to ensure using same server
730
+ const remoteAddress = this.socket.remoteAddress;
731
+ const connOption = remoteAddress ? Object.assign({}, this.opts, { host: remoteAddress }) : this.opts;
732
+
733
+ const killCon = new Connection(connOption);
400
734
  killCon
401
735
  .connect()
402
736
  .then(() => {
403
737
  //*************************************************
404
738
  //kill connection
405
739
  //*************************************************
406
- const killResHandler = () => {
407
- const destroyError = Errors.createError(
740
+ new Promise(killCon.query.bind(killCon, { sql: `KILL ${self.info.threadId}` })).finally((err) => {
741
+ const destroyError = Errors.createFatalError(
408
742
  'Connection destroyed, command was killed',
409
- null,
410
- true,
411
- info,
412
- '08S01',
413
- Errors.ER_CMD_NOT_EXECUTED_DESTROYED
743
+ Errors.ER_CMD_NOT_EXECUTED_DESTROYED,
744
+ self.info
414
745
  );
415
- socketErrorDispatchToQueries(destroyError);
416
- process.nextTick(() => {
417
- if (_socket) _socket.destroy();
418
- });
419
- _status = Status.CLOSED;
420
- killCon.end().catch(() => {});
421
- };
422
-
423
- killCon
424
- .query('KILL ' + info.threadId)
425
- .then(killResHandler)
426
- .catch(killResHandler);
746
+ if (self.opts.logger.error) self.opts.logger.error(destroyError);
747
+ self.socketErrorDispatchToQueries(destroyError);
748
+ if (self.socket) {
749
+ const sok = self.socket;
750
+ process.nextTick(() => {
751
+ sok.destroy();
752
+ });
753
+ }
754
+ self.status = Status.CLOSED;
755
+ self.clear();
756
+ new Promise(killCon.end.bind(killCon)).catch(() => {});
757
+ });
427
758
  })
428
- .catch((err) => {
759
+ .catch(() => {
429
760
  //*************************************************
430
761
  //failing to create a kill connection, end normally
431
762
  //*************************************************
432
763
  const ended = () => {
433
- let sock = _socket;
434
- _clear();
435
- _status = Status.CLOSED;
436
- setImmediate(resolve);
764
+ let sock = self.socket;
765
+ self.clear();
766
+ self.status = Status.CLOSED;
437
767
  sock.destroy();
438
- _receiveQueue.clear();
768
+ self.receiveQueue.clear();
439
769
  };
440
770
  const quitCmd = new Quit(ended, ended);
441
- _sendQueue.push(quitCmd);
442
- _receiveQueue.push(quitCmd);
443
- if (_sendQueue.length === 1) {
444
- process.nextTick(_nextSendCmd.bind(self));
771
+ self.sendQueue.push(quitCmd);
772
+ self.receiveQueue.push(quitCmd);
773
+ if (self.sendQueue.length === 1) {
774
+ process.nextTick(self.nextSendCmd.bind(self));
445
775
  }
446
776
  });
447
777
  } else {
448
- _status = Status.CLOSED;
449
- _socket.destroy();
778
+ this.status = Status.CLOSED;
779
+ this.socket.destroy();
780
+ this.clear();
450
781
  }
451
782
  }
452
- _clear();
453
- };
783
+ }
454
784
 
455
- this.pause = () => {
456
- _socket.pause();
457
- };
785
+ pause() {
786
+ this.socket.pause();
787
+ }
458
788
 
459
- this.resume = () => {
460
- _socket.resume();
461
- };
789
+ resume() {
790
+ this.socket.resume();
791
+ }
462
792
 
463
- this.format = (sql, values) => {
464
- throw Errors.createError(
793
+ format(sql, values) {
794
+ const err = Errors.createError(
465
795
  '"Connection.format intentionally not implemented. please use Connection.query(sql, values), it will be more secure and faster',
466
- null,
467
- false,
468
- info,
469
- '0A000',
470
- Errors.ER_NOT_IMPLEMENTED_FORMAT
796
+ Errors.ER_NOT_IMPLEMENTED_FORMAT,
797
+ this.info,
798
+ '0A000'
471
799
  );
472
- };
800
+ if (this.opts.logger.error) this.opts.logger.error(err);
801
+ throw err;
802
+ }
473
803
 
474
804
  //*****************************************************************
475
805
  // additional public methods
@@ -480,594 +810,560 @@ function Connection(options) {
480
810
  *
481
811
  * @returns {*}
482
812
  */
483
- this.serverVersion = () => {
484
- if (!info.serverVersion)
485
- throw new Error('cannot know if server information until connection is established');
486
- return info.serverVersion.raw;
487
- };
813
+ serverVersion() {
814
+ if (!this.info.serverVersion) {
815
+ const err = new Error('cannot know if server information until connection is established');
816
+ if (this.opts.logger.error) this.opts.logger.error(err);
817
+ throw err;
818
+ }
819
+
820
+ return this.info.serverVersion.raw;
821
+ }
488
822
 
489
823
  /**
490
824
  * Change option "debug" during connection.
491
825
  * @param val debug value
492
826
  */
493
- this.debug = (val) => {
494
- opts.debug = val;
495
- opts.emit('debug', opts.logPackets, opts.debug);
496
- };
497
-
498
- this.debugCompress = (val) => {
499
- opts.debugCompress = val;
500
- };
827
+ debug(val) {
828
+ if (typeof val === 'boolean') {
829
+ if (val && !this.opts.logger.network) this.opts.logger.network = console.log;
830
+ } else if (typeof val === 'function') {
831
+ this.opts.logger.network = val;
832
+ }
833
+ this.opts.emit('debug', val);
834
+ }
835
+
836
+ debugCompress(val) {
837
+ if (val) {
838
+ if (typeof val === 'boolean') {
839
+ this.opts.debugCompress = val;
840
+ if (val && !this.opts.logger.network) this.opts.logger.network = console.log;
841
+ } else if (typeof val === 'function') {
842
+ this.opts.debugCompress = true;
843
+ this.opts.logger.network = val;
844
+ }
845
+ } else this.opts.debugCompress = false;
846
+ }
501
847
 
502
848
  //*****************************************************************
503
849
  // internal public testing methods
504
850
  //*****************************************************************
505
851
 
506
- function TestMethods() {}
507
-
508
- TestMethods.prototype.getCollation = () => {
509
- return opts.collation;
510
- };
511
-
512
- TestMethods.prototype.getSocket = () => {
513
- return _socket;
514
- };
515
-
516
- this.__tests = new TestMethods();
852
+ get __tests() {
853
+ return new TestMethods(this.info.collation, this.socket);
854
+ }
517
855
 
518
856
  //*****************************************************************
519
857
  // internal methods
520
858
  //*****************************************************************
521
859
 
522
- this._status = () => {
523
- return _status;
524
- };
525
-
526
860
  /**
527
- * Execute query using text protocol with callback emit columns/data/end/error
528
- * events to permit streaming big result-set
861
+ * Determine if the bulk protocol can be used for batch operations
529
862
  *
530
- * @param sql sql parameter Object can be used to supersede default option.
531
- * Object must then have sql property.
532
- * @param values object / array of placeholder values (not mandatory)
533
- * @param cb callback
534
- * @returns {Query} query
863
+ * @param {Array} values - Batch values array
864
+ * @param {Object} options - Batch options
865
+ * @return {boolean} Whether bulk protocol can be used
866
+ * @private
535
867
  */
536
- this._queryCallback = (sql, values, cb) => {
537
- let _cmdOpts,
538
- _sql,
539
- _values = values,
540
- _cb = cb;
868
+ _canUseBulk(values, options) {
869
+ // 1. Check compatibility with fullResult option
870
+ if (options && options.fullResult && (this.info.clientCapabilities & Capabilities.BULK_UNIT_RESULTS) === 0n) {
871
+ return false;
872
+ }
541
873
 
542
- if (typeof values === 'function') {
543
- _cb = values;
544
- _values = undefined;
874
+ // 2. Determine if bulk operations are enabled
875
+ const bulkEnable =
876
+ options === undefined || options === null
877
+ ? this.opts.bulk
878
+ : options.bulk !== undefined && options.bulk !== null
879
+ ? options.bulk
880
+ : this.opts.bulk;
881
+
882
+ // 3. Check if server supports bulk operations
883
+ const serverSupportsBulk =
884
+ this.info.serverVersion &&
885
+ this.info.serverVersion.mariaDb &&
886
+ this.info.hasMinVersion(10, 2, 7) &&
887
+ (this.info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > 0n;
888
+
889
+ // If server doesn't support bulk or it's disabled, return false
890
+ if (!serverSupportsBulk || !bulkEnable) {
891
+ return false;
545
892
  }
546
893
 
547
- if (typeof sql === 'object') {
548
- _cmdOpts = sql;
549
- _sql = _cmdOpts.sql;
550
- if (sql.values) _values = sql.values;
551
- } else {
552
- _sql = sql;
894
+ // 4. No need to validate values if none provided
895
+ if (values === undefined) {
896
+ return true;
553
897
  }
554
898
 
555
- let cmd;
556
- if (_cb) {
557
- const resolve = (rows) => {
558
- const meta = rows.meta;
559
- delete rows.meta;
560
- _cb(null, rows, meta);
561
- };
562
- cmd = new Query(resolve, _cb, _cmdOpts, opts, _sql, _values);
899
+ // 5. Validate values based on placeholder type
900
+ if (!this.opts.namedPlaceholders) {
901
+ // For positional parameters
902
+ return this._validatePositionalParameters(values);
563
903
  } else {
564
- cmd = new Query(
565
- () => {},
566
- () => {},
567
- _cmdOpts,
568
- opts,
569
- _sql,
570
- _values
571
- );
904
+ // For named parameters
905
+ return this._validateNamedParameters(values);
572
906
  }
573
- cmd.handleNewRows = (row) => {
574
- cmd._rows[cmd._responseIndex].push(row);
575
- cmd.emit('data', row);
576
- };
577
-
578
- if (opts.trace) Error.captureStackTrace(cmd);
579
- _addCommand(cmd);
580
- return cmd;
581
- };
907
+ }
582
908
 
583
909
  /**
584
- * Execute a batch using text protocol with callback emit columns/data/end/error
585
- * events to permit streaming big result-set
910
+ * Validate batch values for positional parameters
586
911
  *
587
- * @param sql sql parameter Object can be used to supersede default option.
588
- * Object must then have sql property.
589
- * @param values object / array of placeholder values (not mandatory)
590
- * @param cb callback
591
- * @returns {Query} query
912
+ * @param {Array} values - Batch values array
913
+ * @return {boolean} Whether values are valid for bulk protocol
914
+ * @private
592
915
  */
593
- this._batchCallback = (sql, values, cb) => {
594
- let _cmdOpts,
595
- _sql,
596
- _values = values,
597
- _cb = cb;
598
-
599
- if (typeof values === 'function') {
600
- _cb = values;
601
- _values = undefined;
602
- }
916
+ _validatePositionalParameters(values) {
917
+ // Determine expected parameter length
918
+ const paramLen = Array.isArray(values[0]) ? values[0].length : values[0] ? 1 : 0;
603
919
 
604
- if (typeof sql === 'object') {
605
- _cmdOpts = sql;
606
- _sql = _cmdOpts.sql;
607
- if (sql.values) _values = sql.values;
608
- } else {
609
- _sql = sql;
610
- }
611
-
612
- if (_values !== undefined) {
613
- _values = Array.isArray(_values) ? _values : [_values];
920
+ // If no parameters, can't use bulk
921
+ if (paramLen === 0) {
922
+ return false;
614
923
  }
615
924
 
616
- let cmd;
925
+ // Check parameter consistency and streaming
926
+ for (const row of values) {
927
+ const rowArray = Array.isArray(row) ? row : [row];
617
928
 
618
- if (!_values) {
619
- if (_cb) {
620
- _cb(
621
- Errors.createError(
622
- 'Batch must have values set',
623
- _sql,
624
- false,
625
- info,
626
- 'HY000',
627
- Errors.ER_BATCH_WITH_NO_VALUES
628
- )
629
- );
929
+ // All parameter sets must have same length
930
+ if (paramLen !== rowArray.length) {
931
+ return false;
630
932
  }
631
- return null;
632
- }
633
-
634
- let useBulk = canUseBulk(_values);
635
933
 
636
- const fct = useBulk ? BatchBulk : BatchRewrite;
637
-
638
- if (_cb) {
639
- const resolve = (rows) => {
640
- const meta = rows.meta;
641
- delete rows.meta;
642
- _cb(null, rows, meta);
643
- };
644
- cmd = new fct(resolve, _cb, _cmdOpts, opts, _sql, _values);
645
- } else {
646
- cmd = new fct(
647
- () => {},
648
- () => {},
649
- _cmdOpts,
650
- opts,
651
- _sql,
652
- _values
653
- );
934
+ // Check for streaming data (not permitted)
935
+ for (const val of rowArray) {
936
+ if (this._isStreamingValue(val)) {
937
+ return false;
938
+ }
939
+ }
654
940
  }
655
- cmd.handleNewRows = (row) => {
656
- cmd._rows[cmd._responseIndex].push(row);
657
- cmd.emit('data', row);
658
- };
659
941
 
660
- if (opts.trace) Error.captureStackTrace(cmd);
661
- _addCommand(cmd);
662
- return cmd;
663
- };
942
+ return true;
943
+ }
664
944
 
665
945
  /**
666
- * Use Batch rewrite or MariaDB bulk protocol.
946
+ * Validate batch values for named parameters
667
947
  *
668
- * @param values current batch values
669
- * @return {boolean} indicating if must use rewrite or bulk
948
+ * @param {Array} values - Batch values array
949
+ * @return {boolean} Whether values are valid for bulk protocol
950
+ * @private
670
951
  */
671
- const canUseBulk = (values) => {
672
- // not using info.isMariaDB() directly in case of callback use,
673
- // without connection beeing completly finished.
674
- let useBulk =
675
- info.serverVersion &&
676
- info.serverVersion.mariaDb &&
677
- info.hasMinVersion(10, 2, 7) &&
678
- opts.bulk &&
679
- (info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) > BigInt(0);
680
-
681
- if (useBulk) {
682
- //ensure that there is no stream object
683
- if (values !== undefined) {
684
- if (!opts.namedPlaceholders) {
685
- //ensure that all parameters have same length
686
- //single array is considered as an array of single element.
687
- const paramLen = Array.isArray(values[0]) ? values[0].length : values[0] ? 1 : 0;
688
- if (paramLen == 0) return false;
689
- for (let r = 0; r < values.length; r++) {
690
- let row = values[r];
691
- if (!Array.isArray(row)) row = [row];
692
- if (paramLen !== row.length) {
693
- return false;
694
- }
695
- for (let j = 0; j < paramLen; j++) {
696
- const val = row[j];
697
- if (
698
- val !== null &&
699
- typeof val === 'object' &&
700
- typeof val.pipe === 'function' &&
701
- typeof val.read === 'function'
702
- ) {
703
- return false;
704
- }
705
- }
706
- }
707
- } else {
708
- for (let r = 0; r < values.length; r++) {
709
- let row = values[r];
710
- const keys = Object.keys(row);
711
- for (let j = 0; j < keys.length; j++) {
712
- const val = row[keys[j]];
713
- if (
714
- val !== null &&
715
- typeof val === 'object' &&
716
- typeof val.pipe === 'function' &&
717
- typeof val.read === 'function'
718
- ) {
719
- return false;
720
- }
721
- }
722
- }
952
+ _validateNamedParameters(values) {
953
+ // Check each row for streaming values
954
+ for (const row of values) {
955
+ for (const val of Object.values(row)) {
956
+ if (this._isStreamingValue(val)) {
957
+ return false;
723
958
  }
724
959
  }
725
960
  }
726
- return useBulk;
727
- };
961
+ return true;
962
+ }
728
963
 
729
964
  /**
730
- * Add handshake command to queue.
965
+ * Check if a value is a streaming value
731
966
  *
967
+ * @param {*} val - Value to check
968
+ * @return {boolean} Whether value is a streaming value
732
969
  * @private
733
970
  */
734
- const _registerHandshakeCmd = (resolve, rejected) => {
735
- const _authFail = _authFailHandler.bind(this, rejected);
736
- const _authSucceed = _authSucceedHandler.bind(this, resolve, _authFail);
737
-
738
- const handshake = new Handshake(
739
- _authSucceed,
740
- _authFail,
741
- _createSecureContext.bind(this, _authFail),
742
- _addCommandEnable.bind(this),
743
- _getSocket
744
- );
745
- Error.captureStackTrace(handshake);
746
-
747
- handshake.once('end', () => {
748
- process.nextTick(_nextSendCmd);
749
- });
750
-
751
- _receiveQueue.push(handshake);
752
- _initSocket(_authFail);
753
- };
971
+ _isStreamingValue(val) {
972
+ return val != null && typeof val === 'object' && typeof val.pipe === 'function' && typeof val.read === 'function';
973
+ }
754
974
 
755
- const _executeSessionVariableQuery = () => {
756
- if (opts.sessionVariables) {
975
+ executeSessionVariableQuery() {
976
+ if (this.opts.sessionVariables) {
757
977
  const values = [];
758
978
  let sessionQuery = 'set ';
759
- let keys = Object.keys(opts.sessionVariables);
979
+ let keys = Object.keys(this.opts.sessionVariables);
760
980
  if (keys.length > 0) {
761
- return new Promise(function (resolve, reject) {
762
- for (let k = 0; k < keys.length; ++k) {
763
- sessionQuery +=
764
- (k !== 0 ? ',' : '') + '@@' + keys[k].replace(/[^a-z0-9_]/gi, '') + '=?';
765
- values.push(opts.sessionVariables[keys[k]]);
766
- }
767
- const errorHandling = (initialErr) => {
768
- reject(
769
- Errors.createError(
770
- 'Error setting session variable (value ' +
771
- JSON.stringify(opts.sessionVariables) +
772
- '). Error: ' +
773
- initialErr.message,
774
- sessionQuery,
775
- true,
776
- info,
777
- '08S01',
778
- Errors.ER_SETTING_SESSION_ERROR,
779
- null
780
- )
781
- );
782
- };
783
- const cmd = new Query(resolve, errorHandling, null, opts, sessionQuery, values);
784
- if (opts.trace) Error.captureStackTrace(cmd);
785
- _addCommand(cmd);
981
+ for (let k = 0; k < keys.length; ++k) {
982
+ sessionQuery += (k !== 0 ? ',' : '') + '@@' + keys[k].replace(/[^a-z0-9_]/gi, '') + '=?';
983
+ values.push(this.opts.sessionVariables[keys[k]]);
984
+ }
985
+
986
+ return new Promise(
987
+ this.query.bind(this, {
988
+ sql: sessionQuery,
989
+ values: values
990
+ })
991
+ ).catch((initialErr) => {
992
+ const err = Errors.createFatalError(
993
+ `Error setting session variable (value ${JSON.stringify(this.opts.sessionVariables)}). Error: ${
994
+ initialErr.message
995
+ }`,
996
+ Errors.ER_SETTING_SESSION_ERROR,
997
+ this.info,
998
+ '08S01',
999
+ sessionQuery
1000
+ );
1001
+ if (this.opts.logger.error) this.opts.logger.error(err);
1002
+ return Promise.reject(err);
786
1003
  });
787
1004
  }
788
1005
  }
789
1006
  return Promise.resolve();
790
- };
1007
+ }
1008
+
1009
+ /**
1010
+ * set charset to charset/collation if set or utf8mb4 if not.
1011
+ * @returns {Promise<void>}
1012
+ * @private
1013
+ */
1014
+ handleCharset() {
1015
+ if (this.opts.collation) {
1016
+ // if index <= 255, skip command, since collation has already been set during handshake response.
1017
+ if (this.opts.collation.index <= 255) return Promise.resolve();
1018
+ const charset =
1019
+ this.opts.collation.charset === 'utf8' && this.opts.collation.maxLength === 4
1020
+ ? 'utf8mb4'
1021
+ : this.opts.collation.charset;
1022
+ return new Promise(
1023
+ this.query.bind(this, {
1024
+ sql: `SET NAMES ${charset} COLLATE ${this.opts.collation.name}`
1025
+ })
1026
+ );
1027
+ }
1028
+
1029
+ // MXS-4635: server can some information directly on first Ok_Packet, like not truncated collation
1030
+ // in this case, avoid useless SET NAMES utf8mb4 command
1031
+ if (
1032
+ !this.opts.charset &&
1033
+ this.info.collation &&
1034
+ this.info.collation.charset === 'utf8' &&
1035
+ this.info.collation.maxLength === 4
1036
+ ) {
1037
+ this.info.collation = Collations.fromCharset('utf8mb4');
1038
+ return Promise.resolve();
1039
+ }
1040
+ const connCharset = this.opts.charset ? this.opts.charset : 'utf8mb4';
1041
+ this.info.collation = Collations.fromCharset(connCharset);
1042
+ return new Promise(
1043
+ this.query.bind(this, {
1044
+ sql: `SET NAMES ${connCharset}`
1045
+ })
1046
+ );
1047
+ }
791
1048
 
792
1049
  /**
793
1050
  * Asking server timezone if not set in case of 'auto'
794
1051
  * @returns {Promise<void>}
795
1052
  * @private
796
1053
  */
797
- const _checkServerTimezone = () => {
798
- if (opts.timezone === 'auto') {
799
- return this._queryPromise('SELECT @@system_time_zone stz, @@time_zone tz').then((res) => {
1054
+ handleTimezone() {
1055
+ const conn = this;
1056
+ if (this.opts.timezone === 'local') this.opts.timezone = undefined;
1057
+ if (this.opts.timezone === 'auto') {
1058
+ return new Promise(
1059
+ this.query.bind(this, {
1060
+ sql: 'SELECT @@system_time_zone stz, @@time_zone tz'
1061
+ })
1062
+ ).then((res) => {
800
1063
  const serverTimezone = res[0].tz === 'SYSTEM' ? res[0].stz : res[0].tz;
801
- const serverZone = moment.tz.zone(serverTimezone);
802
- if (serverZone) {
803
- const localTz = moment.tz.guess();
804
- if (serverTimezone === localTz) {
805
- //db server and client use same timezone, avoid any conversion
806
- opts.tz = null;
807
- } else {
808
- opts._localTz = localTz;
809
- opts.tz = serverTimezone;
810
- }
811
- } else {
812
- return Promise.reject(
813
- Errors.createError(
814
- "Automatic timezone setting fails. Server timezone '" +
815
- serverTimezone +
816
- "' does't have a corresponding IANA timezone. Option timezone must be set according to server timezone",
817
- null,
818
- true,
819
- info,
820
- '08S01',
821
- Errors.ER_WRONG_AUTO_TIMEZONE
822
- )
823
- );
1064
+ const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1065
+ if (serverTimezone === localTz || convertFixedTime(serverTimezone, conn) === convertFixedTime(localTz, conn)) {
1066
+ //server timezone is identical to client tz, skipping setting
1067
+ this.opts.timezone = localTz;
1068
+ return Promise.resolve();
824
1069
  }
825
- return Promise.resolve();
1070
+ return this._setSessionTimezone(convertFixedTime(localTz, conn));
826
1071
  });
827
1072
  }
828
- if (opts.tz && !opts.skipSetTimezone) {
829
- let tz = opts.tz;
830
- if (opts.tz === 'Etc/UTC') {
831
- tz = '+00:00';
832
- } else if (opts.tz.startsWith('Etc/GMT')) {
833
- let zone = moment.tz.zone(opts.tz);
834
- tz = zone.abbrs[0] + ':00';
835
- }
836
1073
 
837
- return this._queryPromise('SET time_zone=?', tz)
838
- .then((res) => {
839
- return Promise.resolve();
840
- })
841
- .catch((err) => {
842
- console.log(
843
- `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\``
844
- );
845
- return Promise.resolve();
846
- });
1074
+ if (this.opts.timezone) {
1075
+ return this._setSessionTimezone(convertFixedTime(this.opts.timezone, conn));
847
1076
  }
848
1077
  return Promise.resolve();
849
- };
1078
+ }
1079
+
1080
+ _setSessionTimezone(tz) {
1081
+ return new Promise(
1082
+ this.query.bind(this, {
1083
+ sql: 'SET time_zone=?',
1084
+ values: [tz]
1085
+ })
1086
+ ).catch((err) => {
1087
+ const er = Errors.createFatalError(
1088
+ `setting timezone '${tz}' fails on server.\n look at https://mariadb.com/kb/en/mysql_tzinfo_to_sql/ to load IANA timezone. `,
1089
+ Errors.ER_WRONG_IANA_TIMEZONE,
1090
+ this.info
1091
+ );
1092
+ if (this.opts.logger.error) this.opts.logger.error(er);
1093
+ return Promise.reject(er);
1094
+ });
1095
+ }
850
1096
 
851
- const _checkServerVersion = () => {
852
- if (!opts.forceVersionCheck) {
1097
+ checkServerVersion() {
1098
+ if (!this.opts.forceVersionCheck) {
853
1099
  return Promise.resolve();
854
1100
  }
855
- return this._queryPromise('SELECT @@VERSION AS v').then((res) => {
856
- info.serverVersion.raw = res[0].v;
857
- info.serverVersion.mariaDb = info.serverVersion.raw.includes('MariaDB');
858
- ConnectionInformation.parseVersionString(info);
859
- return Promise.resolve();
860
- });
861
- };
1101
+ return new Promise(
1102
+ this.query.bind(this, {
1103
+ sql: 'SELECT @@VERSION AS v'
1104
+ })
1105
+ ).then(
1106
+ function (res) {
1107
+ this.info.serverVersion.raw = res[0].v;
1108
+ this.info.serverVersion.mariaDb = this.info.serverVersion.raw.includes('MariaDB');
1109
+ ConnectionInformation.parseVersionString(this.info);
1110
+ return Promise.resolve();
1111
+ }.bind(this)
1112
+ );
1113
+ }
862
1114
 
863
- const _executeInitQuery = () => {
864
- if (opts.initSql) {
865
- const initialArr = Array.isArray(opts.initSql) ? opts.initSql : [opts.initSql];
1115
+ executeInitQuery() {
1116
+ if (this.opts.initSql) {
1117
+ const initialArr = Array.isArray(this.opts.initSql) ? this.opts.initSql : [this.opts.initSql];
866
1118
  const initialPromises = [];
867
1119
  initialArr.forEach((sql) => {
868
- initialPromises.push(this._queryPromise(sql));
1120
+ initialPromises.push(
1121
+ new Promise(
1122
+ this.query.bind(this, {
1123
+ sql: sql
1124
+ })
1125
+ )
1126
+ );
869
1127
  });
870
1128
 
871
1129
  return Promise.all(initialPromises).catch((initialErr) => {
872
- return Promise.reject(
873
- Errors.createError(
874
- 'Error executing initial sql command: ' + initialErr.message,
875
- null,
876
- true,
877
- info,
878
- '08S01',
879
- Errors.ER_INITIAL_SQL_ERROR,
880
- null
881
- )
1130
+ const err = Errors.createFatalError(
1131
+ `Error executing initial sql command: ${initialErr.message}`,
1132
+ Errors.ER_INITIAL_SQL_ERROR,
1133
+ this.info
882
1134
  );
1135
+ if (this.opts.logger.error) this.opts.logger.error(err);
1136
+ return Promise.reject(err);
883
1137
  });
884
1138
  }
885
1139
  return Promise.resolve();
886
- };
887
-
888
- const _executeSessionTimeout = () => {
889
- if (opts.queryTimeout) {
890
- if (info.isMariaDB() && info.hasMinVersion(10, 1, 2)) {
891
- const query = 'SET max_statement_time=' + opts.queryTimeout / 1000;
892
- this._queryPromise(query).catch((initialErr) => {
893
- return Promise.reject(
894
- Errors.createError(
895
- 'Error setting session queryTimeout: ' + initialErr.message,
896
- query,
897
- true,
898
- info,
899
- '08S01',
1140
+ }
1141
+
1142
+ executeSessionTimeout() {
1143
+ if (this.opts.queryTimeout) {
1144
+ if (this.info.isMariaDB() && this.info.hasMinVersion(10, 1, 2)) {
1145
+ const query = `SET max_statement_time=${this.opts.queryTimeout / 1000}`;
1146
+ new Promise(
1147
+ this.query.bind(this, {
1148
+ sql: query
1149
+ })
1150
+ ).catch(
1151
+ function (initialErr) {
1152
+ const err = Errors.createFatalError(
1153
+ `Error setting session queryTimeout: ${initialErr.message}`,
900
1154
  Errors.ER_INITIAL_TIMEOUT_ERROR,
901
- null
902
- )
903
- );
904
- });
1155
+ this.info,
1156
+ '08S01',
1157
+ query
1158
+ );
1159
+ if (this.opts.logger.error) this.opts.logger.error(err);
1160
+ return Promise.reject(err);
1161
+ }.bind(this)
1162
+ );
905
1163
  } else {
906
- return Promise.reject(
907
- Errors.createError(
908
- 'Can only use queryTimeout for MariaDB server after 10.1.1. queryTimeout value: ' +
909
- null,
910
- opts.queryTimeout,
911
- false,
912
- info,
913
- 'HY000',
914
- Errors.ER_TIMEOUT_NOT_SUPPORTED
915
- )
1164
+ const err = Errors.createError(
1165
+ `Can only use queryTimeout for MariaDB server after 10.1.1. queryTimeout value: ${this.opts.queryTimeout}`,
1166
+ Errors.ER_TIMEOUT_NOT_SUPPORTED,
1167
+ this.info,
1168
+ 'HY000',
1169
+ this.opts.queryTimeout
916
1170
  );
1171
+ if (this.opts.logger.error) this.opts.logger.error(err);
1172
+ return Promise.reject(err);
917
1173
  }
918
1174
  }
919
1175
  return Promise.resolve();
920
- };
1176
+ }
921
1177
 
922
- const _getSocket = () => {
923
- return _socket;
924
- };
1178
+ getSocket() {
1179
+ return this.socket;
1180
+ }
925
1181
 
926
1182
  /**
927
1183
  * Initialize socket and associate events.
928
1184
  * @private
929
1185
  */
930
- const _initSocket = (authFailHandler) => {
931
- if (opts.socketPath) {
932
- _socket = Net.connect(opts.socketPath);
933
- } else {
934
- _socket = Net.connect(opts.port, opts.host);
1186
+ streamInitSocket() {
1187
+ if (this.opts.connectTimeout) {
1188
+ this.timeout = setTimeout(this.connectTimeoutReached.bind(this), this.opts.connectTimeout, Date.now());
935
1189
  }
936
-
937
- if (opts.connectTimeout) {
938
- _timeout = setTimeout(
939
- _connectTimeoutReached,
940
- opts.connectTimeout,
941
- authFailHandler,
942
- Date.now()
943
- );
1190
+ if (this.opts.socketPath) {
1191
+ this.socket = Net.connect(this.opts.socketPath);
1192
+ } else if (this.opts.stream) {
1193
+ if (typeof this.opts.stream === 'function') {
1194
+ const tmpSocket = this.opts.stream(
1195
+ function (err, stream) {
1196
+ if (err) {
1197
+ this.authFailHandler(err);
1198
+ return;
1199
+ }
1200
+ this.socket = stream ? stream : Net.connect(this.opts.port, this.opts.host);
1201
+ this.socketInit();
1202
+ }.bind(this)
1203
+ );
1204
+ if (tmpSocket) {
1205
+ this.socket = tmpSocket;
1206
+ this.socketInit();
1207
+ }
1208
+ } else {
1209
+ this.authFailHandler(
1210
+ Errors.createError(
1211
+ 'stream option is not a function. stream must be a function with (error, callback) parameter',
1212
+ Errors.ER_BAD_PARAMETER_VALUE,
1213
+ this.info
1214
+ )
1215
+ );
1216
+ }
1217
+ return;
1218
+ } else {
1219
+ this.socket = Net.connect(this.opts.port, this.opts.host);
1220
+ this.socket.setNoDelay(true);
944
1221
  }
1222
+ this.socketInit();
1223
+ }
945
1224
 
946
- const _socketError = _socketErrorHandler.bind(this, authFailHandler);
1225
+ socketInit() {
1226
+ this.socket.on('data', this.streamIn.onData.bind(this.streamIn));
1227
+ this.socket.on('error', this.socketErrorHandler.bind(this));
1228
+ this.socket.on('end', this.socketErrorHandler.bind(this));
947
1229
 
948
- _socket.on('data', _in.onData.bind(_in));
949
- _socket.on('error', _socketError);
950
- _socket.on('end', _socketError);
951
- _socket.on(
1230
+ this.socket.on(
952
1231
  'connect',
953
1232
  function () {
954
- clearTimeout(_timeout);
955
- if (_status === Status.CONNECTING) {
956
- _status = Status.AUTHENTICATING;
957
- _socketConnected = true;
958
- _socket.setTimeout(opts.socketTimeout, _socketTimeoutReached.bind(this, authFailHandler));
959
- _socket.setNoDelay(true);
960
-
1233
+ if (this.status === Status.CONNECTING) {
1234
+ this.status = Status.AUTHENTICATING;
1235
+ this.socket.setNoDelay(true);
1236
+ this.socket.setTimeout(this.opts.socketTimeout, this.socketTimeoutReached.bind(this));
961
1237
  // keep alive for socket. This won't reset server wait_timeout use pool option idleTimeout for that
962
- if (opts.keepAliveDelay) {
963
- _socket.setKeepAlive(true, opts.keepAliveDelay);
1238
+ if (this.opts.keepAliveDelay >= 0) {
1239
+ this.socket.setKeepAlive(true, this.opts.keepAliveDelay);
1240
+ } else {
1241
+ this.socket.setKeepAlive(true);
964
1242
  }
965
1243
  }
966
1244
  }.bind(this)
967
1245
  );
968
1246
 
969
- _socket.writeBuf = (buf) => _socket.write(buf);
970
- _socket.flush = () => {};
971
- _out.setStream(_socket);
972
- };
1247
+ this.socket.writeBuf = (buf) => this.socket.write(buf);
1248
+ this.socket.flush = () => {};
1249
+ this.streamOut.setStream(this.socket);
1250
+ }
973
1251
 
974
1252
  /**
975
1253
  * Authentication success result handler.
976
1254
  *
977
1255
  * @private
978
1256
  */
979
- const _authSucceedHandler = (resolve, rejected) => {
1257
+ authSucceedHandler() {
980
1258
  //enable packet compression according to option
981
- if (opts.logPackets) info.enableLogPacket();
982
- if (opts.compress) {
983
- if (info.serverCapabilities & Capabilities.COMPRESS) {
984
- _out.setStream(new CompressionOutputStream(_socket, opts, info));
985
- _in = new CompressionInputStream(_in, _receiveQueue, opts, info);
986
- _socket.removeAllListeners('data');
987
- _socket.on('data', _in.onData.bind(_in));
988
- } else {
989
- console.error(
990
- "connection is configured to use packet compression, but the server doesn't have this capability"
1259
+ if (this.opts.compress) {
1260
+ if (this.info.serverCapabilities & Capabilities.COMPRESS) {
1261
+ this.streamOut.setStream(new CompressionOutputStream(this.socket, this.opts, this.info));
1262
+ this.streamIn = new CompressionInputStream(this.streamIn, this.receiveQueue, this.opts, this.info);
1263
+ this.socket.removeAllListeners('data');
1264
+ this.socket.on('data', this.streamIn.onData.bind(this.streamIn));
1265
+ } else if (this.opts.logger.error) {
1266
+ this.opts.logger.error(
1267
+ Errors.createError(
1268
+ "connection is configured to use packet compression, but the server doesn't have this capability",
1269
+ Errors.ER_COMPRESSION_NOT_SUPPORTED,
1270
+ this.info
1271
+ )
991
1272
  );
992
1273
  }
993
1274
  }
994
1275
 
995
- _addCommand = opts.pipelining ? _addCommandEnablePipeline : _addCommandEnable;
1276
+ this.addCommand = this.opts.pipelining ? this.addCommandEnablePipeline : this.addCommandEnable;
1277
+ const conn = this;
1278
+ this.status = Status.INIT_CMD;
1279
+ this.executeSessionVariableQuery()
1280
+ .then(conn.handleCharset.bind(conn))
1281
+ .then(this.handleTimezone.bind(this))
1282
+ .then(this.checkServerVersion.bind(this))
1283
+ .then(this.executeInitQuery.bind(this))
1284
+ .then(this.executeSessionTimeout.bind(this))
1285
+ .then(() => {
1286
+ clearTimeout(this.timeout);
1287
+ conn.status = Status.CONNECTED;
1288
+ process.nextTick(conn.connectResolveFct, conn);
996
1289
 
997
- const commands = _waitingAuthenticationQueue.toArray();
998
- commands.forEach((cmd) => {
999
- _addCommand(cmd);
1000
- });
1290
+ const commands = conn.waitingAuthenticationQueue.toArray();
1291
+ commands.forEach((cmd) => {
1292
+ conn.addCommand(cmd, true);
1293
+ });
1294
+ conn.waitingAuthenticationQueue = null;
1001
1295
 
1002
- const errorInitialQueries = (err) => {
1003
- if (!err.fatal) this.end().catch((err) => {});
1004
- process.nextTick(rejected, err);
1005
- };
1006
- _status = Status.INIT_CMD;
1007
- _executeSessionVariableQuery()
1008
- .then(() => {
1009
- return _checkServerTimezone();
1296
+ conn.connectRejectFct = null;
1297
+ conn.connectResolveFct = null;
1010
1298
  })
1011
- .then(() => {
1012
- return _checkServerVersion();
1013
- })
1014
- .then(() => {
1015
- return _executeInitQuery();
1016
- })
1017
- .then(() => {
1018
- return _executeSessionTimeout();
1019
- })
1020
- .then(() => {
1021
- _status = Status.CONNECTED;
1022
- process.nextTick(resolve, this);
1023
- })
1024
- .catch(errorInitialQueries);
1025
- };
1299
+ .catch((err) => {
1300
+ if (!err.fatal) {
1301
+ const res = () => {
1302
+ conn.authFailHandler.call(conn, err);
1303
+ };
1304
+ conn.end(res, res);
1305
+ } else {
1306
+ conn.authFailHandler.call(conn, err);
1307
+ }
1308
+ return Promise.reject(err);
1309
+ });
1310
+ }
1026
1311
 
1027
1312
  /**
1028
1313
  * Authentication failed result handler.
1029
1314
  *
1030
1315
  * @private
1031
1316
  */
1032
- const _authFailHandler = (reject, err) => {
1033
- process.nextTick(reject, err);
1034
- //remove handshake command
1035
- _receiveQueue.shift();
1036
-
1037
- _fatalError(err, true);
1038
- };
1317
+ authFailHandler(err) {
1318
+ clearTimeout(this.timeout);
1319
+ if (this.connectRejectFct) {
1320
+ if (this.opts.logger.error) this.opts.logger.error(err);
1321
+ //remove handshake command
1322
+ this.receiveQueue.shift();
1323
+ this.fatalError(err, true);
1324
+
1325
+ process.nextTick(this.connectRejectFct, err);
1326
+ this.connectRejectFct = null;
1327
+ }
1328
+ }
1039
1329
 
1040
1330
  /**
1041
1331
  * Create TLS socket and associate events.
1042
1332
  *
1043
- * @param rejected rejected function when error
1333
+ * @param info current connection information
1044
1334
  * @param callback callback function when done
1045
1335
  * @private
1046
1336
  */
1047
- const _createSecureContext = (rejected, callback) => {
1048
- const _socketError = _socketErrorHandler.bind(this, rejected);
1049
- const sslOption = Object.assign({}, opts.ssl, {
1050
- servername: opts.host,
1051
- socket: _socket
1052
- });
1337
+ createSecureContext(info, callback) {
1338
+ info.requireValidCert =
1339
+ this.opts.ssl === true ||
1340
+ this.opts.ssl.rejectUnauthorized === undefined ||
1341
+ this.opts.ssl.rejectUnauthorized === true;
1342
+
1343
+ const baseConf = { socket: this.socket };
1344
+ if (info.isMariaDB()) {
1345
+ // for MariaDB servers, permit self-signed certificated
1346
+ // this will be replaced by fingerprint validation with ending OK_PACKET
1347
+ baseConf['rejectUnauthorized'] = false;
1348
+ }
1349
+ const sslOption = this.opts.ssl === true ? baseConf : Object.assign({}, this.opts.ssl, baseConf);
1053
1350
 
1054
1351
  try {
1055
1352
  const secureSocket = tls.connect(sslOption, callback);
1056
-
1057
- secureSocket.on('data', _in.onData.bind(_in));
1058
- secureSocket.on('error', _socketError);
1059
- secureSocket.on('end', _socketError);
1353
+ secureSocket.on('data', this.streamIn.onData.bind(this.streamIn));
1354
+ secureSocket.on('error', this.socketErrorHandler.bind(this));
1355
+ secureSocket.on('end', this.socketErrorHandler.bind(this));
1060
1356
  secureSocket.writeBuf = (buf) => secureSocket.write(buf);
1061
1357
  secureSocket.flush = () => {};
1062
1358
 
1063
- _socket.removeAllListeners('data');
1064
- _socket = secureSocket;
1359
+ this.socket.removeAllListeners('data');
1360
+ this.socket = secureSocket;
1065
1361
 
1066
- _out.setStream(secureSocket);
1362
+ this.streamOut.setStream(secureSocket);
1067
1363
  } catch (err) {
1068
- _socketError(err);
1364
+ this.socketErrorHandler(err);
1069
1365
  }
1070
- };
1366
+ }
1071
1367
 
1072
1368
  /**
1073
1369
  * Handle packet when no packet is expected.
@@ -1076,184 +1372,116 @@ function Connection(options) {
1076
1372
  * @param packet packet
1077
1373
  * @private
1078
1374
  */
1079
- const _unexpectedPacket = function (packet) {
1375
+ unexpectedPacket(packet) {
1080
1376
  if (packet && packet.peek() === 0xff) {
1081
1377
  //can receive unexpected error packet from server/proxy
1082
1378
  //to inform that connection is closed (usually by timeout)
1083
- let err = packet.readError(info);
1084
- if (err.fatal && _status !== Status.CLOSING && _status !== Status.CLOSED) {
1379
+ let err = packet.readError(this.info);
1380
+ if (err.fatal && this.status < Status.CLOSING) {
1085
1381
  this.emit('error', err);
1086
- this.end();
1382
+ if (this.opts.logger.error) this.opts.logger.error(err);
1383
+ this.end(
1384
+ () => {},
1385
+ () => {}
1386
+ );
1087
1387
  }
1088
- } else if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
1089
- this.emit(
1090
- 'error',
1091
- Errors.createError(
1092
- 'receiving packet from server without active commands\n' +
1093
- 'conn:' +
1094
- (info.threadId ? info.threadId : -1) +
1095
- '(' +
1096
- packet.pos +
1097
- ',' +
1098
- packet.end +
1099
- ')\n' +
1100
- Utils.log(opts, packet.buf, packet.pos, packet.end),
1101
- null,
1102
- true,
1103
- info,
1104
- '08S01',
1105
- Errors.ER_UNEXPECTED_PACKET
1106
- )
1388
+ } else if (this.status < Status.CLOSING) {
1389
+ const err = Errors.createFatalError(
1390
+ `receiving packet from server without active commands\nconn:${this.info.threadId ? this.info.threadId : -1}(${
1391
+ packet.pos
1392
+ },${packet.end})\n${Utils.log(this.opts, packet.buf, packet.pos, packet.end)}`,
1393
+ Errors.ER_UNEXPECTED_PACKET,
1394
+ this.info
1107
1395
  );
1396
+ if (this.opts.logger.error) this.opts.logger.error(err);
1397
+ this.emit('error', err);
1108
1398
  this.destroy();
1109
1399
  }
1110
- };
1111
-
1112
- /**
1113
- * Change transaction state.
1114
- *
1115
- * @param sql sql
1116
- * @returns {Promise} promise
1117
- * @private
1118
- */
1119
- const _changeTransaction = (sql) => {
1120
- //if command in progress, driver cannot rely on status and must execute query
1121
- if (_status === Status.CLOSING || _status === Status.CLOSED) {
1122
- return Promise.reject(
1123
- Errors.createError(
1124
- 'Cannot execute new commands: connection closed',
1125
- sql,
1126
- true,
1127
- info,
1128
- '08S01',
1129
- Errors.ER_CMD_CONNECTION_CLOSED
1130
- )
1131
- );
1132
- }
1133
-
1134
- //Command in progress => must execute query
1135
- //or if no command in progress, can rely on status to know if query is needed
1136
- if (_receiveQueue.peekFront() || info.status & ServerStatus.STATUS_IN_TRANS) {
1137
- return new Promise(function (resolve, reject) {
1138
- const cmd = new Query(resolve, reject, null, opts, sql, null);
1139
- if (opts.trace) Error.captureStackTrace(cmd);
1140
- _addCommand(cmd);
1141
- });
1142
- }
1143
- return Promise.resolve();
1144
- };
1400
+ }
1145
1401
 
1146
1402
  /**
1147
1403
  * Handle connection timeout.
1148
1404
  *
1149
1405
  * @private
1150
1406
  */
1151
- const _connectTimeoutReached = function (authFailHandler, initialConnectionTime) {
1152
- _timeout = null;
1153
- const handshake = _receiveQueue.peekFront();
1154
- authFailHandler(
1155
- Errors.createError(
1156
- 'Connection timeout: failed to create socket after ' +
1157
- (Date.now() - initialConnectionTime) +
1158
- 'ms',
1159
- null,
1160
- true,
1161
- info,
1162
- '08S01',
1163
- Errors.ER_CONNECTION_TIMEOUT,
1164
- handshake ? handshake.stack : null
1165
- )
1407
+ connectTimeoutReached(initialConnectionTime) {
1408
+ this.timeout = null;
1409
+ const handshake = this.receiveQueue.peekFront();
1410
+ const err = Errors.createFatalError(
1411
+ `Connection timeout: failed to create socket after ${Date.now() - initialConnectionTime}ms`,
1412
+ Errors.ER_CONNECTION_TIMEOUT,
1413
+ this.info,
1414
+ '08S01',
1415
+ null,
1416
+ handshake ? handshake.stack : null
1166
1417
  );
1167
- };
1418
+ if (this.opts.logger.error) this.opts.logger.error(err);
1419
+ this.authFailHandler(err);
1420
+ }
1168
1421
 
1169
1422
  /**
1170
1423
  * Handle socket timeout.
1171
1424
  *
1172
1425
  * @private
1173
1426
  */
1174
- const _socketTimeoutReached = function () {
1175
- const err = Errors.createError(
1176
- 'socket timeout',
1177
- null,
1178
- true,
1179
- info,
1180
- '08S01',
1181
- Errors.ER_SOCKET_TIMEOUT
1182
- );
1183
- const packetMsgs = info.getLastPackets();
1184
- if (packetMsgs !== '') {
1185
- err.message = err.message + '\nlast received packets:\n' + packetMsgs;
1186
- }
1187
- _fatalError(err, true);
1188
- };
1427
+ socketTimeoutReached() {
1428
+ clearTimeout(this.timeout);
1429
+ const err = Errors.createFatalError('socket timeout', Errors.ER_SOCKET_TIMEOUT, this.info);
1430
+ if (this.opts.logger.error) this.opts.logger.error(err);
1431
+ this.fatalError(err, true);
1432
+ }
1189
1433
 
1190
1434
  /**
1191
1435
  * Add command to waiting queue until authentication.
1192
1436
  *
1193
1437
  * @param cmd command
1194
- * @returns {*} current command
1195
1438
  * @private
1196
1439
  */
1197
- const _addCommandQueue = (cmd) => {
1198
- _waitingAuthenticationQueue.push(cmd);
1199
- return cmd;
1200
- };
1440
+ addCommandQueue(cmd) {
1441
+ this.waitingAuthenticationQueue.push(cmd);
1442
+ }
1201
1443
 
1202
1444
  /**
1203
1445
  * Add command to command sending and receiving queue.
1204
1446
  *
1205
1447
  * @param cmd command
1206
- * @returns {*} current command
1448
+ * @param expectResponse queue command response
1207
1449
  * @private
1208
1450
  */
1209
- const _addCommandEnable = (cmd) => {
1210
- cmd.once('end', () => {
1211
- setImmediate(_nextSendCmd);
1212
- });
1451
+ addCommandEnable(cmd, expectResponse) {
1452
+ cmd.once('end', this._sendNextCmdImmediate.bind(this));
1213
1453
 
1214
1454
  //send immediately only if no current active receiver
1215
- if (_sendQueue.isEmpty() && (_status === Status.INIT_CMD || _status === Status.CONNECTED)) {
1216
- if (_receiveQueue.peekFront()) {
1217
- _receiveQueue.push(cmd);
1218
- _sendQueue.push(cmd);
1219
- return cmd;
1220
- }
1221
-
1222
- _receiveQueue.push(cmd);
1223
- cmd.start(_out, opts, info);
1455
+ if (this.sendQueue.isEmpty() && this.receiveQueue.isEmpty()) {
1456
+ if (expectResponse) this.receiveQueue.push(cmd);
1457
+ cmd.start(this.streamOut, this.opts, this.info);
1224
1458
  } else {
1225
- _receiveQueue.push(cmd);
1226
- _sendQueue.push(cmd);
1459
+ if (expectResponse) this.receiveQueue.push(cmd);
1460
+ this.sendQueue.push(cmd);
1227
1461
  }
1228
- return cmd;
1229
- };
1462
+ }
1230
1463
 
1231
1464
  /**
1232
1465
  * Add command to command sending and receiving queue using pipelining
1233
1466
  *
1234
- * @param cmd command
1235
- * @returns {*} current command
1467
+ * @param cmd command
1468
+ * @param expectResponse queue command response
1236
1469
  * @private
1237
1470
  */
1238
- const _addCommandEnablePipeline = (cmd) => {
1239
- cmd.once('send_end', () => {
1240
- setImmediate(_nextSendCmd);
1241
- });
1471
+ addCommandEnablePipeline(cmd, expectResponse) {
1472
+ cmd.once('send_end', this._sendNextCmdImmediate.bind(this));
1242
1473
 
1243
- _receiveQueue.push(cmd);
1244
- if (_sendQueue.isEmpty()) {
1245
- cmd.start(_out, opts, info);
1474
+ if (expectResponse) this.receiveQueue.push(cmd);
1475
+ if (this.sendQueue.isEmpty()) {
1476
+ cmd.start(this.streamOut, this.opts, this.info);
1246
1477
  if (cmd.sending) {
1247
- _sendQueue.push(cmd);
1248
- cmd.prependOnceListener('send_end', () => {
1249
- _sendQueue.shift();
1250
- });
1478
+ this.sendQueue.push(cmd);
1479
+ cmd.prependOnceListener('send_end', this.sendQueue.shift.bind(this.sendQueue));
1251
1480
  }
1252
1481
  } else {
1253
- _sendQueue.push(cmd);
1482
+ this.sendQueue.push(cmd);
1254
1483
  }
1255
- return cmd;
1256
- };
1484
+ }
1257
1485
 
1258
1486
  /**
1259
1487
  * Replacing command when connection is closing or closed to send a proper error message.
@@ -1261,111 +1489,95 @@ function Connection(options) {
1261
1489
  * @param cmd command
1262
1490
  * @private
1263
1491
  */
1264
- const _addCommandDisabled = (cmd) => {
1265
- cmd.throwNewError(
1492
+ addCommandDisabled(cmd) {
1493
+ const err = cmd.throwNewError(
1266
1494
  'Cannot execute new commands: connection closed',
1267
1495
  true,
1268
- info,
1496
+ this.info,
1269
1497
  '08S01',
1270
1498
  Errors.ER_CMD_CONNECTION_CLOSED
1271
1499
  );
1272
- };
1500
+ if (this.opts.logger.error) this.opts.logger.error(err);
1501
+ }
1273
1502
 
1274
1503
  /**
1275
1504
  * Handle socket error.
1276
1505
  *
1277
- * @param authFailHandler authentication handler
1278
1506
  * @param err socket error
1279
1507
  * @private
1280
1508
  */
1281
- const _socketErrorHandler = function (authFailHandler, err) {
1282
- if (_status === Status.CLOSING || _status === Status.CLOSED) return;
1283
- if (_socket) {
1284
- _socket.writeBuf = () => {};
1285
- _socket.flush = () => {};
1509
+ socketErrorHandler(err) {
1510
+ if (this.status >= Status.CLOSING) return;
1511
+ if (this.socket) {
1512
+ this.socket.writeBuf = () => {};
1513
+ this.socket.flush = () => {};
1286
1514
  }
1287
1515
 
1288
1516
  //socket has been ended without error
1289
1517
  if (!err) {
1290
- err = Errors.createError(
1518
+ err = Errors.createFatalError(
1291
1519
  'socket has unexpectedly been closed',
1292
- null,
1293
- true,
1294
- info,
1295
- '08S01',
1296
- Errors.ER_SOCKET_UNEXPECTED_CLOSE
1520
+ Errors.ER_SOCKET_UNEXPECTED_CLOSE,
1521
+ this.info
1297
1522
  );
1298
1523
  } else {
1299
1524
  err.fatal = true;
1300
- this.sqlState = 'HY000';
1525
+ err.sqlState = 'HY000';
1301
1526
  }
1302
1527
 
1303
- const packetMsgs = info.getLastPackets();
1304
- if (packetMsgs !== '') {
1305
- err.message += '\nlast received packets:\n' + packetMsgs;
1306
- }
1307
-
1308
- switch (_status) {
1528
+ switch (this.status) {
1309
1529
  case Status.CONNECTING:
1310
1530
  case Status.AUTHENTICATING:
1311
- const currentCmd = _receiveQueue.peekFront();
1531
+ const currentCmd = this.receiveQueue.peekFront();
1312
1532
  if (currentCmd && currentCmd.stack && err) {
1313
- err.stack +=
1314
- '\n From event:\n' + currentCmd.stack.substring(currentCmd.stack.indexOf('\n') + 1);
1533
+ err.stack += '\n From event:\n' + currentCmd.stack.substring(currentCmd.stack.indexOf('\n') + 1);
1315
1534
  }
1316
- authFailHandler(err);
1535
+ this.authFailHandler(err);
1317
1536
  break;
1318
1537
 
1319
1538
  default:
1320
- _fatalError(err, false);
1539
+ this.fatalError(err, false);
1321
1540
  }
1322
- };
1541
+ }
1323
1542
 
1324
1543
  /**
1325
1544
  * Fatal unexpected error : closing connection, and throw exception.
1326
- *
1327
- * @param self current connection
1328
- * @private
1329
1545
  */
1330
- const _fatalErrorHandler = function (self) {
1331
- return function (err, avoidThrowError) {
1332
- if (_status === Status.CLOSING || _status === Status.CLOSED) {
1333
- socketErrorDispatchToQueries(err);
1334
- return;
1335
- }
1336
- const mustThrowError = _status !== Status.CONNECTING;
1337
- _status = Status.CLOSING;
1338
-
1339
- //prevent executing new commands
1340
- _addCommand = _addCommandDisabled;
1341
-
1342
- if (_socket) {
1343
- _socket.removeAllListeners('error');
1344
- _socket.removeAllListeners('timeout');
1345
- _socket.removeAllListeners('close');
1346
- _socket.removeAllListeners('data');
1347
- if (!_socket.destroyed) _socket.destroy();
1348
- _socket = undefined;
1349
- }
1350
- _status = Status.CLOSED;
1351
-
1352
- const errorThrownByCmd = socketErrorDispatchToQueries(err);
1353
- if (mustThrowError) {
1354
- if (self.listenerCount('error') > 0) {
1355
- self.emit('error', err);
1356
- self.emit('end');
1357
- _clear();
1358
- } else {
1359
- self.emit('end');
1360
- _clear();
1361
- //error will be thrown if no error listener and no command did throw the exception
1362
- if (!avoidThrowError && !errorThrownByCmd) throw err;
1363
- }
1546
+ fatalError(err, avoidThrowError) {
1547
+ if (this.status >= Status.CLOSING) {
1548
+ this.socketErrorDispatchToQueries(err);
1549
+ return;
1550
+ }
1551
+ const mustThrowError = this.status !== Status.CONNECTING;
1552
+ this.status = Status.CLOSING;
1553
+
1554
+ //prevent executing new commands
1555
+ this.addCommand = this.addCommandDisabled;
1556
+
1557
+ if (this.socket) {
1558
+ this.socket.removeAllListeners();
1559
+ if (!this.socket.destroyed) this.socket.destroy();
1560
+ this.socket = undefined;
1561
+ }
1562
+ this.status = Status.CLOSED;
1563
+
1564
+ const errorThrownByCmd = this.socketErrorDispatchToQueries(err);
1565
+ if (mustThrowError) {
1566
+ if (this.opts.logger.error) this.opts.logger.error(err);
1567
+ if (this.listenerCount('error') > 0) {
1568
+ this.emit('error', err);
1569
+ this.emit('end');
1570
+ this.clear();
1364
1571
  } else {
1365
- _clear();
1572
+ this.emit('end');
1573
+ this.clear();
1574
+ //error will be thrown if no error listener and no command did throw the exception
1575
+ if (!avoidThrowError && !errorThrownByCmd) throw err;
1366
1576
  }
1367
- };
1368
- };
1577
+ } else {
1578
+ this.clear();
1579
+ }
1580
+ }
1369
1581
 
1370
1582
  /**
1371
1583
  * Dispatch fatal error to current running queries.
@@ -1373,88 +1585,599 @@ function Connection(options) {
1373
1585
  * @param err the fatal error
1374
1586
  * @return {boolean} return if error has been relayed to queries
1375
1587
  */
1376
- const socketErrorDispatchToQueries = (err) => {
1588
+ socketErrorDispatchToQueries(err) {
1377
1589
  let receiveCmd;
1378
1590
  let errorThrownByCmd = false;
1379
- while ((receiveCmd = _receiveQueue.shift())) {
1591
+ while ((receiveCmd = this.receiveQueue.shift())) {
1380
1592
  if (receiveCmd && receiveCmd.onPacketReceive) {
1381
1593
  errorThrownByCmd = true;
1382
- setImmediate(receiveCmd.throwError.bind(receiveCmd), err, info);
1594
+ setImmediate(receiveCmd.throwError.bind(receiveCmd, err, this.info));
1383
1595
  }
1384
1596
  }
1385
1597
  return errorThrownByCmd;
1386
- };
1598
+ }
1387
1599
 
1388
1600
  /**
1389
1601
  * Will send next command in queue if any.
1390
1602
  *
1391
1603
  * @private
1392
1604
  */
1393
- const _nextSendCmd = () => {
1605
+ nextSendCmd() {
1394
1606
  let sendCmd;
1395
- if ((sendCmd = _sendQueue.shift())) {
1607
+ if ((sendCmd = this.sendQueue.shift())) {
1396
1608
  if (sendCmd.sending) {
1397
- _sendQueue.unshift(sendCmd);
1609
+ this.sendQueue.unshift(sendCmd);
1398
1610
  } else {
1399
- sendCmd.start(_out, opts, info);
1611
+ sendCmd.start(this.streamOut, this.opts, this.info);
1400
1612
  if (sendCmd.sending) {
1401
- sendCmd.prependOnceListener('send_end', () => {
1402
- _sendQueue.shift();
1403
- });
1404
- _sendQueue.unshift(sendCmd);
1613
+ this.sendQueue.unshift(sendCmd);
1614
+ sendCmd.prependOnceListener('send_end', this.sendQueue.shift.bind(this.sendQueue));
1405
1615
  }
1406
1616
  }
1407
1617
  }
1408
- };
1618
+ }
1619
+
1620
+ /**
1621
+ * Change transaction state.
1622
+ *
1623
+ * @param cmdParam command parameter
1624
+ * @param resolve success function to call
1625
+ * @param reject error function to call
1626
+ * @private
1627
+ */
1628
+ changeTransaction(cmdParam, resolve, reject) {
1629
+ //if command in progress, driver cannot rely on status and must execute query
1630
+ if (this.status >= Status.CLOSING) {
1631
+ const err = Errors.createFatalError(
1632
+ 'Cannot execute new commands: connection closed',
1633
+ Errors.ER_CMD_CONNECTION_CLOSED,
1634
+ this.info,
1635
+ '08S01',
1636
+ cmdParam.sql
1637
+ );
1638
+ this._logAndReject(reject, err);
1639
+ return;
1640
+ }
1641
+
1642
+ //Command in progress => must execute query
1643
+ //or if no command in progress, can rely on status to know if query is needed
1644
+ if (this.receiveQueue.peekFront() || this.info.status & ServerStatus.STATUS_IN_TRANS) {
1645
+ const cmd = new Query(resolve, (err) => this._logAndReject(reject, err), this.opts, cmdParam);
1646
+ this.addCommand(cmd, true);
1647
+ } else resolve();
1648
+ }
1649
+
1650
+ changeUser(cmdParam, resolve, reject) {
1651
+ if (!this.info.isMariaDB()) {
1652
+ const err = Errors.createError(
1653
+ 'method changeUser not available for MySQL server due to Bug #83472',
1654
+ Errors.ER_MYSQL_CHANGE_USER_BUG,
1655
+ this.info,
1656
+ '0A000'
1657
+ );
1658
+ this._logAndReject(reject, err);
1659
+ return;
1660
+ }
1661
+ if (this.status < Status.CLOSING) {
1662
+ this.addCommand = this.addCommandEnable;
1663
+ }
1664
+ let conn = this;
1665
+ if (cmdParam.opts && cmdParam.opts.collation && typeof cmdParam.opts.collation === 'string') {
1666
+ const val = cmdParam.opts.collation.toUpperCase();
1667
+ cmdParam.opts.collation = Collations.fromName(cmdParam.opts.collation.toUpperCase());
1668
+ if (cmdParam.opts.collation === undefined) return reject(new RangeError(`Unknown collation '${val}'`));
1669
+ }
1670
+
1671
+ this.addCommand(
1672
+ new ChangeUser(
1673
+ cmdParam,
1674
+ this.opts,
1675
+ (res) => {
1676
+ if (conn.status < Status.CLOSING && conn.opts.pipelining) conn.addCommand = conn.addCommandEnablePipeline;
1677
+ if (cmdParam.opts && cmdParam.opts.collation) conn.opts.collation = cmdParam.opts.collation;
1678
+ conn
1679
+ .handleCharset()
1680
+ .then(() => {
1681
+ if (cmdParam.opts && cmdParam.opts.collation) {
1682
+ conn.info.collation = cmdParam.opts.collation;
1683
+ conn.opts.emit('collation', cmdParam.opts.collation);
1684
+ }
1685
+ resolve(res);
1686
+ })
1687
+ .catch((err) => {
1688
+ const res = () => conn.authFailHandler.call(conn, err);
1689
+ if (!err.fatal) {
1690
+ conn.end(res, res);
1691
+ } else {
1692
+ res();
1693
+ }
1694
+ reject(err);
1695
+ });
1696
+ },
1697
+ this.authFailHandler.bind(this, reject),
1698
+ this.getSocket.bind(this)
1699
+ ),
1700
+ true
1701
+ );
1702
+ }
1703
+
1704
+ query(cmdParam, resolve, reject) {
1705
+ if (!cmdParam.sql)
1706
+ return reject(
1707
+ Errors.createError(
1708
+ 'sql parameter is mandatory',
1709
+ Errors.ER_UNDEFINED_SQL,
1710
+ this.info,
1711
+ 'HY000',
1712
+ null,
1713
+ false,
1714
+ cmdParam.stack
1715
+ )
1716
+ );
1717
+ const cmd = new Query(resolve, (err) => this._logAndReject(reject, err), this.opts, cmdParam);
1718
+ this.addCommand(cmd, true);
1719
+ }
1720
+
1721
+ prepare(cmdParam, resolve, reject) {
1722
+ if (!cmdParam.sql) {
1723
+ reject(Errors.createError('sql parameter is mandatory', Errors.ER_UNDEFINED_SQL, this.info, 'HY000'));
1724
+ return;
1725
+ }
1726
+ if (this.prepareCache && (this.sendQueue.isEmpty() || !this.receiveQueue.peekFront())) {
1727
+ // no command in queue, database is then considered ok, and cache can be search right now
1728
+ const cachedPrepare = this.prepareCache.get(cmdParam.sql);
1729
+ if (cachedPrepare) {
1730
+ resolve(cachedPrepare);
1731
+ return;
1732
+ }
1733
+ }
1734
+
1735
+ const cmd = new Prepare(resolve, (err) => this._logAndReject(reject, err), this.opts, cmdParam, this);
1736
+ this.addCommand(cmd, true);
1737
+ }
1738
+
1739
+ prepareExecute(cmdParam, resolve, reject) {
1740
+ if (!cmdParam.sql) {
1741
+ reject(Errors.createError('sql parameter is mandatory', Errors.ER_UNDEFINED_SQL, this.info, 'HY000'));
1742
+ return;
1743
+ }
1744
+
1745
+ if (this.prepareCache && (this.sendQueue.isEmpty() || !this.receiveQueue.peekFront())) {
1746
+ // no command in queue, current database is known, so cache can be search right now
1747
+ const cachedPrepare = this.prepareCache.get(cmdParam.sql);
1748
+ if (cachedPrepare) {
1749
+ this.executePromise(
1750
+ cmdParam,
1751
+ cachedPrepare,
1752
+ (res) => {
1753
+ resolve(res);
1754
+ cachedPrepare.close();
1755
+ },
1756
+ (err) => {
1757
+ reject(err);
1758
+ cachedPrepare.close();
1759
+ }
1760
+ );
1761
+ return;
1762
+ }
1763
+ }
1764
+
1765
+ // permit pipelining PREPARE and EXECUTE if mariadb 10.2.4+ and has no streaming
1766
+ const conn = this;
1767
+ if (this.opts.pipelining && this.info.isMariaDB() && this.info.hasMinVersion(10, 2, 4)) {
1768
+ let hasStreamingValue = false;
1769
+ const vals = cmdParam.values ? (Array.isArray(cmdParam.values) ? cmdParam.values : [cmdParam.values]) : [];
1770
+ for (let i = 0; i < vals.length; i++) {
1771
+ const val = vals[i];
1772
+ if (
1773
+ val != null &&
1774
+ typeof val === 'object' &&
1775
+ typeof val.pipe === 'function' &&
1776
+ typeof val.read === 'function'
1777
+ ) {
1778
+ hasStreamingValue = true;
1779
+ }
1780
+ }
1781
+ if (!hasStreamingValue) {
1782
+ let nbExecute = 0;
1783
+ const executeCommand = new Execute(
1784
+ (res) => {
1785
+ if (nbExecute++ === 0) {
1786
+ executeCommand.prepare.close();
1787
+ resolve(res);
1788
+ }
1789
+ },
1790
+ (err) => {
1791
+ if (nbExecute++ === 0) {
1792
+ if (conn.opts.logger.error) conn.opts.logger.error(err);
1793
+ reject(err);
1794
+ if (executeCommand.prepare) {
1795
+ executeCommand.prepare.close();
1796
+ }
1797
+ }
1798
+ },
1799
+ conn.opts,
1800
+ cmdParam,
1801
+ null
1802
+ );
1803
+ cmdParam.executeCommand = executeCommand;
1804
+ const cmd = new Prepare(
1805
+ (prep) => {
1806
+ if (nbExecute > 0) prep.close();
1807
+ },
1808
+ (err) => {
1809
+ if (nbExecute++ === 0) {
1810
+ if (conn.opts.logger.error) conn.opts.logger.error(err);
1811
+ reject(err);
1812
+ }
1813
+ },
1814
+ conn.opts,
1815
+ cmdParam,
1816
+ conn
1817
+ );
1818
+ conn.addCommand(cmd, true);
1819
+ conn.addCommand(executeCommand, true);
1820
+ return;
1821
+ }
1822
+ }
1823
+
1824
+ // execute PREPARE, then EXECUTE
1825
+ const cmd = new Prepare(
1826
+ (prepare) => {
1827
+ conn.executePromise(
1828
+ cmdParam,
1829
+ prepare,
1830
+ (res) => {
1831
+ resolve(res);
1832
+ prepare.close();
1833
+ },
1834
+ (err) => {
1835
+ if (conn.opts.logger.error) conn.opts.logger.error(err);
1836
+ reject(err);
1837
+ prepare.close();
1838
+ }
1839
+ );
1840
+ },
1841
+ (err) => {
1842
+ if (conn.opts.logger.error) conn.opts.logger.error(err);
1843
+ reject(err);
1844
+ },
1845
+ this.opts,
1846
+ cmdParam,
1847
+ conn
1848
+ );
1849
+ conn.addCommand(cmd, true);
1850
+ }
1851
+
1852
+ importFile(cmdParam, resolve, reject) {
1853
+ const conn = this;
1854
+ if (!cmdParam || !cmdParam.file) {
1855
+ return reject(
1856
+ Errors.createError(
1857
+ 'SQL file parameter is mandatory',
1858
+ Errors.ER_MISSING_SQL_PARAMETER,
1859
+ conn.info,
1860
+ 'HY000',
1861
+ null,
1862
+ false,
1863
+ cmdParam.stack
1864
+ )
1865
+ );
1866
+ }
1867
+
1868
+ const prevAddCommand = this.addCommand.bind(conn);
1869
+
1870
+ this.waitingAuthenticationQueue = new Queue();
1871
+ this.addCommand = this.addCommandQueue;
1872
+ const tmpQuery = function (sql, resolve, reject) {
1873
+ const cmd = new Query(
1874
+ resolve,
1875
+ (err) => {
1876
+ if (conn.opts.logger.error) conn.opts.logger.error(err);
1877
+ reject(err);
1878
+ },
1879
+ conn.opts,
1880
+ {
1881
+ sql: sql,
1882
+ opts: {}
1883
+ }
1884
+ );
1885
+ prevAddCommand(cmd, true);
1886
+ };
1887
+
1888
+ let prevDatabase = null;
1889
+ return (
1890
+ cmdParam.skipDbCheck ? Promise.resolve() : new Promise(tmpQuery.bind(conn, 'SELECT DATABASE() as db'))
1891
+ ).then((res) => {
1892
+ prevDatabase = res ? res[0].db : null;
1893
+ if (
1894
+ (cmdParam.skipDbCheck && !conn.opts.database) ||
1895
+ (!cmdParam.skipDbCheck && !cmdParam.database && !prevDatabase)
1896
+ ) {
1897
+ return reject(
1898
+ Errors.createError(
1899
+ 'Database parameter is not set and no database is selected',
1900
+ Errors.ER_MISSING_DATABASE_PARAMETER,
1901
+ conn.info,
1902
+ 'HY000',
1903
+ null,
1904
+ false,
1905
+ cmdParam.stack
1906
+ )
1907
+ );
1908
+ }
1909
+ const searchDbPromise = cmdParam.database
1910
+ ? new Promise(tmpQuery.bind(conn, `USE \`${cmdParam.database.replace(/`/gi, '``')}\``))
1911
+ : Promise.resolve();
1912
+ return searchDbPromise.then(() => {
1913
+ const endingFunction = () => {
1914
+ if (conn.status < Status.CLOSING) {
1915
+ conn.addCommand = conn.addCommandEnable.bind(conn);
1916
+ if (conn.status < Status.CLOSING && conn.opts.pipelining) {
1917
+ conn.addCommand = conn.addCommandEnablePipeline.bind(conn);
1918
+ }
1919
+ const commands = conn.waitingAuthenticationQueue.toArray();
1920
+ commands.forEach((cmd) => conn.addCommand(cmd, true));
1921
+ conn.waitingAuthenticationQueue = null;
1922
+ }
1923
+ };
1924
+ return fsPromises
1925
+ .open(cmdParam.file, 'r')
1926
+ .then(async (fd) => {
1927
+ const buf = {
1928
+ buffer: Buffer.allocUnsafe(16384),
1929
+ offset: 0,
1930
+ end: 0
1931
+ };
1932
+
1933
+ const queryPromises = [];
1934
+ let cmdError = null;
1935
+ while (!cmdError) {
1936
+ try {
1937
+ const res = await fd.read(buf.buffer, buf.end, buf.buffer.length - buf.end, null);
1938
+ if (res.bytesRead === 0) {
1939
+ // end of file reached.
1940
+ fd.close().catch(() => {});
1941
+ if (cmdError) {
1942
+ endingFunction();
1943
+ reject(cmdError);
1944
+ return;
1945
+ }
1946
+ await Promise.allSettled(queryPromises)
1947
+ .then(() => {
1948
+ // reset connection to initial database if was set
1949
+ if (
1950
+ !cmdParam.skipDbCheck &&
1951
+ prevDatabase &&
1952
+ cmdParam.database &&
1953
+ cmdParam.database !== prevDatabase
1954
+ ) {
1955
+ return new Promise(tmpQuery.bind(conn, `USE \`${prevDatabase.replace(/`/gi, '``')}\``));
1956
+ }
1957
+ return Promise.resolve();
1958
+ })
1959
+ .then(() => {
1960
+ endingFunction();
1961
+ if (cmdError) {
1962
+ reject(cmdError);
1963
+ } else {
1964
+ resolve();
1965
+ }
1966
+ })
1967
+ .catch((err) => {
1968
+ endingFunction();
1969
+ reject(err);
1970
+ });
1971
+ return;
1972
+ } else {
1973
+ buf.end += res.bytesRead;
1974
+ const queries = Parse.parseQueries(buf);
1975
+ const queryIntermediatePromise = queries.flatMap((element) => {
1976
+ return new Promise(tmpQuery.bind(conn, element)).catch((err) => {
1977
+ cmdError = err;
1978
+ });
1979
+ });
1980
+
1981
+ queryPromises.push(...queryIntermediatePromise);
1982
+ if (buf.offset === buf.end) {
1983
+ buf.offset = 0;
1984
+ buf.end = 0;
1985
+ } else {
1986
+ // ensure that buffer can at least read 8k bytes,
1987
+ // either by copying remaining data on used part or growing buffer
1988
+ if (buf.offset > 8192) {
1989
+ // reuse buffer, copying remaining data begin of buffer
1990
+ buf.buffer.copy(buf.buffer, 0, buf.offset, buf.end);
1991
+ buf.end -= buf.offset;
1992
+ buf.offset = 0;
1993
+ } else if (buf.buffer.length - buf.end < 8192) {
1994
+ // grow buffer
1995
+ const tmpBuf = Buffer.allocUnsafe(buf.buffer.length << 1);
1996
+ buf.buffer.copy(tmpBuf, 0, buf.offset, buf.end);
1997
+ buf.buffer = tmpBuf;
1998
+ buf.end -= buf.offset;
1999
+ buf.offset = 0;
2000
+ }
2001
+ }
2002
+ }
2003
+ } catch (e) {
2004
+ fd.close().catch(() => {});
2005
+ endingFunction();
2006
+ Promise.allSettled(queryPromises).catch(() => {});
2007
+ return reject(
2008
+ Errors.createError(
2009
+ e.message,
2010
+ Errors.ER_SQL_FILE_ERROR,
2011
+ conn.info,
2012
+ 'HY000',
2013
+ null,
2014
+ false,
2015
+ cmdParam.stack
2016
+ )
2017
+ );
2018
+ }
2019
+ }
2020
+ if (cmdError) {
2021
+ endingFunction();
2022
+ reject(cmdError);
2023
+ }
2024
+ })
2025
+ .catch((err) => {
2026
+ endingFunction();
2027
+ if (err.code === 'ENOENT') {
2028
+ return reject(
2029
+ Errors.createError(
2030
+ `SQL file parameter '${cmdParam.file}' doesn't exists`,
2031
+ Errors.ER_MISSING_SQL_FILE,
2032
+ conn.info,
2033
+ 'HY000',
2034
+ null,
2035
+ false,
2036
+ cmdParam.stack
2037
+ )
2038
+ );
2039
+ }
2040
+ return reject(
2041
+ Errors.createError(err.message, Errors.ER_SQL_FILE_ERROR, conn.info, 'HY000', null, false, cmdParam.stack)
2042
+ );
2043
+ });
2044
+ });
2045
+ });
2046
+ }
1409
2047
 
1410
2048
  /**
1411
2049
  * Clearing connection variables when ending.
1412
2050
  *
1413
2051
  * @private
1414
2052
  */
1415
- const _clear = () => {
1416
- _sendQueue.clear();
1417
- opts.removeAllListeners();
1418
- _out = undefined;
1419
- _socket = undefined;
1420
- };
2053
+ clear() {
2054
+ this.sendQueue.clear();
2055
+ this.opts.removeAllListeners();
2056
+ this.streamOut = undefined;
2057
+ this.socket = undefined;
2058
+ }
1421
2059
 
1422
- //*****************************************************************
1423
- // internal variables
1424
- //*****************************************************************
2060
+ /**
2061
+ * Redirecting connection to server indicated value.
2062
+ * @param value server host string
2063
+ * @param resolve promise result when done
2064
+ */
2065
+ redirect(value, resolve) {
2066
+ if (this.opts.permitRedirect && value) {
2067
+ // redirect only if :
2068
+ // * when pipelining, having received all waiting responses.
2069
+ // * not in a transaction
2070
+ if (this.receiveQueue.length <= 1 && (this.info.status & ServerStatus.STATUS_IN_TRANS) === 0) {
2071
+ this.info.redirectRequest = null;
2072
+ const matchResults = value.match(redirectUrlFormat);
2073
+ if (!matchResults) {
2074
+ if (this.opts.logger.error)
2075
+ this.opts.logger.error(
2076
+ new Error(
2077
+ `error parsing redirection string '${value}'. format must be 'mariadb/mysql://[<user>[:<password>]@]<host>[:<port>]/[<db>[?<opt1>=<value1>[&<opt2>=<value2>]]]'`
2078
+ )
2079
+ );
2080
+ return resolve();
2081
+ }
1425
2082
 
1426
- EventEmitter.call(this);
1427
- const opts = Object.assign(new EventEmitter(), options);
1428
- const info = new ConnectionInformation();
1429
- const _sendQueue = new Queue();
1430
- const _receiveQueue = new Queue();
1431
- const _waitingAuthenticationQueue = new Queue();
1432
- let _status = Status.NOT_CONNECTED;
1433
- let _socketConnected = false;
1434
- let _socket = null;
1435
- let _timeout = null;
1436
- let _addCommand = _addCommandQueue;
1437
- const _fatalError = _fatalErrorHandler(this);
1438
- let _out = new PacketOutputStream(opts, info);
1439
- let _in = new PacketInputStream(_unexpectedPacket.bind(this), _receiveQueue, _out, opts, info);
1440
-
1441
- this.query = this._queryPromise;
1442
- this.escape = Utils.escape.bind(this, opts, info);
1443
- this.escapeId = Utils.escapeId.bind(this, opts, info);
1444
-
1445
- //add alias threadId for mysql/mysql2 compatibility
1446
- Object.defineProperty(this, 'threadId', {
1447
- get() {
1448
- return info ? info.threadId : undefined;
1449
- }
1450
- });
1451
- Object.defineProperty(this, 'info', {
1452
- get() {
1453
- return info;
1454
- }
1455
- });
2083
+ const options = {
2084
+ host: matchResults[7] ? decodeURIComponent(matchResults[7]) : matchResults[6],
2085
+ port: matchResults[9] ? parseInt(matchResults[9]) : 3306
2086
+ };
2087
+
2088
+ if (options.host === this.opts.host && options.port === this.opts.port) {
2089
+ // redirection to the same host, skip loop redirection
2090
+ return resolve();
2091
+ }
2092
+
2093
+ // actually only options accepted are user and password
2094
+ // there might be additional possible options in the future
2095
+ if (matchResults[3]) options.user = matchResults[3];
2096
+ if (matchResults[5]) options.password = matchResults[5];
2097
+
2098
+ const redirectOpts = ConnectionOptions.parseOptionDataType(options);
2099
+
2100
+ const finalRedirectOptions = new ConnOptions(Object.assign({}, this.opts, redirectOpts));
2101
+ const conn = new Connection(finalRedirectOptions);
2102
+ conn
2103
+ .connect()
2104
+ .then(
2105
+ async function () {
2106
+ await new Promise(this.end.bind(this, {}));
2107
+ this.status = Status.CONNECTED;
2108
+ this.info = conn.info;
2109
+ this.opts = conn.opts;
2110
+ this.socket = conn.socket;
2111
+ if (this.prepareCache) this.prepareCache.reset();
2112
+ this.streamOut = conn.streamOut;
2113
+ this.streamIn = conn.streamIn;
2114
+ resolve();
2115
+ }.bind(this)
2116
+ )
2117
+ .catch(
2118
+ function (e) {
2119
+ if (this.opts.logger.error) {
2120
+ const err = new Error(`fail to redirect to '${value}'`);
2121
+ err.cause = e;
2122
+ this.opts.logger.error(err);
2123
+ }
2124
+ resolve();
2125
+ }.bind(this)
2126
+ );
2127
+ } else {
2128
+ this.info.redirectRequest = value;
2129
+ resolve();
2130
+ }
2131
+ } else {
2132
+ this.info.redirectRequest = null;
2133
+ resolve();
2134
+ }
2135
+ }
2136
+
2137
+ get threadId() {
2138
+ return this.info ? this.info.threadId : null;
2139
+ }
2140
+
2141
+ _sendNextCmdImmediate() {
2142
+ if (!this.sendQueue.isEmpty()) {
2143
+ setImmediate(this.nextSendCmd.bind(this));
2144
+ }
2145
+ }
2146
+
2147
+ _closePrepare(prepareResultPacket) {
2148
+ this.addCommand(
2149
+ new ClosePrepare(
2150
+ {},
2151
+ () => {},
2152
+ () => {},
2153
+ prepareResultPacket
2154
+ ),
2155
+ false
2156
+ );
2157
+ }
2158
+
2159
+ _logAndReject(reject, err) {
2160
+ if (this.opts.logger.error) this.opts.logger.error(err);
2161
+ reject(err);
2162
+ }
1456
2163
  }
1457
2164
 
1458
- util.inherits(Connection, EventEmitter);
2165
+ class TestMethods {
2166
+ #collation;
2167
+ #socket;
2168
+
2169
+ constructor(collation, socket) {
2170
+ this.#collation = collation;
2171
+ this.#socket = socket;
2172
+ }
2173
+
2174
+ getCollation() {
2175
+ return this.#collation;
2176
+ }
2177
+
2178
+ getSocket() {
2179
+ return this.#socket;
2180
+ }
2181
+ }
1459
2182
 
1460
2183
  module.exports = Connection;