query.js 10.0 KB

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