'use strict'; const CursorType = require('../constants/cursor'); const CommandCodes = require('../constants/commands'); const Types = require('../constants/types'); const Packet = require('../packets/packet'); const CharsetToEncoding = require('../constants/charset_encodings.js'); function isJSON(value) { return ( Array.isArray(value) || value.constructor === Object || (typeof value.toJSON === 'function' && !Buffer.isBuffer(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; let length; let writer = function(value) { // eslint-disable-next-line no-invalid-this return Packet.prototype.writeLengthCodedString.call(this, value, encoding); }; if (value !== null) { switch (typeof value) { case 'undefined': throw new TypeError('Bind parameters must not contain undefined'); case 'number': type = Types.DOUBLE; length = 8; writer = Packet.prototype.writeDouble; break; case 'boolean': value = value | 0; type = Types.TINY; length = 1; writer = Packet.prototype.writeInt8; break; case 'object': 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); }; } 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; } break; default: value = value.toString(); } } else { value = ''; type = Types.NULL; } if (!length) { length = Packet.lengthCodedStringLength(value, encoding); } return { value, type, length, writer }; } class Execute { constructor(id, parameters, charsetNumber, timezone) { this.id = id; this.parameters = parameters; this.encoding = CharsetToEncoding[charsetNumber]; this.timezone = timezone; } toPacket() { // 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 // 4 + 1 - COM_EXECUTE // 5 + 4 - stmtId // 9 + 1 - flags // 10 + 4 - iteration-count (always 1) let length = 14; let parameters; if (this.parameters && this.parameters.length > 0) { 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) ); length += parameters.reduce( (accumulator, parameter) => accumulator + parameter.length, 0 ); } const buffer = Buffer.allocUnsafe(length); const packet = new Packet(0, buffer, 0, length); packet.offset = 4; packet.writeInt8(CommandCodes.STMT_EXECUTE); packet.writeInt32(this.id); packet.writeInt8(CursorType.NO_CURSOR); // flags packet.writeInt32(1); // iteration-count, always 1 if (parameters) { let bitmap = 0; let bitValue = 1; parameters.forEach(parameter => { if (parameter.type === Types.NULL) { bitmap += bitValue; } bitValue *= 2; if (bitValue === 256) { packet.writeInt8(bitmap); bitmap = 0; bitValue = 1; } }); if (bitValue !== 1) { packet.writeInt8(bitmap); } // TODO: explain meaning of the flag // afaik, if set n*2 bytes with type of parameter are sent before parameters // if not, previous execution types are used (TODO prooflink) packet.writeInt8(1); // new-params-bound-flag // Write parameter types parameters.forEach(parameter => { packet.writeInt8(parameter.type); // field type packet.writeInt8(0); // parameter flag }); // Write parameter values parameters.forEach(parameter => { if (parameter.type !== Types.NULL) { parameter.writer.call(packet, parameter.value); } }); } return packet; } } module.exports = Execute;