123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- '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;
|