mysql2 3.24.4-canary.9178c82 → 3.24.4

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.
@@ -43,6 +43,49 @@ let _connectionId = 0;
43
43
 
44
44
  let convertNamedPlaceholders = null;
45
45
 
46
+ // Building a secure context parses every certificate in the ssl config
47
+ // (the bundled AWS profile alone carries over a hundred), which costs
48
+ // milliseconds per connection. The context is immutable once built, so
49
+ // pooled connections sharing one ssl config object share the context, as
50
+ // long as none of the material it was built from has been replaced.
51
+ const secureContexts = new WeakMap();
52
+
53
+ function getSecureContext(ssl) {
54
+ const cached = secureContexts.get(ssl);
55
+ if (
56
+ cached !== undefined &&
57
+ cached.ca === ssl.ca &&
58
+ cached.cert === ssl.cert &&
59
+ cached.ciphers === ssl.ciphers &&
60
+ cached.key === ssl.key &&
61
+ cached.passphrase === ssl.passphrase &&
62
+ cached.minVersion === ssl.minVersion &&
63
+ cached.maxVersion === ssl.maxVersion
64
+ ) {
65
+ return cached.secureContext;
66
+ }
67
+ const secureContext = Tls.createSecureContext({
68
+ ca: ssl.ca,
69
+ cert: ssl.cert,
70
+ ciphers: ssl.ciphers,
71
+ key: ssl.key,
72
+ passphrase: ssl.passphrase,
73
+ minVersion: ssl.minVersion,
74
+ maxVersion: ssl.maxVersion,
75
+ });
76
+ secureContexts.set(ssl, {
77
+ secureContext,
78
+ ca: ssl.ca,
79
+ cert: ssl.cert,
80
+ ciphers: ssl.ciphers,
81
+ key: ssl.key,
82
+ passphrase: ssl.passphrase,
83
+ minVersion: ssl.minVersion,
84
+ maxVersion: ssl.maxVersion,
85
+ });
86
+ return secureContext;
87
+ }
88
+
46
89
  class BaseConnection extends EventEmitter {
47
90
  constructor(opts) {
48
91
  super();
@@ -382,15 +425,7 @@ class BaseConnection extends EventEmitter {
382
425
  if (this.config.debug) {
383
426
  console.log('Upgrading connection to TLS');
384
427
  }
385
- const secureContext = Tls.createSecureContext({
386
- ca: this.config.ssl.ca,
387
- cert: this.config.ssl.cert,
388
- ciphers: this.config.ssl.ciphers,
389
- key: this.config.ssl.key,
390
- passphrase: this.config.ssl.passphrase,
391
- minVersion: this.config.ssl.minVersion,
392
- maxVersion: this.config.ssl.maxVersion,
393
- });
428
+ const secureContext = getSecureContext(this.config.ssl);
394
429
  const rejectUnauthorized = this.config.ssl.rejectUnauthorized;
395
430
  const verifyIdentity = this.config.ssl.verifyIdentity;
396
431
  const servername = Net.isIP(this.config.host)
@@ -5,6 +5,7 @@ const Timers = require('timers');
5
5
  const Command = require('./command.js');
6
6
  const Query = require('./query.js');
7
7
  const Packets = require('../packets/index.js');
8
+ const ConnectionConfig = require('../connection_config.js');
8
9
 
9
10
  const getBinaryParser = require('../parsers/binary_parser.js');
10
11
  const getStaticBinaryParser = require('../parsers/static_binary_parser.js');
@@ -45,7 +46,10 @@ class Execute extends Command {
45
46
 
46
47
  start(packet, connection) {
47
48
  this._connection = connection;
48
- this.options = Object.assign({}, connection.config, this._executeOptions);
49
+ this.options = ConnectionConfig.queryOptions(
50
+ connection.config,
51
+ this._executeOptions
52
+ );
49
53
  this._setTimeout();
50
54
  const clientFlags =
51
55
  connection.config.clientFlags & (connection.serverCapabilityFlags || 0);
@@ -10,6 +10,7 @@ const Packets = require('../packets/index.js');
10
10
  const getTextParser = require('../parsers/text_parser.js');
11
11
  const staticParser = require('../parsers/static_text_parser.js');
12
12
  const ServerStatus = require('../constants/server_status.js');
13
+ const ConnectionConfig = require('../connection_config.js');
13
14
 
14
15
  const EmptyPacket = new Packets.Packet(0, Buffer.allocUnsafe(4), 0, 4);
15
16
 
@@ -56,7 +57,10 @@ class Query extends Command {
56
57
  console.log(' Sending query command: %s', this.sql);
57
58
  }
58
59
  this._connection = connection;
59
- this.options = Object.assign({}, connection.config, this._queryOptions);
60
+ this.options = ConnectionConfig.queryOptions(
61
+ connection.config,
62
+ this._queryOptions
63
+ );
60
64
  this._setTimeout();
61
65
 
62
66
  const clientFlags =
@@ -6,6 +6,15 @@
6
6
  const zlib = require('zlib');
7
7
  const PacketParser = require('./packet_parser.js');
8
8
 
9
+ // the server sends payloads shorter than this uncompressed, and so does
10
+ // the client: deflate cannot shrink them
11
+ const MIN_COMPRESS_LENGTH = 50;
12
+ // zlib work below these sizes takes a few microseconds, less than the trip
13
+ // through the thread pool and the queue behind it, so it runs inline;
14
+ // larger payloads stay asynchronous and keep the event loop free
15
+ const MAX_SYNC_INFLATE_LENGTH = 16384;
16
+ const MAX_SYNC_DEFLATE_LENGTH = 4096;
17
+
9
18
  class Queue {
10
19
  constructor() {
11
20
  this._queue = [];
@@ -40,6 +49,28 @@ function handleCompressedPacket(packet) {
40
49
  // the queued (asynchronous) inflate step must be read out now
41
50
  const numPackets = packet.numPackets;
42
51
 
52
+ // an inline step is only in order while no earlier packet is still
53
+ // being inflated on the thread pool
54
+ if (!connection.inflateQueue._running) {
55
+ if (deflatedLength === 0) {
56
+ connection._bumpCompressedSequenceId(numPackets);
57
+ connection._inflatedPacketsParser.execute(body);
58
+ return;
59
+ }
60
+ if (deflatedLength <= MAX_SYNC_INFLATE_LENGTH) {
61
+ let data;
62
+ try {
63
+ data = zlib.inflateSync(body, { maxOutputLength: deflatedLength });
64
+ } catch (err) {
65
+ connection._handleNetworkError(err);
66
+ return;
67
+ }
68
+ connection._bumpCompressedSequenceId(numPackets);
69
+ connection._inflatedPacketsParser.execute(data);
70
+ return;
71
+ }
72
+ }
73
+
43
74
  if (deflatedLength !== 0) {
44
75
  connection.inflateQueue.push((task) => {
45
76
  zlib.inflate(body, { maxOutputLength: deflatedLength }, (err, data) => {
@@ -61,6 +92,31 @@ function handleCompressedPacket(packet) {
61
92
  }
62
93
  }
63
94
 
95
+ function writeCompressedFrame(
96
+ connection,
97
+ seqId,
98
+ packetLen,
99
+ buffer,
100
+ compressed
101
+ ) {
102
+ const compressHeader = Buffer.allocUnsafe(7);
103
+ const compressedLength = compressed === null ? packetLen : compressed.length;
104
+ if (compressed === null) {
105
+ // http://dev.mysql.com/doc/internals/en/uncompressed-payload.html
106
+ // To send an uncompressed payload:
107
+ // - set length of payload before compression to 0
108
+ // - the compressed payload contains the uncompressed payload instead.
109
+ packetLen = 0;
110
+ }
111
+ compressHeader.writeUInt8(compressedLength & 0xff, 0);
112
+ compressHeader.writeUInt16LE(compressedLength >> 8, 1);
113
+ compressHeader.writeUInt8(seqId, 3);
114
+ compressHeader.writeUInt8(packetLen & 0xff, 4);
115
+ compressHeader.writeUInt16LE(packetLen >> 8, 5);
116
+ connection.writeUncompressed(compressHeader);
117
+ connection.writeUncompressed(compressed === null ? buffer : compressed);
118
+ }
119
+
64
120
  function writeCompressed(buffer) {
65
121
  // http://dev.mysql.com/doc/internals/en/example-several-mysql-packets.html
66
122
  // note: sending a MySQL Packet of the size 2^24−5 to 2^24−1 via compression
@@ -82,49 +138,55 @@ function writeCompressed(buffer) {
82
138
 
83
139
  const connection = this;
84
140
 
85
- let packetLen = buffer.length;
86
- const compressHeader = Buffer.allocUnsafe(7);
141
+ const packetLen = buffer.length;
142
+ const seqId = connection.compressedSequenceId;
143
+ connection._bumpCompressedSequenceId(1);
144
+
145
+ // an inline write is only in order while no earlier packet is still
146
+ // being deflated on the thread pool
147
+ if (!connection.deflateQueue._running) {
148
+ if (packetLen < MIN_COMPRESS_LENGTH) {
149
+ writeCompressedFrame(connection, seqId, packetLen, buffer, null);
150
+ return;
151
+ }
152
+ if (packetLen <= MAX_SYNC_DEFLATE_LENGTH) {
153
+ let compressed;
154
+ try {
155
+ compressed = zlib.deflateSync(buffer);
156
+ } catch (err) {
157
+ connection._handleFatalError(err);
158
+ return;
159
+ }
160
+ writeCompressedFrame(
161
+ connection,
162
+ seqId,
163
+ packetLen,
164
+ buffer,
165
+ compressed.length < packetLen ? compressed : null
166
+ );
167
+ return;
168
+ }
169
+ }
87
170
 
88
171
  // seqqueue is used here because zlib async execution is routed via thread pool
89
172
  // internally and when we have multiple compressed packets arriving we need
90
173
  // to assemble uncompressed result sequentially
91
- (function (seqId) {
92
- connection.deflateQueue.push((task) => {
93
- zlib.deflate(buffer, (err, compressed) => {
94
- if (err) {
95
- connection._handleFatalError(err);
96
- return;
97
- }
98
- let compressedLength = compressed.length;
99
-
100
- if (compressedLength < packetLen) {
101
- compressHeader.writeUInt8(compressedLength & 0xff, 0);
102
- compressHeader.writeUInt16LE(compressedLength >> 8, 1);
103
- compressHeader.writeUInt8(seqId, 3);
104
- compressHeader.writeUInt8(packetLen & 0xff, 4);
105
- compressHeader.writeUInt16LE(packetLen >> 8, 5);
106
- connection.writeUncompressed(compressHeader);
107
- connection.writeUncompressed(compressed);
108
- } else {
109
- // http://dev.mysql.com/doc/internals/en/uncompressed-payload.html
110
- // To send an uncompressed payload:
111
- // - set length of payload before compression to 0
112
- // - the compressed payload contains the uncompressed payload instead.
113
- compressedLength = packetLen;
114
- packetLen = 0;
115
- compressHeader.writeUInt8(compressedLength & 0xff, 0);
116
- compressHeader.writeUInt16LE(compressedLength >> 8, 1);
117
- compressHeader.writeUInt8(seqId, 3);
118
- compressHeader.writeUInt8(packetLen & 0xff, 4);
119
- compressHeader.writeUInt16LE(packetLen >> 8, 5);
120
- connection.writeUncompressed(compressHeader);
121
- connection.writeUncompressed(buffer);
122
- }
123
- task.done();
124
- });
174
+ connection.deflateQueue.push((task) => {
175
+ zlib.deflate(buffer, (err, compressed) => {
176
+ if (err) {
177
+ connection._handleFatalError(err);
178
+ return;
179
+ }
180
+ writeCompressedFrame(
181
+ connection,
182
+ seqId,
183
+ packetLen,
184
+ buffer,
185
+ compressed.length < packetLen ? compressed : null
186
+ );
187
+ task.done();
125
188
  });
126
- })(connection.compressedSequenceId);
127
- connection._bumpCompressedSequenceId(1);
189
+ });
128
190
  }
129
191
 
130
192
  function enableCompression(connection) {
@@ -196,6 +196,65 @@ class ConnectionConfig {
196
196
  this.gracefulEnd = options.gracefulEnd || false;
197
197
  }
198
198
 
199
+ // Same result as Object.assign({}, config, overrides), built from a
200
+ // fixed-shape literal: one allocation instead of a transition per option,
201
+ // and every later options read stays a monomorphic in-object load. The
202
+ // key list mirrors the constructor above; test/unit/connection/
203
+ // test-query-options.test.mts fails when the two drift apart.
204
+ static queryOptions(config, overrides) {
205
+ if (!(config instanceof ConnectionConfig)) {
206
+ return Object.assign({}, config, overrides);
207
+ }
208
+ const options = {
209
+ isServer: config.isServer,
210
+ stream: config.stream,
211
+ host: config.host,
212
+ port: config.port,
213
+ localAddress: config.localAddress,
214
+ socketPath: config.socketPath,
215
+ user: config.user,
216
+ password: config.password,
217
+ password2: config.password2,
218
+ password3: config.password3,
219
+ passwordSha1: config.passwordSha1,
220
+ database: config.database,
221
+ connectTimeout: config.connectTimeout,
222
+ insecureAuth: config.insecureAuth,
223
+ infileStreamFactory: config.infileStreamFactory,
224
+ supportBigNumbers: config.supportBigNumbers,
225
+ bigNumberStrings: config.bigNumberStrings,
226
+ decimalNumbers: config.decimalNumbers,
227
+ dateStrings: config.dateStrings,
228
+ debug: config.debug,
229
+ trace: config.trace,
230
+ stringifyObjects: config.stringifyObjects,
231
+ enableKeepAlive: config.enableKeepAlive,
232
+ keepAliveInitialDelay: config.keepAliveInitialDelay,
233
+ timezone: config.timezone,
234
+ queryFormat: config.queryFormat,
235
+ pool: config.pool,
236
+ ssl: config.ssl,
237
+ multipleStatements: config.multipleStatements,
238
+ rowsAsArray: config.rowsAsArray,
239
+ namedPlaceholders: config.namedPlaceholders,
240
+ nestTables: config.nestTables,
241
+ typeCast: config.typeCast,
242
+ disableEval: config.disableEval,
243
+ enableCleartextPlugin: config.enableCleartextPlugin,
244
+ maxPacketSize: config.maxPacketSize,
245
+ charsetNumber: config.charsetNumber,
246
+ compress: config.compress,
247
+ authPlugins: config.authPlugins,
248
+ authSwitchHandler: config.authSwitchHandler,
249
+ clientFlags: config.clientFlags,
250
+ connectAttributes: config.connectAttributes,
251
+ maxPreparedStatements: config.maxPreparedStatements,
252
+ jsonStrings: config.jsonStrings,
253
+ gracefulEnd: config.gracefulEnd,
254
+ };
255
+ return Object.assign(options, overrides);
256
+ }
257
+
199
258
  static mergeFlags(default_flags, user_flags) {
200
259
  let flags = 0x0,
201
260
  i;
@@ -52,7 +52,7 @@ class ColumnDefinition {
52
52
  packet.skip(1); // length of the following fields (always 0x0c)
53
53
  this.characterSet = packet.readInt16();
54
54
  this.encoding = CharsetToEncoding[this.characterSet];
55
- this.name = StringParser.decode(
55
+ this.name = StringParser.decodeShort(
56
56
  this._buf,
57
57
  this.encoding === 'binary' ? this._clientEncoding : this.encoding,
58
58
  _nameStart,
@@ -9,6 +9,7 @@ const ErrorCodeToName = require('../constants/errors.js');
9
9
  const NativeBuffer = require('buffer').Buffer;
10
10
  const Long = require('long');
11
11
  const StringParser = require('../parsers/string.js');
12
+ const { localDate } = require('../parsers/local_date.js');
12
13
  const Types = require('../constants/types.js');
13
14
  const ZERO_DATE = '0000-00-00';
14
15
 
@@ -335,7 +336,7 @@ class Packet {
335
336
  if (timezone === 'Z') {
336
337
  return new Date(Date.UTC(y, m - 1, d, H, M, S, ms));
337
338
  }
338
- return new Date(y, m - 1, d, H, M, S, ms);
339
+ return localDate(y, m, d, H, M, S, ms);
339
340
  }
340
341
  let str = this.readDateTimeString(6, 'T', null);
341
342
  if (str.startsWith(ZERO_DATE)) {
@@ -441,6 +442,14 @@ class Packet {
441
442
  this.offset += len;
442
443
  // TODO: Use characterSetCode to get proper encoding
443
444
  // https://github.com/sidorares/node-mysql2/pull/374
445
+ if (len <= StringParser.SHORT_STRING_MAX_LENGTH) {
446
+ return StringParser.decodeShort(
447
+ this.buffer,
448
+ encoding,
449
+ this.offset - len,
450
+ this.offset
451
+ );
452
+ }
444
453
  return StringParser.decode(
445
454
  this.buffer,
446
455
  encoding,
@@ -735,7 +744,7 @@ class Packet {
735
744
  this.offset++; // -
736
745
  const d = this.parseInt(2);
737
746
  if (!timezone || timezone === 'local') {
738
- return new Date(y, m - 1, d);
747
+ return localDate(y, m, d, 0, 0, 0, 0);
739
748
  }
740
749
  if (timezone === 'Z') {
741
750
  return new Date(Date.UTC(y, m - 1, d));
@@ -797,7 +806,7 @@ class Packet {
797
806
  }
798
807
  this.offset += len;
799
808
  if (!timezone || timezone === 'local') {
800
- return new Date(y, mo - 1, d, h, mi, se, ms);
809
+ return localDate(y, mo, d, h, mi, se, ms);
801
810
  }
802
811
  const utc = Date.UTC(y, mo - 1, d, h, mi, se, ms);
803
812
  if (timezone === 'Z') {
@@ -6,6 +6,7 @@ const Types = require('../constants/types.js');
6
6
  const helpers = require('../helpers');
7
7
  const genFunc = require('generate-function');
8
8
  const parserCache = require('./parser_cache.js');
9
+ const LocalDate = require('./local_date.js');
9
10
  const typeNames = [];
10
11
  for (const t in Types) {
11
12
  typeNames[Types[t]] = t;
@@ -173,6 +174,9 @@ function compile(fields, options, config) {
173
174
  parserFn('(function(){');
174
175
  parserFn('return class BinaryRow {');
175
176
  parserFn('constructor() {');
177
+ if (fields.some((field) => LocalDate.usesLocalDate(field, options, config))) {
178
+ parserFn('LocalDate.checkTimezone();');
179
+ }
176
180
  parserFn('}');
177
181
 
178
182
  parserFn('next(packet, fields, options) {');
@@ -264,7 +268,7 @@ function compile(fields, options, config) {
264
268
  parserFn.toString()
265
269
  );
266
270
  }
267
- return parserFn.toFunction({ wrap, wrapNull });
271
+ return parserFn.toFunction({ wrap, wrapNull, LocalDate });
268
272
  }
269
273
 
270
274
  function getBinaryParser(fields, options, config) {
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const Types = require('../constants/types.js');
4
+ const helpers = require('../helpers.js');
5
+
6
+ // new Date(y, m, d, h, mi, s, ms) interprets its arguments as local wall
7
+ // time; the wall-time to UTC conversion inside that constructor dominates
8
+ // the cost of materializing a DATE, DATETIME or TIMESTAMP value. The
9
+ // conversion depends only on the wall-clock instant, and the offset it
10
+ // applies is constant across almost every hour (it changes only at DST
11
+ // transitions), so the offset is cached per wall-clock hour once the real
12
+ // constructor has confirmed it holds over the whole hour. Hours where it
13
+ // does not hold keep using the constructor.
14
+
15
+ const MS_PER_HOUR = 3600000;
16
+ const MS_PER_DAY = 86400000;
17
+ const MAX_CACHED_HOURS = 4096;
18
+
19
+ const hourOffsets = new Map();
20
+ let lastTimezone = readTimezone();
21
+
22
+ function readTimezone() {
23
+ try {
24
+ return process.env.TZ;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ // Node resets its own time zone cache when process.env.TZ is assigned;
31
+ // called once per result set so the offsets cached here follow suit
32
+ function checkTimezone() {
33
+ const timezone = readTimezone();
34
+ if (timezone !== lastTimezone) {
35
+ lastTimezone = timezone;
36
+ hourOffsets.clear();
37
+ }
38
+ }
39
+
40
+ // days since 1970-01-01 in the proleptic Gregorian calendar, the same value
41
+ // ECMAScript's MakeDay produces for a month in 1..12 and any integer day
42
+ function daysFromCivil(year, month, day) {
43
+ const y = month <= 2 ? year - 1 : year;
44
+ const era = Math.floor(y / 400);
45
+ const yearOfEra = y - era * 400;
46
+ const dayOfYear = (((153 * ((month + 9) % 12) + 2) / 5) | 0) + day - 1;
47
+ const dayOfEra =
48
+ yearOfEra * 365 +
49
+ ((yearOfEra / 4) | 0) -
50
+ ((yearOfEra / 100) | 0) +
51
+ dayOfYear;
52
+ return era * 146097 + dayOfEra - 719468;
53
+ }
54
+
55
+ function localTime(wallTime) {
56
+ const wall = new Date(wallTime);
57
+ return new Date(
58
+ wall.getUTCFullYear(),
59
+ wall.getUTCMonth(),
60
+ wall.getUTCDate(),
61
+ wall.getUTCHours(),
62
+ wall.getUTCMinutes(),
63
+ wall.getUTCSeconds(),
64
+ wall.getUTCMilliseconds()
65
+ ).getTime();
66
+ }
67
+
68
+ // the offset is trusted for the hour only when the constructor applies the
69
+ // same one at its start, middle and end; null marks an hour that contains a
70
+ // transition
71
+ function proveHourOffset(hour) {
72
+ const start = hour * MS_PER_HOUR;
73
+ const offset = start - localTime(start);
74
+ if (
75
+ offset !== start + MS_PER_HOUR / 2 - localTime(start + MS_PER_HOUR / 2) ||
76
+ offset !== start + MS_PER_HOUR - 1 - localTime(start + MS_PER_HOUR - 1) ||
77
+ Number.isNaN(offset)
78
+ ) {
79
+ return null;
80
+ }
81
+ return offset;
82
+ }
83
+
84
+ // same value as new Date(year, month - 1, day, hours, minutes, seconds,
85
+ // milliseconds), for the integer arguments the wire protocols carry
86
+ function localDate(year, month, day, hours, minutes, seconds, milliseconds) {
87
+ if (year < 100 || month < 1 || month > 12) {
88
+ return new Date(
89
+ year,
90
+ month - 1,
91
+ day,
92
+ hours,
93
+ minutes,
94
+ seconds,
95
+ milliseconds
96
+ );
97
+ }
98
+ const wallTime =
99
+ daysFromCivil(year, month, day) * MS_PER_DAY +
100
+ hours * MS_PER_HOUR +
101
+ minutes * 60000 +
102
+ seconds * 1000 +
103
+ Math.trunc(milliseconds);
104
+ const hour = Math.floor(wallTime / MS_PER_HOUR);
105
+ let offset = hourOffsets.get(hour);
106
+ if (offset === undefined) {
107
+ if (hourOffsets.size >= MAX_CACHED_HOURS) {
108
+ hourOffsets.clear();
109
+ }
110
+ offset = proveHourOffset(hour);
111
+ hourOffsets.set(hour, offset);
112
+ }
113
+ if (offset === null) {
114
+ return new Date(
115
+ year,
116
+ month - 1,
117
+ day,
118
+ hours,
119
+ minutes,
120
+ seconds,
121
+ milliseconds
122
+ );
123
+ }
124
+ return new Date(wallTime - offset);
125
+ }
126
+
127
+ // whether a column's values are built through localDate, so the parser
128
+ // for a result set knows to call checkTimezone first
129
+ function usesLocalDate(field, options, config) {
130
+ const timezone = options.timezone || config.timezone;
131
+ if (timezone && timezone !== 'local') {
132
+ return false;
133
+ }
134
+ const type = field.columnType;
135
+ if (
136
+ type !== Types.DATE &&
137
+ type !== Types.DATETIME &&
138
+ type !== Types.TIMESTAMP &&
139
+ type !== Types.NEWDATE
140
+ ) {
141
+ return false;
142
+ }
143
+ return !helpers.typeMatch(
144
+ type,
145
+ options.dateStrings || config.dateStrings,
146
+ Types
147
+ );
148
+ }
149
+
150
+ module.exports = {
151
+ localDate,
152
+ checkTimezone,
153
+ usesLocalDate,
154
+ _daysFromCivil: daysFromCivil,
155
+ _clear() {
156
+ hourOffsets.clear();
157
+ },
158
+ };
@@ -6,58 +6,223 @@ const parserCache = createLRU({
6
6
  max: 15000,
7
7
  });
8
8
 
9
- // the fixed-size options head keeps JSON.stringify (cheap for a flat array,
10
- // and it normalizes exotic values the same way as before); the per-field part
11
- // is built by hand with length-prefixed strings, so no delimiter collision is
12
- // possible and no throwaway nested arrays are allocated per query
13
- function appendString(key, value) {
14
- if (value === undefined || value === null) {
15
- return `${key}u/`;
9
+ // A compiled row parser depends on the parsing options and, per column, on
10
+ // exactly the metadata the code generators bake into the function: type,
11
+ // charset, flags, decimals, name, MariaDB extended metadata, and the table
12
+ // name when rows are nested by table. The cache is keyed by an integer hash
13
+ // of that data and every hit is confirmed against the full metadata kept in
14
+ // the entry, so a hash collision can never hand out a parser compiled for
15
+ // different columns or options.
16
+
17
+ const TYPE_CAST_FALSE = 1;
18
+ const TYPE_CAST_FUNCTION = 2;
19
+ const TYPE_CAST_DEFAULT = 3;
20
+
21
+ function typeCastKind(typeCast) {
22
+ if (typeCast === false) {
23
+ return TYPE_CAST_FALSE;
24
+ }
25
+ if (typeof typeCast === 'function') {
26
+ return TYPE_CAST_FUNCTION;
27
+ }
28
+ return TYPE_CAST_DEFAULT;
29
+ }
30
+
31
+ // helpers.typeMatch reads an array by the type names it lists and anything
32
+ // else by truthiness
33
+ function dateStringsKey(dateStrings) {
34
+ if (Array.isArray(dateStrings)) {
35
+ return dateStrings.map(String);
36
+ }
37
+ return Boolean(dateStrings);
38
+ }
39
+
40
+ function nestTablesKey(nestTables) {
41
+ if (typeof nestTables === 'string') {
42
+ return nestTables;
16
43
  }
17
- return `${key}${value.length}#${value}/`;
44
+ return Boolean(nestTables);
18
45
  }
19
46
 
20
- function keyFromFields(type, fields, options, config) {
21
- let key = JSON.stringify([
22
- type,
23
- typeof options.nestTables,
24
- options.nestTables,
25
- Boolean(options.rowsAsArray),
26
- Boolean(options.supportBigNumbers || config.supportBigNumbers),
27
- Boolean(options.bigNumberStrings || config.bigNumberStrings),
28
- typeof options.typeCast === 'boolean'
29
- ? options.typeCast
30
- : typeof options.typeCast,
31
- options.timezone || config.timezone,
32
- Boolean(options.decimalNumbers),
33
- options.dateStrings,
34
- Boolean(config.jsonStrings),
35
- ]);
47
+ function optionBits(type, options, config, nestTables, dateStrings) {
48
+ return (
49
+ (type === 'binary' ? 1 : 0) |
50
+ (options.rowsAsArray ? 2 : 0) |
51
+ (options.supportBigNumbers || config.supportBigNumbers ? 4 : 0) |
52
+ (options.bigNumberStrings || config.bigNumberStrings ? 8 : 0) |
53
+ (typeCastKind(options.typeCast) << 4) |
54
+ (options.decimalNumbers ? 64 : 0) |
55
+ (config.jsonStrings ? 128 : 0) |
56
+ (nestTables === true ? 256 : 0) |
57
+ (typeof nestTables === 'string' ? 512 : 0) |
58
+ (dateStrings === true ? 1024 : 0) |
59
+ (Array.isArray(dateStrings) ? 2048 : 0)
60
+ );
61
+ }
36
62
 
63
+ function mixNumber(hash, value) {
64
+ return Math.imul(hash ^ (value | 0), 0x9e3779b1) | 0;
65
+ }
66
+
67
+ function mixString(hash, string) {
68
+ if (typeof string !== 'string') {
69
+ return mixNumber(hash, string === undefined ? -1 : -2);
70
+ }
71
+ hash = mixNumber(hash, string.length);
72
+ for (let i = 0; i < string.length; ++i) {
73
+ hash = (Math.imul(hash, 31) + string.charCodeAt(i)) | 0;
74
+ }
75
+ return hash;
76
+ }
77
+
78
+ function hashKey(type, fields, options, config) {
79
+ const nestTables = nestTablesKey(options.nestTables);
80
+ const dateStrings = dateStringsKey(options.dateStrings || config.dateStrings);
81
+ const includeTable = nestTables !== false;
82
+ let hash = mixNumber(
83
+ 0x811c9dc5,
84
+ optionBits(type, options, config, nestTables, dateStrings)
85
+ );
86
+ hash = mixString(hash, String(options.timezone || config.timezone));
87
+ if (typeof nestTables === 'string') {
88
+ hash = mixString(hash, nestTables);
89
+ }
90
+ if (Array.isArray(dateStrings)) {
91
+ for (let i = 0; i < dateStrings.length; ++i) {
92
+ hash = mixString(hash, dateStrings[i]);
93
+ }
94
+ }
95
+ hash = mixNumber(hash, fields.length);
37
96
  for (let i = 0; i < fields.length; ++i) {
38
97
  const field = fields[i];
39
- key = appendString(key, field.name);
40
- key += `${field.columnType}/${field.length}/`;
41
- key = appendString(key, field.schema);
42
- key = appendString(key, field.table);
43
- key += `${field.flags}/${field.characterSet}/`;
44
- key = appendString(key, field.extendedTypeName);
45
- key = appendString(key, field.extendedFormat);
98
+ hash = mixNumber(
99
+ hash,
100
+ field.columnType | (field.characterSet << 8) | (field.decimals << 24)
101
+ );
102
+ hash = mixNumber(hash, field.flags);
103
+ hash = mixString(hash, field.name);
104
+ if (field.extendedTypeName !== undefined) {
105
+ hash = mixString(hash, field.extendedTypeName);
106
+ }
107
+ if (field.extendedFormat !== undefined) {
108
+ hash = mixString(hash, field.extendedFormat);
109
+ }
110
+ if (includeTable) {
111
+ hash = mixString(hash, field.table);
112
+ }
46
113
  }
114
+ // small non-negative integers keep the Map keys as Smis
115
+ return hash & 0x3fffffff;
116
+ }
47
117
 
48
- return key;
118
+ function sameList(a, b) {
119
+ if (a.length !== b.length) {
120
+ return false;
121
+ }
122
+ for (let i = 0; i < a.length; ++i) {
123
+ if (a[i] !== b[i]) {
124
+ return false;
125
+ }
126
+ }
127
+ return true;
49
128
  }
50
129
 
51
- function getParser(type, fields, options, config, compiler) {
52
- const key = keyFromFields(type, fields, options, config);
53
- let parser = parserCache.get(key);
130
+ class Entry {
131
+ constructor(parser, type, fields, options, config) {
132
+ this.parser = parser;
133
+ this.nestTables = nestTablesKey(options.nestTables);
134
+ this.dateStrings = dateStringsKey(
135
+ options.dateStrings || config.dateStrings
136
+ );
137
+ this.optionBits = optionBits(
138
+ type,
139
+ options,
140
+ config,
141
+ this.nestTables,
142
+ this.dateStrings
143
+ );
144
+ this.timezone = String(options.timezone || config.timezone);
145
+ const count = fields.length;
146
+ this.columnTypes = new Array(count);
147
+ this.characterSets = new Array(count);
148
+ this.flags = new Array(count);
149
+ this.decimals = new Array(count);
150
+ this.names = new Array(count);
151
+ this.extendedTypeNames = new Array(count);
152
+ this.extendedFormats = new Array(count);
153
+ this.tables = this.nestTables === false ? null : new Array(count);
154
+ for (let i = 0; i < count; ++i) {
155
+ const field = fields[i];
156
+ this.columnTypes[i] = field.columnType;
157
+ this.characterSets[i] = field.characterSet;
158
+ this.flags[i] = field.flags;
159
+ this.decimals[i] = field.decimals;
160
+ this.names[i] = field.name;
161
+ this.extendedTypeNames[i] = field.extendedTypeName;
162
+ this.extendedFormats[i] = field.extendedFormat;
163
+ if (this.tables !== null) {
164
+ this.tables[i] = field.table;
165
+ }
166
+ }
167
+ }
54
168
 
55
- if (parser) {
56
- return parser;
169
+ matches(type, fields, options, config) {
170
+ const nestTables = nestTablesKey(options.nestTables);
171
+ const dateStrings = dateStringsKey(
172
+ options.dateStrings || config.dateStrings
173
+ );
174
+ if (
175
+ this.optionBits !==
176
+ optionBits(type, options, config, nestTables, dateStrings) ||
177
+ this.nestTables !== nestTables ||
178
+ this.timezone !== String(options.timezone || config.timezone) ||
179
+ this.names.length !== fields.length
180
+ ) {
181
+ return false;
182
+ }
183
+ // equal option bits already guarantee both sides are lists (or the
184
+ // same boolean), so only the list contents are left to compare
185
+ if (
186
+ Array.isArray(dateStrings) &&
187
+ !sameList(this.dateStrings, dateStrings)
188
+ ) {
189
+ return false;
190
+ }
191
+ for (let i = 0; i < fields.length; ++i) {
192
+ const field = fields[i];
193
+ if (
194
+ this.columnTypes[i] !== field.columnType ||
195
+ this.characterSets[i] !== field.characterSet ||
196
+ this.flags[i] !== field.flags ||
197
+ this.decimals[i] !== field.decimals ||
198
+ this.names[i] !== field.name ||
199
+ this.extendedTypeNames[i] !== field.extendedTypeName ||
200
+ this.extendedFormats[i] !== field.extendedFormat ||
201
+ (this.tables !== null && this.tables[i] !== field.table)
202
+ ) {
203
+ return false;
204
+ }
205
+ }
206
+ return true;
57
207
  }
208
+ }
58
209
 
59
- parser = compiler(fields, options, config);
60
- parserCache.set(key, parser);
210
+ function getParser(type, fields, options, config, compiler) {
211
+ const hash = hashKey(type, fields, options, config);
212
+ let entries = parserCache.get(hash);
213
+ if (entries !== undefined) {
214
+ for (let i = 0; i < entries.length; ++i) {
215
+ if (entries[i].matches(type, fields, options, config)) {
216
+ return entries[i].parser;
217
+ }
218
+ }
219
+ }
220
+ const parser = compiler(fields, options, config);
221
+ if (entries === undefined) {
222
+ entries = [];
223
+ parserCache.set(hash, entries);
224
+ }
225
+ entries.push(new Entry(parser, type, fields, options, config));
61
226
  return parser;
62
227
  }
63
228
 
@@ -73,5 +238,6 @@ module.exports = {
73
238
  getParser: getParser,
74
239
  setMaxCache: setMaxCache,
75
240
  clearCache: clearCache,
76
- _keyFromFields: keyFromFields,
241
+ _hashKey: hashKey,
242
+ _Entry: Entry,
77
243
  };
@@ -4,13 +4,19 @@ const FieldFlags = require('../constants/field_flags.js');
4
4
  const Charsets = require('../constants/charsets.js');
5
5
  const Types = require('../constants/types.js');
6
6
  const helpers = require('../helpers');
7
+ const LocalDate = require('./local_date.js');
7
8
 
8
9
  const typeNames = [];
9
10
  for (const t in Types) {
10
11
  typeNames[Types[t]] = t;
11
12
  }
12
13
 
13
- function getBinaryParser(fields, _options, config) {
14
+ function getBinaryParser(fields, queryOptions, config) {
15
+ if (
16
+ fields.some((field) => LocalDate.usesLocalDate(field, queryOptions, config))
17
+ ) {
18
+ LocalDate.checkTimezone();
19
+ }
14
20
  function readCode(field, config, options, fieldNum, packet) {
15
21
  const supportBigNumbers = Boolean(
16
22
  options.supportBigNumbers || config.supportBigNumbers
@@ -3,6 +3,7 @@
3
3
  const Types = require('../constants/types.js');
4
4
  const Charsets = require('../constants/charsets.js');
5
5
  const helpers = require('../helpers');
6
+ const LocalDate = require('./local_date.js');
6
7
 
7
8
  const typeNames = [];
8
9
  for (const t in Types) {
@@ -118,7 +119,10 @@ function createTypecastField(field, packet) {
118
119
  };
119
120
  }
120
121
 
121
- function getTextParser(_fields, _options, config) {
122
+ function getTextParser(fields, options, config) {
123
+ if (fields.some((field) => LocalDate.usesLocalDate(field, options, config))) {
124
+ LocalDate.checkTimezone();
125
+ }
122
126
  return {
123
127
  next(packet, fields, options) {
124
128
  const result = options.rowsAsArray ? [] : {};
@@ -19,6 +19,120 @@ const hasFastSlices =
19
19
  // buffer.write(); same stability story as the slice methods above
20
20
  exports.hasFastUtf8Write = typeof Buffer.prototype.utf8Write === 'function';
21
21
 
22
+ // A native slice costs a fixed ~50ns per call, mostly the JS to C++
23
+ // transition, while String.fromCharCode with a handful of arguments stays
24
+ // inside V8 at a fraction of that. Every ASCII byte decodes to the same
25
+ // code unit under utf8, ascii and latin1, so short all-ASCII values skip
26
+ // the transition; anything else returns null and takes the native path.
27
+ function shortAscii(b, s, length) {
28
+ if (b[s] >= 128) {
29
+ return null;
30
+ }
31
+ switch (length) {
32
+ case 1: {
33
+ const c0 = b[s];
34
+ return c0 < 128 ? String.fromCharCode(c0) : null;
35
+ }
36
+ case 2: {
37
+ const c0 = b[s];
38
+ const c1 = b[s + 1];
39
+ return (c0 | c1) < 128 ? String.fromCharCode(c0, c1) : null;
40
+ }
41
+ case 3: {
42
+ const c0 = b[s];
43
+ const c1 = b[s + 1];
44
+ const c2 = b[s + 2];
45
+ return (c0 | c1 | c2) < 128 ? String.fromCharCode(c0, c1, c2) : null;
46
+ }
47
+ case 4: {
48
+ const c0 = b[s];
49
+ const c1 = b[s + 1];
50
+ const c2 = b[s + 2];
51
+ const c3 = b[s + 3];
52
+ return (c0 | c1 | c2 | c3) < 128
53
+ ? String.fromCharCode(c0, c1, c2, c3)
54
+ : null;
55
+ }
56
+ case 5: {
57
+ const c0 = b[s];
58
+ const c1 = b[s + 1];
59
+ const c2 = b[s + 2];
60
+ const c3 = b[s + 3];
61
+ const c4 = b[s + 4];
62
+ return (c0 | c1 | c2 | c3 | c4) < 128
63
+ ? String.fromCharCode(c0, c1, c2, c3, c4)
64
+ : null;
65
+ }
66
+ case 6: {
67
+ const c0 = b[s];
68
+ const c1 = b[s + 1];
69
+ const c2 = b[s + 2];
70
+ const c3 = b[s + 3];
71
+ const c4 = b[s + 4];
72
+ const c5 = b[s + 5];
73
+ return (c0 | c1 | c2 | c3 | c4 | c5) < 128
74
+ ? String.fromCharCode(c0, c1, c2, c3, c4, c5)
75
+ : null;
76
+ }
77
+ case 7: {
78
+ const c0 = b[s];
79
+ const c1 = b[s + 1];
80
+ const c2 = b[s + 2];
81
+ const c3 = b[s + 3];
82
+ const c4 = b[s + 4];
83
+ const c5 = b[s + 5];
84
+ const c6 = b[s + 6];
85
+ return (c0 | c1 | c2 | c3 | c4 | c5 | c6) < 128
86
+ ? String.fromCharCode(c0, c1, c2, c3, c4, c5, c6)
87
+ : null;
88
+ }
89
+ case 8: {
90
+ const c0 = b[s];
91
+ const c1 = b[s + 1];
92
+ const c2 = b[s + 2];
93
+ const c3 = b[s + 3];
94
+ const c4 = b[s + 4];
95
+ const c5 = b[s + 5];
96
+ const c6 = b[s + 6];
97
+ const c7 = b[s + 7];
98
+ return (c0 | c1 | c2 | c3 | c4 | c5 | c6 | c7) < 128
99
+ ? String.fromCharCode(c0, c1, c2, c3, c4, c5, c6, c7)
100
+ : null;
101
+ }
102
+ default:
103
+ return null;
104
+ }
105
+ }
106
+
107
+ exports.SHORT_STRING_MAX_LENGTH = 8;
108
+
109
+ // decode() for values of at most SHORT_STRING_MAX_LENGTH bytes, with the
110
+ // ASCII shortcut in front of the native slice; kept apart from decode() so
111
+ // the long-value path stays as small and inlinable as before
112
+ exports.decodeShort = function (buffer, encoding, start, end) {
113
+ if (hasFastSlices && start >= 0 && start <= end && end <= buffer.length) {
114
+ switch (encoding) {
115
+ case 'utf8':
116
+ case 'utf-8': {
117
+ const short = shortAscii(buffer, start, end - start);
118
+ return short !== null ? short : buffer.utf8Slice(start, end);
119
+ }
120
+ case 'latin1':
121
+ case 'binary': {
122
+ const short = shortAscii(buffer, start, end - start);
123
+ return short !== null ? short : buffer.latin1Slice(start, end);
124
+ }
125
+ case 'ascii': {
126
+ const short = shortAscii(buffer, start, end - start);
127
+ return short !== null ? short : buffer.asciiSlice(start, end);
128
+ }
129
+ default:
130
+ break;
131
+ }
132
+ }
133
+ return exports.decode(buffer, encoding, start, end);
134
+ };
135
+
22
136
  exports.decode = function (buffer, encoding, start, end, options) {
23
137
  if (hasFastSlices) {
24
138
  // replicate buffer.toString() bounds coercion exactly (the *Slice
@@ -5,6 +5,7 @@ const Charsets = require('../constants/charsets.js');
5
5
  const helpers = require('../helpers');
6
6
  const genFunc = require('generate-function');
7
7
  const parserCache = require('./parser_cache.js');
8
+ const LocalDate = require('./local_date.js');
8
9
 
9
10
  const typeNames = [];
10
11
  for (const t in Types) {
@@ -139,6 +140,9 @@ function compile(fields, options, config) {
139
140
  parserFn('this[`wrap${i}`] = wrap(fields[i], _this);');
140
141
  parserFn('}');
141
142
  }
143
+ if (fields.some((field) => LocalDate.usesLocalDate(field, options, config))) {
144
+ parserFn('LocalDate.checkTimezone();');
145
+ }
142
146
  parserFn('}');
143
147
 
144
148
  // next method
@@ -207,10 +211,7 @@ function compile(fields, options, config) {
207
211
  parserFn.toString()
208
212
  );
209
213
  }
210
- if (typeof options.typeCast === 'function') {
211
- return parserFn.toFunction({ wrap });
212
- }
213
- return parserFn.toFunction();
214
+ return parserFn.toFunction({ wrap, LocalDate });
214
215
  }
215
216
 
216
217
  function getTextParser(fields, options, config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysql2",
3
- "version": "3.24.4-canary.9178c82",
3
+ "version": "3.24.4",
4
4
  "description": "fast mysql driver. Implements core protocol, prepared statements, ssl and compression in native JS",
5
5
  "main": "index.js",
6
6
  "typings": "typings/mysql/index",
@@ -20,6 +20,11 @@
20
20
  "test:coverage": "c8 npm test",
21
21
  "test:build": "rollup -c",
22
22
  "typecheck": "cd \"test/tsc-build\" && tsc -p \"tsconfig.json\" && cd .. && tsc -p \"tsconfig.json\" --noEmit",
23
+ "src:typecheck": "tsc -p src/tsconfig.json --noEmit && tsc -p tools/src/tsconfig.json",
24
+ "src:build": "tsx tools/src/clean.mts && tsc -p src/tsconfig.json",
25
+ "src:test": "npm run src:build && tsx tools/src/prepare-tests.mts && poku --config=tools/src/poku.config.mts",
26
+ "src:test:bun": "npm run src:build && tsx tools/src/prepare-tests.mts && bun poku --config=tools/src/poku.config.mts",
27
+ "src:test:deno": "npm run src:build && tsx tools/src/prepare-tests.mts && deno run -A npm:poku --config=tools/src/poku.config.mts",
23
28
  "benchmark": "node ./benchmarks/benchmark.js",
24
29
  "wait-port": "wait-on"
25
30
  },