323 lines
12 KiB
JavaScript
323 lines
12 KiB
JavaScript
|
"use strict";
|
||
|
const AbstractQuery = require("../abstract/query");
|
||
|
const sequelizeErrors = require("../../errors");
|
||
|
const parserStore = require("../parserStore")("mssql");
|
||
|
const _ = require("lodash");
|
||
|
const { logger } = require("../../utils/logger");
|
||
|
const debug = logger.debugContext("sql:mssql");
|
||
|
function getScale(aNum) {
|
||
|
if (!Number.isFinite(aNum))
|
||
|
return 0;
|
||
|
let e = 1;
|
||
|
while (Math.round(aNum * e) / e !== aNum)
|
||
|
e *= 10;
|
||
|
return Math.log10(e);
|
||
|
}
|
||
|
class Query extends AbstractQuery {
|
||
|
getInsertIdField() {
|
||
|
return "id";
|
||
|
}
|
||
|
getSQLTypeFromJsType(value, TYPES) {
|
||
|
const paramType = { type: TYPES.VarChar, typeOptions: {} };
|
||
|
paramType.type = TYPES.NVarChar;
|
||
|
if (typeof value === "number") {
|
||
|
if (Number.isInteger(value)) {
|
||
|
if (value >= -2147483648 && value <= 2147483647) {
|
||
|
paramType.type = TYPES.Int;
|
||
|
} else {
|
||
|
paramType.type = TYPES.BigInt;
|
||
|
}
|
||
|
} else {
|
||
|
paramType.type = TYPES.Numeric;
|
||
|
paramType.typeOptions = { precision: 30, scale: getScale(value) };
|
||
|
}
|
||
|
} else if (typeof value === "boolean") {
|
||
|
paramType.type = TYPES.Bit;
|
||
|
}
|
||
|
if (Buffer.isBuffer(value)) {
|
||
|
paramType.type = TYPES.VarBinary;
|
||
|
}
|
||
|
return paramType;
|
||
|
}
|
||
|
async _run(connection, sql, parameters, errStack) {
|
||
|
this.sql = sql;
|
||
|
const { options } = this;
|
||
|
const complete = this._logQuery(sql, debug, parameters);
|
||
|
const query = new Promise((resolve, reject) => {
|
||
|
if (sql.startsWith("BEGIN TRANSACTION")) {
|
||
|
return connection.beginTransaction((error) => error ? reject(error) : resolve([]), options.transaction.name, connection.lib.ISOLATION_LEVEL[options.isolationLevel]);
|
||
|
}
|
||
|
if (sql.startsWith("COMMIT TRANSACTION")) {
|
||
|
return connection.commitTransaction((error) => error ? reject(error) : resolve([]));
|
||
|
}
|
||
|
if (sql.startsWith("ROLLBACK TRANSACTION")) {
|
||
|
return connection.rollbackTransaction((error) => error ? reject(error) : resolve([]), options.transaction.name);
|
||
|
}
|
||
|
if (sql.startsWith("SAVE TRANSACTION")) {
|
||
|
return connection.saveTransaction((error) => error ? reject(error) : resolve([]), options.transaction.name);
|
||
|
}
|
||
|
const rows2 = [];
|
||
|
const request = new connection.lib.Request(sql, (err, rowCount2) => err ? reject(err) : resolve([rows2, rowCount2]));
|
||
|
if (parameters) {
|
||
|
_.forOwn(parameters, (value, key) => {
|
||
|
const paramType = this.getSQLTypeFromJsType(value, connection.lib.TYPES);
|
||
|
request.addParameter(key, paramType.type, value, paramType.typeOptions);
|
||
|
});
|
||
|
}
|
||
|
request.on("row", (columns) => {
|
||
|
rows2.push(columns);
|
||
|
});
|
||
|
connection.execSql(request);
|
||
|
});
|
||
|
let rows, rowCount;
|
||
|
try {
|
||
|
[rows, rowCount] = await query;
|
||
|
} catch (err) {
|
||
|
err.sql = sql;
|
||
|
err.parameters = parameters;
|
||
|
throw this.formatError(err, errStack);
|
||
|
}
|
||
|
complete();
|
||
|
if (Array.isArray(rows)) {
|
||
|
rows = rows.map((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;
|
||
|
}
|
||
|
return row;
|
||
|
});
|
||
|
}
|
||
|
return this.formatResults(rows, rowCount);
|
||
|
}
|
||
|
run(sql, parameters) {
|
||
|
const errForStack = new Error();
|
||
|
return this.connection.queue.enqueue(() => this._run(this.connection, sql, parameters, errForStack.stack));
|
||
|
}
|
||
|
static formatBindParameters(sql, values, dialect) {
|
||
|
const bindParam = {};
|
||
|
const replacementFunc = (match, key, values2) => {
|
||
|
if (values2[key] !== void 0) {
|
||
|
bindParam[key] = values2[key];
|
||
|
return `@${key}`;
|
||
|
}
|
||
|
return void 0;
|
||
|
};
|
||
|
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
|
||
|
return [sql, bindParam];
|
||
|
}
|
||
|
formatResults(data, rowCount) {
|
||
|
if (this.isInsertQuery(data)) {
|
||
|
this.handleInsertQuery(data);
|
||
|
return [this.instance || data, rowCount];
|
||
|
}
|
||
|
if (this.isShowTablesQuery()) {
|
||
|
return this.handleShowTablesQuery(data);
|
||
|
}
|
||
|
if (this.isDescribeQuery()) {
|
||
|
const 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,
|
||
|
comment: _result.Comment
|
||
|
};
|
||
|
if (result[_result.Name].type.includes("CHAR") && _result.Length) {
|
||
|
if (_result.Length === -1) {
|
||
|
result[_result.Name].type += "(MAX)";
|
||
|
} else {
|
||
|
result[_result.Name].type += `(${_result.Length})`;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
if (this.isSelectQuery()) {
|
||
|
return this.handleSelectQuery(data);
|
||
|
}
|
||
|
if (this.isShowIndexesQuery()) {
|
||
|
return this.handleShowIndexesQuery(data);
|
||
|
}
|
||
|
if (this.isCallQuery()) {
|
||
|
return data[0];
|
||
|
}
|
||
|
if (this.isBulkUpdateQuery()) {
|
||
|
if (this.options.returning) {
|
||
|
return this.handleSelectQuery(data);
|
||
|
}
|
||
|
return rowCount;
|
||
|
}
|
||
|
if (this.isBulkDeleteQuery()) {
|
||
|
return data[0] ? data[0].AFFECTEDROWS : 0;
|
||
|
}
|
||
|
if (this.isVersionQuery()) {
|
||
|
return data[0].version;
|
||
|
}
|
||
|
if (this.isForeignKeysQuery()) {
|
||
|
return data;
|
||
|
}
|
||
|
if (this.isUpsertQuery()) {
|
||
|
if (data && data.length === 0) {
|
||
|
return [this.instance || data, false];
|
||
|
}
|
||
|
this.handleInsertQuery(data);
|
||
|
return [this.instance || data, data[0].$action === "INSERT"];
|
||
|
}
|
||
|
if (this.isUpdateQuery()) {
|
||
|
return [this.instance || data, rowCount];
|
||
|
}
|
||
|
if (this.isShowConstraintsQuery()) {
|
||
|
return this.handleShowConstraintsQuery(data);
|
||
|
}
|
||
|
if (this.isRawQuery()) {
|
||
|
return [data, rowCount];
|
||
|
}
|
||
|
return data;
|
||
|
}
|
||
|
handleShowTablesQuery(results) {
|
||
|
return results.map((resultSet) => {
|
||
|
return {
|
||
|
tableName: resultSet.TABLE_NAME,
|
||
|
schema: resultSet.TABLE_SCHEMA
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
handleShowConstraintsQuery(data) {
|
||
|
return data.slice(1).map((result) => {
|
||
|
const constraint = {};
|
||
|
for (const key in result) {
|
||
|
constraint[_.camelCase(key)] = result[key];
|
||
|
}
|
||
|
return constraint;
|
||
|
});
|
||
|
}
|
||
|
formatError(err, errStack) {
|
||
|
let match;
|
||
|
match = err.message.match(/Violation of (?:UNIQUE|PRIMARY) KEY constraint '([^']*)'. 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[3]) {
|
||
|
const values = match[3].split(",").map((part) => part.trim());
|
||
|
if (uniqueKey) {
|
||
|
fields = _.zipObject(uniqueKey.fields, values);
|
||
|
} else {
|
||
|
fields[match[1]] = match[3];
|
||
|
}
|
||
|
}
|
||
|
const errors = [];
|
||
|
_.forOwn(fields, (value, field) => {
|
||
|
errors.push(new sequelizeErrors.ValidationErrorItem(this.getUniqueConstraintErrorMessage(field), "unique violation", field, value, this.instance, "not_unique"));
|
||
|
});
|
||
|
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields, stack: errStack });
|
||
|
}
|
||
|
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|MERGE|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,
|
||
|
stack: errStack
|
||
|
});
|
||
|
}
|
||
|
match = err.message.match(/Could not drop constraint. See previous errors./);
|
||
|
if (match && match.length > 0) {
|
||
|
let constraint = err.sql.match(/(?:constraint|index) \[(.+?)\]/i);
|
||
|
constraint = constraint ? constraint[1] : void 0;
|
||
|
let table = err.sql.match(/table \[(.+?)\]/i);
|
||
|
table = table ? table[1] : void 0;
|
||
|
return new sequelizeErrors.UnknownConstraintError({
|
||
|
message: match[1],
|
||
|
constraint,
|
||
|
table,
|
||
|
parent: err,
|
||
|
stack: errStack
|
||
|
});
|
||
|
}
|
||
|
return new sequelizeErrors.DatabaseError(err, { stack: errStack });
|
||
|
}
|
||
|
isShowOrDescribeQuery() {
|
||
|
let result = false;
|
||
|
result = result || this.sql.toLowerCase().startsWith("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'");
|
||
|
result = result || this.sql.toLowerCase().startsWith("select tablename = t.name, name = ind.name,");
|
||
|
result = result || this.sql.toLowerCase().startsWith("exec sys.sp_helpindex @objname");
|
||
|
return result;
|
||
|
}
|
||
|
isShowIndexesQuery() {
|
||
|
return this.sql.toLowerCase().startsWith("exec sys.sp_helpindex @objname");
|
||
|
}
|
||
|
handleShowIndexesQuery(data) {
|
||
|
data = data.reduce((acc, item) => {
|
||
|
if (!(item.index_name in acc)) {
|
||
|
acc[item.index_name] = item;
|
||
|
item.fields = [];
|
||
|
}
|
||
|
item.index_keys.split(",").forEach((column) => {
|
||
|
let columnName = column.trim();
|
||
|
if (columnName.includes("(-)")) {
|
||
|
columnName = columnName.replace("(-)", "");
|
||
|
}
|
||
|
acc[item.index_name].fields.push({
|
||
|
attribute: columnName,
|
||
|
length: void 0,
|
||
|
order: column.includes("(-)") ? "DESC" : "ASC",
|
||
|
collate: void 0
|
||
|
});
|
||
|
});
|
||
|
delete item.index_keys;
|
||
|
return acc;
|
||
|
}, {});
|
||
|
return _.map(data, (item) => ({
|
||
|
primary: item.index_name.toLowerCase().startsWith("pk"),
|
||
|
fields: item.fields,
|
||
|
name: item.index_name,
|
||
|
tableName: void 0,
|
||
|
unique: item.index_description.toLowerCase().includes("unique"),
|
||
|
type: void 0
|
||
|
}));
|
||
|
}
|
||
|
handleInsertQuery(results, metaData) {
|
||
|
if (this.instance) {
|
||
|
const autoIncrementAttribute = this.model.autoIncrementAttribute;
|
||
|
let id = null;
|
||
|
let autoIncrementAttributeAlias = null;
|
||
|
if (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementAttribute) && this.model.rawAttributes[autoIncrementAttribute].field !== void 0)
|
||
|
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;
|
||
|
if (this.instance.dataValues) {
|
||
|
for (const key in results[0]) {
|
||
|
if (Object.prototype.hasOwnProperty.call(results[0], key)) {
|
||
|
const record = results[0][key];
|
||
|
const attr = _.find(this.model.rawAttributes, (attribute) => attribute.fieldName === key || attribute.field === key);
|
||
|
this.instance.dataValues[attr && attr.fieldName || key] = record;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
module.exports = Query;
|
||
|
module.exports.Query = Query;
|
||
|
module.exports.default = Query;
|
||
|
//# sourceMappingURL=query.js.map
|