mysql2 3.7.1 → 3.9.0

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.
@@ -279,7 +279,7 @@ class Query extends Command {
279
279
  });
280
280
  this.on('end', () => {
281
281
  stream.push(null); // pushing null, indicating EOF
282
- stream.emit('close'); // notify readers that query has completed
282
+ setImmediate(() => stream.emit('close')); // notify readers that query has completed
283
283
  });
284
284
  this.on('fields', fields => {
285
285
  stream.emit('fields', fields); // replicate old emitter
@@ -80,12 +80,35 @@ function readCodeFor(field, config, options, fieldNum) {
80
80
 
81
81
  function compile(fields, options, config) {
82
82
  const parserFn = genFunc();
83
- let i = 0;
84
83
  const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
85
84
 
86
- /* eslint-disable no-trailing-spaces */
87
- /* eslint-disable no-spaced-func */
88
- /* eslint-disable no-unexpected-multiline */
85
+ function wrap(field, packet) {
86
+ return {
87
+ type: typeNames[field.columnType],
88
+ length: field.columnLength,
89
+ db: field.schema,
90
+ table: field.table,
91
+ name: field.name,
92
+ string: function (encoding = field.encoding) {
93
+ if (field.columnType === Types.JSON && encoding === field.encoding) {
94
+ // Since for JSON columns mysql always returns charset 63 (BINARY),
95
+ // we have to handle it according to JSON specs and use "utf8",
96
+ // see https://github.com/sidorares/node-mysql2/issues/1661
97
+ console.warn(
98
+ `typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``,
99
+ );
100
+ }
101
+
102
+ return packet.readLengthCodedString(encoding);
103
+ },
104
+ buffer: function () {
105
+ return packet.readLengthCodedBuffer();
106
+ },
107
+ geometry: function () {
108
+ return packet.parseGeometryValue();
109
+ },
110
+ };
111
+ }
89
112
 
90
113
  parserFn('(function(){');
91
114
  parserFn('return class BinaryRow {');
@@ -96,24 +119,19 @@ function compile(fields, options, config) {
96
119
  if (options.rowsAsArray) {
97
120
  parserFn(`const result = new Array(${fields.length});`);
98
121
  } else {
99
- parserFn("const result = {};");
122
+ parserFn('const result = {};');
100
123
  }
101
124
 
102
- const resultTables = {};
103
- let resultTablesArray = [];
104
-
105
- if (options.nestTables === true) {
106
- for (i = 0; i < fields.length; i++) {
107
- resultTables[fields[i].table] = 1;
108
- }
109
- resultTablesArray = Object.keys(resultTables);
110
- for (i = 0; i < resultTablesArray.length; i++) {
111
- parserFn(`result[${helpers.srcEscape(resultTablesArray[i])}] = {};`);
112
- }
125
+ // Global typeCast
126
+ if (
127
+ typeof config.typeCast === 'function' &&
128
+ typeof options.typeCast !== 'function'
129
+ ) {
130
+ options.typeCast = config.typeCast;
113
131
  }
114
132
 
115
133
  parserFn('packet.readInt8();'); // status byte
116
- for (i = 0; i < nullBitmapLength; ++i) {
134
+ for (let i = 0; i < nullBitmapLength; ++i) {
117
135
  parserFn(`const nullBitmaskByte${i} = packet.readInt8();`);
118
136
  }
119
137
 
@@ -123,38 +141,44 @@ function compile(fields, options, config) {
123
141
  let fieldName = '';
124
142
  let tableName = '';
125
143
 
126
- for (i = 0; i < fields.length; i++) {
144
+ for (let i = 0; i < fields.length; i++) {
127
145
  fieldName = helpers.srcEscape(fields[i].name);
128
146
  parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`);
129
147
 
130
148
  if (typeof options.nestTables === 'string') {
131
- tableName = helpers.srcEscape(fields[i].table);
132
149
  lvalue = `result[${helpers.srcEscape(
133
- fields[i].table + options.nestTables + fields[i].name
150
+ fields[i].table + options.nestTables + fields[i].name,
134
151
  )}]`;
135
152
  } else if (options.nestTables === true) {
136
153
  tableName = helpers.srcEscape(fields[i].table);
154
+ parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`);
137
155
  lvalue = `result[${tableName}][${fieldName}]`;
138
156
  } else if (options.rowsAsArray) {
139
157
  lvalue = `result[${i.toString(10)}]`;
140
158
  } else {
141
- lvalue = `result[${helpers.srcEscape(fields[i].name)}]`;
159
+ lvalue = `result[${fieldName}]`;
160
+ }
161
+
162
+ if (options.typeCast === false) {
163
+ parserFn(`${lvalue} = packet.readLengthCodedBuffer();`);
164
+ } else {
165
+ const fieldWrapperVar = `fieldWrapper${i}`;
166
+ parserFn(`const ${fieldWrapperVar} = wrap(fields[${i}], packet);`);
167
+ const readCode = readCodeFor(fields[i], config, options, i);
168
+
169
+ parserFn(`if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit})`);
170
+ parserFn(`${lvalue} = null;`);
171
+ parserFn('else {');
172
+ if (typeof options.typeCast === 'function') {
173
+ parserFn(
174
+ `${lvalue} = options.typeCast(${fieldWrapperVar}, function() { return ${readCode} });`,
175
+ );
176
+ } else {
177
+ parserFn(`${lvalue} = ${readCode};`);
178
+ }
179
+ parserFn('}');
142
180
  }
143
181
 
144
- // TODO: this used to be an optimisation ( if column marked as NOT_NULL don't include code to check null
145
- // bitmap at all, but it seems that we can't rely on this flag, see #178
146
- // TODO: benchmark performance difference
147
- //
148
- // if (fields[i].flags & FieldFlags.NOT_NULL) { // don't need to check null bitmap if field can't be null.
149
- // result.push(lvalue + ' = ' + readCodeFor(fields[i], config));
150
- // } else if (fields[i].columnType == Types.NULL) {
151
- // result.push(lvalue + ' = null;');
152
- // } else {
153
- parserFn(`if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit})`);
154
- parserFn(`${lvalue} = null;`);
155
- parserFn('else');
156
- parserFn(`${lvalue} = ${readCodeFor(fields[i], config, options, i)}`);
157
- // }
158
182
  currentFieldNullBit *= 2;
159
183
  if (currentFieldNullBit === 0x100) {
160
184
  currentFieldNullBit = 1;
@@ -166,17 +190,13 @@ function compile(fields, options, config) {
166
190
  parserFn('}');
167
191
  parserFn('};')('})()');
168
192
 
169
- /* eslint-enable no-trailing-spaces */
170
- /* eslint-enable no-spaced-func */
171
- /* eslint-enable no-unexpected-multiline */
172
-
173
193
  if (config.debug) {
174
194
  helpers.printDebugWithCode(
175
195
  'Compiled binary protocol row parser',
176
- parserFn.toString()
196
+ parserFn.toString(),
177
197
  );
178
198
  }
179
- return parserFn.toFunction();
199
+ return parserFn.toFunction({ wrap });
180
200
  }
181
201
 
182
202
  function getBinaryParser(fields, options, config) {
@@ -1,13 +1,34 @@
1
1
  'use strict';
2
2
 
3
3
  const Iconv = require('iconv-lite');
4
+ const LRU = require('lru-cache').default;
4
5
 
5
- exports.decode = function(buffer, encoding, start, end, options) {
6
+ const decoderCache = new LRU({
7
+ max: 500,
8
+ });
9
+
10
+ exports.decode = function (buffer, encoding, start, end, options) {
6
11
  if (Buffer.isEncoding(encoding)) {
7
12
  return buffer.toString(encoding, start, end);
8
13
  }
9
14
 
10
- const decoder = Iconv.getDecoder(encoding, options || {});
15
+ // Optimize for common case: encoding="short_string", options=undefined.
16
+ let decoder;
17
+ if (!options) {
18
+ decoder = decoderCache.get(encoding);
19
+ if (!decoder) {
20
+ decoder = Iconv.getDecoder(encoding);
21
+ decoderCache.set(encoding, decoder);
22
+ }
23
+ } else {
24
+ const decoderArgs = { encoding, options };
25
+ const decoderKey = JSON.stringify(decoderArgs);
26
+ decoder = decoderCache.get(decoderKey);
27
+ if (!decoder) {
28
+ decoder = Iconv.getDecoder(decoderArgs.encoding, decoderArgs.options);
29
+ decoderCache.set(decoderKey, decoder);
30
+ }
31
+ }
11
32
 
12
33
  const res = decoder.write(buffer.slice(start, end));
13
34
  const trail = decoder.end();
@@ -15,7 +36,7 @@ exports.decode = function(buffer, encoding, start, end, options) {
15
36
  return trail ? res + trail : res;
16
37
  };
17
38
 
18
- exports.encode = function(string, encoding, options) {
39
+ exports.encode = function (string, encoding, options) {
19
40
  if (Buffer.isEncoding(encoding)) {
20
41
  return Buffer.from(string, encoding);
21
42
  }
package/lib/pool.js CHANGED
@@ -100,6 +100,7 @@ class Pool extends EventEmitter {
100
100
 
101
101
  end(cb) {
102
102
  this._closed = true;
103
+ clearTimeout(this._removeIdleTimeoutConnectionsTimer);
103
104
  if (typeof cb !== 'function') {
104
105
  cb = function(err) {
105
106
  if (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysql2",
3
- "version": "3.7.1",
3
+ "version": "3.9.0",
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",
@@ -76,7 +76,7 @@
76
76
  "eslint-config-prettier": "^9.0.0",
77
77
  "eslint-plugin-async-await": "0.0.0",
78
78
  "eslint-plugin-markdown": "^3.0.0",
79
- "husky": "^8.0.2",
79
+ "husky": "^9.0.2",
80
80
  "lint-staged": "^15.0.1",
81
81
  "portfinder": "^1.0.28",
82
82
  "prettier": "^3.0.0",
@@ -19,6 +19,7 @@ import { Connection as PromiseConnection } from '../../../promise.js';
19
19
  import { AuthPlugin } from './Auth.js';
20
20
  import { QueryableBase } from './protocol/sequences/QueryableBase.js';
21
21
  import { ExecutableBase } from './protocol/sequences/ExecutableBase.js';
22
+ import { TypeCast } from './parsers/typeCast.js';
22
23
 
23
24
  export interface SslOptions {
24
25
  /**
@@ -172,26 +173,44 @@ export interface ConnectionOptions {
172
173
  infileStreamFactory?: (path: string) => Readable;
173
174
 
174
175
  /**
175
- * Determines if column values should be converted to native JavaScript types. It is not recommended (and may go away / change in the future)
176
- * to disable type casting, but you can currently do so on either the connection or query level. (Default: true)
176
+ * Determines if column values should be converted to native JavaScript types.
177
177
  *
178
- * You can also specify a function (field: any, next: () => void) => {} to do the type casting yourself.
178
+ * @default true
179
179
  *
180
- * WARNING: YOU MUST INVOKE the parser using one of these three field functions in your custom typeCast callback. They can only be called once.
180
+ * It is not recommended (and may go away / change in the future) to disable type casting, but you can currently do so on either the connection or query level.
181
181
  *
182
- * field.string()
183
- * field.buffer()
184
- * field.geometry()
182
+ * ---
185
183
  *
186
- * are aliases for
184
+ * You can also specify a function to do the type casting yourself:
185
+ * ```ts
186
+ * (field: Field, next: () => void) => {
187
+ * return next();
188
+ * }
189
+ * ```
187
190
  *
188
- * parser.parseLengthCodedString()
189
- * parser.parseLengthCodedBuffer()
190
- * parser.parseGeometryValue()
191
+ * ---
191
192
  *
192
- * You can find which field function you need to use by looking at: RowDataPacket.prototype._typeCast
193
+ * **WARNING:**
194
+ *
195
+ * YOU MUST INVOKE the parser using one of these three field functions in your custom typeCast callback. They can only be called once:
196
+ *
197
+ * ```js
198
+ * field.string();
199
+ * field.buffer();
200
+ * field.geometry();
201
+ * ```
202
+
203
+ * Which are aliases for:
204
+ *
205
+ * ```js
206
+ * parser.parseLengthCodedString();
207
+ * parser.parseLengthCodedBuffer();
208
+ * parser.parseGeometryValue();
209
+ * ```
210
+ *
211
+ * You can find which field function you need to use by looking at `RowDataPacket.prototype._typeCast`.
193
212
  */
194
- typeCast?: boolean | ((field: any, next: () => void) => any);
213
+ typeCast?: TypeCast;
195
214
 
196
215
  /**
197
216
  * A custom query format function
@@ -0,0 +1,53 @@
1
+ type Geometry = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+
6
+ type Type = {
7
+ type:
8
+ | 'DECIMAL'
9
+ | 'TINY'
10
+ | 'SHORT'
11
+ | 'LONG'
12
+ | 'FLOAT'
13
+ | 'DOUBLE'
14
+ | 'NULL'
15
+ | 'TIMESTAMP'
16
+ | 'TIMESTAMP2'
17
+ | 'LONGLONG'
18
+ | 'INT24'
19
+ | 'DATE'
20
+ | 'TIME'
21
+ | 'TIME2'
22
+ | 'DATETIME'
23
+ | 'DATETIME2'
24
+ | 'YEAR'
25
+ | 'NEWDATE'
26
+ | 'VARCHAR'
27
+ | 'BIT'
28
+ | 'JSON'
29
+ | 'NEWDECIMAL'
30
+ | 'ENUM'
31
+ | 'SET'
32
+ | 'TINY_BLOB'
33
+ | 'MEDIUM_BLOB'
34
+ | 'LONG_BLOB'
35
+ | 'BLOB'
36
+ | 'VAR_STRING'
37
+ | 'STRING'
38
+ | 'GEOMETRY';
39
+ };
40
+
41
+ type Field = Type & {
42
+ length: number;
43
+ db: string;
44
+ table: string;
45
+ name: string;
46
+ string: () => string | null;
47
+ buffer: () => Buffer | null;
48
+ geometry: () => Geometry | Geometry[] | null;
49
+ };
50
+
51
+ type Next = () => void;
52
+
53
+ export type TypeCast = ((field: Field, next: Next) => any) | boolean;
@@ -1,6 +1,7 @@
1
1
  import { Sequence } from './Sequence.js';
2
2
  import { OkPacket, RowDataPacket, FieldPacket } from '../packets/index.js';
3
3
  import { Readable } from 'stream';
4
+ import { TypeCast } from '../../parsers/typeCast.js';
4
5
 
5
6
  export interface QueryOptions {
6
7
  /**
@@ -33,26 +34,44 @@ export interface QueryOptions {
33
34
  nestTables?: any;
34
35
 
35
36
  /**
36
- * Determines if column values should be converted to native JavaScript types. It is not recommended (and may go away / change in the future)
37
- * to disable type casting, but you can currently do so on either the connection or query level. (Default: true)
37
+ * Determines if column values should be converted to native JavaScript types.
38
38
  *
39
- * You can also specify a function (field: any, next: () => void) => {} to do the type casting yourself.
39
+ * @default true
40
40
  *
41
- * WARNING: YOU MUST INVOKE the parser using one of these three field functions in your custom typeCast callback. They can only be called once.
41
+ * It is not recommended (and may go away / change in the future) to disable type casting, but you can currently do so on either the connection or query level.
42
42
  *
43
- * field.string()
44
- * field.buffer()
45
- * field.geometry()
43
+ * ---
46
44
  *
47
- * are aliases for
45
+ * You can also specify a function to do the type casting yourself:
46
+ * ```ts
47
+ * (field: Field, next: () => void) => {
48
+ * return next();
49
+ * }
50
+ * ```
48
51
  *
49
- * parser.parseLengthCodedString()
50
- * parser.parseLengthCodedBuffer()
51
- * parser.parseGeometryValue()
52
+ * ---
52
53
  *
53
- * You can find which field function you need to use by looking at: RowDataPacket.prototype._typeCast
54
+ * **WARNING:**
55
+ *
56
+ * YOU MUST INVOKE the parser using one of these three field functions in your custom typeCast callback. They can only be called once:
57
+ *
58
+ * ```js
59
+ * field.string();
60
+ * field.buffer();
61
+ * field.geometry();
62
+ * ```
63
+
64
+ * Which are aliases for:
65
+ *
66
+ * ```js
67
+ * parser.parseLengthCodedString();
68
+ * parser.parseLengthCodedBuffer();
69
+ * parser.parseGeometryValue();
70
+ * ```
71
+ *
72
+ * You can find which field function you need to use by looking at `RowDataPacket.prototype._typeCast`.
54
73
  */
55
- typeCast?: any;
74
+ typeCast?: TypeCast;
56
75
 
57
76
  /**
58
77
  * This overrides the same option set at the connection level.
@@ -137,11 +156,11 @@ declare class Query extends Sequence {
137
156
  on(event: 'error', listener: (err: QueryError) => any): this;
138
157
  on(
139
158
  event: 'fields',
140
- listener: (fields: FieldPacket, index: number) => any
159
+ listener: (fields: FieldPacket, index: number) => any,
141
160
  ): this;
142
161
  on(
143
162
  event: 'result',
144
- listener: (result: RowDataPacket | OkPacket, index: number) => any
163
+ listener: (result: RowDataPacket | OkPacket, index: number) => any,
145
164
  ): this;
146
165
  on(event: 'end', listener: () => any): this;
147
166
  }