Skip to content

Commit

Permalink
[misc] execute performance improvement when lots of parameters
Browse files Browse the repository at this point in the history
And some micro improvement:
** Object.prototype.toString '[object Date]' for testing Date in place of costly instanceof
  • Loading branch information
rusher committed May 17, 2024
1 parent 5de51bf commit 15198b9
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 116 deletions.
4 changes: 2 additions & 2 deletions lib/cmd/batch-bulk.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class BatchBulk extends Parser {
parameterHeaderType[i] = FieldType.VAR_STRING;
break;
case 'object':
if (val instanceof Date) {
if (Object.prototype.toString.call(val) === '[object Date]') {
parameterHeaderType[i] = FieldType.TIMESTAMP;
} else if (Buffer.isBuffer(val)) {
parameterHeaderType[i] = FieldType.BLOB;
Expand Down Expand Up @@ -171,7 +171,7 @@ class BatchBulk extends Parser {
if (parameterHeaderType[i] !== FieldType.VAR_STRING) return false;
break;
case 'object':
if (val instanceof Date) {
if (Object.prototype.toString.call(val) === '[object Date]') {
if (parameterHeaderType[i] !== FieldType.TIMESTAMP) return false;
} else if (Buffer.isBuffer(val)) {
if (parameterHeaderType[i] !== FieldType.BLOB) return false;
Expand Down
2 changes: 1 addition & 1 deletion lib/cmd/encoder/binary-encoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class BinaryEncoder {
out.writeLengthEncodedString(value);
break;
case 'object':
if (value instanceof Date) {
if (Object.prototype.toString.call(value) === '[object Date]') {
out.writeBinaryDate(value);
} else if (Buffer.isBuffer(value)) {
out.writeLengthEncodedBuffer(value);
Expand Down
2 changes: 1 addition & 1 deletion lib/cmd/encoder/text-encoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class TextEncoder {
case 'object':
if (value == null) {
out.writeStringAscii('NULL');
} else if (value instanceof Date) {
} else if (Object.prototype.toString.call(value) === '[object Date]') {
out.writeStringAscii(TextEncoder.getLocalDate(value));
} else if (Buffer.isBuffer(value)) {
out.writeStringAscii("_BINARY '");
Expand Down
253 changes: 142 additions & 111 deletions lib/cmd/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const Parse = require('../misc/parse');
class Execute extends Parser {
constructor(resolve, reject, connOpts, cmdParam, prepare) {
super(resolve, reject, connOpts, cmdParam);
this.writeParam = BinaryEncoder.writeParam;
this.binary = true;
this.prepare = prepare;
this.canSkipMeta = true;
Expand Down Expand Up @@ -56,26 +55,104 @@ class Execute extends Parser {

if (!this.validateParameters(info)) return;

// send long data using COM_STMT_SEND_LONG_DATA
this.longDataStep = false; // send long data
// 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++) {
const value = this.values[i];
val = this.values[i];
// special check for GEOJSON that can be null even if object is not
if (
value != null &&
((typeof value === 'object' && typeof value.pipe === 'function' && typeof value.read === 'function') ||
(Buffer.isBuffer(value) && this.prepare))
val &&
val.type != null &&
[
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
].includes(val.type)
) {
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;
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 || val === undefined) {
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 {
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;
}
this.sendComStmtLongData(out, info, value);
return;
}
}

Expand Down Expand Up @@ -108,39 +185,16 @@ class Execute extends Parser {
return false;
}

//validate parameter is defined.
for (let i = 0; i < this.parameterCount; i++) {
if (this.opts.namedPlaceholders && this.placeHolderIndex && 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;
}

// special check for GEOJSON that can be null even if object is not
if (
this.values[i] &&
this.values[i].type != null &&
[
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
].includes(this.values[i].type)
) {
const geoBuff = BinaryEncoder.getBufferFromGeometryValue(this.values[i]);
if (geoBuff == null) {
this.values[i] = null;
} else {
this.values[i] = Buffer.concat([
Buffer.from([0, 0, 0, 0]), // SRID
geoBuff // WKB
]);
// 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;
}
}
}
Expand Down Expand Up @@ -200,66 +254,16 @@ class Execute extends Parser {

// send types
for (let i = 0; i < this.parameterCount; i++) {
const val = this.values[i];
if (val != null) {
switch (typeof val) {
case 'boolean':
out.writeInt8(FieldType.TINY);
break;
case 'bigint':
if (val >= 2n ** 63n) {
out.writeInt8(FieldType.NEWDECIMAL);
} else {
out.writeInt8(FieldType.BIGINT);
}
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) {
out.writeInt8(FieldType.INT);
break;
}
out.writeInt8(FieldType.DOUBLE);
break;
case 'string':
out.writeInt8(FieldType.VAR_STRING);
break;
case 'object':
if (val instanceof Date) {
out.writeInt8(FieldType.DATETIME);
} else if (Buffer.isBuffer(val)) {
out.writeInt8(FieldType.BLOB);
} else if (typeof val.toSqlString === 'function') {
out.writeInt8(FieldType.VAR_STRING);
} else if (typeof val.pipe === 'function' && typeof val.read === 'function') {
out.writeInt8(FieldType.BLOB);
} else {
out.writeInt8(FieldType.VAR_STRING);
}
break;
default:
out.writeInt8(FieldType.BLOB);
break;
}
} else {
out.writeInt8(FieldType.VAR_STRING);
}
out.writeInt8(this.parametersType[i].type);
out.writeInt8(0);
}

//********************************************
// send not null / not streaming values
//********************************************
for (let i = 0; i < this.parameterCount; i++) {
const value = this.values[i];
if (
value != null &&
!(typeof value === 'object' && typeof value.pipe === 'function' && typeof value.read === 'function') &&
!(Buffer.isBuffer(value) && this.prepare)
) {
this.writeParam(out, value, this.opts, info);
}
const parameterType = this.parametersType[i];
if (parameterType.encoder) parameterType.encoder(out, this.values[i]);
}
out.flush();
this.sending = false;
Expand All @@ -277,14 +281,8 @@ class Execute extends Parser {
this.paramWritten = function () {
if (this.longDataStep) {
for (; this.currentParam < this.parameterCount; this.currentParam++) {
const value = this.values[this.currentParam];
if (
(value != null &&
typeof value === 'object' &&
typeof value.pipe === 'function' &&
typeof value.read === 'function') ||
Buffer.isBuffer(value)
) {
if (this.parametersType[this.currentParam].isLongData()) {
const value = this.values[this.currentParam];
this.sendComStmtLongData(out, info, value);
return;
}
Expand All @@ -299,4 +297,37 @@ class Execute extends Parser {
}
}

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 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;
2 changes: 1 addition & 1 deletion lib/misc/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const escapeParameters = (opts, info, value) => {
case 'number':
return `${value}`;
case 'object':
if (value instanceof Date) {
if (Object.prototype.toString.call(value) === '[object Date]') {
return TextEncoder.getFixedFormatDate(value);
} else if (Buffer.isBuffer(value)) {
let stValue;
Expand Down

0 comments on commit 15198b9

Please sign in to comment.