diff --git a/lib/commands/execute.js b/lib/commands/execute.js index d5c2d8ab79..79c4ea1107 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -39,7 +39,7 @@ class Execute extends Command { this.options = Object.assign({}, connection.config, this._executeOptions); this._setTimeout(); const executePacket = new Packets.Execute( - this.statement.id, + this.statement, this.parameters, connection.config.charsetNumber, connection.config.timezone diff --git a/lib/packets/execute.js b/lib/packets/execute.js index c612f66e12..83a586ba85 100644 --- a/lib/packets/execute.js +++ b/lib/packets/execute.js @@ -2,6 +2,7 @@ const CursorType = require('../constants/cursor'); const CommandCodes = require('../constants/commands'); +const FieldFlags = require('../constants/field_flags.js'); const Types = require('../constants/types'); const Packet = require('../packets/packet'); const CharsetToEncoding = require('../constants/charset_encodings.js'); @@ -18,49 +19,96 @@ function isJSON(value) { * Converts a value to an object describing type, String/Buffer representation and length * @param {*} value */ -function toParameter(value, encoding, timezone) { - let type = Types.VAR_STRING; +function toParameter(value, encoding, timezone, parameterHints) { + let type = parameterHints.columnType; let length; let writer = function(value) { // eslint-disable-next-line no-invalid-this return Packet.prototype.writeLengthCodedString.call(this, value, encoding); }; + const unsigned = parameterHints.flags & FieldFlags.UNSIGNED; + if (value !== null) { - switch (typeof value) { - case 'undefined': - throw new TypeError('Bind parameters must not contain undefined'); + if (typeof value === 'undefined') { + throw new TypeError('Bind parameters must not contain undefined'); + } - case 'number': - type = Types.DOUBLE; - length = 8; - writer = Packet.prototype.writeDouble; - break; + // currently exception for Date and plain object input parameters - even if statement expects another type we'll still send date/stringified json + // this might change in the future + if (Object.prototype.toString.call(value) === '[object Date]') { + type = Types.DATETIME; + length = 12; + writer = function(value) { + // eslint-disable-next-line no-invalid-this + return Packet.prototype.writeDate.call(this, value, timezone); + }; + return { value, type, length, writer }; + } + + if (isJSON(value)) { + value = JSON.stringify(value); + type = Types.JSON; + length = Packet.lengthCodedStringLength(value, encoding); + return { value, type, length, writer }; + } - case 'boolean': + if (Buffer.isBuffer(value)) { + length = Packet.lengthCodedNumberLength(value.length) + value.length; + writer = Packet.prototype.writeLengthCodedBuffer; + type = Types.VAR_STRING; + return { value, type, length, writer }; + } + + switch(parameterHints.columnType) { + case Types.TINY: value = value | 0; - type = Types.TINY; length = 1; - writer = Packet.prototype.writeInt8; + writer = unsigned ? Packet.prototype.writeInt8 : Packet.prototype.writeUInt8; break; - - case 'object': - if (Object.prototype.toString.call(value) === '[object Date]') { - type = Types.DATETIME; + case Types.YEAR: + case Types.SHORT: + value = value | 0; + length = 2; + writer = unsigned ? Packet.prototype.writeInt16 : Packet.prototype.writeSInt16; + break; + case Types.LONG: + case Types.INT24: // in binary protocol int24 is encoded in 4 bytes int32 + value = value | 0; + length = 4; + writer = unsigned ? Packet.prototype.writeInt32() : Packet.prototype.writeSInt32(); + break; + case Types.DATETIME: + if (!(Object.prototype.toString.call(value) === '[object Date]')) { length = 12; writer = function(value) { // eslint-disable-next-line no-invalid-this return Packet.prototype.writeDate.call(this, value, timezone); }; - } else if (isJSON(value)) { - value = JSON.stringify(value); - type = Types.JSON; - } else if (Buffer.isBuffer(value)) { - length = Packet.lengthCodedNumberLength(value.length) + value.length; - writer = Packet.prototype.writeLengthCodedBuffer; + } else { + // if parameter is not a date serialize it as string by default + value = value.toString(); + type = Types.VAR_STRING; } break; - + case Types.FLOAT: + length = 4; + writer = Packet.prototype.writeFloat; + break; + case Types.DOUBLE: + length = 8; + writer = Packet.prototype.writeDouble; + break; + case Types.JSON: + value = JSON.stringify(value); + type = parameterHints.type; + break; + case Types.LONGLONG: + // this should also cover anything that serializes to a string ( BigInt etc ) + type = Types.VAR_STRING; + value = value.toString(); + break; default: + type = Types.VAR_STRING; value = value.toString(); } } else { @@ -74,14 +122,19 @@ function toParameter(value, encoding, timezone) { } class Execute { - constructor(id, parameters, charsetNumber, timezone) { - this.id = id; + constructor(statement, parameters, charsetNumber, timezone) { + this.statement = statement; this.parameters = parameters; this.encoding = CharsetToEncoding[charsetNumber]; this.timezone = timezone; } toPacket() { + + if ((this.statement.parameters.length > 0 && !this.parameters) || (this.parameters && this.parameters.length !== this.statement.parameters.length)) { + throw new TypeError(`Incorrect number of bind parameters, expected ${this.statement.parameters.length} but supplied ${this.parameters.length}`); + } + // TODO: don't try to calculate packet length in advance, allocate some big buffer in advance (header + 256 bytes?) // and copy + reallocate if not enough // 0 + 4 - length, seqId @@ -95,8 +148,8 @@ class Execute { length += Math.floor((this.parameters.length + 7) / 8); length += 1; // new-params-bound-flag length += 2 * this.parameters.length; // type byte for each parameter if new-params-bound-flag is set - parameters = this.parameters.map(value => - toParameter(value, this.encoding, this.timezone) + parameters = this.parameters.map((value, index) => + toParameter(value, this.encoding, this.timezone, this.statement.parameters[index]) ); length += parameters.reduce( (accumulator, parameter) => accumulator + parameter.length, @@ -107,7 +160,7 @@ class Execute { const packet = new Packet(0, buffer, 0, length); packet.offset = 4; packet.writeInt8(CommandCodes.STMT_EXECUTE); - packet.writeInt32(this.id); + packet.writeInt32(this.statement.id); packet.writeInt8(CursorType.NO_CURSOR); // flags packet.writeInt32(1); // iteration-count, always 1 if (parameters) { diff --git a/test/integration/connection/test-execute-bind-boolean.js b/test/integration/connection/test-execute-bind-boolean.js index 39ec561803..36af266b81 100644 --- a/test/integration/connection/test-execute-bind-boolean.js +++ b/test/integration/connection/test-execute-bind-boolean.js @@ -18,5 +18,5 @@ connection.execute( ); process.on('exit', () => { - assert.deepEqual(rows, [{ trueValue: 1, falseValue: 0 }]); + assert.deepEqual(rows, [{ trueValue: 'true', falseValue: 'false' }]); }); diff --git a/test/integration/connection/test-execute-type-casting.js b/test/integration/connection/test-execute-type-casting.js index 02c416f790..8c4ae4cceb 100644 --- a/test/integration/connection/test-execute-type-casting.js +++ b/test/integration/connection/test-execute-type-casting.js @@ -38,8 +38,8 @@ connection.query('select 1', waitConnectErr => { let row; connection.execute( - `SELECT * FROM ${table} WHERE id = ?;`, - [1], + `SELECT * FROM ${table} WHERE id = ? LIMIT ?`, + [1, 1], (err, rows) => { if (err) { throw err; diff --git a/test/integration/connection/test-signed-tinyint.js b/test/integration/connection/test-signed-tinyint.js index 3a365b0456..10660c8a27 100644 --- a/test/integration/connection/test-signed-tinyint.js +++ b/test/integration/connection/test-signed-tinyint.js @@ -12,7 +12,7 @@ connection.query( connection.query('INSERT INTO signed_ints values (-3, -120, 500)'); connection.query('INSERT INTO signed_ints values (3, -110, -500)'); -connection.execute('SELECT * from signed_ints', [5], (err, _rows) => { +connection.execute('SELECT * from signed_ints', [], (err, _rows) => { if (err) { throw err; }