Skip to content

Commit

Permalink
feat: introduce typeCast for execute method (#2398)
Browse files Browse the repository at this point in the history
* 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`
  • Loading branch information
wellwelwel committed Jan 26, 2024
1 parent 15a8a57 commit baaa92a
Show file tree
Hide file tree
Showing 11 changed files with 497 additions and 53 deletions.
102 changes: 61 additions & 41 deletions lib/parsers/binary_parser.js
Expand Up @@ -80,12 +80,35 @@ function readCodeFor(field, config, options, fieldNum) {

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

/* eslint-disable no-trailing-spaces */
/* eslint-disable no-spaced-func */
/* eslint-disable no-unexpected-multiline */
function wrap(field, packet) {
return {
type: typeNames[field.columnType],
length: field.columnLength,
db: field.schema,
table: field.table,
name: field.name,
string: function (encoding = field.encoding) {
if (field.columnType === Types.JSON && encoding === field.encoding) {
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/1661
console.warn(
`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``,
);
}

return packet.readLengthCodedString(encoding);
},
buffer: function () {
return packet.readLengthCodedBuffer();
},
geometry: function () {
return packet.parseGeometryValue();
},
};
}

parserFn('(function(){');
parserFn('return class BinaryRow {');
Expand All @@ -96,24 +119,19 @@ function compile(fields, options, config) {
if (options.rowsAsArray) {
parserFn(`const result = new Array(${fields.length});`);
} else {
parserFn("const result = {};");
parserFn('const result = {};');
}

const resultTables = {};
let resultTablesArray = [];

if (options.nestTables === true) {
for (i = 0; i < fields.length; i++) {
resultTables[fields[i].table] = 1;
}
resultTablesArray = Object.keys(resultTables);
for (i = 0; i < resultTablesArray.length; i++) {
parserFn(`result[${helpers.srcEscape(resultTablesArray[i])}] = {};`);
}
// Global typeCast
if (
typeof config.typeCast === 'function' &&
typeof options.typeCast !== 'function'
) {
options.typeCast = config.typeCast;
}

parserFn('packet.readInt8();'); // status byte
for (i = 0; i < nullBitmapLength; ++i) {
for (let i = 0; i < nullBitmapLength; ++i) {
parserFn(`const nullBitmaskByte${i} = packet.readInt8();`);
}

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

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

if (typeof options.nestTables === 'string') {
tableName = helpers.srcEscape(fields[i].table);
lvalue = `result[${helpers.srcEscape(
fields[i].table + options.nestTables + fields[i].name
fields[i].table + options.nestTables + fields[i].name,
)}]`;
} else if (options.nestTables === true) {
tableName = helpers.srcEscape(fields[i].table);
parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`);
lvalue = `result[${tableName}][${fieldName}]`;
} else if (options.rowsAsArray) {
lvalue = `result[${i.toString(10)}]`;
} else {
lvalue = `result[${helpers.srcEscape(fields[i].name)}]`;
lvalue = `result[${fieldName}]`;
}

if (options.typeCast === false) {
parserFn(`${lvalue} = packet.readLengthCodedBuffer();`);
} else {
const fieldWrapperVar = `fieldWrapper${i}`;
parserFn(`const ${fieldWrapperVar} = wrap(fields[${i}], packet);`);
const readCode = readCodeFor(fields[i], config, options, i);

parserFn(`if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit})`);
parserFn(`${lvalue} = null;`);
parserFn('else {');
if (typeof options.typeCast === 'function') {
parserFn(
`${lvalue} = options.typeCast(${fieldWrapperVar}, function() { return ${readCode} });`,
);
} else {
parserFn(`${lvalue} = ${readCode};`);
}
parserFn('}');
}

// TODO: this used to be an optimisation ( if column marked as NOT_NULL don't include code to check null
// bitmap at all, but it seems that we can't rely on this flag, see #178
// TODO: benchmark performance difference
//
// if (fields[i].flags & FieldFlags.NOT_NULL) { // don't need to check null bitmap if field can't be null.
// result.push(lvalue + ' = ' + readCodeFor(fields[i], config));
// } else if (fields[i].columnType == Types.NULL) {
// result.push(lvalue + ' = null;');
// } else {
parserFn(`if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit})`);
parserFn(`${lvalue} = null;`);
parserFn('else');
parserFn(`${lvalue} = ${readCodeFor(fields[i], config, options, i)}`);
// }
currentFieldNullBit *= 2;
if (currentFieldNullBit === 0x100) {
currentFieldNullBit = 1;
Expand All @@ -166,17 +190,13 @@ function compile(fields, options, config) {
parserFn('}');
parserFn('};')('})()');

/* eslint-enable no-trailing-spaces */
/* eslint-enable no-spaced-func */
/* eslint-enable no-unexpected-multiline */

if (config.debug) {
helpers.printDebugWithCode(
'Compiled binary protocol row parser',
parserFn.toString()
parserFn.toString(),
);
}
return parserFn.toFunction();
return parserFn.toFunction({ wrap });
}

function getBinaryParser(fields, options, config) {
Expand Down
10 changes: 8 additions & 2 deletions test/integration/connection/test-select-1.js
Expand Up @@ -9,8 +9,14 @@ connection.query('SELECT 1 as result', (err, rows, fields) => {
assert.deepEqual(rows, [{ result: 1 }]);
assert.equal(fields[0].name, 'result');

connection.end(err => {
connection.execute('SELECT 1 as result', (err, rows, fields) => {
assert.ifError(err);
process.exit(0);
assert.deepEqual(rows, [{ result: 1 }]);
assert.equal(fields[0].name, 'result');

connection.end(err => {
assert.ifError(err);
process.exit(0);
});
});
});
13 changes: 11 additions & 2 deletions test/integration/connection/test-select-ssl.js
Expand Up @@ -12,8 +12,17 @@ connection.query(`SHOW STATUS LIKE 'Ssl_cipher'`, (err, rows) => {
assert.deepEqual(rows, [{ Variable_name: 'Ssl_cipher', Value: '' }]);
}

connection.end(err => {
connection.execute(`SHOW STATUS LIKE 'Ssl_cipher'`, (err, rows) => {
assert.ifError(err);
process.exit(0);
if (process.env.MYSQL_USE_TLS === '1') {
assert.equal(rows[0].Value.length > 0, true);
} else {
assert.deepEqual(rows, [{ Variable_name: 'Ssl_cipher', Value: '' }]);
}

connection.end(err => {
assert.ifError(err);
process.exit(0);
});
});
});
45 changes: 45 additions & 0 deletions test/integration/connection/test-type-cast-null-fields-execute.js
@@ -0,0 +1,45 @@
'use strict';

const common = require('../../common');
const connection = common.createConnection();
const assert = require('assert');

common.useTestDb(connection);

const table = 'insert_test';
connection.execute(
[
`CREATE TEMPORARY TABLE \`${table}\` (`,
'`id` int(11) unsigned NOT NULL AUTO_INCREMENT,',
'`date` DATETIME NULL,',
'`number` INT NULL,',
'PRIMARY KEY (`id`)',
') ENGINE=InnoDB DEFAULT CHARSET=utf8',
].join('\n'),
err => {
if (err) throw err;
},
);

connection.execute(
`INSERT INTO ${table} (date, number) VALUES (?, ?)`,
[null, null],
err => {
if (err) throw err;
},
);

let results;
connection.execute(`SELECT * FROM ${table}`, (err, _results) => {
if (err) {
throw err;
}

results = _results;
connection.end();
});

process.on('exit', () => {
assert.strictEqual(results[0].date, null);
assert.strictEqual(results[0].number, null);
});
95 changes: 95 additions & 0 deletions test/integration/connection/test-type-casting-execute.js
@@ -0,0 +1,95 @@
'use strict';

const common = require('../../common');
const driver = require('../../../index.js'); //needed to check driver.Types
const connection = common.createConnection();
const assert = require('assert');

common.useTestDb(connection);

connection.execute('select 1', waitConnectErr => {
assert.ifError(waitConnectErr);

const tests = require('./type-casting-tests')(connection);

const table = 'type_casting';

const schema = [];
const inserts = [];

tests.forEach((test, index) => {
const escaped = test.insertRaw || connection.escape(test.insert);

test.columnName = `${test.type}_${index}`;

schema.push(`\`${test.columnName}\` ${test.type},`);
inserts.push(`\`${test.columnName}\` = ${escaped}`);
});

const createTable = [
`CREATE TEMPORARY TABLE \`${table}\` (`,
'`id` int(11) unsigned NOT NULL AUTO_INCREMENT,',
]
.concat(schema)
.concat(['PRIMARY KEY (`id`)', ') ENGINE=InnoDB DEFAULT CHARSET=utf8'])
.join('\n');

connection.execute(createTable);

connection.execute(`INSERT INTO ${table} SET ${inserts.join(',\n')}`);

let row;
let fieldData; // to lookup field types
connection.execute(`SELECT * FROM ${table}`, (err, rows, fields) => {
if (err) {
throw err;
}

row = rows[0];
// build a fieldName: fieldType lookup table
fieldData = fields.reduce((a, v) => {
a[v['name']] = v['type'];
return a;
}, {});
connection.end();
});

process.on('exit', () => {
tests.forEach(test => {
// check that the column type matches the type name stored in driver.Types
const columnType = fieldData[test.columnName];
assert.equal(
test.columnType === driver.Types[columnType],
true,
test.columnName,
);
let expected = test.expect || test.insert;
let got = row[test.columnName];
let message;

if (expected instanceof Date) {
assert.equal(got instanceof Date, true, test.type);

expected = String(expected);
got = String(got);
} else if (Buffer.isBuffer(expected)) {
assert.equal(Buffer.isBuffer(got), true, test.type);

expected = String(Array.prototype.slice.call(expected));
got = String(Array.prototype.slice.call(got));
}

if (test.deep) {
message = `got: "${JSON.stringify(got)}" expected: "${JSON.stringify(
expected,
)}" test: ${test.type}`;
assert.deepEqual(expected, got, message);
} else {
message = `got: "${got}" (${typeof got}) expected: "${expected}" (${typeof expected}) test: ${
test.type
}`;
assert.strictEqual(expected, got, message);
}
});
});
});

0 comments on commit baaa92a

Please sign in to comment.