query.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. 'use strict';
  2. const AbstractQuery = require('../abstract/query');
  3. const sequelizeErrors = require('../../errors');
  4. const _ = require('lodash');
  5. const DataTypes = require('../../data-types');
  6. const { logger } = require('../../utils/logger');
  7. const ER_DUP_ENTRY = 1062;
  8. const ER_DEADLOCK = 1213;
  9. const ER_ROW_IS_REFERENCED = 1451;
  10. const ER_NO_REFERENCED_ROW = 1452;
  11. const debug = logger.debugContext('sql:mariadb');
  12. class Query extends AbstractQuery {
  13. constructor(connection, sequelize, options) {
  14. super(connection, sequelize, { showWarnings: false, ...options });
  15. }
  16. static formatBindParameters(sql, values, dialect) {
  17. const bindParam = [];
  18. const replacementFunc = (match, key, values_) => {
  19. if (values_[key] !== undefined) {
  20. bindParam.push(values_[key]);
  21. return '?';
  22. }
  23. return undefined;
  24. };
  25. sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
  26. return [sql, bindParam.length > 0 ? bindParam : undefined];
  27. }
  28. async run(sql, parameters) {
  29. this.sql = sql;
  30. const { connection, options } = this;
  31. const showWarnings = this.sequelize.options.showWarnings || options.showWarnings;
  32. const complete = this._logQuery(sql, debug, parameters);
  33. if (parameters) {
  34. debug('parameters(%j)', parameters);
  35. }
  36. let results;
  37. try {
  38. results = await connection.query(this.sql, parameters);
  39. } catch (error) {
  40. if (options.transaction && error.errno === ER_DEADLOCK) {
  41. // MariaDB automatically rolls-back transactions in the event of a deadlock.
  42. // However, we still initiate a manual rollback to ensure the connection gets released - see #13102.
  43. try {
  44. await options.transaction.rollback();
  45. } catch (error_) {
  46. // Ignore errors - since MariaDB automatically rolled back, we're
  47. // not that worried about this redundant rollback failing.
  48. }
  49. options.transaction.finished = 'rollback';
  50. }
  51. error.sql = sql;
  52. error.parameters = parameters;
  53. throw this.formatError(error);
  54. } finally {
  55. complete();
  56. }
  57. if (showWarnings && results && results.warningStatus > 0) {
  58. await this.logWarnings(results);
  59. }
  60. return 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.isBulkUpdateQuery() || this.isBulkDeleteQuery()) {
  82. return data.affectedRows;
  83. }
  84. if (this.isUpsertQuery()) {
  85. return [result, data.affectedRows === 1];
  86. }
  87. if (this.isInsertQuery(data)) {
  88. this.handleInsertQuery(data);
  89. if (!this.instance) {
  90. // handle bulkCreate AI primary key
  91. if (
  92. this.model
  93. && this.model.autoIncrementAttribute
  94. && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
  95. && this.model.rawAttributes[this.model.primaryKeyAttribute]
  96. ) {
  97. // ONLY TRUE IF @auto_increment_increment is set to 1 !!
  98. // Doesn't work with GALERA => each node will reserve increment (x for first server, x+1 for next node...)
  99. const startId = data[this.getInsertIdField()];
  100. result = new Array(data.affectedRows);
  101. const pkField = this.model.rawAttributes[this.model.primaryKeyAttribute].field;
  102. for (let i = 0; i < data.affectedRows; i++) {
  103. result[i] = { [pkField]: startId + i };
  104. }
  105. return [result, data.affectedRows];
  106. }
  107. return [data[this.getInsertIdField()], data.affectedRows];
  108. }
  109. }
  110. if (this.isSelectQuery()) {
  111. this.handleJsonSelectQuery(data);
  112. return this.handleSelectQuery(data);
  113. }
  114. if (this.isInsertQuery() || this.isUpdateQuery()) {
  115. return [result, data.affectedRows];
  116. }
  117. if (this.isCallQuery()) {
  118. return data[0];
  119. }
  120. if (this.isRawQuery()) {
  121. const meta = data.meta;
  122. delete data.meta;
  123. return [data, meta];
  124. }
  125. if (this.isShowIndexesQuery()) {
  126. return this.handleShowIndexesQuery(data);
  127. }
  128. if (this.isForeignKeysQuery() || this.isShowConstraintsQuery()) {
  129. return data;
  130. }
  131. if (this.isShowTablesQuery()) {
  132. return this.handleShowTablesQuery(data);
  133. }
  134. if (this.isDescribeQuery()) {
  135. result = {};
  136. for (const _result of data) {
  137. result[_result.Field] = {
  138. type: _result.Type.toLowerCase().startsWith('enum') ? _result.Type.replace(/^enum/i,
  139. 'ENUM') : _result.Type.toUpperCase(),
  140. allowNull: _result.Null === 'YES',
  141. defaultValue: _result.Default,
  142. primaryKey: _result.Key === 'PRI',
  143. autoIncrement: Object.prototype.hasOwnProperty.call(_result, 'Extra')
  144. && _result.Extra.toLowerCase() === 'auto_increment',
  145. comment: _result.Comment ? _result.Comment : null
  146. };
  147. }
  148. return result;
  149. }
  150. if (this.isVersionQuery()) {
  151. return data[0].version;
  152. }
  153. return result;
  154. }
  155. handleJsonSelectQuery(rows) {
  156. if (!this.model || !this.model.fieldRawAttributesMap) {
  157. return;
  158. }
  159. for (const _field of Object.keys(this.model.fieldRawAttributesMap)) {
  160. const modelField = this.model.fieldRawAttributesMap[_field];
  161. if (modelField.type instanceof DataTypes.JSON) {
  162. // Value is returned as String, not JSON
  163. rows = rows.map(row => {
  164. row[modelField.fieldName] = row[modelField.fieldName] ? JSON.parse(
  165. row[modelField.fieldName]) : null;
  166. if (DataTypes.JSON.parse) {
  167. return DataTypes.JSON.parse(modelField, this.sequelize.options,
  168. row[modelField.fieldName]);
  169. }
  170. return row;
  171. });
  172. }
  173. }
  174. }
  175. async logWarnings(results) {
  176. const warningResults = await this.run('SHOW WARNINGS');
  177. const warningMessage = `MariaDB Warnings (${this.connection.uuid || 'default'}): `;
  178. const messages = [];
  179. for (const _warningRow of warningResults) {
  180. if (_warningRow === undefined || typeof _warningRow[Symbol.iterator] !== 'function') {
  181. continue;
  182. }
  183. for (const _warningResult of _warningRow) {
  184. if (Object.prototype.hasOwnProperty.call(_warningResult, 'Message')) {
  185. messages.push(_warningResult.Message);
  186. } else {
  187. for (const _objectKey of _warningResult.keys()) {
  188. messages.push([_objectKey, _warningResult[_objectKey]].join(': '));
  189. }
  190. }
  191. }
  192. }
  193. this.sequelize.log(warningMessage + messages.join('; '), this.options);
  194. return results;
  195. }
  196. formatError(err) {
  197. switch (err.errno) {
  198. case ER_DUP_ENTRY: {
  199. const match = err.message.match(
  200. /Duplicate entry '([\s\S]*)' for key '?((.|\s)*?)'?\s.*$/);
  201. let fields = {};
  202. let message = 'Validation error';
  203. const values = match ? match[1].split('-') : undefined;
  204. const fieldKey = match ? match[2] : undefined;
  205. const fieldVal = match ? match[1] : undefined;
  206. const uniqueKey = this.model && this.model.uniqueKeys[fieldKey];
  207. if (uniqueKey) {
  208. if (uniqueKey.msg) message = uniqueKey.msg;
  209. fields = _.zipObject(uniqueKey.fields, values);
  210. } else {
  211. fields[fieldKey] = fieldVal;
  212. }
  213. const errors = [];
  214. _.forOwn(fields, (value, field) => {
  215. errors.push(new sequelizeErrors.ValidationErrorItem(
  216. this.getUniqueConstraintErrorMessage(field),
  217. 'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
  218. field,
  219. value,
  220. this.instance,
  221. 'not_unique'
  222. ));
  223. });
  224. return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
  225. }
  226. case ER_ROW_IS_REFERENCED:
  227. case ER_NO_REFERENCED_ROW: {
  228. // e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
  229. const match = err.message.match(
  230. /CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/
  231. );
  232. const quoteChar = match ? match[1] : '`';
  233. const fields = match ? match[3].split(new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
  234. return new sequelizeErrors.ForeignKeyConstraintError({
  235. reltype: err.errno === ER_ROW_IS_REFERENCED ? 'parent' : 'child',
  236. table: match ? match[4] : undefined,
  237. fields,
  238. value: fields && fields.length && this.instance && this.instance[fields[0]] || undefined,
  239. index: match ? match[2] : undefined,
  240. parent: err
  241. });
  242. }
  243. default:
  244. return new sequelizeErrors.DatabaseError(err);
  245. }
  246. }
  247. handleShowTablesQuery(results) {
  248. return results.map(resultSet => ({
  249. tableName: resultSet.TABLE_NAME,
  250. schema: resultSet.TABLE_SCHEMA
  251. }));
  252. }
  253. handleShowIndexesQuery(data) {
  254. let currItem;
  255. const result = [];
  256. data.forEach(item => {
  257. if (!currItem || currItem.name !== item.Key_name) {
  258. currItem = {
  259. primary: item.Key_name === 'PRIMARY',
  260. fields: [],
  261. name: item.Key_name,
  262. tableName: item.Table,
  263. unique: item.Non_unique !== 1,
  264. type: item.Index_type
  265. };
  266. result.push(currItem);
  267. }
  268. currItem.fields[item.Seq_in_index - 1] = {
  269. attribute: item.Column_name,
  270. length: item.Sub_part || undefined,
  271. order: item.Collation === 'A' ? 'ASC' : undefined
  272. };
  273. });
  274. return result;
  275. }
  276. }
  277. module.exports = Query;