query.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. 'use strict';
  2. const Promise = require('../../promise');
  3. const AbstractQuery = require('../abstract/query');
  4. const sequelizeErrors = require('../../errors');
  5. const parserStore = require('../parserStore')('mssql');
  6. const _ = require('lodash');
  7. const { logger } = require('../../utils/logger');
  8. const debug = logger.debugContext('sql:mssql');
  9. function getScale(aNum) {
  10. if (!Number.isFinite(aNum)) return 0;
  11. let e = 1;
  12. while (Math.round(aNum * e) / e !== aNum) e *= 10;
  13. return Math.log10(e);
  14. }
  15. class Query extends AbstractQuery {
  16. getInsertIdField() {
  17. return 'id';
  18. }
  19. getSQLTypeFromJsType(value, TYPES) {
  20. const paramType = { type: TYPES.VarChar, typeOptions: {} };
  21. paramType.type = TYPES.NVarChar;
  22. if (typeof value === 'number') {
  23. if (Number.isInteger(value)) {
  24. if (value >= -2147483648 && value <= 2147483647) {
  25. paramType.type = TYPES.Int;
  26. } else {
  27. paramType.type = TYPES.BigInt;
  28. }
  29. } else {
  30. paramType.type = TYPES.Numeric;
  31. //Default to a reasonable numeric precision/scale pending more sophisticated logic
  32. paramType.typeOptions = { precision: 30, scale: getScale(value) };
  33. }
  34. }
  35. if (Buffer.isBuffer(value)) {
  36. paramType.type = TYPES.VarBinary;
  37. }
  38. return paramType;
  39. }
  40. _run(connection, sql, parameters) {
  41. this.sql = sql;
  42. const { options } = this;
  43. const complete = this._logQuery(sql, debug, parameters);
  44. return new Promise((resolve, reject) => {
  45. const handleTransaction = err => {
  46. if (err) {
  47. reject(this.formatError(err));
  48. return;
  49. }
  50. resolve(this.formatResults());
  51. };
  52. // TRANSACTION SUPPORT
  53. if (sql.startsWith('BEGIN TRANSACTION')) {
  54. return connection.beginTransaction(handleTransaction, options.transaction.name, connection.lib.ISOLATION_LEVEL[options.isolationLevel]);
  55. }
  56. if (sql.startsWith('COMMIT TRANSACTION')) {
  57. return connection.commitTransaction(handleTransaction);
  58. }
  59. if (sql.startsWith('ROLLBACK TRANSACTION')) {
  60. return connection.rollbackTransaction(handleTransaction, options.transaction.name);
  61. }
  62. if (sql.startsWith('SAVE TRANSACTION')) {
  63. return connection.saveTransaction(handleTransaction, options.transaction.name);
  64. }
  65. const results = [];
  66. const request = new connection.lib.Request(sql, (err, rowCount) => {
  67. complete();
  68. if (err) {
  69. err.sql = sql;
  70. err.parameters = parameters;
  71. reject(this.formatError(err));
  72. } else {
  73. resolve(this.formatResults(results, rowCount));
  74. }
  75. });
  76. if (parameters) {
  77. _.forOwn(parameters, (value, key) => {
  78. const paramType = this.getSQLTypeFromJsType(value, connection.lib.TYPES);
  79. request.addParameter(key, paramType.type, value, paramType.typeOptions);
  80. });
  81. }
  82. request.on('row', columns => {
  83. const row = {};
  84. for (const column of columns) {
  85. const typeid = column.metadata.type.id;
  86. const parse = parserStore.get(typeid);
  87. let value = column.value;
  88. if (value !== null & !!parse) {
  89. value = parse(value);
  90. }
  91. row[column.metadata.colName] = value;
  92. }
  93. results.push(row);
  94. });
  95. connection.execSql(request);
  96. });
  97. }
  98. run(sql, parameters) {
  99. return Promise.using(this.connection.lock(), connection => this._run(connection, sql, parameters));
  100. }
  101. static formatBindParameters(sql, values, dialect) {
  102. const bindParam = {};
  103. const replacementFunc = (match, key, values) => {
  104. if (values[key] !== undefined) {
  105. bindParam[key] = values[key];
  106. return `@${key}`;
  107. }
  108. return undefined;
  109. };
  110. sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
  111. return [sql, bindParam];
  112. }
  113. /**
  114. * High level function that handles the results of a query execution.
  115. *
  116. * @param {Array} data - The result of the query execution.
  117. * @param {number} rowCount
  118. * @private
  119. * @example
  120. * Example:
  121. * query.formatResults([
  122. * {
  123. * id: 1, // this is from the main table
  124. * attr2: 'snafu', // this is from the main table
  125. * Tasks.id: 1, // this is from the associated table
  126. * Tasks.title: 'task' // this is from the associated table
  127. * }
  128. * ])
  129. */
  130. formatResults(data, rowCount) {
  131. let result = this.instance;
  132. if (this.isInsertQuery(data)) {
  133. this.handleInsertQuery(data);
  134. if (!this.instance) {
  135. if (this.options.plain) {
  136. // NOTE: super contrived. This just passes the newly added query-interface
  137. // test returning only the PK. There isn't a way in MSSQL to identify
  138. // that a given return value is the PK, and we have no schema information
  139. // because there was no calling Model.
  140. const record = data[0];
  141. result = record[Object.keys(record)[0]];
  142. } else {
  143. result = data;
  144. }
  145. }
  146. }
  147. if (this.isShowTablesQuery()) {
  148. return this.handleShowTablesQuery(data);
  149. }
  150. if (this.isDescribeQuery()) {
  151. result = {};
  152. for (const _result of data) {
  153. if (_result.Default) {
  154. _result.Default = _result.Default.replace("('", '').replace("')", '').replace(/'/g, '');
  155. }
  156. result[_result.Name] = {
  157. type: _result.Type.toUpperCase(),
  158. allowNull: _result.IsNull === 'YES' ? true : false,
  159. defaultValue: _result.Default,
  160. primaryKey: _result.Constraint === 'PRIMARY KEY',
  161. autoIncrement: _result.IsIdentity === 1,
  162. comment: _result.Comment
  163. };
  164. if (
  165. result[_result.Name].type.includes('CHAR')
  166. && _result.Length
  167. ) {
  168. if (_result.Length === -1) {
  169. result[_result.Name].type += '(MAX)';
  170. } else {
  171. result[_result.Name].type += `(${_result.Length})`;
  172. }
  173. }
  174. }
  175. }
  176. if (this.isSelectQuery()) {
  177. return this.handleSelectQuery(data);
  178. }
  179. if (this.isShowIndexesQuery()) {
  180. return this.handleShowIndexesQuery(data);
  181. }
  182. if (this.isUpsertQuery()) {
  183. this.handleInsertQuery(data);
  184. return this.instance || data[0];
  185. }
  186. if (this.isCallQuery()) {
  187. return data[0];
  188. }
  189. if (this.isBulkUpdateQuery()) {
  190. if (this.options.returning) {
  191. return this.handleSelectQuery(data);
  192. }
  193. return rowCount;
  194. }
  195. if (this.isBulkDeleteQuery()) {
  196. return data[0] && data[0].AFFECTEDROWS;
  197. }
  198. if (this.isVersionQuery()) {
  199. return data[0].version;
  200. }
  201. if (this.isForeignKeysQuery()) {
  202. return data;
  203. }
  204. if (this.isInsertQuery() || this.isUpdateQuery()) {
  205. return [result, rowCount];
  206. }
  207. if (this.isShowConstraintsQuery()) {
  208. return this.handleShowConstraintsQuery(data);
  209. }
  210. if (this.isRawQuery()) {
  211. // MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
  212. return [data, data];
  213. }
  214. return result;
  215. }
  216. handleShowTablesQuery(results) {
  217. return results.map(resultSet => {
  218. return {
  219. tableName: resultSet.TABLE_NAME,
  220. schema: resultSet.TABLE_SCHEMA
  221. };
  222. });
  223. }
  224. handleShowConstraintsQuery(data) {
  225. //Convert snake_case keys to camelCase as it's generated by stored procedure
  226. return data.slice(1).map(result => {
  227. const constraint = {};
  228. for (const key in result) {
  229. constraint[_.camelCase(key)] = result[key];
  230. }
  231. return constraint;
  232. });
  233. }
  234. formatError(err) {
  235. let match;
  236. match = err.message.match(/Violation of (?:UNIQUE|PRIMARY) KEY constraint '((.|\s)*)'. Cannot insert duplicate key in object '.*'.(:? The duplicate key value is \((.*)\).)?/);
  237. match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
  238. if (match && match.length > 1) {
  239. let fields = {};
  240. const uniqueKey = this.model && this.model.uniqueKeys[match[1]];
  241. let message = 'Validation error';
  242. if (uniqueKey && !!uniqueKey.msg) {
  243. message = uniqueKey.msg;
  244. }
  245. if (match[4]) {
  246. const values = match[4].split(',').map(part => part.trim());
  247. if (uniqueKey) {
  248. fields = _.zipObject(uniqueKey.fields, values);
  249. } else {
  250. fields[match[1]] = match[4];
  251. }
  252. }
  253. const errors = [];
  254. _.forOwn(fields, (value, field) => {
  255. errors.push(new sequelizeErrors.ValidationErrorItem(
  256. this.getUniqueConstraintErrorMessage(field),
  257. 'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
  258. field,
  259. value,
  260. this.instance,
  261. 'not_unique'
  262. ));
  263. });
  264. return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
  265. }
  266. match = err.message.match(/Failed on step '(.*)'.Could not create constraint. See previous errors./) ||
  267. err.message.match(/The DELETE statement conflicted with the REFERENCE constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
  268. err.message.match(/The (?:INSERT|MERGE|UPDATE) statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./);
  269. if (match && match.length > 0) {
  270. return new sequelizeErrors.ForeignKeyConstraintError({
  271. fields: null,
  272. index: match[1],
  273. parent: err
  274. });
  275. }
  276. match = err.message.match(/Could not drop constraint. See previous errors./);
  277. if (match && match.length > 0) {
  278. let constraint = err.sql.match(/(?:constraint|index) \[(.+?)\]/i);
  279. constraint = constraint ? constraint[1] : undefined;
  280. let table = err.sql.match(/table \[(.+?)\]/i);
  281. table = table ? table[1] : undefined;
  282. return new sequelizeErrors.UnknownConstraintError({
  283. message: match[1],
  284. constraint,
  285. table,
  286. parent: err
  287. });
  288. }
  289. return new sequelizeErrors.DatabaseError(err);
  290. }
  291. isShowOrDescribeQuery() {
  292. let result = false;
  293. result = result || this.sql.toLowerCase().startsWith("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'");
  294. result = result || this.sql.toLowerCase().startsWith('select tablename = t.name, name = ind.name,');
  295. result = result || this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname');
  296. return result;
  297. }
  298. isShowIndexesQuery() {
  299. return this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname');
  300. }
  301. handleShowIndexesQuery(data) {
  302. // Group by index name, and collect all fields
  303. data = data.reduce((acc, item) => {
  304. if (!(item.index_name in acc)) {
  305. acc[item.index_name] = item;
  306. item.fields = [];
  307. }
  308. item.index_keys.split(',').forEach(column => {
  309. let columnName = column.trim();
  310. if (columnName.includes('(-)')) {
  311. columnName = columnName.replace('(-)', '');
  312. }
  313. acc[item.index_name].fields.push({
  314. attribute: columnName,
  315. length: undefined,
  316. order: column.includes('(-)') ? 'DESC' : 'ASC',
  317. collate: undefined
  318. });
  319. });
  320. delete item.index_keys;
  321. return acc;
  322. }, {});
  323. return _.map(data, item => ({
  324. primary: item.index_name.toLowerCase().startsWith('pk'),
  325. fields: item.fields,
  326. name: item.index_name,
  327. tableName: undefined,
  328. unique: item.index_description.toLowerCase().includes('unique'),
  329. type: undefined
  330. }));
  331. }
  332. handleInsertQuery(results, metaData) {
  333. if (this.instance) {
  334. // add the inserted row id to the instance
  335. const autoIncrementAttribute = this.model.autoIncrementAttribute;
  336. let id = null;
  337. let autoIncrementAttributeAlias = null;
  338. if (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementAttribute) &&
  339. this.model.rawAttributes[autoIncrementAttribute].field !== undefined)
  340. autoIncrementAttributeAlias = this.model.rawAttributes[autoIncrementAttribute].field;
  341. id = id || results && results[0][this.getInsertIdField()];
  342. id = id || metaData && metaData[this.getInsertIdField()];
  343. id = id || results && results[0][autoIncrementAttribute];
  344. id = id || autoIncrementAttributeAlias && results && results[0][autoIncrementAttributeAlias];
  345. this.instance[autoIncrementAttribute] = id;
  346. if (this.instance.dataValues) {
  347. for (const key in results[0]) {
  348. if (Object.prototype.hasOwnProperty.call(results[0], key)) {
  349. const record = results[0][key];
  350. const attr = _.find(this.model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key);
  351. this.instance.dataValues[attr && attr.fieldName || key] = record;
  352. }
  353. }
  354. }
  355. }
  356. }
  357. }
  358. module.exports = Query;
  359. module.exports.Query = Query;
  360. module.exports.default = Query;