Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce typeCast for execute method #2398

Merged
merged 5 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 61 additions & 41 deletions lib/parsers/binary_parser.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});