339 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| //  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;
 |