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.
926 lines
31 KiB
926 lines
31 KiB
3 years ago
|
'use strict';
|
||
|
|
||
|
const _ = require('lodash');
|
||
|
const Utils = require('../../utils');
|
||
|
const DataTypes = require('../../data-types');
|
||
|
const TableHints = require('../../table-hints');
|
||
|
const AbstractQueryGenerator = require('../abstract/query-generator');
|
||
|
const randomBytes = require('crypto').randomBytes;
|
||
|
const semver = require('semver');
|
||
|
|
||
|
const Op = require('../../operators');
|
||
|
|
||
|
/* istanbul ignore next */
|
||
|
const throwMethodUndefined = function(methodName) {
|
||
|
throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.');
|
||
|
};
|
||
|
|
||
|
const QueryGenerator = {
|
||
|
__proto__: AbstractQueryGenerator,
|
||
|
options: {},
|
||
|
dialect: 'mssql',
|
||
|
|
||
|
createSchema(schema) {
|
||
|
return [
|
||
|
'IF NOT EXISTS (SELECT schema_name',
|
||
|
'FROM information_schema.schemata',
|
||
|
'WHERE schema_name =', wrapSingleQuote(schema), ')',
|
||
|
'BEGIN',
|
||
|
"EXEC sp_executesql N'CREATE SCHEMA",
|
||
|
this.quoteIdentifier(schema),
|
||
|
";'",
|
||
|
'END;'
|
||
|
].join(' ');
|
||
|
},
|
||
|
|
||
|
dropSchema(schema) {
|
||
|
// Mimics Postgres CASCADE, will drop objects belonging to the schema
|
||
|
const quotedSchema = wrapSingleQuote(schema);
|
||
|
return [
|
||
|
'IF EXISTS (SELECT schema_name',
|
||
|
'FROM information_schema.schemata',
|
||
|
'WHERE schema_name =', quotedSchema, ')',
|
||
|
'BEGIN',
|
||
|
'DECLARE @id INT, @ms_sql NVARCHAR(2000);',
|
||
|
'DECLARE @cascade TABLE (',
|
||
|
'id INT NOT NULL IDENTITY PRIMARY KEY,',
|
||
|
'ms_sql NVARCHAR(2000) NOT NULL );',
|
||
|
'INSERT INTO @cascade ( ms_sql )',
|
||
|
"SELECT CASE WHEN o.type IN ('F','PK')",
|
||
|
"THEN N'ALTER TABLE ['+ s.name + N'].[' + p.name + N'] DROP CONSTRAINT [' + o.name + N']'",
|
||
|
"ELSE N'DROP TABLE ['+ s.name + N'].[' + o.name + N']' END",
|
||
|
'FROM sys.objects o',
|
||
|
'JOIN sys.schemas s on o.schema_id = s.schema_id',
|
||
|
'LEFT OUTER JOIN sys.objects p on o.parent_object_id = p.object_id',
|
||
|
"WHERE o.type IN ('F', 'PK', 'U') AND s.name = ", quotedSchema,
|
||
|
'ORDER BY o.type ASC;',
|
||
|
'SELECT TOP 1 @id = id, @ms_sql = ms_sql FROM @cascade ORDER BY id;',
|
||
|
'WHILE @id IS NOT NULL',
|
||
|
'BEGIN',
|
||
|
'BEGIN TRY EXEC sp_executesql @ms_sql; END TRY',
|
||
|
'BEGIN CATCH BREAK; THROW; END CATCH;',
|
||
|
'DELETE FROM @cascade WHERE id = @id;',
|
||
|
'SELECT @id = NULL, @ms_sql = NULL;',
|
||
|
'SELECT TOP 1 @id = id, @ms_sql = ms_sql FROM @cascade ORDER BY id;',
|
||
|
'END',
|
||
|
"EXEC sp_executesql N'DROP SCHEMA", this.quoteIdentifier(schema), ";'",
|
||
|
'END;'
|
||
|
].join(' ');
|
||
|
},
|
||
|
|
||
|
showSchemasQuery() {
|
||
|
return [
|
||
|
'SELECT "name" as "schema_name" FROM sys.schemas as s',
|
||
|
'WHERE "s"."name" NOT IN (',
|
||
|
"'INFORMATION_SCHEMA', 'dbo', 'guest', 'sys', 'archive'",
|
||
|
')', 'AND', '"s"."name" NOT LIKE', "'db_%'"
|
||
|
].join(' ');
|
||
|
},
|
||
|
|
||
|
versionQuery() {
|
||
|
// Uses string manipulation to convert the MS Maj.Min.Patch.Build to semver Maj.Min.Patch
|
||
|
return [
|
||
|
'DECLARE @ms_ver NVARCHAR(20);',
|
||
|
"SET @ms_ver = REVERSE(CONVERT(NVARCHAR(20), SERVERPROPERTY('ProductVersion')));",
|
||
|
"SELECT REVERSE(SUBSTRING(@ms_ver, CHARINDEX('.', @ms_ver)+1, 20)) AS 'version'"
|
||
|
].join(' ');
|
||
|
},
|
||
|
|
||
|
createTableQuery(tableName, attributes, options) {
|
||
|
const query = "IF OBJECT_ID('<%= table %>', 'U') IS NULL CREATE TABLE <%= table %> (<%= attributes %>)",
|
||
|
primaryKeys = [],
|
||
|
foreignKeys = {},
|
||
|
attrStr = [];
|
||
|
|
||
|
for (const attr in attributes) {
|
||
|
if (attributes.hasOwnProperty(attr)) {
|
||
|
const dataType = attributes[attr];
|
||
|
let match;
|
||
|
|
||
|
if (_.includes(dataType, 'PRIMARY KEY')) {
|
||
|
primaryKeys.push(attr);
|
||
|
|
||
|
if (_.includes(dataType, 'REFERENCES')) {
|
||
|
// MSSQL doesn't support inline REFERENCES declarations: move to the end
|
||
|
match = dataType.match(/^(.+) (REFERENCES.*)$/);
|
||
|
attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1].replace(/PRIMARY KEY/, ''));
|
||
|
foreignKeys[attr] = match[2];
|
||
|
} else {
|
||
|
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType.replace(/PRIMARY KEY/, ''));
|
||
|
}
|
||
|
} else if (_.includes(dataType, 'REFERENCES')) {
|
||
|
// MSSQL doesn't support inline REFERENCES declarations: move to the end
|
||
|
match = dataType.match(/^(.+) (REFERENCES.*)$/);
|
||
|
attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1]);
|
||
|
foreignKeys[attr] = match[2];
|
||
|
} else {
|
||
|
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const values = {
|
||
|
table: this.quoteTable(tableName),
|
||
|
attributes: attrStr.join(', ')
|
||
|
},
|
||
|
pkString = primaryKeys.map(pk => { return this.quoteIdentifier(pk); }).join(', ');
|
||
|
|
||
|
if (options.uniqueKeys) {
|
||
|
_.each(options.uniqueKeys, (columns, indexName) => {
|
||
|
if (columns.customIndex) {
|
||
|
if (!_.isString(indexName)) {
|
||
|
indexName = 'uniq_' + tableName + '_' + columns.fields.join('_');
|
||
|
}
|
||
|
values.attributes += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (pkString.length > 0) {
|
||
|
values.attributes += `, PRIMARY KEY (${pkString})`;
|
||
|
}
|
||
|
|
||
|
for (const fkey in foreignKeys) {
|
||
|
if (foreignKeys.hasOwnProperty(fkey)) {
|
||
|
values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return _.template(query, this._templateSettings)(values).trim() + ';';
|
||
|
},
|
||
|
|
||
|
describeTableQuery(tableName, schema) {
|
||
|
let sql = [
|
||
|
'SELECT',
|
||
|
"c.COLUMN_NAME AS 'Name',",
|
||
|
"c.DATA_TYPE AS 'Type',",
|
||
|
"c.CHARACTER_MAXIMUM_LENGTH AS 'Length',",
|
||
|
"c.IS_NULLABLE as 'IsNull',",
|
||
|
"COLUMN_DEFAULT AS 'Default',",
|
||
|
"pk.CONSTRAINT_TYPE AS 'Constraint',",
|
||
|
"COLUMNPROPERTY(OBJECT_ID(c.TABLE_SCHEMA+'.'+c.TABLE_NAME), c.COLUMN_NAME, 'IsIdentity') as 'IsIdentity'",
|
||
|
'FROM',
|
||
|
'INFORMATION_SCHEMA.TABLES t',
|
||
|
'INNER JOIN',
|
||
|
'INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME AND t.TABLE_SCHEMA = c.TABLE_SCHEMA',
|
||
|
'LEFT JOIN (SELECT tc.table_schema, tc.table_name, ',
|
||
|
'cu.column_name, tc.constraint_type ',
|
||
|
'FROM information_schema.TABLE_CONSTRAINTS tc ',
|
||
|
'JOIN information_schema.KEY_COLUMN_USAGE cu ',
|
||
|
'ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name ',
|
||
|
'and tc.constraint_name=cu.constraint_name ',
|
||
|
'and tc.constraint_type=\'PRIMARY KEY\') pk ',
|
||
|
'ON pk.table_schema=c.table_schema ',
|
||
|
'AND pk.table_name=c.table_name ',
|
||
|
'AND pk.column_name=c.column_name ',
|
||
|
'WHERE t.TABLE_NAME =', wrapSingleQuote(tableName)
|
||
|
].join(' ');
|
||
|
|
||
|
if (schema) {
|
||
|
sql += 'AND t.TABLE_SCHEMA =' + wrapSingleQuote(schema);
|
||
|
}
|
||
|
|
||
|
return sql;
|
||
|
},
|
||
|
|
||
|
renameTableQuery(before, after) {
|
||
|
const query = 'EXEC sp_rename <%= before %>, <%= after %>;';
|
||
|
return _.template(query, this._templateSettings)({
|
||
|
before: this.quoteTable(before),
|
||
|
after: this.quoteTable(after)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
showTablesQuery() {
|
||
|
return 'SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES;';
|
||
|
},
|
||
|
|
||
|
dropTableQuery(tableName) {
|
||
|
const query = "IF OBJECT_ID('<%= table %>', 'U') IS NOT NULL DROP TABLE <%= table %>";
|
||
|
const values = {
|
||
|
table: this.quoteTable(tableName)
|
||
|
};
|
||
|
|
||
|
return _.template(query, this._templateSettings)(values).trim() + ';';
|
||
|
},
|
||
|
|
||
|
addColumnQuery(table, key, dataType) {
|
||
|
// FIXME: attributeToSQL SHOULD be using attributes in addColumnQuery
|
||
|
// but instead we need to pass the key along as the field here
|
||
|
dataType.field = key;
|
||
|
|
||
|
const query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;',
|
||
|
attribute = _.template('<%= key %> <%= definition %>', this._templateSettings)({
|
||
|
key: this.quoteIdentifier(key),
|
||
|
definition: this.attributeToSQL(dataType, {
|
||
|
context: 'addColumn'
|
||
|
})
|
||
|
});
|
||
|
|
||
|
return _.template(query, this._templateSettings)({
|
||
|
table: this.quoteTable(table),
|
||
|
attribute
|
||
|
});
|
||
|
},
|
||
|
|
||
|
removeColumnQuery(tableName, attributeName) {
|
||
|
const query = 'ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;';
|
||
|
return _.template(query, this._templateSettings)({
|
||
|
tableName: this.quoteTable(tableName),
|
||
|
attributeName: this.quoteIdentifier(attributeName)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
changeColumnQuery(tableName, attributes) {
|
||
|
const query = 'ALTER TABLE <%= tableName %> <%= query %>;';
|
||
|
const attrString = [],
|
||
|
constraintString = [];
|
||
|
|
||
|
for (const attributeName in attributes) {
|
||
|
const definition = attributes[attributeName];
|
||
|
if (definition.match(/REFERENCES/)) {
|
||
|
constraintString.push(_.template('<%= fkName %> FOREIGN KEY (<%= attrName %>) <%= definition %>', this._templateSettings)({
|
||
|
fkName: this.quoteIdentifier(attributeName + '_foreign_idx'),
|
||
|
attrName: this.quoteIdentifier(attributeName),
|
||
|
definition: definition.replace(/.+?(?=REFERENCES)/, '')
|
||
|
}));
|
||
|
} else {
|
||
|
attrString.push(_.template('<%= attrName %> <%= definition %>', this._templateSettings)({
|
||
|
attrName: this.quoteIdentifier(attributeName),
|
||
|
definition
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let finalQuery = '';
|
||
|
if (attrString.length) {
|
||
|
finalQuery += 'ALTER COLUMN ' + attrString.join(', ');
|
||
|
finalQuery += constraintString.length ? ' ' : '';
|
||
|
}
|
||
|
if (constraintString.length) {
|
||
|
finalQuery += 'ADD CONSTRAINT ' + constraintString.join(', ');
|
||
|
}
|
||
|
|
||
|
return _.template(query, this._templateSettings)({
|
||
|
tableName: this.quoteTable(tableName),
|
||
|
query: finalQuery
|
||
|
});
|
||
|
},
|
||
|
|
||
|
renameColumnQuery(tableName, attrBefore, attributes) {
|
||
|
const query = "EXEC sp_rename '<%= tableName %>.<%= before %>', '<%= after %>', 'COLUMN';",
|
||
|
newName = Object.keys(attributes)[0];
|
||
|
|
||
|
return _.template(query, this._templateSettings)({
|
||
|
tableName: this.quoteTable(tableName),
|
||
|
before: attrBefore,
|
||
|
after: newName
|
||
|
});
|
||
|
},
|
||
|
|
||
|
bulkInsertQuery(tableName, attrValueHashes, options, attributes) {
|
||
|
options = options || {};
|
||
|
attributes = attributes || {};
|
||
|
const query = 'INSERT INTO <%= table %> (<%= attributes %>)<%= output %> VALUES <%= tuples %>;',
|
||
|
emptyQuery = 'INSERT INTO <%= table %><%= output %> DEFAULT VALUES',
|
||
|
tuples = [],
|
||
|
allAttributes = [],
|
||
|
allQueries = [];
|
||
|
|
||
|
let needIdentityInsertWrapper = false,
|
||
|
outputFragment;
|
||
|
|
||
|
if (options.returning) {
|
||
|
outputFragment = ' OUTPUT INSERTED.*';
|
||
|
}
|
||
|
|
||
|
_.forEach(attrValueHashes, attrValueHash => {
|
||
|
// special case for empty objects with primary keys
|
||
|
const fields = Object.keys(attrValueHash);
|
||
|
const firstAttr = attributes[fields[0]];
|
||
|
if (fields.length === 1 && firstAttr && firstAttr.autoIncrement && attrValueHash[fields[0]] === null) {
|
||
|
allQueries.push(emptyQuery);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// normal case
|
||
|
_.forOwn(attrValueHash, (value, key) => {
|
||
|
if (value !== null && attributes[key] && attributes[key].autoIncrement) {
|
||
|
needIdentityInsertWrapper = true;
|
||
|
}
|
||
|
|
||
|
if (allAttributes.indexOf(key) === -1) {
|
||
|
if (value === null && attributes[key] && attributes[key].autoIncrement)
|
||
|
return;
|
||
|
|
||
|
allAttributes.push(key);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
if (allAttributes.length > 0) {
|
||
|
_.forEach(attrValueHashes, attrValueHash => {
|
||
|
tuples.push('(' +
|
||
|
allAttributes.map(key =>
|
||
|
this.escape(attrValueHash[key])).join(',') +
|
||
|
')');
|
||
|
});
|
||
|
|
||
|
allQueries.push(query);
|
||
|
}
|
||
|
const commands = [];
|
||
|
let offset = 0;
|
||
|
const batch = Math.floor(250 / (allAttributes.length + 1)) + 1;
|
||
|
while (offset < Math.max(tuples.length, 1)) {
|
||
|
const replacements = {
|
||
|
table: this.quoteTable(tableName),
|
||
|
attributes: allAttributes.map(attr =>
|
||
|
this.quoteIdentifier(attr)).join(','),
|
||
|
tuples: tuples.slice(offset, Math.min(tuples.length, offset + batch)),
|
||
|
output: outputFragment
|
||
|
};
|
||
|
|
||
|
let generatedQuery = _.template(allQueries.join(';'), this._templateSettings)(replacements);
|
||
|
if (needIdentityInsertWrapper) {
|
||
|
generatedQuery = [
|
||
|
'SET IDENTITY_INSERT', this.quoteTable(tableName), 'ON;',
|
||
|
generatedQuery,
|
||
|
'SET IDENTITY_INSERT', this.quoteTable(tableName), 'OFF;'
|
||
|
].join(' ');
|
||
|
}
|
||
|
commands.push(generatedQuery);
|
||
|
offset += batch;
|
||
|
}
|
||
|
return commands.join(';');
|
||
|
},
|
||
|
|
||
|
updateQuery(tableName, attrValueHash, where, options, attributes) {
|
||
|
let sql = super.updateQuery(tableName, attrValueHash, where, options, attributes);
|
||
|
if (options.limit) {
|
||
|
const updateArgs = `UPDATE TOP(${this.escape(options.limit)})`;
|
||
|
sql = sql.replace('UPDATE', updateArgs);
|
||
|
}
|
||
|
return sql;
|
||
|
},
|
||
|
|
||
|
upsertQuery(tableName, insertValues, updateValues, where, model) {
|
||
|
const targetTableAlias = this.quoteTable(`${tableName}_target`);
|
||
|
const sourceTableAlias = this.quoteTable(`${tableName}_source`);
|
||
|
const primaryKeysAttrs = [];
|
||
|
const identityAttrs = [];
|
||
|
const uniqueAttrs = [];
|
||
|
const tableNameQuoted = this.quoteTable(tableName);
|
||
|
let needIdentityInsertWrapper = false;
|
||
|
|
||
|
//Obtain primaryKeys, uniquekeys and identity attrs from rawAttributes as model is not passed
|
||
|
for (const key in model.rawAttributes) {
|
||
|
if (model.rawAttributes[key].primaryKey) {
|
||
|
primaryKeysAttrs.push(model.rawAttributes[key].field || key);
|
||
|
}
|
||
|
if (model.rawAttributes[key].unique) {
|
||
|
uniqueAttrs.push(model.rawAttributes[key].field || key);
|
||
|
}
|
||
|
if (model.rawAttributes[key].autoIncrement) {
|
||
|
identityAttrs.push(model.rawAttributes[key].field || key);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//Add unique indexes defined by indexes option to uniqueAttrs
|
||
|
for (const index of model.options.indexes) {
|
||
|
if (index.unique && index.fields) {
|
||
|
for (const field of index.fields) {
|
||
|
const fieldName = typeof field === 'string' ? field : field.name || field.attribute;
|
||
|
if (uniqueAttrs.indexOf(fieldName) === -1 && model.rawAttributes[fieldName]) {
|
||
|
uniqueAttrs.push(fieldName);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const updateKeys = Object.keys(updateValues);
|
||
|
const insertKeys = Object.keys(insertValues);
|
||
|
const insertKeysQuoted = insertKeys.map(key => this.quoteIdentifier(key)).join(', ');
|
||
|
const insertValuesEscaped = insertKeys.map(key => this.escape(insertValues[key])).join(', ');
|
||
|
const sourceTableQuery = `VALUES(${insertValuesEscaped})`; //Virtual Table
|
||
|
let joinCondition;
|
||
|
|
||
|
//IDENTITY_INSERT Condition
|
||
|
identityAttrs.forEach(key => {
|
||
|
if (updateValues[key] && updateValues[key] !== null) {
|
||
|
needIdentityInsertWrapper = true;
|
||
|
/*
|
||
|
* IDENTITY_INSERT Column Cannot be updated, only inserted
|
||
|
* http://stackoverflow.com/a/30176254/2254360
|
||
|
*/
|
||
|
}
|
||
|
});
|
||
|
|
||
|
//Filter NULL Clauses
|
||
|
const clauses = where[Op.or].filter(clause => {
|
||
|
let valid = true;
|
||
|
/*
|
||
|
* Exclude NULL Composite PK/UK. Partial Composite clauses should also be excluded as it doesn't guarantee a single row
|
||
|
*/
|
||
|
for (const key in clause) {
|
||
|
if (!clause[key]) {
|
||
|
valid = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return valid;
|
||
|
});
|
||
|
|
||
|
/*
|
||
|
* Generate ON condition using PK(s).
|
||
|
* If not, generate using UK(s). Else throw error
|
||
|
*/
|
||
|
const getJoinSnippet = array => {
|
||
|
return array.map(key => {
|
||
|
key = this.quoteIdentifier(key);
|
||
|
return `${targetTableAlias}.${key} = ${sourceTableAlias}.${key}`;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
if (clauses.length === 0) {
|
||
|
throw new Error('Primary Key or Unique key should be passed to upsert query');
|
||
|
} else {
|
||
|
// Search for primary key attribute in clauses -- Model can have two separate unique keys
|
||
|
for (const key in clauses) {
|
||
|
const keys = Object.keys(clauses[key]);
|
||
|
if (primaryKeysAttrs.indexOf(keys[0]) !== -1) {
|
||
|
joinCondition = getJoinSnippet(primaryKeysAttrs).join(' AND ');
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (!joinCondition) {
|
||
|
joinCondition = getJoinSnippet(uniqueAttrs).join(' AND ');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove the IDENTITY_INSERT Column from update
|
||
|
const updateSnippet = updateKeys.filter(key => {
|
||
|
if (identityAttrs.indexOf(key) === -1) {
|
||
|
return true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
})
|
||
|
.map(key => {
|
||
|
const value = this.escape(updateValues[key]);
|
||
|
key = this.quoteIdentifier(key);
|
||
|
return `${targetTableAlias}.${key} = ${value}`;
|
||
|
}).join(', ');
|
||
|
|
||
|
const insertSnippet = `(${insertKeysQuoted}) VALUES(${insertValuesEscaped})`;
|
||
|
let query = `MERGE INTO ${tableNameQuoted} WITH(HOLDLOCK) AS ${targetTableAlias} USING (${sourceTableQuery}) AS ${sourceTableAlias}(${insertKeysQuoted}) ON ${joinCondition}`;
|
||
|
query += ` WHEN MATCHED THEN UPDATE SET ${updateSnippet} WHEN NOT MATCHED THEN INSERT ${insertSnippet} OUTPUT $action, INSERTED.*;`;
|
||
|
if (needIdentityInsertWrapper) {
|
||
|
query = `SET IDENTITY_INSERT ${tableNameQuoted} ON; ${query} SET IDENTITY_INSERT ${tableNameQuoted} OFF;`;
|
||
|
}
|
||
|
return query;
|
||
|
},
|
||
|
|
||
|
deleteQuery(tableName, where, options) {
|
||
|
options = options || {};
|
||
|
|
||
|
const table = this.quoteTable(tableName);
|
||
|
if (options.truncate === true) {
|
||
|
// Truncate does not allow LIMIT and WHERE
|
||
|
return 'TRUNCATE TABLE ' + table;
|
||
|
}
|
||
|
|
||
|
where = this.getWhereConditions(where);
|
||
|
let limit = '';
|
||
|
const query = 'DELETE<%= limit %> FROM <%= table %><%= where %>; ' +
|
||
|
'SELECT @@ROWCOUNT AS AFFECTEDROWS;';
|
||
|
|
||
|
if (_.isUndefined(options.limit)) {
|
||
|
options.limit = 1;
|
||
|
}
|
||
|
|
||
|
if (options.limit) {
|
||
|
limit = ' TOP(' + this.escape(options.limit) + ')';
|
||
|
}
|
||
|
|
||
|
const replacements = {
|
||
|
limit,
|
||
|
table,
|
||
|
where
|
||
|
};
|
||
|
|
||
|
if (replacements.where) {
|
||
|
replacements.where = ' WHERE ' + replacements.where;
|
||
|
}
|
||
|
|
||
|
return _.template(query, this._templateSettings)(replacements);
|
||
|
},
|
||
|
|
||
|
showIndexesQuery(tableName) {
|
||
|
const sql = "EXEC sys.sp_helpindex @objname = N'<%= tableName %>';";
|
||
|
return _.template(sql, this._templateSettings)({
|
||
|
tableName: this.quoteTable(tableName)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
showConstraintsQuery(tableName) {
|
||
|
return `EXEC sp_helpconstraint @objname = ${this.escape(this.quoteTable(tableName))};`;
|
||
|
},
|
||
|
|
||
|
removeIndexQuery(tableName, indexNameOrAttributes) {
|
||
|
const sql = 'DROP INDEX <%= indexName %> ON <%= tableName %>';
|
||
|
let indexName = indexNameOrAttributes;
|
||
|
|
||
|
if (typeof indexName !== 'string') {
|
||
|
indexName = Utils.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
|
||
|
}
|
||
|
|
||
|
const values = {
|
||
|
tableName: this.quoteIdentifiers(tableName),
|
||
|
indexName: this.quoteIdentifiers(indexName)
|
||
|
};
|
||
|
|
||
|
return _.template(sql, this._templateSettings)(values);
|
||
|
},
|
||
|
|
||
|
attributeToSQL(attribute) {
|
||
|
if (!_.isPlainObject(attribute)) {
|
||
|
attribute = {
|
||
|
type: attribute
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// handle self referential constraints
|
||
|
if (attribute.references) {
|
||
|
|
||
|
if (attribute.Model && attribute.Model.tableName === attribute.references.model) {
|
||
|
this.sequelize.log('MSSQL does not support self referencial constraints, '
|
||
|
+ 'we will remove it but we recommend restructuring your query');
|
||
|
attribute.onDelete = '';
|
||
|
attribute.onUpdate = '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let template;
|
||
|
|
||
|
if (attribute.type instanceof DataTypes.ENUM) {
|
||
|
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
|
||
|
|
||
|
// enums are a special case
|
||
|
template = attribute.type.toSql();
|
||
|
template += ' CHECK (' + this.quoteIdentifier(attribute.field) + ' IN(' + _.map(attribute.values, value => {
|
||
|
return this.escape(value);
|
||
|
}).join(', ') + '))';
|
||
|
return template;
|
||
|
} else {
|
||
|
template = attribute.type.toString();
|
||
|
}
|
||
|
|
||
|
if (attribute.allowNull === false) {
|
||
|
template += ' NOT NULL';
|
||
|
} else if (!attribute.primaryKey && !Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||
|
template += ' NULL';
|
||
|
}
|
||
|
|
||
|
if (attribute.autoIncrement) {
|
||
|
template += ' IDENTITY(1,1)';
|
||
|
}
|
||
|
|
||
|
// Blobs/texts cannot have a defaultValue
|
||
|
if (attribute.type !== 'TEXT' && attribute.type._binary !== true &&
|
||
|
Utils.defaultValueSchemable(attribute.defaultValue)) {
|
||
|
template += ' DEFAULT ' + this.escape(attribute.defaultValue);
|
||
|
}
|
||
|
|
||
|
if (attribute.unique === true) {
|
||
|
template += ' UNIQUE';
|
||
|
}
|
||
|
|
||
|
if (attribute.primaryKey) {
|
||
|
template += ' PRIMARY KEY';
|
||
|
}
|
||
|
|
||
|
if (attribute.references) {
|
||
|
template += ' REFERENCES ' + this.quoteTable(attribute.references.model);
|
||
|
|
||
|
if (attribute.references.key) {
|
||
|
template += ' (' + this.quoteIdentifier(attribute.references.key) + ')';
|
||
|
} else {
|
||
|
template += ' (' + this.quoteIdentifier('id') + ')';
|
||
|
}
|
||
|
|
||
|
if (attribute.onDelete) {
|
||
|
template += ' ON DELETE ' + attribute.onDelete.toUpperCase();
|
||
|
}
|
||
|
|
||
|
if (attribute.onUpdate) {
|
||
|
template += ' ON UPDATE ' + attribute.onUpdate.toUpperCase();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return template;
|
||
|
},
|
||
|
|
||
|
attributesToSQL(attributes, options) {
|
||
|
const result = {},
|
||
|
existingConstraints = [];
|
||
|
let key,
|
||
|
attribute;
|
||
|
|
||
|
for (key in attributes) {
|
||
|
attribute = attributes[key];
|
||
|
|
||
|
if (attribute.references) {
|
||
|
|
||
|
if (existingConstraints.indexOf(attribute.references.model.toString()) !== -1) {
|
||
|
// no cascading constraints to a table more than once
|
||
|
attribute.onDelete = '';
|
||
|
attribute.onUpdate = '';
|
||
|
} else {
|
||
|
existingConstraints.push(attribute.references.model.toString());
|
||
|
|
||
|
// NOTE: this really just disables cascading updates for all
|
||
|
// definitions. Can be made more robust to support the
|
||
|
// few cases where MSSQL actually supports them
|
||
|
attribute.onUpdate = '';
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if (key && !attribute.field) attribute.field = key;
|
||
|
result[attribute.field || key] = this.attributeToSQL(attribute, options);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
},
|
||
|
|
||
|
createTrigger() {
|
||
|
throwMethodUndefined('createTrigger');
|
||
|
},
|
||
|
|
||
|
dropTrigger() {
|
||
|
throwMethodUndefined('dropTrigger');
|
||
|
},
|
||
|
|
||
|
renameTrigger() {
|
||
|
throwMethodUndefined('renameTrigger');
|
||
|
},
|
||
|
|
||
|
createFunction() {
|
||
|
throwMethodUndefined('createFunction');
|
||
|
},
|
||
|
|
||
|
dropFunction() {
|
||
|
throwMethodUndefined('dropFunction');
|
||
|
},
|
||
|
|
||
|
renameFunction() {
|
||
|
throwMethodUndefined('renameFunction');
|
||
|
},
|
||
|
|
||
|
quoteIdentifier(identifier) {
|
||
|
if (identifier === '*') return identifier;
|
||
|
return '[' + identifier.replace(/[\[\]']+/g, '') + ']';
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Generate common SQL prefix for ForeignKeysQuery.
|
||
|
* @returns {String}
|
||
|
*/
|
||
|
_getForeignKeysQueryPrefix(catalogName) {
|
||
|
return 'SELECT ' +
|
||
|
'constraint_name = OBJ.NAME, ' +
|
||
|
'constraintName = OBJ.NAME, ' +
|
||
|
(catalogName ? `constraintCatalog = '${catalogName}', ` : '') +
|
||
|
'constraintSchema = SCHEMA_NAME(OBJ.SCHEMA_ID), ' +
|
||
|
'tableName = TB.NAME, ' +
|
||
|
'tableSchema = SCHEMA_NAME(TB.SCHEMA_ID), ' +
|
||
|
(catalogName ? `tableCatalog = '${catalogName}', ` : '') +
|
||
|
'columnName = COL.NAME, ' +
|
||
|
'referencedTableSchema = SCHEMA_NAME(RTB.SCHEMA_ID), ' +
|
||
|
(catalogName ? `referencedCatalog = '${catalogName}', ` : '') +
|
||
|
'referencedTableName = RTB.NAME, ' +
|
||
|
'referencedColumnName = RCOL.NAME ' +
|
||
|
'FROM SYS.FOREIGN_KEY_COLUMNS FKC ' +
|
||
|
'INNER JOIN SYS.OBJECTS OBJ ON OBJ.OBJECT_ID = FKC.CONSTRAINT_OBJECT_ID ' +
|
||
|
'INNER JOIN SYS.TABLES TB ON TB.OBJECT_ID = FKC.PARENT_OBJECT_ID ' +
|
||
|
'INNER JOIN SYS.COLUMNS COL ON COL.COLUMN_ID = PARENT_COLUMN_ID AND COL.OBJECT_ID = TB.OBJECT_ID ' +
|
||
|
'INNER JOIN SYS.TABLES RTB ON RTB.OBJECT_ID = FKC.REFERENCED_OBJECT_ID ' +
|
||
|
'INNER JOIN SYS.COLUMNS RCOL ON RCOL.COLUMN_ID = REFERENCED_COLUMN_ID AND RCOL.OBJECT_ID = RTB.OBJECT_ID';
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Generates an SQL query that returns all foreign keys details of a table.
|
||
|
* @param {Stirng|Object} table
|
||
|
* @param {String} catalogName database name
|
||
|
* @returns {String}
|
||
|
*/
|
||
|
getForeignKeysQuery(table, catalogName) {
|
||
|
const tableName = table.tableName || table;
|
||
|
let sql = this._getForeignKeysQueryPrefix(catalogName) +
|
||
|
' WHERE TB.NAME =' + wrapSingleQuote(tableName);
|
||
|
|
||
|
if (table.schema) {
|
||
|
sql += ' AND SCHEMA_NAME(TB.SCHEMA_ID) =' + wrapSingleQuote(table.schema);
|
||
|
}
|
||
|
return sql;
|
||
|
},
|
||
|
|
||
|
getForeignKeyQuery(table, attributeName) {
|
||
|
const tableName = table.tableName || table;
|
||
|
let sql = this._getForeignKeysQueryPrefix() +
|
||
|
' WHERE TB.NAME =' + wrapSingleQuote(tableName) +
|
||
|
' AND COL.NAME =' + wrapSingleQuote(attributeName);
|
||
|
|
||
|
if (table.schema) {
|
||
|
sql += ' AND SCHEMA_NAME(TB.SCHEMA_ID) =' + wrapSingleQuote(table.schema);
|
||
|
}
|
||
|
|
||
|
return sql;
|
||
|
},
|
||
|
|
||
|
getPrimaryKeyConstraintQuery(table, attributeName) {
|
||
|
const tableName = wrapSingleQuote(table.tableName || table);
|
||
|
return [
|
||
|
'SELECT K.TABLE_NAME AS tableName,',
|
||
|
'K.COLUMN_NAME AS columnName,',
|
||
|
'K.CONSTRAINT_NAME AS constraintName',
|
||
|
'FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS C',
|
||
|
'JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K',
|
||
|
'ON C.TABLE_NAME = K.TABLE_NAME',
|
||
|
'AND C.CONSTRAINT_CATALOG = K.CONSTRAINT_CATALOG',
|
||
|
'AND C.CONSTRAINT_SCHEMA = K.CONSTRAINT_SCHEMA',
|
||
|
'AND C.CONSTRAINT_NAME = K.CONSTRAINT_NAME',
|
||
|
'WHERE C.CONSTRAINT_TYPE = \'PRIMARY KEY\'',
|
||
|
`AND K.COLUMN_NAME = ${wrapSingleQuote(attributeName)}`,
|
||
|
`AND K.TABLE_NAME = ${tableName};`
|
||
|
].join(' ');
|
||
|
},
|
||
|
|
||
|
dropForeignKeyQuery(tableName, foreignKey) {
|
||
|
return _.template('ALTER TABLE <%= table %> DROP <%= key %>', this._templateSettings)({
|
||
|
table: this.quoteTable(tableName),
|
||
|
key: this.quoteIdentifier(foreignKey)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
getDefaultConstraintQuery(tableName, attributeName) {
|
||
|
const sql = 'SELECT name FROM SYS.DEFAULT_CONSTRAINTS ' +
|
||
|
"WHERE PARENT_OBJECT_ID = OBJECT_ID('<%= table %>', 'U') " +
|
||
|
"AND PARENT_COLUMN_ID = (SELECT column_id FROM sys.columns WHERE NAME = ('<%= column %>') " +
|
||
|
"AND object_id = OBJECT_ID('<%= table %>', 'U'));";
|
||
|
return _.template(sql, this._templateSettings)({
|
||
|
table: this.quoteTable(tableName),
|
||
|
column: attributeName
|
||
|
});
|
||
|
},
|
||
|
|
||
|
dropConstraintQuery(tableName, constraintName) {
|
||
|
const sql = 'ALTER TABLE <%= table %> DROP CONSTRAINT <%= constraint %>;';
|
||
|
return _.template(sql, this._templateSettings)({
|
||
|
table: this.quoteTable(tableName),
|
||
|
constraint: this.quoteIdentifier(constraintName)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
setAutocommitQuery() {
|
||
|
return '';
|
||
|
},
|
||
|
|
||
|
setIsolationLevelQuery() {
|
||
|
|
||
|
},
|
||
|
|
||
|
generateTransactionId() {
|
||
|
return randomBytes(10).toString('hex');
|
||
|
},
|
||
|
|
||
|
startTransactionQuery(transaction) {
|
||
|
if (transaction.parent) {
|
||
|
return 'SAVE TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
|
||
|
}
|
||
|
|
||
|
return 'BEGIN TRANSACTION;';
|
||
|
},
|
||
|
|
||
|
commitTransactionQuery(transaction) {
|
||
|
if (transaction.parent) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
return 'COMMIT TRANSACTION;';
|
||
|
},
|
||
|
|
||
|
rollbackTransactionQuery(transaction) {
|
||
|
if (transaction.parent) {
|
||
|
return 'ROLLBACK TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
|
||
|
}
|
||
|
|
||
|
return 'ROLLBACK TRANSACTION;';
|
||
|
},
|
||
|
|
||
|
selectFromTableFragment(options, model, attributes, tables, mainTableAs, where) {
|
||
|
let topFragment = '';
|
||
|
let mainFragment = 'SELECT ' + attributes.join(', ') + ' FROM ' + tables;
|
||
|
|
||
|
// Handle SQL Server 2008 with TOP instead of LIMIT
|
||
|
if (semver.valid(this.sequelize.options.databaseVersion) && semver.lt(this.sequelize.options.databaseVersion, '11.0.0')) {
|
||
|
if (options.limit) {
|
||
|
topFragment = 'TOP ' + options.limit + ' ';
|
||
|
}
|
||
|
if (options.offset) {
|
||
|
const offset = options.offset || 0,
|
||
|
isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation;
|
||
|
let orders = { mainQueryOrder: [] };
|
||
|
if (options.order) {
|
||
|
orders = this.getQueryOrders(options, model, isSubQuery);
|
||
|
}
|
||
|
|
||
|
if (!orders.mainQueryOrder.length) {
|
||
|
orders.mainQueryOrder.push(this.quoteIdentifier(model.primaryKeyField));
|
||
|
}
|
||
|
|
||
|
const tmpTable = mainTableAs ? mainTableAs : 'OffsetTable';
|
||
|
const whereFragment = where ? ' WHERE ' + where : '';
|
||
|
|
||
|
/*
|
||
|
* For earlier versions of SQL server, we need to nest several queries
|
||
|
* in order to emulate the OFFSET behavior.
|
||
|
*
|
||
|
* 1. The outermost query selects all items from the inner query block.
|
||
|
* This is due to a limitation in SQL server with the use of computed
|
||
|
* columns (e.g. SELECT ROW_NUMBER()...AS x) in WHERE clauses.
|
||
|
* 2. The next query handles the LIMIT and OFFSET behavior by getting
|
||
|
* the TOP N rows of the query where the row number is > OFFSET
|
||
|
* 3. The innermost query is the actual set we want information from
|
||
|
*/
|
||
|
const fragment = 'SELECT TOP 100 PERCENT ' + attributes.join(', ') + ' FROM ' +
|
||
|
'(SELECT ' + topFragment + '*' +
|
||
|
' FROM (SELECT ROW_NUMBER() OVER (ORDER BY ' + orders.mainQueryOrder.join(', ') + ') as row_num, * ' +
|
||
|
' FROM ' + tables + ' AS ' + tmpTable + whereFragment + ')' +
|
||
|
' AS ' + tmpTable + ' WHERE row_num > ' + offset + ')' +
|
||
|
' AS ' + tmpTable;
|
||
|
return fragment;
|
||
|
} else {
|
||
|
mainFragment = 'SELECT ' + topFragment + attributes.join(', ') + ' FROM ' + tables;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (mainTableAs) {
|
||
|
mainFragment += ' AS ' + mainTableAs;
|
||
|
}
|
||
|
|
||
|
if (options.tableHint && TableHints[options.tableHint]) {
|
||
|
mainFragment += ` WITH (${TableHints[options.tableHint]})`;
|
||
|
}
|
||
|
|
||
|
return mainFragment;
|
||
|
},
|
||
|
|
||
|
addLimitAndOffset(options, model) {
|
||
|
// Skip handling of limit and offset as postfixes for older SQL Server versions
|
||
|
if (semver.valid(this.sequelize.options.databaseVersion) && semver.lt(this.sequelize.options.databaseVersion, '11.0.0')) {
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
const offset = options.offset || 0;
|
||
|
const isSubQuery = options.subQuery === undefined
|
||
|
? options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation
|
||
|
: options.subQuery;
|
||
|
|
||
|
let fragment = '';
|
||
|
let orders = {};
|
||
|
|
||
|
if (options.order) {
|
||
|
orders = this.getQueryOrders(options, model, isSubQuery);
|
||
|
}
|
||
|
|
||
|
if (options.limit || options.offset) {
|
||
|
if (!options.order || options.include && !orders.subQueryOrder.length) {
|
||
|
fragment += options.order && !isSubQuery ? ', ' : ' ORDER BY ';
|
||
|
fragment += this.quoteTable(options.tableAs || model.name) + '.' + this.quoteIdentifier(model.primaryKeyField);
|
||
|
}
|
||
|
|
||
|
if (options.offset || options.limit) {
|
||
|
fragment += ' OFFSET ' + this.escape(offset) + ' ROWS';
|
||
|
}
|
||
|
|
||
|
if (options.limit) {
|
||
|
fragment += ' FETCH NEXT ' + this.escape(options.limit) + ' ROWS ONLY';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return fragment;
|
||
|
},
|
||
|
|
||
|
booleanValue(value) {
|
||
|
return value ? 1 : 0;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// private methods
|
||
|
function wrapSingleQuote(identifier) {
|
||
|
return Utils.addTicks(Utils.removeTicks(identifier, "'"), "'");
|
||
|
}
|
||
|
|
||
|
module.exports = QueryGenerator;
|