diff --git a/lib/parsers/binary_parser.js b/lib/parsers/binary_parser.js index bbd29596ec..431606891e 100644 --- a/lib/parsers/binary_parser.js +++ b/lib/parsers/binary_parser.js @@ -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 {'); @@ -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();`); } @@ -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; @@ -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) { diff --git a/test/integration/connection/test-select-1.js b/test/integration/connection/test-select-1.js index 0b4f8ef77c..b70228c829 100644 --- a/test/integration/connection/test-select-1.js +++ b/test/integration/connection/test-select-1.js @@ -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); + }); }); }); diff --git a/test/integration/connection/test-select-ssl.js b/test/integration/connection/test-select-ssl.js index ec34da82eb..0b28282c15 100644 --- a/test/integration/connection/test-select-ssl.js +++ b/test/integration/connection/test-select-ssl.js @@ -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); + }); }); }); diff --git a/test/integration/connection/test-type-cast-null-fields-execute.js b/test/integration/connection/test-type-cast-null-fields-execute.js new file mode 100644 index 0000000000..d85ef34a7a --- /dev/null +++ b/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); +}); diff --git a/test/integration/connection/test-type-casting-execute.js b/test/integration/connection/test-type-casting-execute.js new file mode 100644 index 0000000000..a1258f1d7a --- /dev/null +++ b/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); + } + }); + }); +}); diff --git a/test/integration/connection/test-typecast-execute.js b/test/integration/connection/test-typecast-execute.js new file mode 100644 index 0000000000..431562e562 --- /dev/null +++ b/test/integration/connection/test-typecast-execute.js @@ -0,0 +1,145 @@ +'use strict'; + +const common = require('../../common'); +const connection = common.createConnection(); +const assert = require('assert'); + +connection.execute('CREATE TEMPORARY TABLE json_test (json_test JSON)'); +connection.execute('INSERT INTO json_test VALUES (?)', [ + JSON.stringify({ test: 42 }), +]); + +connection.execute( + 'CREATE TEMPORARY TABLE geom_test (p POINT, g GEOMETRY NOT NULL)', +); +connection.execute( + 'INSERT INTO geom_test VALUES (ST_GeomFromText(?), ST_GeomFromText(?))', + [ + 'POINT(1 1)', + 'LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)', + ], +); + +connection.execute( + { + sql: 'select "foo uppercase" as foo', + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string().toUpperCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + }, +); + +connection.execute( + { + sql: 'select "foobar" as foo', + typeCast: false, + }, + (err, res) => { + assert.ifError(err); + assert(Buffer.isBuffer(res[0].foo)); + assert.equal(res[0].foo.toString('utf8'), 'foobar'); + }, +); + +connection.execute( + { + sql: 'SELECT NULL as test, 6 as value;', + typeCast: function (field, next) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].test, null); + assert.equal(_rows[0].value, 6); + }, +); + +connection.execute( + { + sql: 'SELECT * from json_test', + typeCast: function (_field, next) { + return next(); + }, + }, + (err, _rows) => { + assert.ifError(err); + assert.equal(_rows[0].json_test.test, 42); + }, +); + +// read geo fields +connection.execute( + { + sql: 'select * from geom_test', + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual({ x: 1, y: 1 }, res[0].p); + assert.deepEqual( + [ + { x: -71.160281, y: 42.258729 }, + { x: -71.160837, y: 42.259113 }, + { x: -71.161144, y: 42.25932 }, + ], + res[0].g, + ); + }, +); + +connection.execute( + { + sql: 'select * from geom_test', + typeCast: function (field, next) { + assert.equal('geom_test', field.table); + + if (field.name === 'p' && field.type === 'GEOMETRY') { + assert.deepEqual({ x: 1, y: 1 }, field.geometry()); + return { x: 2, y: 2 }; + } + + if (field.name === 'g' && field.type === 'GEOMETRY') { + assert.deepEqual( + [ + { x: -71.160281, y: 42.258729 }, + { x: -71.160837, y: 42.259113 }, + { x: -71.161144, y: 42.25932 }, + ], + field.geometry(), + ); + + return [ + { x: -70, y: 40 }, + { x: -60, y: 50 }, + { x: -50, y: 60 }, + ]; + } + + assert.fail('should not reach here'); + + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual({ x: 2, y: 2 }, res[0].p); + assert.deepEqual( + [ + { x: -70, y: 40 }, + { x: -60, y: 50 }, + { x: -50, y: 60 }, + ], + res[0].g, + ); + }, +); + +connection.end(); diff --git a/test/integration/connection/test-typecast-geometry-execute.js b/test/integration/connection/test-typecast-geometry-execute.js new file mode 100644 index 0000000000..b63008b73d --- /dev/null +++ b/test/integration/connection/test-typecast-geometry-execute.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../../common'); +const connection = common.createConnection(); +const assert = require('assert'); + +connection.execute('select 1', () => { + const serverVersion = connection._handshakePacket.serverVersion; + // mysql8 renamed some standard functions + // see https://dev.mysql.com/doc/refman/8.0/en/gis-wkb-functions.html + const stPrefix = serverVersion[0] === '8' ? 'ST_' : ''; + + connection.execute( + { + sql: `select ${stPrefix}GeomFromText('POINT(11 0)') as foo`, + typeCast: function(field, next) { + if (field.type === 'GEOMETRY') { + return field.geometry(); + } + return next(); + } + }, + (err, res) => { + assert.ifError(err); + assert.deepEqual(res[0].foo, { x: 11, y: 0 }); + } + ); + + connection.execute( + { + sql: `select ${stPrefix}GeomFromText('POINT(11 0)') as foo`, + typeCast: function(field, next) { + if (field.type === 'GEOMETRY') { + return field.buffer(); + } + return next(); + } + }, + (err, res) => { + assert.ifError(err); + assert.equal(Buffer.isBuffer(res[0].foo), true); + } + ); + + connection.end(); +}); diff --git a/test/integration/connection/test-typecast-overwriting-execute.js b/test/integration/connection/test-typecast-overwriting-execute.js new file mode 100644 index 0000000000..ac4c2a78ba --- /dev/null +++ b/test/integration/connection/test-typecast-overwriting-execute.js @@ -0,0 +1,43 @@ +'use strict'; + +const assert = require('assert'); +const common = require('../../common'); + +const connection = common.createConnection({ + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string().toUpperCase(); + } + return next(); + }, +}); + +connection.execute( + { + sql: 'select "foo uppercase" as foo', + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + }, +); + +connection.execute( + { + sql: 'select "foo lowercase" as foo', + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string().toLowerCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'foo lowercase'); + }, +); + +connection.end(); diff --git a/test/integration/connection/test-typecast-overwriting.js b/test/integration/connection/test-typecast-overwriting.js new file mode 100644 index 0000000000..474359727d --- /dev/null +++ b/test/integration/connection/test-typecast-overwriting.js @@ -0,0 +1,43 @@ +'use strict'; + +const assert = require('assert'); +const common = require('../../common'); + +const connection = common.createConnection({ + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string().toUpperCase(); + } + return next(); + }, +}); + +connection.query( + { + sql: 'select "foo uppercase" as foo', + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'FOO UPPERCASE'); + }, +); + +connection.query( + { + sql: 'select "foo lowercase" as foo', + typeCast: function (field, next) { + assert.equal('number', typeof field.length); + if (field.type === 'VAR_STRING') { + return field.string().toLowerCase(); + } + return next(); + }, + }, + (err, res) => { + assert.ifError(err); + assert.equal(res[0].foo, 'foo lowercase'); + }, +); + +connection.end(); diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index 334a1437f5..ade871d1b7 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -209,10 +209,6 @@ export interface ConnectionOptions { * ``` * * You can find which field function you need to use by looking at `RowDataPacket.prototype._typeCast`. - * - * --- - * - * For `execute`, please see: [typeCast not supported with .execute #649](https://github.com/sidorares/node-mysql2/issues/649). */ typeCast?: TypeCast; diff --git a/typings/mysql/lib/protocol/sequences/Query.d.ts b/typings/mysql/lib/protocol/sequences/Query.d.ts index 5c4cb65084..44d6c910c9 100644 --- a/typings/mysql/lib/protocol/sequences/Query.d.ts +++ b/typings/mysql/lib/protocol/sequences/Query.d.ts @@ -70,10 +70,6 @@ export interface QueryOptions { * ``` * * You can find which field function you need to use by looking at `RowDataPacket.prototype._typeCast`. - * - * --- - * - * For `execute`, please see: [typeCast not supported with .execute #649](https://github.com/sidorares/node-mysql2/issues/649). */ typeCast?: TypeCast;