mariadb
Version:
fast mariadb or mysql connector.
678 lines (582 loc) • 18.6 kB
JavaScript
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
import Parser from './parser.js';
import * as Errors from '../misc/errors.js';
import BinaryEncoder from './encoder/binary-encoder.js';
import * as FieldType from '../const/field-type.js';
import OkPacket from './class/ok-packet.js';
import * as Capabilities from '../const/capabilities.js';
import * as ServerStatus from '../const/server-status.js';
// GeoJSON types supported by MariaDB
const GEOJSON_TYPES = [
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
];
/**
* Protocol COM_STMT_BULK_EXECUTE implementation
* Provides efficient batch operations for MariaDB servers >= 10.2.7
*
* @see https://mariadb.com/kb/en/library/com_stmt_bulk_execute/
*/
class BatchBulk extends Parser {
constructor(resolve, reject, connOpts, prepare, cmdParam) {
super(resolve, reject, connOpts, cmdParam);
this.cmdOpts = cmdParam.opts;
this.binary = true;
this.prepare = prepare;
this.canSkipMeta = true;
this.bulkPacketNo = 0;
this.sending = false;
this.firstError = null;
}
/**
* Initiates the batch operation
*
* @param {Object} out - Output writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
*/
start(out, opts, info) {
this.info = info;
this.values = this.initialValues;
// Batch operations don't support timeouts
if (this.cmdOpts && this.cmdOpts.timeout) {
return this.handleTimeoutError(info);
}
this.onPacketReceive = this.readResponsePacket;
// Process named placeholders if needed
if (this.opts.namedPlaceholders && this.prepare._placeHolderIndex) {
this.processNamedPlaceholders();
}
// Validate parameters before proceeding
if (!this.validateParameters(info)) return;
// Send the bulk execute command
this.sendComStmtBulkExecute(out, opts, info);
}
/**
* Handle timeout error case
* @param {Object} info - Connection information
* @private
*/
handleTimeoutError(info) {
this.bulkPacketNo = 1;
this.sending = false;
return this.sendCancelled('Cannot use timeout for Batch statement', Errors.client.ER_TIMEOUT_NOT_SUPPORTED);
}
/**
* Process named placeholders to positional parameters
* @private
*/
processNamedPlaceholders() {
this.values = [];
if (!this.initialValues) return;
const placeHolderIndex = this.prepare._placeHolderIndex;
const paramCount = this.prepare.parameterCount;
for (let r = 0; r < this.initialValues.length; r++) {
const val = this.initialValues[r];
const newRow = new Array(paramCount);
for (let i = 0; i < placeHolderIndex.length; i++) {
newRow[i] = val[placeHolderIndex[i]];
}
this.values[r] = newRow;
}
}
/**
* Determine parameter header types based on value types
*
* @param {Array} value - Parameter values
* @param {Number} parameterCount - Number of parameters
* @returns {Array} Array of parameter header types
*/
parameterHeaderFromValue(value, parameterCount) {
const parameterHeaderType = new Array(parameterCount);
for (let i = 0; i < parameterCount; i++) {
const val = value[i];
if (val == null) {
parameterHeaderType[i] = FieldType.VAR_STRING;
continue;
}
const type = typeof val;
switch (type) {
case 'boolean':
parameterHeaderType[i] = FieldType.TINY;
break;
case 'bigint':
parameterHeaderType[i] = val >= 2n ** 63n ? FieldType.NEWDECIMAL : FieldType.BIGINT;
break;
case 'number':
if (Number.isInteger(val) && val >= -2147483648 && val < 2147483647) {
parameterHeaderType[i] = FieldType.INT;
} else {
parameterHeaderType[i] = FieldType.DOUBLE;
}
break;
case 'string':
parameterHeaderType[i] = FieldType.VAR_STRING;
break;
case 'object':
parameterHeaderType[i] = this.getObjectFieldType(val);
break;
default:
parameterHeaderType[i] = FieldType.BLOB;
}
}
return parameterHeaderType;
}
/**
* Determine field type for object values
*
* @param {Object} val - Object value
* @returns {Number} Field type constant
* @private
*/
getObjectFieldType(val) {
if (Object.prototype.toString.call(val) === '[object Date]') {
return FieldType.DATETIME;
}
if (Buffer.isBuffer(val)) {
return FieldType.BLOB;
}
if (typeof val.toSqlString === 'function') {
return FieldType.VAR_STRING;
}
if (val.type != null && GEOJSON_TYPES.includes(val.type)) {
return FieldType.BLOB;
}
return FieldType.VAR_STRING;
}
/**
* Check if current value has same header as set in initial BULK header
*
* @param {Array} parameterHeaderType - Current header types
* @param {Array} value - Current values
* @param {Number} parameterCount - Number of parameters
* @returns {Boolean} True if headers are identical
*/
checkSameHeader(parameterHeaderType, value, parameterCount) {
for (let i = 0; i < parameterCount; i++) {
const val = value[i];
if (val == null) continue;
const type = typeof val;
switch (type) {
case 'boolean':
if (parameterHeaderType[i] !== FieldType.TINY) return false;
break;
case 'bigint':
if (val >= 2n ** 63n) {
if (parameterHeaderType[i] !== FieldType.VAR_STRING) return false;
} else {
if (parameterHeaderType[i] !== FieldType.BIGINT) return false;
}
break;
case 'number':
if (Number.isInteger(val) && val >= -2147483648 && val < 2147483647) {
if (parameterHeaderType[i] !== FieldType.INT) return false;
} else {
if (parameterHeaderType[i] !== FieldType.DOUBLE) return false;
}
break;
case 'string':
if (parameterHeaderType[i] !== FieldType.VAR_STRING) return false;
break;
case 'object':
if (!this.checkObjectHeaderType(val, parameterHeaderType[i])) {
return false;
}
break;
default:
if (parameterHeaderType[i] !== FieldType.BLOB) return false;
}
}
return true;
}
/**
* Check if object value matches expected header type
*
* @param {Object} val - Object value
* @param {Number} headerType - Expected header type
* @returns {Boolean} True if types match
* @private
*/
checkObjectHeaderType(val, headerType) {
if (Object.prototype.toString.call(val) === '[object Date]') {
return headerType === FieldType.TIMESTAMP;
}
if (Buffer.isBuffer(val)) {
return headerType === FieldType.BLOB;
}
if (typeof val.toSqlString === 'function') {
return headerType === FieldType.VAR_STRING;
}
if (val.type != null && GEOJSON_TYPES.includes(val.type)) {
return headerType === FieldType.BLOB;
}
return headerType === FieldType.VAR_STRING;
}
/**
* Send a COM_STMT_BULK_EXECUTE command
*
* @param {Object} out - Output packet writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
*/
sendComStmtBulkExecute(out, opts, info) {
if (opts.logger.query) {
opts.logger.query(`BULK: (${this.prepare.id}) sql: ${opts.logParam ? this.displaySql() : this.sql}`);
}
const parameterCount = this.prepare.parameterCount;
this.rowIdx = 0;
this.vals = this.values[this.rowIdx++];
let parameterHeaderType = this.parameterHeaderFromValue(this.vals, parameterCount);
let lastCmdData = null;
this.bulkPacketNo = 0;
this.sending = true;
// Main processing loop for batching parameters
main_loop: while (true) {
this.bulkPacketNo++;
out.startPacket(this);
out.writeInt8(0xfa); // COM_STMT_BULK_EXECUTE
out.writeInt32(this.prepare.id); // Statement id
// Set flags: SEND_TYPES_TO_SERVER + SEND_UNIT_RESULTS if possible
this.useUnitResult = (info.clientCapabilities & Capabilities.BULK_UNIT_RESULTS) > 0;
out.writeInt16(this.useUnitResult ? 192 : 128);
// Write parameter header types
for (let i = 0; i < parameterCount; i++) {
out.writeInt16(parameterHeaderType[i]);
}
// Handle leftover data from previous packet
if (lastCmdData != null) {
const err = out.checkMaxAllowedLength(lastCmdData.length, info);
if (err) {
this.sending = false;
this.throwError(err, info);
return;
}
out.writeBuffer(lastCmdData, 0, lastCmdData.length);
out.mark();
lastCmdData = null;
if (this.rowIdx >= this.values.length) {
break;
}
this.vals = this.values[this.rowIdx++];
}
parameter_loop: while (true) {
// Write each parameter value
for (let i = 0; i < parameterCount; i++) {
const param = this.vals[i];
if (param != null) {
// Special handling for GeoJSON
if (param.type != null && GEOJSON_TYPES.includes(param.type)) {
this.writeGeoJSONParam(out, param, info);
} else {
out.writeInt8(0x00); // value follows
BinaryEncoder.writeParam(out, param, this.opts, info);
}
} else {
out.writeInt8(0x01); // value is null
}
}
// Buffer management for packet boundaries
if (out.isMarked() && (out.hasDataAfterMark() || out.bufIsAfterMaxPacketLength())) {
// Packet length was ok at last mark, but won't be with new data
out.flushBufferStopAtMark();
out.mark();
lastCmdData = out.resetMark();
break;
}
out.mark();
if (out.hasDataAfterMark()) {
// Flush has been done
lastCmdData = out.resetMark();
break;
}
if (this.rowIdx >= this.values.length) {
break main_loop;
}
this.vals = this.values[this.rowIdx++];
// Check if parameter types have changed
if (!this.checkSameHeader(parameterHeaderType, this.vals, parameterCount)) {
out.flush();
// Reset header type for new packet
parameterHeaderType = this.parameterHeaderFromValue(this.vals, parameterCount);
break parameter_loop;
}
}
}
out.flush();
this.sending = false;
this.emit('send_end');
}
/**
* Write GeoJSON parameter to output buffer
*
* @param {Object} out - Output buffer
* @param {Object} param - GeoJSON parameter
* @param {Object} info - connection info data
* @private
*/
writeGeoJSONParam(out, param, info) {
const geoBuff = BinaryEncoder.getBufferFromGeometryValue(param);
if (geoBuff == null) {
out.writeInt8(0x01); // value is null
} else {
out.writeInt8(0x00); // value follows
const paramBuff = Buffer.concat([
Buffer.from([0, 0, 0, 0]), // SRID
geoBuff // WKB
]);
BinaryEncoder.writeParam(out, paramBuff, this.opts, info);
}
}
/**
* Format SQL with parameters for logging
*
* @returns {String} Formatted SQL string
*/
displaySql() {
if (this.sql.length > this.opts.debugLen) {
return this.sql.substring(0, this.opts.debugLen) + '...';
}
let sqlMsg = this.sql + ' - parameters:[';
for (let i = 0; i < this.initialValues.length; i++) {
if (i !== 0) sqlMsg += ',';
let param = this.initialValues[i];
sqlMsg = Parser.logParameters(this.opts, sqlMsg, param);
if (sqlMsg.length > this.opts.debugLen) {
return sqlMsg.substring(0, this.opts.debugLen) + '...';
}
}
sqlMsg += ']';
return sqlMsg;
}
/**
* Process successful query execution
*
* @param {Object} initVal - Query result
*/
success(initVal) {
this.bulkPacketNo--;
if (!this.sending && this.bulkPacketNo === 0) {
this.packet = null;
if (this.firstError) {
this.resolve = null;
this.onPacketReceive = null;
this._columns = null;
this._rows = null;
process.nextTick(this.reject, this.firstError);
this.reject = null;
this.emit('end', this.firstError);
} else {
this.processResults();
}
return;
}
if (!this.firstError) {
this._responseIndex++;
this.onPacketReceive = this.readResponsePacket;
}
}
/**
* Process successful results based on result type
* @private
*/
processResults() {
if (this._rows[0] && this._rows[0][0] && this._rows[0][0]['Affected_rows'] !== undefined) {
this.processUnitResults();
} else if (
this._rows[0].affectedRows !== undefined &&
!(this.opts.fullResult === undefined || this.opts.fullResult === true)
) {
this.processAggregatedResults();
} else {
this.processRowResults();
}
this._columns = null;
this._rows = null;
}
/**
* Process unit results (for bulk operations with unit results)
* @private
*/
processUnitResults() {
if (this.opts.fullResult === undefined || this.opts.fullResult === true) {
const rs = [];
this._rows.forEach((row) => {
row.forEach((unitRow) => {
rs.push(new OkPacket(Number(unitRow['Affected_rows']), BigInt(unitRow['Id']), 0));
});
});
this.successEnd(this.opts.metaAsArray ? [rs, []] : rs);
} else {
let totalAffectedRows = 0;
this._rows.forEach((row) => {
row.forEach((unitRow) => {
totalAffectedRows += Number(unitRow['Affected_rows']);
});
});
const rs = new OkPacket(totalAffectedRows, BigInt(this._rows[0][0]['Id']), 0);
this.successEnd(this.opts.metaAsArray ? [rs, []] : rs);
}
}
/**
* Process aggregated results (for non-fullResult mode)
* @private
*/
processAggregatedResults() {
let totalAffectedRows = 0;
this._rows.forEach((row) => {
totalAffectedRows += row.affectedRows;
});
const rs = new OkPacket(totalAffectedRows, this._rows[0].insertId, this._rows[this._rows.length - 1].warningStatus);
this.successEnd(this.opts.metaAsArray ? [rs, []] : rs);
}
/**
* Process row results (for SELECT queries)
* @private
*/
processRowResults() {
if (this._rows.length === 1) {
this.successEnd(this.opts.metaAsArray ? [this._rows[0], this._columns] : this._rows[0]);
return;
}
if (this.opts.metaAsArray) {
if (this.useUnitResult) {
const rs = [];
this._rows.forEach((row, i) => {
if (i % 2 === 0) rs.push(...row);
});
this.successEnd([rs, this.prepare.columns]);
} else {
const rs = [];
this._rows.forEach((row) => {
rs.push(...row);
});
this.successEnd([rs, this._columns]);
}
} else {
if (this.useUnitResult) {
const rs = [];
this._rows.forEach((row, i) => {
if (i % 2 === 0) rs.push(...row);
});
Object.defineProperty(rs, 'meta', {
value: this._columns,
writable: true,
enumerable: this.opts.metaEnumerable
});
this.successEnd(rs);
} else {
if (this._rows.length === 1) {
this.successEnd(this._rows[0]);
} else {
const rs = [];
if (Array.isArray(this._rows[0])) {
this._rows.forEach((row) => {
rs.push(...row);
});
} else rs.push(...this._rows);
Object.defineProperty(rs, 'meta', {
value: this._columns,
writable: true,
enumerable: this.opts.metaEnumerable
});
this.successEnd(rs);
}
}
}
}
/**
* Handle OK packet success
*
* @param {Object} okPacket - OK packet
* @param {Object} info - Connection information
*/
okPacketSuccess(okPacket, info) {
this._rows.push(okPacket);
if (info.status & ServerStatus.MORE_RESULTS_EXISTS) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
if (this.opts.metaAsArray) {
if (!this._meta) {
this._meta = new Array(this._responseIndex);
}
this._meta[this._responseIndex] = null;
this.success([this._rows, this._meta]);
} else {
this.success(this._rows);
}
}
/**
* Handle errors during query execution
*
* @param {Error} err - Error object
* @param {Object} info - Connection information
*/
throwError(err, info) {
this.bulkPacketNo--;
if (!this.firstError) {
if (err.fatal) {
this.bulkPacketNo = 0;
}
if (this.cmdParam.stack) {
err = Errors.createError(
err.message,
err.errno,
info,
err.sqlState,
this.opts.logParam ? this.displaySql() : this.sql,
err.fatal,
this.cmdParam.stack,
false
);
}
this.firstError = err;
}
if (!this.sending && this.bulkPacketNo === 0) {
this.resolve = null;
this.emit('send_end');
process.nextTick(this.reject, this.firstError);
this.reject = null;
this.onPacketReceive = null;
this.emit('end', this.firstError);
} else {
this.onPacketReceive = this.readResponsePacket;
}
}
/**
* Validate that parameters exist and are defined
*
* @param {Object} info - Connection information
* @returns {Boolean} Returns false if any error occurs
*/
validateParameters(info) {
const nbParameter = this.prepare.parameterCount;
for (let r = 0; r < this.values.length; r++) {
if (!Array.isArray(this.values[r])) {
this.values[r] = [this.values[r]];
}
if (this.values[r].length < nbParameter) {
this.emit('send_end');
this.throwNewError(
`Expect ${nbParameter} parameters, but at index ${r}, parameters only contains ${this.values[r].length}\n ${
this.opts.logParam ? this.displaySql() : this.sql
}`,
false,
info,
'HY000',
Errors.client.ER_PARAMETER_UNDEFINED
);
return false;
}
}
return true;
}
}
export default BatchBulk;