query.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. 'use strict';
  2. const AbstractQuery = require('../abstract/query');
  3. const sequelizeErrors = require('../../errors');
  4. const _ = require('lodash');
  5. const { logger } = require('../../utils/logger');
  6. const debug = logger.debugContext('sql:mysql');
  7. class Query extends AbstractQuery {
  8. constructor(connection, sequelize, options) {
  9. super(connection, sequelize, { showWarnings: false, ...options });
  10. }
  11. static formatBindParameters(sql, values, dialect) {
  12. const bindParam = [];
  13. const replacementFunc = (match, key, values) => {
  14. if (values[key] !== undefined) {
  15. bindParam.push(values[key]);
  16. return '?';
  17. }
  18. return undefined;
  19. };
  20. sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
  21. return [sql, bindParam.length > 0 ? bindParam : undefined];
  22. }
  23. async run(sql, parameters) {
  24. this.sql = sql;
  25. const { connection, options } = this;
  26. //do we need benchmark for this query execution
  27. const showWarnings = this.sequelize.options.showWarnings || options.showWarnings;
  28. const complete = this._logQuery(sql, debug, parameters);
  29. const query = parameters && parameters.length
  30. ? new Promise((resolve, reject) => connection.execute(sql, parameters, (error, result) => error ? reject(error) : resolve(result)).setMaxListeners(100))
  31. : new Promise((resolve, reject) => connection.query({ sql }, (error, result) => error ? reject(error) : resolve(result)).setMaxListeners(100));
  32. let results;
  33. try {
  34. results = await query;
  35. } catch (err) {
  36. // MySQL automatically rolls-back transactions in the event of a deadlock
  37. if (options.transaction && err.errno === 1213) {
  38. options.transaction.finished = 'rollback';
  39. }
  40. err.sql = sql;
  41. err.parameters = parameters;
  42. throw this.formatError(err);
  43. }
  44. complete();
  45. // Log warnings if we've got them.
  46. if (showWarnings && results && results.warningStatus > 0) {
  47. await this.logWarnings(results);
  48. }
  49. // Return formatted results...
  50. return this.formatResults(results);
  51. }
  52. /**
  53. * High level function that handles the results of a query execution.
  54. *
  55. *
  56. * Example:
  57. * query.formatResults([
  58. * {
  59. * id: 1, // this is from the main table
  60. * attr2: 'snafu', // this is from the main table
  61. * Tasks.id: 1, // this is from the associated table
  62. * Tasks.title: 'task' // this is from the associated table
  63. * }
  64. * ])
  65. *
  66. * @param {Array} data - The result of the query execution.
  67. * @private
  68. */
  69. formatResults(data) {
  70. let result = this.instance;
  71. if (this.isInsertQuery(data)) {
  72. this.handleInsertQuery(data);
  73. if (!this.instance) {
  74. // handle bulkCreate AI primiary key
  75. if (
  76. data.constructor.name === 'ResultSetHeader'
  77. && this.model
  78. && this.model.autoIncrementAttribute
  79. && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
  80. && this.model.rawAttributes[this.model.primaryKeyAttribute]
  81. ) {
  82. const startId = data[this.getInsertIdField()];
  83. result = [];
  84. for (let i = startId; i < startId + data.affectedRows; i++) {
  85. result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i });
  86. }
  87. } else {
  88. result = data[this.getInsertIdField()];
  89. }
  90. }
  91. }
  92. if (this.isSelectQuery()) {
  93. return this.handleSelectQuery(data);
  94. }
  95. if (this.isShowTablesQuery()) {
  96. return this.handleShowTablesQuery(data);
  97. }
  98. if (this.isDescribeQuery()) {
  99. result = {};
  100. for (const _result of data) {
  101. const enumRegex = /^enum/i;
  102. result[_result.Field] = {
  103. type: enumRegex.test(_result.Type) ? _result.Type.replace(enumRegex, 'ENUM') : _result.Type.toUpperCase(),
  104. allowNull: _result.Null === 'YES',
  105. defaultValue: _result.Default,
  106. primaryKey: _result.Key === 'PRI',
  107. autoIncrement: Object.prototype.hasOwnProperty.call(_result, 'Extra') && _result.Extra.toLowerCase() === 'auto_increment',
  108. comment: _result.Comment ? _result.Comment : null
  109. };
  110. }
  111. return result;
  112. }
  113. if (this.isShowIndexesQuery()) {
  114. return this.handleShowIndexesQuery(data);
  115. }
  116. if (this.isCallQuery()) {
  117. return data[0];
  118. }
  119. if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery()) {
  120. return data.affectedRows;
  121. }
  122. if (this.isVersionQuery()) {
  123. return data[0].version;
  124. }
  125. if (this.isForeignKeysQuery()) {
  126. return data;
  127. }
  128. if (this.isUpsertQuery()) {
  129. return [result, data.affectedRows === 1];
  130. }
  131. if (this.isInsertQuery() || this.isUpdateQuery()) {
  132. return [result, data.affectedRows];
  133. }
  134. if (this.isShowConstraintsQuery()) {
  135. return data;
  136. }
  137. if (this.isRawQuery()) {
  138. // MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
  139. return [data, data];
  140. }
  141. return result;
  142. }
  143. async logWarnings(results) {
  144. const warningResults = await this.run('SHOW WARNINGS');
  145. const warningMessage = `MySQL Warnings (${this.connection.uuid || 'default'}): `;
  146. const messages = [];
  147. for (const _warningRow of warningResults) {
  148. if (_warningRow === undefined || typeof _warningRow[Symbol.iterator] !== 'function') continue;
  149. for (const _warningResult of _warningRow) {
  150. if (Object.prototype.hasOwnProperty.call(_warningResult, 'Message')) {
  151. messages.push(_warningResult.Message);
  152. } else {
  153. for (const _objectKey of _warningResult.keys()) {
  154. messages.push([_objectKey, _warningResult[_objectKey]].join(': '));
  155. }
  156. }
  157. }
  158. }
  159. this.sequelize.log(warningMessage + messages.join('; '), this.options);
  160. return results;
  161. }
  162. formatError(err) {
  163. const errCode = err.errno || err.code;
  164. switch (errCode) {
  165. case 1062: {
  166. const match = err.message.match(/Duplicate entry '([\s\S]*)' for key '?((.|\s)*?)'?$/);
  167. let fields = {};
  168. let message = 'Validation error';
  169. const values = match ? match[1].split('-') : undefined;
  170. const fieldKey = match ? match[2] : undefined;
  171. const fieldVal = match ? match[1] : undefined;
  172. const uniqueKey = this.model && this.model.uniqueKeys[fieldKey];
  173. if (uniqueKey) {
  174. if (uniqueKey.msg) message = uniqueKey.msg;
  175. fields = _.zipObject(uniqueKey.fields, values);
  176. } else {
  177. fields[fieldKey] = fieldVal;
  178. }
  179. const errors = [];
  180. _.forOwn(fields, (value, field) => {
  181. errors.push(new sequelizeErrors.ValidationErrorItem(
  182. this.getUniqueConstraintErrorMessage(field),
  183. 'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
  184. field,
  185. value,
  186. this.instance,
  187. 'not_unique'
  188. ));
  189. });
  190. return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
  191. }
  192. case 1451:
  193. case 1452: {
  194. // e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
  195. const match = err.message.match(/CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/);
  196. const quoteChar = match ? match[1] : '`';
  197. const fields = match ? match[3].split(new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
  198. return new sequelizeErrors.ForeignKeyConstraintError({
  199. reltype: String(errCode) === '1451' ? 'parent' : 'child',
  200. table: match ? match[4] : undefined,
  201. fields,
  202. value: fields && fields.length && this.instance && this.instance[fields[0]] || undefined,
  203. index: match ? match[2] : undefined,
  204. parent: err
  205. });
  206. }
  207. default:
  208. return new sequelizeErrors.DatabaseError(err);
  209. }
  210. }
  211. handleShowIndexesQuery(data) {
  212. // Group by index name, and collect all fields
  213. data = data.reduce((acc, item) => {
  214. if (!(item.Key_name in acc)) {
  215. acc[item.Key_name] = item;
  216. item.fields = [];
  217. }
  218. acc[item.Key_name].fields[item.Seq_in_index - 1] = {
  219. attribute: item.Column_name,
  220. length: item.Sub_part || undefined,
  221. order: item.Collation === 'A' ? 'ASC' : undefined
  222. };
  223. delete item.column_name;
  224. return acc;
  225. }, {});
  226. return _.map(data, item => ({
  227. primary: item.Key_name === 'PRIMARY',
  228. fields: item.fields,
  229. name: item.Key_name,
  230. tableName: item.Table,
  231. unique: item.Non_unique !== 1,
  232. type: item.Index_type
  233. }));
  234. }
  235. }
  236. module.exports = Query;
  237. module.exports.Query = Query;
  238. module.exports.default = Query;