UNPKG

mariadb

Version:
339 lines (313 loc) 11.5 kB
// SPDX-License-Identifier: LGPL-2.1-or-later // Copyright (c) 2015-2024 MariaDB Corporation Ab 'use strict'; const Parser = require('./parser'); const Errors = require('../misc/errors'); const BinaryEncoder = require('./encoder/binary-encoder'); const FieldType = require('../const/field-type'); const Parse = require('../misc/parse'); /** * Protocol COM_STMT_EXECUTE * see : https://mariadb.com/kb/en/com_stmt_execute/ */ class Execute extends Parser { constructor(resolve, reject, connOpts, cmdParam, prepare) { super(resolve, reject, connOpts, cmdParam); this.binary = true; this.prepare = prepare; this.canSkipMeta = true; } /** * Send COM_QUERY * * @param out output writer * @param opts connection options * @param info connection information */ start(out, opts, info) { this.onPacketReceive = this.readResponsePacket; this.values = []; if (this.opts.namedPlaceholders) { if (this.prepare) { // using named placeholders, so change values accordingly this.values = new Array(this.prepare.parameterCount); this.placeHolderIndex = this.prepare._placeHolderIndex; } else { const res = Parse.searchPlaceholder(this.sql); this.placeHolderIndex = res.placeHolderIndex; this.values = new Array(this.placeHolderIndex.length); } if (this.initialValues) { for (let i = 0; i < this.placeHolderIndex.length; i++) { this.values[i] = this.initialValues[this.placeHolderIndex[i]]; } } } else { if (this.initialValues) this.values = Array.isArray(this.initialValues) ? this.initialValues : [this.initialValues]; } this.parameterCount = this.prepare ? this.prepare.parameterCount : this.values.length; if (!this.validateParameters(info)) return; // fill parameter data type this.parametersType = new Array(this.parameterCount); let hasLongData = false; // send long data let val; for (let i = 0; i < this.parameterCount; i++) { val = this.values[i]; // special check for GEOJSON that can be null even if object is not if ( val && val.type != null && [ 'Point', 'LineString', 'Polygon', 'MultiPoint', 'MultiLineString', 'MultiPolygon', 'GeometryCollection' ].includes(val.type) ) { const geoBuff = BinaryEncoder.getBufferFromGeometryValue(val); if (geoBuff == null) { this.values[i] = null; val = null; } else { this.values[i] = Buffer.concat([ Buffer.from([0, 0, 0, 0]), // SRID geoBuff // WKB ]); val = this.values[i]; } } if (val == null) { this.parametersType[i] = NULL_PARAM_TYPE; } else { switch (typeof val) { case 'boolean': this.parametersType[i] = BOOLEAN_TYPE; break; case 'bigint': if (val >= 2n ** 63n) { this.parametersType[i] = BIG_BIGINT_TYPE; } else { this.parametersType[i] = BIGINT_TYPE; } break; case 'number': // additional verification, to permit query without type, // like 'SELECT ?' returning same type of value if (Number.isInteger(val) && val >= -2147483648 && val < 2147483647) { this.parametersType[i] = INT_TYPE; break; } this.parametersType[i] = DOUBLE_TYPE; break; case 'string': this.parametersType[i] = STRING_TYPE; break; case 'object': if (Object.prototype.toString.call(val) === '[object Date]') { this.parametersType[i] = DATE_TYPE; } else if (Buffer.isBuffer(val)) { if (val.length < 16384 || !this.prepare) { this.parametersType[i] = BLOB_TYPE; } else { this.parametersType[i] = LONGBLOB_TYPE; hasLongData = true; } } else if (typeof val.toSqlString === 'function') { this.parametersType[i] = STRING_FCT_TYPE; } else if (typeof val.pipe === 'function' && typeof val.read === 'function') { hasLongData = true; this.parametersType[i] = STREAM_TYPE; } else if (String === val.constructor) { this.parametersType[i] = STRING_TOSTR_TYPE; } else { this.parametersType[i] = STRINGIFY_TYPE; } break; } } } // send long data using COM_STMT_SEND_LONG_DATA this.longDataStep = false; // send long data if (hasLongData) { for (let i = 0; i < this.parameterCount; i++) { if (this.parametersType[i].isLongData()) { if (opts.logger.query) opts.logger.query( `EXECUTE: (${this.prepare ? this.prepare.id : -1}) sql: ${opts.logParam ? this.displaySql() : this.sql}` ); if (!this.longDataStep) { this.longDataStep = true; this.registerStreamSendEvent(out, info); this.currentParam = i; } this.sendComStmtLongData(out, info, this.values[i]); return; } } } if (!this.longDataStep) { // no stream parameter, so can send directly if (opts.logger.query) opts.logger.query( `EXECUTE: (${this.prepare ? this.prepare.id : -1}) sql: ${opts.logParam ? this.displaySql() : this.sql}` ); this.sendComStmtExecute(out, info); } } /** * Validate that parameters exists and are defined. * * @param info connection info * @returns {boolean} return false if any error occur. */ validateParameters(info) { //validate parameter size. if (this.parameterCount > this.values.length) { this.sendCancelled( `Parameter at position ${this.values.length} is not set\\nsql: ${ this.opts.logParam ? this.displaySql() : this.sql }`, Errors.ER_MISSING_PARAMETER, info ); return false; } // validate placeholder if (this.opts.namedPlaceholders && this.placeHolderIndex) { for (let i = 0; i < this.parameterCount; i++) { if (this.values[i] === undefined) { let errMsg = `Parameter named ${this.placeHolderIndex[i]} is not set`; if (this.placeHolderIndex.length < this.parameterCount) { errMsg = `Command expect ${this.parameterCount} parameters, but found only ${this.placeHolderIndex.length} named parameters. You probably use question mark in place of named parameters`; } this.sendCancelled(errMsg, Errors.ER_PARAMETER_UNDEFINED, info); return false; } } } return true; } sendComStmtLongData(out, info, value) { out.startPacket(this); out.writeInt8(0x18); out.writeInt32(this.prepare.id); out.writeInt16(this.currentParam); if (Buffer.isBuffer(value)) { out.writeBuffer(value, 0, value.length); out.flush(); this.currentParam++; return this.paramWritten(); } this.sending = true; // streaming value.on('data', function (chunk) { out.writeBuffer(chunk, 0, chunk.length); }); value.on( 'end', function () { out.flush(); this.currentParam++; this.paramWritten(); }.bind(this) ); } /** * Send a COM_STMT_EXECUTE * @param out * @param info */ sendComStmtExecute(out, info) { let nullCount = ~~((this.parameterCount + 7) / 8); const nullBitsBuffer = Buffer.alloc(nullCount); for (let i = 0; i < this.parameterCount; i++) { if (this.values[i] == null) { nullBitsBuffer[~~(i / 8)] |= 1 << i % 8; } } out.startPacket(this); out.writeInt8(0x17); // COM_STMT_EXECUTE out.writeInt32(this.prepare ? this.prepare.id : -1); // Statement id out.writeInt8(0); // no cursor flag out.writeInt32(1); // 1 command out.writeBuffer(nullBitsBuffer, 0, nullCount); // null buffer out.writeInt8(1); // always send type to server // send types for (let i = 0; i < this.parameterCount; i++) { out.writeInt8(this.parametersType[i].type); out.writeInt8(0); } //******************************************** // send not null / not streaming values //******************************************** for (let i = 0; i < this.parameterCount; i++) { const parameterType = this.parametersType[i]; if (parameterType.encoder) parameterType.encoder(out, this.values[i]); } out.flush(); this.sending = false; this.emit('send_end'); } /** * Define params events. * Each parameter indicate that he is written to socket, * emitting event so next stream parameter can be written. */ registerStreamSendEvent(out, info) { // note : Implementation use recursive calls, but stack won't get near v8 max call stack size //since event launched for stream parameter only this.paramWritten = function () { if (this.longDataStep) { for (; this.currentParam < this.parameterCount; this.currentParam++) { if (this.parametersType[this.currentParam].isLongData()) { const value = this.values[this.currentParam]; this.sendComStmtLongData(out, info, value); return; } } this.longDataStep = false; // all streams have been send } if (!this.longDataStep) { this.sendComStmtExecute(out, info); } }.bind(this); } } class ParameterType { constructor(type, encoder, pipe = false, isNull = false) { this.pipe = pipe; this.type = type; this.encoder = encoder; this.isNull = isNull; } isLongData() { return this.encoder === null && !this.isNull; } } const NULL_PARAM_TYPE = new ParameterType(FieldType.VAR_STRING, null, false, true); const BOOLEAN_TYPE = new ParameterType(FieldType.TINY, (out, value) => out.writeInt8(value ? 0x01 : 0x00)); const BIG_BIGINT_TYPE = new ParameterType(FieldType.NEWDECIMAL, (out, value) => out.writeLengthEncodedString(value.toString()) ); const BIGINT_TYPE = new ParameterType(FieldType.BIGINT, (out, value) => out.writeBigInt(value)); const INT_TYPE = new ParameterType(FieldType.INT, (out, value) => out.writeInt32(value)); const DOUBLE_TYPE = new ParameterType(FieldType.DOUBLE, (out, value) => out.writeDouble(value)); const STRING_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) => out.writeLengthEncodedString(value)); const STRING_TOSTR_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) => out.writeLengthEncodedString(value.toString()) ); const DATE_TYPE = new ParameterType(FieldType.DATETIME, (out, value) => out.writeBinaryDate(value)); const BLOB_TYPE = new ParameterType(FieldType.BLOB, (out, value) => out.writeLengthEncodedBuffer(value)); const LONGBLOB_TYPE = new ParameterType(FieldType.BLOB, null); const STRING_FCT_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) => out.writeLengthEncodedString(String(value.toSqlString())) ); const STREAM_TYPE = new ParameterType(FieldType.BLOB, null, true); const STRINGIFY_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) => out.writeLengthEncodedString(JSON.stringify(value)) ); module.exports = Execute;