Backend half
This commit is contained in:
+338
@@ -0,0 +1,338 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user