API Reference Source

lib/dialects/mssql/query.js

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