Last active
August 16, 2018 19:47
-
-
Save jsanta/1a8618b505f38f438b0994d6411532c3 to your computer and use it in GitHub Desktop.
Sequelize sequelize/lib/dialects/mssql/query-generator.js file for case sensitive MS SQL Server databases ( constraint_type has to be in lowercase and watch for changes on _getForeignKeysQueryPrefix method)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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 ' + (catalogName ? catalogName + '.' : '') + 'sys.foreign_key_columns FKC ' + | |
'INNER JOIN ' + (catalogName ? catalogName + '.' : '') + 'sys.objects OBJ ON OBJ.OBJECT_ID = FKC.CONSTRAINT_OBJECT_ID ' + | |
'INNER JOIN ' + (catalogName ? catalogName + '.' : '') + 'sys.tables TB ON TB.OBJECT_ID = FKC.PARENT_OBJECT_ID ' + | |
'INNER JOIN ' + (catalogName ? catalogName + '.' : '') + 'sys.columns COL ON COL.COLUMN_ID = PARENT_COLUMN_ID AND COL.OBJECT_ID = TB.OBJECT_ID ' + | |
'INNER JOIN ' + (catalogName ? catalogName + '.' : '') + 'sys.tables RTB ON RTB.OBJECT_ID = FKC.REFERENCED_OBJECT_ID ' + | |
'INNER JOIN ' + (catalogName ? catalogName + '.' : '') + '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 ''; | |
} | |
let fragment = ''; | |
const offset = options.offset || 0, | |
isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation; | |
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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment