query.js 8.8 KB

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