Skip to content

Commit baaa92a

Browse files
authored
feat: introduce typeCast for execute method (#2398)
* ci(bun): include `execute` to simple query tests * ci: include typeCast tests for `execute` * ci: include connection level and overwriting typeCast tests * feat: introduce typeCast for `execute` method * chore: remove typeCast comment warnings for `execute`
1 parent 15a8a57 commit baaa92a

11 files changed

+497
-53
lines changed

lib/parsers/binary_parser.js

+61-41
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,35 @@ function readCodeFor(field, config, options, fieldNum) {
8080

8181
function compile(fields, options, config) {
8282
const parserFn = genFunc();
83-
let i = 0;
8483
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
8584

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+
}
89112

90113
parserFn('(function(){');
91114
parserFn('return class BinaryRow {');
@@ -96,24 +119,19 @@ function compile(fields, options, config) {
96119
if (options.rowsAsArray) {
97120
parserFn(`const result = new Array(${fields.length});`);
98121
} else {
99-
parserFn("const result = {};");
122+
parserFn('const result = {};');
100123
}
101124

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;
113131
}
114132

115133
parserFn('packet.readInt8();'); // status byte
116-
for (i = 0; i < nullBitmapLength; ++i) {
134+
for (let i = 0; i < nullBitmapLength; ++i) {
117135
parserFn(`const nullBitmaskByte${i} = packet.readInt8();`);
118136
}
119137

@@ -123,38 +141,44 @@ function compile(fields, options, config) {
123141
let fieldName = '';
124142
let tableName = '';
125143

126-
for (i = 0; i < fields.length; i++) {
144+
for (let i = 0; i < fields.length; i++) {
127145
fieldName = helpers.srcEscape(fields[i].name);
128146
parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`);
129147

130148
if (typeof options.nestTables === 'string') {
131-
tableName = helpers.srcEscape(fields[i].table);
132149
lvalue = `result[${helpers.srcEscape(
133-
fields[i].table + options.nestTables + fields[i].name
150+
fields[i].table + options.nestTables + fields[i].name,
134151
)}]`;
135152
} else if (options.nestTables === true) {
136153
tableName = helpers.srcEscape(fields[i].table);
154+
parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`);
137155
lvalue = `result[${tableName}][${fieldName}]`;
138156
} else if (options.rowsAsArray) {
139157
lvalue = `result[${i.toString(10)}]`;
140158
} 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('}');
142180
}
143181

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-
// }
158182
currentFieldNullBit *= 2;
159183
if (currentFieldNullBit === 0x100) {
160184
currentFieldNullBit = 1;
@@ -166,17 +190,13 @@ function compile(fields, options, config) {
166190
parserFn('}');
167191
parserFn('};')('})()');
168192

169-
/* eslint-enable no-trailing-spaces */
170-
/* eslint-enable no-spaced-func */
171-
/* eslint-enable no-unexpected-multiline */
172-
173193
if (config.debug) {
174194
helpers.printDebugWithCode(
175195
'Compiled binary protocol row parser',
176-
parserFn.toString()
196+
parserFn.toString(),
177197
);
178198
}
179-
return parserFn.toFunction();
199+
return parserFn.toFunction({ wrap });
180200
}
181201

182202
function getBinaryParser(fields, options, config) {

test/integration/connection/test-select-1.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ connection.query('SELECT 1 as result', (err, rows, fields) => {
99
assert.deepEqual(rows, [{ result: 1 }]);
1010
assert.equal(fields[0].name, 'result');
1111

12-
connection.end(err => {
12+
connection.execute('SELECT 1 as result', (err, rows, fields) => {
1313
assert.ifError(err);
14-
process.exit(0);
14+
assert.deepEqual(rows, [{ result: 1 }]);
15+
assert.equal(fields[0].name, 'result');
16+
17+
connection.end(err => {
18+
assert.ifError(err);
19+
process.exit(0);
20+
});
1521
});
1622
});

test/integration/connection/test-select-ssl.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@ connection.query(`SHOW STATUS LIKE 'Ssl_cipher'`, (err, rows) => {
1212
assert.deepEqual(rows, [{ Variable_name: 'Ssl_cipher', Value: '' }]);
1313
}
1414

15-
connection.end(err => {
15+
connection.execute(`SHOW STATUS LIKE 'Ssl_cipher'`, (err, rows) => {
1616
assert.ifError(err);
17-
process.exit(0);
17+
if (process.env.MYSQL_USE_TLS === '1') {
18+
assert.equal(rows[0].Value.length > 0, true);
19+
} else {
20+
assert.deepEqual(rows, [{ Variable_name: 'Ssl_cipher', Value: '' }]);
21+
}
22+
23+
connection.end(err => {
24+
assert.ifError(err);
25+
process.exit(0);
26+
});
1827
});
1928
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const connection = common.createConnection();
5+
const assert = require('assert');
6+
7+
common.useTestDb(connection);
8+
9+
const table = 'insert_test';
10+
connection.execute(
11+
[
12+
`CREATE TEMPORARY TABLE \`${table}\` (`,
13+
'`id` int(11) unsigned NOT NULL AUTO_INCREMENT,',
14+
'`date` DATETIME NULL,',
15+
'`number` INT NULL,',
16+
'PRIMARY KEY (`id`)',
17+
') ENGINE=InnoDB DEFAULT CHARSET=utf8',
18+
].join('\n'),
19+
err => {
20+
if (err) throw err;
21+
},
22+
);
23+
24+
connection.execute(
25+
`INSERT INTO ${table} (date, number) VALUES (?, ?)`,
26+
[null, null],
27+
err => {
28+
if (err) throw err;
29+
},
30+
);
31+
32+
let results;
33+
connection.execute(`SELECT * FROM ${table}`, (err, _results) => {
34+
if (err) {
35+
throw err;
36+
}
37+
38+
results = _results;
39+
connection.end();
40+
});
41+
42+
process.on('exit', () => {
43+
assert.strictEqual(results[0].date, null);
44+
assert.strictEqual(results[0].number, null);
45+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const driver = require('../../../index.js'); //needed to check driver.Types
5+
const connection = common.createConnection();
6+
const assert = require('assert');
7+
8+
common.useTestDb(connection);
9+
10+
connection.execute('select 1', waitConnectErr => {
11+
assert.ifError(waitConnectErr);
12+
13+
const tests = require('./type-casting-tests')(connection);
14+
15+
const table = 'type_casting';
16+
17+
const schema = [];
18+
const inserts = [];
19+
20+
tests.forEach((test, index) => {
21+
const escaped = test.insertRaw || connection.escape(test.insert);
22+
23+
test.columnName = `${test.type}_${index}`;
24+
25+
schema.push(`\`${test.columnName}\` ${test.type},`);
26+
inserts.push(`\`${test.columnName}\` = ${escaped}`);
27+
});
28+
29+
const createTable = [
30+
`CREATE TEMPORARY TABLE \`${table}\` (`,
31+
'`id` int(11) unsigned NOT NULL AUTO_INCREMENT,',
32+
]
33+
.concat(schema)
34+
.concat(['PRIMARY KEY (`id`)', ') ENGINE=InnoDB DEFAULT CHARSET=utf8'])
35+
.join('\n');
36+
37+
connection.execute(createTable);
38+
39+
connection.execute(`INSERT INTO ${table} SET ${inserts.join(',\n')}`);
40+
41+
let row;
42+
let fieldData; // to lookup field types
43+
connection.execute(`SELECT * FROM ${table}`, (err, rows, fields) => {
44+
if (err) {
45+
throw err;
46+
}
47+
48+
row = rows[0];
49+
// build a fieldName: fieldType lookup table
50+
fieldData = fields.reduce((a, v) => {
51+
a[v['name']] = v['type'];
52+
return a;
53+
}, {});
54+
connection.end();
55+
});
56+
57+
process.on('exit', () => {
58+
tests.forEach(test => {
59+
// check that the column type matches the type name stored in driver.Types
60+
const columnType = fieldData[test.columnName];
61+
assert.equal(
62+
test.columnType === driver.Types[columnType],
63+
true,
64+
test.columnName,
65+
);
66+
let expected = test.expect || test.insert;
67+
let got = row[test.columnName];
68+
let message;
69+
70+
if (expected instanceof Date) {
71+
assert.equal(got instanceof Date, true, test.type);
72+
73+
expected = String(expected);
74+
got = String(got);
75+
} else if (Buffer.isBuffer(expected)) {
76+
assert.equal(Buffer.isBuffer(got), true, test.type);
77+
78+
expected = String(Array.prototype.slice.call(expected));
79+
got = String(Array.prototype.slice.call(got));
80+
}
81+
82+
if (test.deep) {
83+
message = `got: "${JSON.stringify(got)}" expected: "${JSON.stringify(
84+
expected,
85+
)}" test: ${test.type}`;
86+
assert.deepEqual(expected, got, message);
87+
} else {
88+
message = `got: "${got}" (${typeof got}) expected: "${expected}" (${typeof expected}) test: ${
89+
test.type
90+
}`;
91+
assert.strictEqual(expected, got, message);
92+
}
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)