DiscofyAPI/node_modules/mariadb/lib/cmd/resultset.js

606 lines
20 KiB
JavaScript

'use strict';
const Command = require('./command');
const ServerStatus = require('../const/server-status');
const ColumnDefinition = require('./column-definition');
const Errors = require('../misc/errors');
const fs = require('fs');
const Parse = require('../misc/parse');
/**
* handle COM_QUERY / COM_STMT_EXECUTE results
* see : https://mariadb.com/kb/en/library/4-server-response-packets/
*/
class ResultSet extends Command {
constructor(resolve, reject) {
super(resolve, reject);
this._responseIndex = 0;
this._rows = [];
}
/**
* Read Query response packet.
* packet can be :
* - a result-set
* - an ERR_Packet
* - a OK_Packet
* - LOCAL_INFILE Packet
*
* @param packet query response
* @param out output writer
* @param opts connection options
* @param info connection info
*/
readResponsePacket(packet, out, opts, info) {
switch (packet.peek()) {
//*********************************************************************************************************
//* OK response
//*********************************************************************************************************
case 0x00:
return this.readOKPacket(packet, out, opts, info);
//*********************************************************************************************************
//* ERROR response
//*********************************************************************************************************
case 0xff:
const err = packet.readError(info, this.displaySql(), this.stack);
//force in transaction status, since query will have created a transaction if autocommit is off
//goal is to avoid unnecessary COMMIT/ROLLBACK.
info.status |= ServerStatus.STATUS_IN_TRANS;
return this.throwError(err, info);
//*********************************************************************************************************
//* LOCAL INFILE response
//*********************************************************************************************************
case 0xfb:
return this.readLocalInfile(packet, out, opts, info);
//*********************************************************************************************************
//* ResultSet
//*********************************************************************************************************
default:
return this.readResultSet(packet);
}
}
/**
* Read result-set packets :
* see https://mariadb.com/kb/en/library/resultset/
*
* @param packet Column count packet
* @returns {ResultSet.readColumn} next packet handler
*/
readResultSet(packet) {
this._columnCount = packet.readUnsignedLength();
this._getValue = this.opts.typeCast ? this.readCastValue : this.readRowData;
this._rows.push([]);
this._columns = [];
this.onPacketReceive = this.readColumn;
}
/**
* Assign global configuration option used by result-set to current query option.
* a little faster than Object.assign() since doest copy all information
*
* @param connOpts connection global configuration
* @param cmdOpts specific command options
*/
configAssign(connOpts, cmdOpts) {
if (!cmdOpts) {
this.opts = connOpts;
return;
}
this.opts = {
timeout: cmdOpts.timeout,
autoJsonMap: connOpts.autoJsonMap,
arrayParenthesis: connOpts.arrayParenthesis,
supportBigInt:
cmdOpts.supportBigInt != undefined ? cmdOpts.supportBigInt : connOpts.supportBigInt,
checkDuplicate:
cmdOpts.checkDuplicate != undefined ? cmdOpts.checkDuplicate : connOpts.checkDuplicate,
typeCast: cmdOpts.typeCast != undefined ? cmdOpts.typeCast : connOpts.typeCast,
rowsAsArray: cmdOpts.rowsAsArray != undefined ? cmdOpts.rowsAsArray : connOpts.rowsAsArray,
nestTables: cmdOpts.nestTables != undefined ? cmdOpts.nestTables : connOpts.nestTables,
dateStrings: cmdOpts.dateStrings != undefined ? cmdOpts.dateStrings : connOpts.dateStrings,
tz: cmdOpts.tz != undefined ? cmdOpts.tz : connOpts.tz,
pipelining: connOpts.pipelining,
localTz: cmdOpts.localTz != undefined ? cmdOpts.localTz : connOpts.localTz,
namedPlaceholders:
cmdOpts.namedPlaceholders != undefined
? cmdOpts.namedPlaceholders
: connOpts.namedPlaceholders,
maxAllowedPacket:
cmdOpts.maxAllowedPacket != undefined
? cmdOpts.maxAllowedPacket
: connOpts.maxAllowedPacket,
supportBigNumbers:
cmdOpts.supportBigNumbers != undefined
? cmdOpts.supportBigNumbers
: connOpts.supportBigNumbers,
permitSetMultiParamEntries:
cmdOpts.permitSetMultiParamEntries != undefined
? cmdOpts.permitSetMultiParamEntries
: connOpts.permitSetMultiParamEntries,
bigNumberStrings:
cmdOpts.bigNumberStrings != undefined ? cmdOpts.bigNumberStrings : connOpts.bigNumberStrings
};
}
/**
* Read OK_Packet.
* see https://mariadb.com/kb/en/library/ok_packet/
*
* @param packet OK_Packet
* @param opts connection options
* @param info connection information
* @param out output writer
* @returns {*} null or {Resultset.readResponsePacket} in case of multi-result-set
*/
readOKPacket(packet, out, opts, info) {
const okPacket = Command.parseOkPacket(packet, out, opts, info);
this._rows.push(okPacket);
if (info.status & ServerStatus.MORE_RESULTS_EXISTS) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
this.success(this._responseIndex === 0 ? this._rows[0] : this._rows);
}
/**
* Read COM_STMT_PREPARE response Packet.
* see https://mariadb.com/kb/en/library/com_stmt_prepare/#com_stmt_prepare-response
*
* @param packet COM_STMT_PREPARE_OK packet
* @param opts connection options
* @param info connection information
* @param out output writer
* @returns {*} null or {Resultset.readResponsePacket} in case of multi-result-set
*/
readPrepareResultPacket(packet, out, opts, info) {
switch (packet.peek()) {
//*********************************************************************************************************
//* OK response
//*********************************************************************************************************
case 0x00:
packet.skip(1); //skip header
this.statementId = packet.readInt32();
this.columnNo = packet.readUInt16();
this.parameterNo = packet.readUInt16();
if (this.columnNo > 0) return (this.onPacketReceive = this.skipColumnsPacket);
if (this.parameterNo > 0) return (this.onPacketReceive = this.skipParameterPacket);
return this.success();
//*********************************************************************************************************
//* ERROR response
//*********************************************************************************************************
case 0xff:
const err = packet.readError(info, this.displaySql(), this.stack);
//force in transaction status, since query will have created a transaction if autocommit is off
//goal is to avoid unnecessary COMMIT/ROLLBACK.
info.status |= ServerStatus.STATUS_IN_TRANS;
this.onPacketReceive = this.readResponsePacket;
return this.throwError(err, info);
//*********************************************************************************************************
//* Unexpected response
//*********************************************************************************************************
default:
info.status |= ServerStatus.STATUS_IN_TRANS;
this.onPacketReceive = this.readResponsePacket;
return this.throwError(Errors.ER_UNEXPECTED_PACKET, info);
}
}
skipColumnsPacket(packet, out, opts, info) {
this.columnNo--;
if (this.columnNo === 0) {
if (info.eofDeprecated) {
if (this.parameterNo > 0) return (this.onPacketReceive = this.skipParameterPacket);
this.success();
}
return (this.onPacketReceive = this.skipEofPacket);
}
}
skipEofPacket(packet, out, opts, info) {
if (this.parameterNo > 0) return (this.onPacketReceive = this.skipParameterPacket);
this.success();
}
skipParameterPacket(packet, out, opts, info) {
this.parameterNo--;
if (this.parameterNo === 0) {
if (info.eofDeprecated) return this.success();
return (this.onPacketReceive = this.skipEofPacket);
}
}
success(val) {
this.successEnd(val);
this._columns = null;
this._rows = null;
}
/**
* Read column information metadata
* see https://mariadb.com/kb/en/library/resultset/#column-definition-packet
*
* @param packet column definition packet
* @param out output writer
* @param opts connection options
* @param info connection information
* @returns {*}
*/
readColumn(packet, out, opts, info) {
if (this._columns.length !== this._columnCount) {
this._columns.push(new ColumnDefinition(packet, info));
}
// last column
if (this._columns.length === this._columnCount) {
if (this.opts.rowsAsArray) {
this.parseRow = this.parseRowAsArray;
} else {
this.tableHeader = new Array(this._columnCount);
if (this.opts.nestTables) {
this.parseRow = this.parseRowStd;
if (typeof this.opts.nestTables === 'string') {
for (let i = 0; i < this._columnCount; i++) {
this.tableHeader[i] =
this._columns[i].table() + this.opts.nestTables + this._columns[i].name();
}
this.checkDuplicates();
} else if (this.opts.nestTables === true) {
this.parseRow = this.parseRowNested;
for (let i = 0; i < this._columnCount; i++) {
this.tableHeader[i] = [this._columns[i].table(), this._columns[i].name()];
}
this.checkNestTablesDuplicates();
}
} else {
this.parseRow = this.parseRowStd;
for (let i = 0; i < this._columnCount; i++) {
this.tableHeader[i] = this._columns[i].name();
}
this.checkDuplicates();
}
}
this.emit('fields', this._columns);
return (this.onPacketReceive = info.eofDeprecated
? this.readResultSetRow
: this.readIntermediateEOF);
}
}
checkDuplicates() {
if (this.opts.checkDuplicate) {
for (let i = 0; i < this._columnCount; i++) {
if (this.tableHeader.indexOf(this.tableHeader[i], i + 1) > 0) {
const dupes = this.tableHeader.reduce(
(acc, v, i, arr) =>
arr.indexOf(v) !== i && acc.indexOf(v) === -1 ? acc.concat(v) : acc,
[]
);
this.throwUnexpectedError(
'Error in results, duplicate field name `' +
dupes[0] +
'`.\n' +
'(see option `checkDuplicate`)',
false,
null,
'42000',
Errors.ER_DUPLICATE_FIELD
);
}
}
}
}
checkNestTablesDuplicates() {
if (this.opts.checkDuplicate) {
for (let i = 0; i < this._columnCount; i++) {
for (let j = 0; j < i; j++) {
if (
this.tableHeader[j][0] === this.tableHeader[i][0] &&
this.tableHeader[j][1] === this.tableHeader[i][1]
) {
this.throwUnexpectedError(
'Error in results, duplicate field name `' +
this.tableHeader[i][0] +
'`.`' +
this.tableHeader[i][1] +
'`\n' +
'(see option `checkDuplicate`)',
false,
null,
'42000',
Errors.ER_DUPLICATE_FIELD
);
}
}
}
}
}
/**
* Read intermediate EOF.
* _only for server before MariaDB 10.2 / MySQL 5.7 that doesn't have CLIENT_DEPRECATE_EOF capability_
* see https://mariadb.com/kb/en/library/eof_packet/
*
* @param packet EOF Packet
* @param out output writer
* @param opts connection options
* @param info connection information
* @returns {*}
*/
readIntermediateEOF(packet, out, opts, info) {
if (packet.peek() !== 0xfe) {
return this.throwNewError(
'Error in protocol, expected EOF packet',
true,
info,
'42000',
Errors.ER_EOF_EXPECTED
);
}
//before MySQL 5.7.5, last EOF doesn't contain the good flag SERVER_MORE_RESULTS_EXISTS
//for OUT parameters. It must be checked here
//(5.7.5 does have the CLIENT_DEPRECATE_EOF capability, so this packet in not even send)
packet.skip(3);
info.status = packet.readUInt16();
this.isOutParameter = info.status & ServerStatus.PS_OUT_PARAMS;
this.onPacketReceive = this.readResultSetRow;
}
handleNewRows(row) {
this._rows[this._responseIndex].push(row);
}
/**
* Check if packet is result-set end = EOF of OK_Packet with EOF header according to CLIENT_DEPRECATE_EOF capability
* or a result-set row
*
* @param packet current packet
* @param out output writer
* @param opts connection options
* @param info connection information
* @returns {*}
*/
readResultSetRow(packet, out, opts, info) {
if (packet.peek() >= 0xfe) {
if (packet.peek() === 0xff) {
const err = packet.readError(info, this.displaySql(), this.stack);
//force in transaction status, since query will have created a transaction if autocommit is off
//goal is to avoid unnecessary COMMIT/ROLLBACK.
info.status |= ServerStatus.STATUS_IN_TRANS;
return this.throwError(err, info);
}
if (
(!info.eofDeprecated && packet.length() < 13) ||
(info.eofDeprecated && packet.length() < 0xffffff)
) {
if (!info.eofDeprecated) {
packet.skip(3);
info.status = packet.readUInt16();
} else {
packet.skip(1); //skip header
packet.skipLengthCodedNumber(); //skip update count
packet.skipLengthCodedNumber(); //skip insert id
info.status = packet.readUInt16();
}
if (opts.metaAsArray) {
//return promise object as array :
// example for SELECT 1 =>
// [
// [ {"1": 1} ], //rows
// [ColumnDefinition] //meta
// ]
if (!this._meta) {
this._meta = new Array(this._responseIndex);
}
this._meta[this._responseIndex] = this._columns;
if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
this.success(
this._responseIndex === 0 ? [this._rows[0], this._meta[0]] : [this._rows, this._meta]
);
} else {
//return promise object as rows that have meta property :
// example for SELECT 1 =>
// [
// {"1": 1},
// meta: [ColumnDefinition]
// ]
this._rows[this._responseIndex].meta = this._columns;
if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
this.success(this._responseIndex === 0 ? this._rows[0] : this._rows);
}
return;
}
}
const row = this.parseRow(this._columns, packet, opts);
this.handleNewRows(row);
}
/**
* Display current SQL with parameters (truncated if too big)
*
* @returns {string}
*/
displaySql() {
if (this.opts && this.initialValues) {
if (this.sql.length > this.opts.debugLen) {
return 'sql: ' + this.sql.substring(0, this.opts.debugLen) + '...';
}
let sqlMsg = 'sql: ' + this.sql + ' - parameters:';
return this.logParameters(sqlMsg, this.initialValues);
}
return 'sql: ' + this.sql + ' - parameters:[]';
}
logParameters(sqlMsg, values) {
if (this.opts.namedPlaceholders) {
sqlMsg += '{';
let first = true;
for (let key in values) {
if (first) {
first = false;
} else {
sqlMsg += ',';
}
sqlMsg += "'" + key + "':";
let param = values[key];
sqlMsg = ResultSet.logParam(sqlMsg, param);
if (sqlMsg.length > this.opts.debugLen) {
sqlMsg = sqlMsg.substr(0, this.opts.debugLen) + '...';
break;
}
}
sqlMsg += '}';
} else {
sqlMsg += '[';
if (Array.isArray(values)) {
for (let i = 0; i < values.length; i++) {
if (i !== 0) sqlMsg += ',';
let param = values[i];
sqlMsg = ResultSet.logParam(sqlMsg, param);
if (sqlMsg.length > this.opts.debugLen) {
sqlMsg = sqlMsg.substr(0, this.opts.debugLen) + '...';
break;
}
}
} else {
sqlMsg = ResultSet.logParam(sqlMsg, values);
if (sqlMsg.length > this.opts.debugLen) {
sqlMsg = sqlMsg.substr(0, this.opts.debugLen) + '...';
}
}
sqlMsg += ']';
}
return sqlMsg;
}
readLocalInfile(packet, out, opts, info) {
packet.skip(1); //skip header
out.startPacket(this);
const fileName = packet.readStringRemaining();
if (!Parse.validateFileName(this.sql, this.initialValues, fileName)) {
out.writeEmptyPacket();
const error = Errors.createError(
"LOCAL INFILE wrong filename. '" +
fileName +
"' doesn't correspond to query " +
this.sql +
'. Query cancelled. Check for malicious server / proxy',
false,
info,
'45034',
Errors.ER_LOCAL_INFILE_WRONG_FILENAME
);
process.nextTick(this.reject, error);
this.reject = null;
this.resolve = null;
return (this.onPacketReceive = this.readResponsePacket);
}
// this.sequenceNo = 2;
// this.compressSequenceNo = 2;
const stream = fs.createReadStream(fileName);
stream.on('error', (err) => {
out.writeEmptyPacket();
const error = Errors.createError(
'LOCAL INFILE command failed: ' + err.message,
false,
info,
'22000',
Errors.ER_LOCAL_INFILE_NOT_READABLE
);
process.nextTick(this.reject, error);
this.reject = null;
this.resolve = null;
});
stream.on('data', (chunk) => {
out.writeBuffer(chunk, 0, chunk.length);
});
stream.on('end', () => {
if (!out.isEmpty()) {
out.flushBuffer(false);
}
out.writeEmptyPacket();
});
this.onPacketReceive = this.readResponsePacket;
}
static logParam(sqlMsg, param) {
if (param === undefined || param === null) {
sqlMsg += param === undefined ? 'undefined' : 'null';
} else {
switch (param.constructor.name) {
case 'Buffer':
sqlMsg += '0x' + param.toString('hex', 0, Math.min(1024, param.length)) + '';
break;
case 'String':
sqlMsg += "'" + param + "'";
break;
case 'Date':
sqlMsg += getStringDate(param);
break;
case 'Object':
sqlMsg += JSON.stringify(param);
break;
default:
sqlMsg += param.toString();
}
}
return sqlMsg;
}
}
function getStringDate(param) {
return (
"'" +
('00' + (param.getMonth() + 1)).slice(-2) +
'/' +
('00' + param.getDate()).slice(-2) +
'/' +
param.getFullYear() +
' ' +
('00' + param.getHours()).slice(-2) +
':' +
('00' + param.getMinutes()).slice(-2) +
':' +
('00' + param.getSeconds()).slice(-2) +
'.' +
('000' + param.getMilliseconds()).slice(-3) +
"'"
);
}
module.exports = ResultSet;