resultset.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. 'use strict';
  2. const Command = require('./command');
  3. const ServerStatus = require('../const/server-status');
  4. const ColumnDefinition = require('./column-definition');
  5. const Errors = require('../misc/errors');
  6. const fs = require('fs');
  7. const Parse = require('../misc/parse');
  8. /**
  9. * handle COM_QUERY / COM_STMT_EXECUTE results
  10. * see : https://mariadb.com/kb/en/library/4-server-response-packets/
  11. */
  12. class ResultSet extends Command {
  13. constructor(resolve, reject) {
  14. super(resolve, reject);
  15. this._responseIndex = 0;
  16. this._rows = [];
  17. }
  18. /**
  19. * Read Query response packet.
  20. * packet can be :
  21. * - a result-set
  22. * - an ERR_Packet
  23. * - a OK_Packet
  24. * - LOCAL_INFILE Packet
  25. *
  26. * @param packet query response
  27. * @param out output writer
  28. * @param opts connection options
  29. * @param info connection info
  30. */
  31. readResponsePacket(packet, out, opts, info) {
  32. switch (packet.peek()) {
  33. //*********************************************************************************************************
  34. //* OK response
  35. //*********************************************************************************************************
  36. case 0x00:
  37. return this.readOKPacket(packet, out, opts, info);
  38. //*********************************************************************************************************
  39. //* ERROR response
  40. //*********************************************************************************************************
  41. case 0xff:
  42. const err = packet.readError(info, this.displaySql(), this.stack);
  43. //force in transaction status, since query will have created a transaction if autocommit is off
  44. //goal is to avoid unnecessary COMMIT/ROLLBACK.
  45. info.status |= ServerStatus.STATUS_IN_TRANS;
  46. return this.throwError(err, info);
  47. //*********************************************************************************************************
  48. //* LOCAL INFILE response
  49. //*********************************************************************************************************
  50. case 0xfb:
  51. return this.readLocalInfile(packet, out, opts, info);
  52. //*********************************************************************************************************
  53. //* ResultSet
  54. //*********************************************************************************************************
  55. default:
  56. return this.readResultSet(packet);
  57. }
  58. }
  59. /**
  60. * Read result-set packets :
  61. * see https://mariadb.com/kb/en/library/resultset/
  62. *
  63. * @param packet Column count packet
  64. * @returns {ResultSet.readColumn} next packet handler
  65. */
  66. readResultSet(packet) {
  67. this._columnCount = packet.readUnsignedLength();
  68. this._getValue = this.opts.typeCast ? this.readCastValue : this.readRowData;
  69. this._rows.push([]);
  70. this._columns = [];
  71. this.onPacketReceive = this.readColumn;
  72. }
  73. /**
  74. * Assign global configuration option used by result-set to current query option.
  75. * a little faster than Object.assign() since doest copy all information
  76. *
  77. * @param connOpts connection global configuration
  78. * @param cmdOpts specific command options
  79. */
  80. configAssign(connOpts, cmdOpts) {
  81. if (!cmdOpts) {
  82. this.opts = connOpts;
  83. return;
  84. }
  85. this.opts = {
  86. timeout: cmdOpts.timeout,
  87. autoJsonMap: connOpts.autoJsonMap,
  88. arrayParenthesis: connOpts.arrayParenthesis,
  89. supportBigInt:
  90. cmdOpts.supportBigInt != undefined ? cmdOpts.supportBigInt : connOpts.supportBigInt,
  91. checkDuplicate:
  92. cmdOpts.checkDuplicate != undefined ? cmdOpts.checkDuplicate : connOpts.checkDuplicate,
  93. typeCast: cmdOpts.typeCast != undefined ? cmdOpts.typeCast : connOpts.typeCast,
  94. rowsAsArray: cmdOpts.rowsAsArray != undefined ? cmdOpts.rowsAsArray : connOpts.rowsAsArray,
  95. nestTables: cmdOpts.nestTables != undefined ? cmdOpts.nestTables : connOpts.nestTables,
  96. dateStrings: cmdOpts.dateStrings != undefined ? cmdOpts.dateStrings : connOpts.dateStrings,
  97. tz: cmdOpts.tz != undefined ? cmdOpts.tz : connOpts.tz,
  98. pipelining: connOpts.pipelining,
  99. localTz: cmdOpts.localTz != undefined ? cmdOpts.localTz : connOpts.localTz,
  100. namedPlaceholders:
  101. cmdOpts.namedPlaceholders != undefined
  102. ? cmdOpts.namedPlaceholders
  103. : connOpts.namedPlaceholders,
  104. maxAllowedPacket:
  105. cmdOpts.maxAllowedPacket != undefined
  106. ? cmdOpts.maxAllowedPacket
  107. : connOpts.maxAllowedPacket,
  108. supportBigNumbers:
  109. cmdOpts.supportBigNumbers != undefined
  110. ? cmdOpts.supportBigNumbers
  111. : connOpts.supportBigNumbers,
  112. permitSetMultiParamEntries:
  113. cmdOpts.permitSetMultiParamEntries != undefined
  114. ? cmdOpts.permitSetMultiParamEntries
  115. : connOpts.permitSetMultiParamEntries,
  116. bigNumberStrings:
  117. cmdOpts.bigNumberStrings != undefined ? cmdOpts.bigNumberStrings : connOpts.bigNumberStrings
  118. };
  119. }
  120. /**
  121. * Read OK_Packet.
  122. * see https://mariadb.com/kb/en/library/ok_packet/
  123. *
  124. * @param packet OK_Packet
  125. * @param opts connection options
  126. * @param info connection information
  127. * @param out output writer
  128. * @returns {*} null or {Resultset.readResponsePacket} in case of multi-result-set
  129. */
  130. readOKPacket(packet, out, opts, info) {
  131. const okPacket = Command.parseOkPacket(packet, out, opts, info);
  132. this._rows.push(okPacket);
  133. if (info.status & ServerStatus.MORE_RESULTS_EXISTS) {
  134. this._responseIndex++;
  135. return (this.onPacketReceive = this.readResponsePacket);
  136. }
  137. this.success(this._responseIndex === 0 ? this._rows[0] : this._rows);
  138. }
  139. /**
  140. * Read COM_STMT_PREPARE response Packet.
  141. * see https://mariadb.com/kb/en/library/com_stmt_prepare/#com_stmt_prepare-response
  142. *
  143. * @param packet COM_STMT_PREPARE_OK packet
  144. * @param opts connection options
  145. * @param info connection information
  146. * @param out output writer
  147. * @returns {*} null or {Resultset.readResponsePacket} in case of multi-result-set
  148. */
  149. readPrepareResultPacket(packet, out, opts, info) {
  150. switch (packet.peek()) {
  151. //*********************************************************************************************************
  152. //* OK response
  153. //*********************************************************************************************************
  154. case 0x00:
  155. packet.skip(1); //skip header
  156. this.statementId = packet.readInt32();
  157. this.columnNo = packet.readUInt16();
  158. this.parameterNo = packet.readUInt16();
  159. if (this.columnNo > 0) return (this.onPacketReceive = this.skipColumnsPacket);
  160. if (this.parameterNo > 0) return (this.onPacketReceive = this.skipParameterPacket);
  161. return this.success();
  162. //*********************************************************************************************************
  163. //* ERROR response
  164. //*********************************************************************************************************
  165. case 0xff:
  166. const err = packet.readError(info, this.displaySql(), this.stack);
  167. //force in transaction status, since query will have created a transaction if autocommit is off
  168. //goal is to avoid unnecessary COMMIT/ROLLBACK.
  169. info.status |= ServerStatus.STATUS_IN_TRANS;
  170. this.onPacketReceive = this.readResponsePacket;
  171. return this.throwError(err, info);
  172. //*********************************************************************************************************
  173. //* Unexpected response
  174. //*********************************************************************************************************
  175. default:
  176. info.status |= ServerStatus.STATUS_IN_TRANS;
  177. this.onPacketReceive = this.readResponsePacket;
  178. return this.throwError(Errors.ER_UNEXPECTED_PACKET, info);
  179. }
  180. }
  181. skipColumnsPacket(packet, out, opts, info) {
  182. this.columnNo--;
  183. if (this.columnNo === 0) {
  184. if (info.eofDeprecated) {
  185. if (this.parameterNo > 0) return (this.onPacketReceive = this.skipParameterPacket);
  186. this.success();
  187. }
  188. return (this.onPacketReceive = this.skipEofPacket);
  189. }
  190. }
  191. skipEofPacket(packet, out, opts, info) {
  192. if (this.parameterNo > 0) return (this.onPacketReceive = this.skipParameterPacket);
  193. this.success();
  194. }
  195. skipParameterPacket(packet, out, opts, info) {
  196. this.parameterNo--;
  197. if (this.parameterNo === 0) {
  198. if (info.eofDeprecated) return this.success();
  199. return (this.onPacketReceive = this.skipEofPacket);
  200. }
  201. }
  202. success(val) {
  203. this.successEnd(val);
  204. this._columns = null;
  205. this._rows = null;
  206. }
  207. /**
  208. * Read column information metadata
  209. * see https://mariadb.com/kb/en/library/resultset/#column-definition-packet
  210. *
  211. * @param packet column definition packet
  212. * @param out output writer
  213. * @param opts connection options
  214. * @param info connection information
  215. * @returns {*}
  216. */
  217. readColumn(packet, out, opts, info) {
  218. if (this._columns.length !== this._columnCount) {
  219. this._columns.push(new ColumnDefinition(packet, info));
  220. }
  221. // last column
  222. if (this._columns.length === this._columnCount) {
  223. if (this.opts.rowsAsArray) {
  224. this.parseRow = this.parseRowAsArray;
  225. } else {
  226. this.tableHeader = new Array(this._columnCount);
  227. if (this.opts.nestTables) {
  228. this.parseRow = this.parseRowStd;
  229. if (typeof this.opts.nestTables === 'string') {
  230. for (let i = 0; i < this._columnCount; i++) {
  231. this.tableHeader[i] =
  232. this._columns[i].table() + this.opts.nestTables + this._columns[i].name();
  233. }
  234. this.checkDuplicates();
  235. } else if (this.opts.nestTables === true) {
  236. this.parseRow = this.parseRowNested;
  237. for (let i = 0; i < this._columnCount; i++) {
  238. this.tableHeader[i] = [this._columns[i].table(), this._columns[i].name()];
  239. }
  240. this.checkNestTablesDuplicates();
  241. }
  242. } else {
  243. this.parseRow = this.parseRowStd;
  244. for (let i = 0; i < this._columnCount; i++) {
  245. this.tableHeader[i] = this._columns[i].name();
  246. }
  247. this.checkDuplicates();
  248. }
  249. }
  250. this.emit('fields', this._columns);
  251. return (this.onPacketReceive = info.eofDeprecated
  252. ? this.readResultSetRow
  253. : this.readIntermediateEOF);
  254. }
  255. }
  256. checkDuplicates() {
  257. if (this.opts.checkDuplicate) {
  258. for (let i = 0; i < this._columnCount; i++) {
  259. if (this.tableHeader.indexOf(this.tableHeader[i], i + 1) > 0) {
  260. const dupes = this.tableHeader.reduce(
  261. (acc, v, i, arr) =>
  262. arr.indexOf(v) !== i && acc.indexOf(v) === -1 ? acc.concat(v) : acc,
  263. []
  264. );
  265. this.throwUnexpectedError(
  266. 'Error in results, duplicate field name `' +
  267. dupes[0] +
  268. '`.\n' +
  269. '(see option `checkDuplicate`)',
  270. false,
  271. null,
  272. '42000',
  273. Errors.ER_DUPLICATE_FIELD
  274. );
  275. }
  276. }
  277. }
  278. }
  279. checkNestTablesDuplicates() {
  280. if (this.opts.checkDuplicate) {
  281. for (let i = 0; i < this._columnCount; i++) {
  282. for (let j = 0; j < i; j++) {
  283. if (
  284. this.tableHeader[j][0] === this.tableHeader[i][0] &&
  285. this.tableHeader[j][1] === this.tableHeader[i][1]
  286. ) {
  287. this.throwUnexpectedError(
  288. 'Error in results, duplicate field name `' +
  289. this.tableHeader[i][0] +
  290. '`.`' +
  291. this.tableHeader[i][1] +
  292. '`\n' +
  293. '(see option `checkDuplicate`)',
  294. false,
  295. null,
  296. '42000',
  297. Errors.ER_DUPLICATE_FIELD
  298. );
  299. }
  300. }
  301. }
  302. }
  303. }
  304. /**
  305. * Read intermediate EOF.
  306. * _only for server before MariaDB 10.2 / MySQL 5.7 that doesn't have CLIENT_DEPRECATE_EOF capability_
  307. * see https://mariadb.com/kb/en/library/eof_packet/
  308. *
  309. * @param packet EOF Packet
  310. * @param out output writer
  311. * @param opts connection options
  312. * @param info connection information
  313. * @returns {*}
  314. */
  315. readIntermediateEOF(packet, out, opts, info) {
  316. if (packet.peek() !== 0xfe) {
  317. return this.throwNewError(
  318. 'Error in protocol, expected EOF packet',
  319. true,
  320. info,
  321. '42000',
  322. Errors.ER_EOF_EXPECTED
  323. );
  324. }
  325. //before MySQL 5.7.5, last EOF doesn't contain the good flag SERVER_MORE_RESULTS_EXISTS
  326. //for OUT parameters. It must be checked here
  327. //(5.7.5 does have the CLIENT_DEPRECATE_EOF capability, so this packet in not even send)
  328. packet.skip(3);
  329. info.status = packet.readUInt16();
  330. this.isOutParameter = info.status & ServerStatus.PS_OUT_PARAMS;
  331. this.onPacketReceive = this.readResultSetRow;
  332. }
  333. handleNewRows(row) {
  334. this._rows[this._responseIndex].push(row);
  335. }
  336. /**
  337. * Check if packet is result-set end = EOF of OK_Packet with EOF header according to CLIENT_DEPRECATE_EOF capability
  338. * or a result-set row
  339. *
  340. * @param packet current packet
  341. * @param out output writer
  342. * @param opts connection options
  343. * @param info connection information
  344. * @returns {*}
  345. */
  346. readResultSetRow(packet, out, opts, info) {
  347. if (packet.peek() >= 0xfe) {
  348. if (packet.peek() === 0xff) {
  349. const err = packet.readError(info, this.displaySql(), this.stack);
  350. //force in transaction status, since query will have created a transaction if autocommit is off
  351. //goal is to avoid unnecessary COMMIT/ROLLBACK.
  352. info.status |= ServerStatus.STATUS_IN_TRANS;
  353. return this.throwError(err, info);
  354. }
  355. if (
  356. (!info.eofDeprecated && packet.length() < 13) ||
  357. (info.eofDeprecated && packet.length() < 0xffffff)
  358. ) {
  359. if (!info.eofDeprecated) {
  360. packet.skip(3);
  361. info.status = packet.readUInt16();
  362. } else {
  363. packet.skip(1); //skip header
  364. packet.skipLengthCodedNumber(); //skip update count
  365. packet.skipLengthCodedNumber(); //skip insert id
  366. info.status = packet.readUInt16();
  367. }
  368. if (opts.metaAsArray) {
  369. //return promise object as array :
  370. // example for SELECT 1 =>
  371. // [
  372. // [ {"1": 1} ], //rows
  373. // [ColumnDefinition] //meta
  374. // ]
  375. if (!this._meta) {
  376. this._meta = new Array(this._responseIndex);
  377. }
  378. this._meta[this._responseIndex] = this._columns;
  379. if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) {
  380. this._responseIndex++;
  381. return (this.onPacketReceive = this.readResponsePacket);
  382. }
  383. this.success(
  384. this._responseIndex === 0 ? [this._rows[0], this._meta[0]] : [this._rows, this._meta]
  385. );
  386. } else {
  387. //return promise object as rows that have meta property :
  388. // example for SELECT 1 =>
  389. // [
  390. // {"1": 1},
  391. // meta: [ColumnDefinition]
  392. // ]
  393. this._rows[this._responseIndex].meta = this._columns;
  394. if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) {
  395. this._responseIndex++;
  396. return (this.onPacketReceive = this.readResponsePacket);
  397. }
  398. this.success(this._responseIndex === 0 ? this._rows[0] : this._rows);
  399. }
  400. return;
  401. }
  402. }
  403. const row = this.parseRow(this._columns, packet, opts);
  404. this.handleNewRows(row);
  405. }
  406. /**
  407. * Display current SQL with parameters (truncated if too big)
  408. *
  409. * @returns {string}
  410. */
  411. displaySql() {
  412. if (this.opts && this.initialValues) {
  413. if (this.sql.length > this.opts.debugLen) {
  414. return 'sql: ' + this.sql.substring(0, this.opts.debugLen) + '...';
  415. }
  416. let sqlMsg = 'sql: ' + this.sql + ' - parameters:';
  417. return this.logParameters(sqlMsg, this.initialValues);
  418. }
  419. return 'sql: ' + this.sql + ' - parameters:[]';
  420. }
  421. logParameters(sqlMsg, values) {
  422. if (this.opts.namedPlaceholders) {
  423. sqlMsg += '{';
  424. let first = true;
  425. for (let key in values) {
  426. if (first) {
  427. first = false;
  428. } else {
  429. sqlMsg += ',';
  430. }
  431. sqlMsg += "'" + key + "':";
  432. let param = values[key];
  433. sqlMsg = ResultSet.logParam(sqlMsg, param);
  434. if (sqlMsg.length > this.opts.debugLen) {
  435. sqlMsg = sqlMsg.substr(0, this.opts.debugLen) + '...';
  436. break;
  437. }
  438. }
  439. sqlMsg += '}';
  440. } else {
  441. sqlMsg += '[';
  442. if (Array.isArray(values)) {
  443. for (let i = 0; i < values.length; i++) {
  444. if (i !== 0) sqlMsg += ',';
  445. let param = values[i];
  446. sqlMsg = ResultSet.logParam(sqlMsg, param);
  447. if (sqlMsg.length > this.opts.debugLen) {
  448. sqlMsg = sqlMsg.substr(0, this.opts.debugLen) + '...';
  449. break;
  450. }
  451. }
  452. } else {
  453. sqlMsg = ResultSet.logParam(sqlMsg, values);
  454. if (sqlMsg.length > this.opts.debugLen) {
  455. sqlMsg = sqlMsg.substr(0, this.opts.debugLen) + '...';
  456. }
  457. }
  458. sqlMsg += ']';
  459. }
  460. return sqlMsg;
  461. }
  462. readLocalInfile(packet, out, opts, info) {
  463. packet.skip(1); //skip header
  464. out.startPacket(this);
  465. const fileName = packet.readStringRemaining();
  466. if (!Parse.validateFileName(this.sql, this.initialValues, fileName)) {
  467. out.writeEmptyPacket();
  468. const error = Errors.createError(
  469. "LOCAL INFILE wrong filename. '" +
  470. fileName +
  471. "' doesn't correspond to query " +
  472. this.sql +
  473. '. Query cancelled. Check for malicious server / proxy',
  474. false,
  475. info,
  476. '45034',
  477. Errors.ER_LOCAL_INFILE_WRONG_FILENAME
  478. );
  479. process.nextTick(this.reject, error);
  480. this.reject = null;
  481. this.resolve = null;
  482. return (this.onPacketReceive = this.readResponsePacket);
  483. }
  484. // this.sequenceNo = 2;
  485. // this.compressSequenceNo = 2;
  486. const stream = fs.createReadStream(fileName);
  487. stream.on('error', (err) => {
  488. out.writeEmptyPacket();
  489. const error = Errors.createError(
  490. 'LOCAL INFILE command failed: ' + err.message,
  491. false,
  492. info,
  493. '22000',
  494. Errors.ER_LOCAL_INFILE_NOT_READABLE
  495. );
  496. process.nextTick(this.reject, error);
  497. this.reject = null;
  498. this.resolve = null;
  499. });
  500. stream.on('data', (chunk) => {
  501. out.writeBuffer(chunk, 0, chunk.length);
  502. });
  503. stream.on('end', () => {
  504. if (!out.isEmpty()) {
  505. out.flushBuffer(false);
  506. }
  507. out.writeEmptyPacket();
  508. });
  509. this.onPacketReceive = this.readResponsePacket;
  510. }
  511. static logParam(sqlMsg, param) {
  512. if (param === undefined || param === null) {
  513. sqlMsg += param === undefined ? 'undefined' : 'null';
  514. } else {
  515. switch (param.constructor.name) {
  516. case 'Buffer':
  517. sqlMsg += '0x' + param.toString('hex', 0, Math.min(1024, param.length)) + '';
  518. break;
  519. case 'String':
  520. sqlMsg += "'" + param + "'";
  521. break;
  522. case 'Date':
  523. sqlMsg += getStringDate(param);
  524. break;
  525. case 'Object':
  526. sqlMsg += JSON.stringify(param);
  527. break;
  528. default:
  529. sqlMsg += param.toString();
  530. }
  531. }
  532. return sqlMsg;
  533. }
  534. }
  535. function getStringDate(param) {
  536. return (
  537. "'" +
  538. ('00' + (param.getMonth() + 1)).slice(-2) +
  539. '/' +
  540. ('00' + param.getDate()).slice(-2) +
  541. '/' +
  542. param.getFullYear() +
  543. ' ' +
  544. ('00' + param.getHours()).slice(-2) +
  545. ':' +
  546. ('00' + param.getMinutes()).slice(-2) +
  547. ':' +
  548. ('00' + param.getSeconds()).slice(-2) +
  549. '.' +
  550. ('000' + param.getMilliseconds()).slice(-3) +
  551. "'"
  552. );
  553. }
  554. module.exports = ResultSet;