query.js 9.7 KB

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