You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
13 KiB
391 lines
13 KiB
3 years ago
|
'use strict';
|
||
|
|
||
|
const Utils = require('../../utils');
|
||
|
const debug = Utils.getLogger().debugContext('sql:mssql');
|
||
|
const Promise = require('../../promise');
|
||
|
const AbstractQuery = require('../abstract/query');
|
||
|
const sequelizeErrors = require('../../errors.js');
|
||
|
const parserStore = require('../parserStore')('mssql');
|
||
|
const _ = require('lodash');
|
||
|
const TYPES = require('tedious').TYPES;
|
||
|
|
||
|
class Query extends AbstractQuery {
|
||
|
constructor(connection, sequelize, options) {
|
||
|
super();
|
||
|
this.connection = connection;
|
||
|
this.instance = options.instance;
|
||
|
this.model = options.model;
|
||
|
this.sequelize = sequelize;
|
||
|
this.options = _.extend({
|
||
|
logging: console.log,
|
||
|
plain: false,
|
||
|
raw: false
|
||
|
}, options || {});
|
||
|
|
||
|
this.checkLoggingOption();
|
||
|
}
|
||
|
|
||
|
getInsertIdField() {
|
||
|
return 'id';
|
||
|
}
|
||
|
|
||
|
getSQLTypeFromJsType(value) {
|
||
|
const paramType = {type: TYPES.VarChar, typeOptions: {} };
|
||
|
paramType.type = TYPES.NVarChar;
|
||
|
if (typeof value === 'number') {
|
||
|
if (Number.isInteger(value)) {
|
||
|
paramType.type = TYPES.Int;
|
||
|
} else {
|
||
|
paramType.type = TYPES.Numeric;
|
||
|
//Default to a reasonable numeric precision/scale pending more sophisticated logic
|
||
|
paramType.typeOptions = {precision: 30, scale: 15};
|
||
|
}
|
||
|
}
|
||
|
return paramType;
|
||
|
}
|
||
|
|
||
|
_run(connection, sql, parameters) {
|
||
|
this.sql = sql;
|
||
|
|
||
|
//do we need benchmark for this query execution
|
||
|
const benchmark = this.sequelize.options.benchmark || this.options.benchmark;
|
||
|
let queryBegin;
|
||
|
if (benchmark) {
|
||
|
queryBegin = Date.now();
|
||
|
} else {
|
||
|
this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
|
||
|
}
|
||
|
|
||
|
debug(`executing(${this.connection.uuid || 'default'}) : ${this.sql}`);
|
||
|
|
||
|
return new Promise((resolve, reject) => {
|
||
|
// TRANSACTION SUPPORT
|
||
|
if (_.startsWith(this.sql, 'BEGIN TRANSACTION')) {
|
||
|
connection.beginTransaction(err => {
|
||
|
if (err) {
|
||
|
reject(this.formatError(err));
|
||
|
} else {
|
||
|
resolve(this.formatResults());
|
||
|
}
|
||
|
}, this.options.transaction.name, Utils.mapIsolationLevelStringToTedious(this.options.isolationLevel, connection.lib));
|
||
|
} else if (_.startsWith(this.sql, 'COMMIT TRANSACTION')) {
|
||
|
connection.commitTransaction(err => {
|
||
|
if (err) {
|
||
|
reject(this.formatError(err));
|
||
|
} else {
|
||
|
resolve(this.formatResults());
|
||
|
}
|
||
|
});
|
||
|
} else if (_.startsWith(this.sql, 'ROLLBACK TRANSACTION')) {
|
||
|
connection.rollbackTransaction(err => {
|
||
|
if (err) {
|
||
|
reject(this.formatError(err));
|
||
|
} else {
|
||
|
resolve(this.formatResults());
|
||
|
}
|
||
|
}, this.options.transaction.name);
|
||
|
} else if (_.startsWith(this.sql, 'SAVE TRANSACTION')) {
|
||
|
connection.saveTransaction(err => {
|
||
|
if (err) {
|
||
|
reject(this.formatError(err));
|
||
|
} else {
|
||
|
resolve(this.formatResults());
|
||
|
}
|
||
|
}, this.options.transaction.name);
|
||
|
} else {
|
||
|
const results = [];
|
||
|
const request = new connection.lib.Request(this.sql, (err, rowCount) => {
|
||
|
|
||
|
debug(`executed(${this.connection.uuid || 'default'}) : ${this.sql}`);
|
||
|
|
||
|
if (benchmark) {
|
||
|
this.sequelize.log('Executed (' + (this.connection.uuid || 'default') + '): ' + this.sql, Date.now() - queryBegin, this.options);
|
||
|
}
|
||
|
|
||
|
if (err) {
|
||
|
err.sql = sql;
|
||
|
reject(this.formatError(err));
|
||
|
} else {
|
||
|
resolve(this.formatResults(results, rowCount));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (parameters) {
|
||
|
_.forOwn(parameters, (value, key) => {
|
||
|
const paramType = this.getSQLTypeFromJsType(value);
|
||
|
request.addParameter(key, paramType.type, value, paramType.typeOptions);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
request.on('row', columns => {
|
||
|
const row = {};
|
||
|
for (const column of columns) {
|
||
|
const typeid = column.metadata.type.id;
|
||
|
const parse = parserStore.get(typeid);
|
||
|
let value = column.value;
|
||
|
|
||
|
if (value !== null & !!parse) {
|
||
|
value = parse(value);
|
||
|
}
|
||
|
row[column.metadata.colName] = value;
|
||
|
}
|
||
|
|
||
|
results.push(row);
|
||
|
});
|
||
|
|
||
|
connection.execSql(request);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
run(sql, parameters) {
|
||
|
return Promise.using(this.connection.lock(), connection => this._run(connection, sql, parameters));
|
||
|
}
|
||
|
|
||
|
static formatBindParameters(sql, values, dialect) {
|
||
|
const bindParam = {};
|
||
|
const replacementFunc = (match, key, values) => {
|
||
|
if (values[key] !== undefined) {
|
||
|
bindParam[key] = values[key];
|
||
|
return '@' + key;
|
||
|
}
|
||
|
return undefined;
|
||
|
};
|
||
|
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
|
||
|
|
||
|
return [sql, bindParam];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* High level function that handles the results of a query execution.
|
||
|
*
|
||
|
*
|
||
|
* Example:
|
||
|
* query.formatResults([
|
||
|
* {
|
||
|
* id: 1, // this is from the main table
|
||
|
* attr2: 'snafu', // this is from the main table
|
||
|
* Tasks.id: 1, // this is from the associated table
|
||
|
* Tasks.title: 'task' // this is from the associated table
|
||
|
* }
|
||
|
* ])
|
||
|
*
|
||
|
* @param {Array} data - The result of the query execution.
|
||
|
* @private
|
||
|
*/
|
||
|
formatResults(data, rowCount) {
|
||
|
let result = this.instance;
|
||
|
if (this.isInsertQuery(data)) {
|
||
|
this.handleInsertQuery(data);
|
||
|
|
||
|
if (!this.instance) {
|
||
|
if (this.options.plain) {
|
||
|
// NOTE: super contrived. This just passes the newly added query-interface
|
||
|
// test returning only the PK. There isn't a way in MSSQL to identify
|
||
|
// that a given return value is the PK, and we have no schema information
|
||
|
// because there was no calling Model.
|
||
|
const record = data[0];
|
||
|
result = record[Object.keys(record)[0]];
|
||
|
} else {
|
||
|
result = data;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.isShowTablesQuery()) {
|
||
|
result = this.handleShowTablesQuery(data);
|
||
|
} else if (this.isDescribeQuery()) {
|
||
|
result = {};
|
||
|
for (const _result of data) {
|
||
|
if (_result.Default) {
|
||
|
_result.Default = _result.Default.replace("('", '').replace("')", '').replace(/'/g, '');
|
||
|
}
|
||
|
|
||
|
result[_result.Name] = {
|
||
|
type: _result.Type.toUpperCase(),
|
||
|
allowNull: _result.IsNull === 'YES' ? true : false,
|
||
|
defaultValue: _result.Default,
|
||
|
primaryKey: _result.Constraint === 'PRIMARY KEY',
|
||
|
autoIncrement: _result.IsIdentity === 1
|
||
|
};
|
||
|
}
|
||
|
} else if (this.isShowIndexesQuery()) {
|
||
|
result = this.handleShowIndexesQuery(data);
|
||
|
} else if (this.isSelectQuery()) {
|
||
|
result = this.handleSelectQuery(data);
|
||
|
} else if (this.isUpsertQuery()) {
|
||
|
result = data[0];
|
||
|
} else if (this.isCallQuery()) {
|
||
|
result = data[0];
|
||
|
} else if (this.isBulkUpdateQuery()) {
|
||
|
result = data.length;
|
||
|
} else if (this.isBulkDeleteQuery()) {
|
||
|
result = data[0] && data[0].AFFECTEDROWS;
|
||
|
} else if (this.isVersionQuery()) {
|
||
|
result = data[0].version;
|
||
|
} else if (this.isForeignKeysQuery()) {
|
||
|
result = data;
|
||
|
} else if (this.isInsertQuery() || this.isUpdateQuery()) {
|
||
|
result = [result, rowCount];
|
||
|
} else if (this.isShowConstraintsQuery()) {
|
||
|
result = this.handleShowConstraintsQuery(data);
|
||
|
} else if (this.isRawQuery()) {
|
||
|
// MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
|
||
|
result = [data, data];
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
handleShowTablesQuery(results) {
|
||
|
return results.map(resultSet => {
|
||
|
return {
|
||
|
tableName: resultSet.TABLE_NAME,
|
||
|
schema: resultSet.TABLE_SCHEMA
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
handleShowConstraintsQuery(data) {
|
||
|
//Convert snake_case keys to camelCase as it's generated by stored procedure
|
||
|
return data.slice(1).map(result => {
|
||
|
const constraint = {};
|
||
|
for (const key in result) {
|
||
|
constraint[_.camelCase(key)] = result[key];
|
||
|
}
|
||
|
return constraint;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
formatError(err) {
|
||
|
let match;
|
||
|
match = err.message.match(/Violation of UNIQUE KEY constraint '((.|\s)*)'. Cannot insert duplicate key in object '.*'.(:? The duplicate key value is \((.*)\).)?/);
|
||
|
match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
|
||
|
if (match && match.length > 1) {
|
||
|
let fields = {};
|
||
|
const uniqueKey = this.model && this.model.uniqueKeys[match[1]];
|
||
|
let message = 'Validation error';
|
||
|
|
||
|
if (uniqueKey && !!uniqueKey.msg) {
|
||
|
message = uniqueKey.msg;
|
||
|
}
|
||
|
if (match[4]) {
|
||
|
const values = match[4].split(',').map(part => part.trim());
|
||
|
if (uniqueKey) {
|
||
|
fields = _.zipObject(uniqueKey.fields, values);
|
||
|
} else {
|
||
|
fields[match[1]] = match[4];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const errors = [];
|
||
|
_.forOwn(fields, (value, field) => {
|
||
|
errors.push(new sequelizeErrors.ValidationErrorItem(
|
||
|
this.getUniqueConstraintErrorMessage(field),
|
||
|
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
|
||
|
field,
|
||
|
value,
|
||
|
this.instance,
|
||
|
'not_unique'
|
||
|
));
|
||
|
});
|
||
|
|
||
|
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
|
||
|
}
|
||
|
|
||
|
match = err.message.match(/Failed on step '(.*)'.Could not create constraint. See previous errors./) ||
|
||
|
err.message.match(/The DELETE statement conflicted with the REFERENCE constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
|
||
|
err.message.match(/The INSERT statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
|
||
|
err.message.match(/The MERGE statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
|
||
|
err.message.match(/The UPDATE statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./);
|
||
|
if (match && match.length > 0) {
|
||
|
return new sequelizeErrors.ForeignKeyConstraintError({
|
||
|
fields: null,
|
||
|
index: match[1],
|
||
|
parent: err
|
||
|
});
|
||
|
}
|
||
|
|
||
|
match = err.message.match(/Could not drop constraint. See previous errors./);
|
||
|
|
||
|
if (match && match.length > 0) {
|
||
|
return new sequelizeErrors.UnknownConstraintError(match[1]);
|
||
|
}
|
||
|
|
||
|
return new sequelizeErrors.DatabaseError(err);
|
||
|
}
|
||
|
|
||
|
isShowOrDescribeQuery() {
|
||
|
let result = false;
|
||
|
|
||
|
result = result || this.sql.toLowerCase().indexOf("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'") === 0;
|
||
|
result = result || this.sql.toLowerCase().indexOf('select tablename = t.name, name = ind.name,') === 0;
|
||
|
result = result || this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0;
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
isShowIndexesQuery() {
|
||
|
return this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0;
|
||
|
}
|
||
|
|
||
|
handleShowIndexesQuery(data) {
|
||
|
// Group by index name, and collect all fields
|
||
|
data = _.reduce(data, (acc, item) => {
|
||
|
if (!(item.index_name in acc)) {
|
||
|
acc[item.index_name] = item;
|
||
|
item.fields = [];
|
||
|
}
|
||
|
|
||
|
_.forEach(item.index_keys.split(','), column => {
|
||
|
let columnName = column.trim();
|
||
|
if (columnName.indexOf('(-)') !== -1) {
|
||
|
columnName = columnName.replace('(-)', '');
|
||
|
}
|
||
|
|
||
|
acc[item.index_name].fields.push({
|
||
|
attribute: columnName,
|
||
|
length: undefined,
|
||
|
order: column.indexOf('(-)') !== -1 ? 'DESC' : 'ASC',
|
||
|
collate: undefined
|
||
|
});
|
||
|
});
|
||
|
delete item.index_keys;
|
||
|
return acc;
|
||
|
}, {});
|
||
|
|
||
|
return _.map(data, item => ({
|
||
|
primary: item.index_name.toLowerCase().indexOf('pk') === 0,
|
||
|
fields: item.fields,
|
||
|
name: item.index_name,
|
||
|
tableName: undefined,
|
||
|
unique: item.index_description.toLowerCase().indexOf('unique') !== -1,
|
||
|
type: undefined
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
handleInsertQuery(results, metaData) {
|
||
|
if (this.instance) {
|
||
|
// add the inserted row id to the instance
|
||
|
const autoIncrementAttribute = this.model.autoIncrementAttribute;
|
||
|
let id = null;
|
||
|
let autoIncrementAttributeAlias = null;
|
||
|
|
||
|
if (this.model.rawAttributes.hasOwnProperty(autoIncrementAttribute) &&
|
||
|
this.model.rawAttributes[autoIncrementAttribute].field !== undefined)
|
||
|
autoIncrementAttributeAlias = this.model.rawAttributes[autoIncrementAttribute].field;
|
||
|
|
||
|
id = id || results && results[0][this.getInsertIdField()];
|
||
|
id = id || metaData && metaData[this.getInsertIdField()];
|
||
|
id = id || results && results[0][autoIncrementAttribute];
|
||
|
id = id || autoIncrementAttributeAlias && results && results[0][autoIncrementAttributeAlias];
|
||
|
|
||
|
this.instance[autoIncrementAttribute] = id;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Query;
|
||
|
module.exports.Query = Query;
|
||
|
module.exports.default = Query;
|