init source

This commit is contained in:
Le Viet
2022-03-07 22:07:57 +07:00
parent e4376f3777
commit 8aba590a8d
11240 changed files with 1012977 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
'use strict';
const AbstractConnectionManager = require('../abstract/connection-manager');
const ResourceLock = require('./resource-lock');
const Promise = require('../../promise');
const { logger } = require('../../utils/logger');
const sequelizeErrors = require('../../errors');
const DataTypes = require('../../data-types').mssql;
const parserStore = require('../parserStore')('mssql');
const debug = logger.debugContext('connection:mssql');
const debugTedious = logger.debugContext('connection:mssql:tedious');
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
sequelize.config.port = sequelize.config.port || 1433;
super(dialect, sequelize);
this.lib = this._loadDialectModule('tedious');
this.refreshTypeParser(DataTypes);
}
_refreshTypeParser(dataType) {
parserStore.refresh(dataType);
}
_clearTypeParser() {
parserStore.clear();
}
connect(config) {
const connectionConfig = {
server: config.host,
authentication: {
type: 'default',
options: {
userName: config.username || undefined,
password: config.password || undefined
}
},
options: {
port: parseInt(config.port, 10),
database: config.database,
encrypt: false
}
};
if (config.dialectOptions) {
// only set port if no instance name was provided
if (
config.dialectOptions.options &&
config.dialectOptions.options.instanceName
) {
delete connectionConfig.options.port;
}
if (config.dialectOptions.authentication) {
Object.assign(connectionConfig.authentication, config.dialectOptions.authentication);
}
Object.assign(connectionConfig.options, config.dialectOptions.options);
}
return new Promise((resolve, reject) => {
const connection = new this.lib.Connection(connectionConfig);
connection.lib = this.lib;
const resourceLock = new ResourceLock(connection);
const connectHandler = error => {
connection.removeListener('end', endHandler);
connection.removeListener('error', errorHandler);
if (error) return reject(error);
debug('connection acquired');
resolve(resourceLock);
};
const endHandler = () => {
connection.removeListener('connect', connectHandler);
connection.removeListener('error', errorHandler);
reject(new Error('Connection was closed by remote server'));
};
const errorHandler = error => {
connection.removeListener('connect', connectHandler);
connection.removeListener('end', endHandler);
reject(error);
};
connection.once('error', errorHandler);
connection.once('end', endHandler);
connection.once('connect', connectHandler);
/*
* Permanently attach this event before connection is even acquired
* tedious sometime emits error even after connect(with error).
*
* If we dont attach this even that unexpected error event will crash node process
*
* E.g. connectTimeout is set higher than requestTimeout
*/
connection.on('error', error => {
switch (error.code) {
case 'ESOCKET':
case 'ECONNRESET':
this.pool.destroy(resourceLock);
}
});
if (config.dialectOptions && config.dialectOptions.debug) {
connection.on('debug', debugTedious.log.bind(debugTedious));
}
}).catch(error => {
if (!error.code) {
throw new sequelizeErrors.ConnectionError(error);
}
switch (error.code) {
case 'ESOCKET':
if (error.message.includes('connect EHOSTUNREACH')) {
throw new sequelizeErrors.HostNotReachableError(error);
}
if (error.message.includes('connect ENETUNREACH')) {
throw new sequelizeErrors.HostNotReachableError(error);
}
if (error.message.includes('connect EADDRNOTAVAIL')) {
throw new sequelizeErrors.HostNotReachableError(error);
}
if (error.message.includes('getaddrinfo ENOTFOUND')) {
throw new sequelizeErrors.HostNotFoundError(error);
}
if (error.message.includes('connect ECONNREFUSED')) {
throw new sequelizeErrors.ConnectionRefusedError(error);
}
throw new sequelizeErrors.ConnectionError(error);
case 'ER_ACCESS_DENIED_ERROR':
case 'ELOGIN':
throw new sequelizeErrors.AccessDeniedError(error);
case 'EINVAL':
throw new sequelizeErrors.InvalidConnectionError(error);
default:
throw new sequelizeErrors.ConnectionError(error);
}
});
}
disconnect(connectionLock) {
/**
* Abstract connection may try to disconnect raw connection used for fetching version
*/
const connection = connectionLock.unwrap
? connectionLock.unwrap()
: connectionLock;
// Don't disconnect a connection that is already disconnected
if (connection.closed) {
return Promise.resolve();
}
return new Promise(resolve => {
connection.on('end', resolve);
connection.close();
debug('connection closed');
});
}
validate(connectionLock) {
/**
* Abstract connection may try to validate raw connection used for fetching version
*/
const connection = connectionLock.unwrap
? connectionLock.unwrap()
: connectionLock;
return connection && connection.loggedIn;
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+216
View File
@@ -0,0 +1,216 @@
'use strict';
const moment = require('moment');
module.exports = BaseTypes => {
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'https://msdn.microsoft.com/en-us/library/ms187752%28v=sql.110%29.aspx');
/**
* Removes unsupported MSSQL options, i.e., LENGTH, UNSIGNED and ZEROFILL, for the integer data types.
*
* @param {Object} dataType The base integer data type.
* @private
*/
function removeUnsupportedIntegerOptions(dataType) {
if (dataType._length || dataType.options.length || dataType._unsigned || dataType._zerofill) {
warn(`MSSQL does not support '${dataType.key}' with options. Plain '${dataType.key}' will be used instead.`);
dataType._length = undefined;
dataType.options.length = undefined;
dataType._unsigned = undefined;
dataType._zerofill = undefined;
}
}
/**
* types: [hex, ...]
* @see hex here https://github.com/tediousjs/tedious/blob/master/src/data-type.js
*/
BaseTypes.DATE.types.mssql = [43];
BaseTypes.STRING.types.mssql = [231, 173];
BaseTypes.CHAR.types.mssql = [175];
BaseTypes.TEXT.types.mssql = false;
// https://msdn.microsoft.com/en-us/library/ms187745(v=sql.110).aspx
BaseTypes.TINYINT.types.mssql = [30];
BaseTypes.SMALLINT.types.mssql = [34];
BaseTypes.MEDIUMINT.types.mssql = false;
BaseTypes.INTEGER.types.mssql = [38];
BaseTypes.BIGINT.types.mssql = false;
BaseTypes.FLOAT.types.mssql = [109];
BaseTypes.TIME.types.mssql = [41];
BaseTypes.DATEONLY.types.mssql = [40];
BaseTypes.BOOLEAN.types.mssql = [104];
BaseTypes.BLOB.types.mssql = [165];
BaseTypes.DECIMAL.types.mssql = [106];
BaseTypes.UUID.types.mssql = false;
BaseTypes.ENUM.types.mssql = false;
BaseTypes.REAL.types.mssql = [109];
BaseTypes.DOUBLE.types.mssql = [109];
// BaseTypes.GEOMETRY.types.mssql = [240]; // not yet supported
BaseTypes.GEOMETRY.types.mssql = false;
class BLOB extends BaseTypes.BLOB {
toSql() {
if (this._length) {
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
warn('MSSQL does not support BLOB with the `length` = `tiny` option. `VARBINARY(256)` will be used instead.');
return 'VARBINARY(256)';
}
warn('MSSQL does not support BLOB with the `length` option. `VARBINARY(MAX)` will be used instead.');
}
return 'VARBINARY(MAX)';
}
_hexify(hex) {
return `0x${hex}`;
}
}
class STRING extends BaseTypes.STRING {
toSql() {
if (!this._binary) {
return `NVARCHAR(${this._length})`;
}
return `BINARY(${this._length})`;
}
_stringify(value, options) {
if (this._binary) {
return BLOB.prototype._stringify(value);
}
return options.escape(value);
}
_bindParam(value, options) {
return options.bindParam(this._binary ? Buffer.from(value) : value);
}
}
STRING.prototype.escape = false;
class TEXT extends BaseTypes.TEXT {
toSql() {
// TEXT is deprecated in mssql and it would normally be saved as a non-unicode string.
// Using unicode is just future proof
if (this._length) {
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
warn('MSSQL does not support TEXT with the `length` = `tiny` option. `NVARCHAR(256)` will be used instead.');
return 'NVARCHAR(256)';
}
warn('MSSQL does not support TEXT with the `length` option. `NVARCHAR(MAX)` will be used instead.');
}
return 'NVARCHAR(MAX)';
}
}
class BOOLEAN extends BaseTypes.BOOLEAN {
toSql() {
return 'BIT';
}
}
class UUID extends BaseTypes.UUID {
toSql() {
return 'CHAR(36)';
}
}
class NOW extends BaseTypes.NOW {
toSql() {
return 'GETDATE()';
}
}
class DATE extends BaseTypes.DATE {
toSql() {
return 'DATETIMEOFFSET';
}
}
class DATEONLY extends BaseTypes.DATEONLY {
static parse(value) {
return moment(value).format('YYYY-MM-DD');
}
}
class INTEGER extends BaseTypes.INTEGER {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class TINYINT extends BaseTypes.TINYINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class SMALLINT extends BaseTypes.SMALLINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class BIGINT extends BaseTypes.BIGINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class REAL extends BaseTypes.REAL {
constructor(length, decimals) {
super(length, decimals);
// MSSQL does not support any options for real
if (this._length || this.options.length || this._unsigned || this._zerofill) {
warn('MSSQL does not support REAL with options. Plain `REAL` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
}
}
class FLOAT extends BaseTypes.FLOAT {
constructor(length, decimals) {
super(length, decimals);
// MSSQL does only support lengths as option.
// Values between 1-24 result in 7 digits precision (4 bytes storage size)
// Values between 25-53 result in 15 digits precision (8 bytes storage size)
// If decimals are provided remove these and print a warning
if (this._decimals) {
warn('MSSQL does not support Float with decimals. Plain `FLOAT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
}
if (this._unsigned) {
warn('MSSQL does not support Float unsigned. `UNSIGNED` was removed.');
this._unsigned = undefined;
}
if (this._zerofill) {
warn('MSSQL does not support Float zerofill. `ZEROFILL` was removed.');
this._zerofill = undefined;
}
}
}
class ENUM extends BaseTypes.ENUM {
toSql() {
return 'VARCHAR(255)';
}
}
return {
BLOB,
BOOLEAN,
ENUM,
STRING,
UUID,
DATE,
DATEONLY,
NOW,
TINYINT,
SMALLINT,
INTEGER,
BIGINT,
REAL,
FLOAT,
TEXT
};
};
+63
View File
@@ -0,0 +1,63 @@
'use strict';
const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').mssql;
class MssqlDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.QueryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
}
}
MssqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
'DEFAULT': true,
'DEFAULT VALUES': true,
'LIMIT ON UPDATE': true,
'ORDER NULLS': false,
lock: false,
transactions: true,
migrations: false,
returnValues: {
output: true
},
schemas: true,
autoIncrement: {
identityInsert: true,
defaultValue: false,
update: false
},
constraints: {
restrict: false,
default: true
},
index: {
collate: false,
length: false,
parser: false,
type: true,
using: false,
where: true
},
NUMERIC: true,
tmpTableTrigger: true
});
ConnectionManager.prototype.defaultVersion = '12.0.2000'; // SQL Server 2014 Express
MssqlDialect.prototype.Query = Query;
MssqlDialect.prototype.name = 'mssql';
MssqlDialect.prototype.TICK_CHAR = '"';
MssqlDialect.prototype.TICK_CHAR_LEFT = '[';
MssqlDialect.prototype.TICK_CHAR_RIGHT = ']';
MssqlDialect.prototype.DataTypes = DataTypes;
module.exports = MssqlDialect;
+899
View File
@@ -0,0 +1,899 @@
'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.`);
};
class MSSQLQueryGenerator extends AbstractQueryGenerator {
createDatabaseQuery(databaseName, options) {
options = Object.assign({
collate: null
}, options || {});
const collation = options.collate ? `COLLATE ${this.escape(options.collate)}` : '';
return [
'IF NOT EXISTS (SELECT * FROM sys.databases WHERE name =', wrapSingleQuote(databaseName), ')',
'BEGIN',
'CREATE DATABASE', this.quoteIdentifier(databaseName),
`${collation};`,
'END;'
].join(' ');
}
dropDatabaseQuery(databaseName) {
return [
'IF EXISTS (SELECT * FROM sys.databases WHERE name =', wrapSingleQuote(databaseName), ')',
'BEGIN',
'DROP DATABASE', this.quoteIdentifier(databaseName), ';',
'END;'
].join(' ');
}
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 = (table, attrs) => `IF OBJECT_ID('${table}', 'U') IS NULL CREATE TABLE ${table} (${attrs})`,
primaryKeys = [],
foreignKeys = {},
attrStr = [];
let commentStr = '';
for (const attr in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
let dataType = attributes[attr];
let match;
if (dataType.includes('COMMENT ')) {
const commentMatch = dataType.match(/^(.+) (COMMENT.*)$/);
const commentText = commentMatch[2].replace('COMMENT', '').trim();
commentStr += this.commentTemplate(commentText, tableName, attr);
// remove comment related substring from dataType
dataType = commentMatch[1];
}
if (dataType.includes('PRIMARY KEY')) {
primaryKeys.push(attr);
if (dataType.includes('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 (dataType.includes('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}`);
}
}
}
let attributesClause = attrStr.join(', ');
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
if (options.uniqueKeys) {
_.each(options.uniqueKeys, (columns, indexName) => {
if (columns.customIndex) {
if (typeof indexName !== 'string') {
indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
}
attributesClause += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
}
});
}
if (pkString.length > 0) {
attributesClause += `, PRIMARY KEY (${pkString})`;
}
for (const fkey in foreignKeys) {
if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
}
}
return `${query(this.quoteTable(tableName), attributesClause)};${commentStr}`;
}
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',",
"prop.value AS 'Comment'",
'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 ',
'INNER JOIN sys.columns AS sc',
"ON sc.object_id = object_id(t.table_schema + '.' + t.table_name) AND sc.name = c.column_name",
'LEFT JOIN sys.extended_properties prop ON prop.major_id = sc.object_id',
'AND prop.minor_id = sc.column_id',
"AND prop.name = 'MS_Description'",
'WHERE t.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
if (schema) {
sql += `AND t.TABLE_SCHEMA =${wrapSingleQuote(schema)}`;
}
return sql;
}
renameTableQuery(before, after) {
return `EXEC sp_rename ${this.quoteTable(before)}, ${this.quoteTable(after)};`;
}
showTablesQuery() {
return "SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';";
}
dropTableQuery(tableName) {
const qouteTbl = this.quoteTable(tableName);
return `IF OBJECT_ID('${qouteTbl}', 'U') IS NOT NULL DROP TABLE ${qouteTbl};`;
}
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;
let commentStr = '';
if (dataType.comment && _.isString(dataType.comment)) {
commentStr = this.commentTemplate(dataType.comment, table, key);
// attributeToSQL will try to include `COMMENT 'Comment Text'` when it returns if the comment key
// is present. This is needed for createTable statement where that part is extracted with regex.
// Here we can intercept the object and remove comment property since we have the original object.
delete dataType['comment'];
}
const def = this.attributeToSQL(dataType, {
context: 'addColumn'
});
return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${def};${commentStr}`;
}
commentTemplate(comment, table, column) {
return ' EXEC sp_addextendedproperty ' +
`@name = N'MS_Description', @value = ${this.escape(comment)}, ` +
'@level0type = N\'Schema\', @level0name = \'dbo\', ' +
`@level1type = N'Table', @level1name = ${this.quoteIdentifier(table)}, ` +
`@level2type = N'Column', @level2name = ${this.quoteIdentifier(column)};`;
}
removeColumnQuery(tableName, attributeName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.quoteIdentifier(attributeName)};`;
}
changeColumnQuery(tableName, attributes) {
const attrString = [],
constraintString = [];
let commentString = '';
for (const attributeName in attributes) {
const quotedAttrName = this.quoteIdentifier(attributeName);
let definition = attributes[attributeName];
if (definition.includes('COMMENT ')) {
const commentMatch = definition.match(/^(.+) (COMMENT.*)$/);
const commentText = commentMatch[2].replace('COMMENT', '').trim();
commentString += this.commentTemplate(commentText, tableName, attributeName);
// remove comment related substring from dataType
definition = commentMatch[1];
}
if (definition.includes('REFERENCES')) {
constraintString.push(`FOREIGN KEY (${quotedAttrName}) ${definition.replace(/.+?(?=REFERENCES)/, '')}`);
} else {
attrString.push(`${quotedAttrName} ${definition}`);
}
}
let finalQuery = '';
if (attrString.length) {
finalQuery += `ALTER COLUMN ${attrString.join(', ')}`;
finalQuery += constraintString.length ? ' ' : '';
}
if (constraintString.length) {
finalQuery += `ADD ${constraintString.join(', ')}`;
}
return `ALTER TABLE ${this.quoteTable(tableName)} ${finalQuery};${commentString}`;
}
renameColumnQuery(tableName, attrBefore, attributes) {
const newName = Object.keys(attributes)[0];
return `EXEC sp_rename '${this.quoteTable(tableName)}.${attrBefore}', '${newName}', 'COLUMN';`;
}
bulkInsertQuery(tableName, attrValueHashes, options, attributes) {
const quotedTable = this.quoteTable(tableName);
options = options || {};
attributes = attributes || {};
const tuples = [];
const allAttributes = [];
const allQueries = [];
let needIdentityInsertWrapper = false,
outputFragment = '';
if (options.returning) {
outputFragment = ' OUTPUT INSERTED.*';
}
const emptyQuery = `INSERT INTO ${quotedTable}${outputFragment} DEFAULT VALUES`;
attrValueHashes.forEach(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.includes(key)) {
if (value === null && attributes[key] && attributes[key].autoIncrement)
return;
allAttributes.push(key);
}
});
});
if (allAttributes.length > 0) {
attrValueHashes.forEach(attrValueHash => {
tuples.push(`(${
allAttributes.map(key =>
this.escape(attrValueHash[key])).join(',')
})`);
});
const quotedAttributes = allAttributes.map(attr => this.quoteIdentifier(attr)).join(',');
allQueries.push(tupleStr => `INSERT INTO ${quotedTable} (${quotedAttributes})${outputFragment} VALUES ${tupleStr};`);
}
const commands = [];
let offset = 0;
const batch = Math.floor(250 / (allAttributes.length + 1)) + 1;
while (offset < Math.max(tuples.length, 1)) {
const tupleStr = tuples.slice(offset, Math.min(tuples.length, offset + batch));
let generatedQuery = allQueries.map(v => typeof v === 'string' ? v : v(tupleStr)).join(';');
if (needIdentityInsertWrapper) {
generatedQuery = `SET IDENTITY_INSERT ${quotedTable} ON; ${generatedQuery}; SET IDENTITY_INSERT ${quotedTable} OFF;`;
}
commands.push(generatedQuery);
offset += batch;
}
return commands.join(';');
}
updateQuery(tableName, attrValueHash, where, options, attributes) {
const sql = super.updateQuery(tableName, attrValueHash, where, options, attributes);
if (options.limit) {
const updateArgs = `UPDATE TOP(${this.escape(options.limit)})`;
sql.query = sql.query.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._indexes) {
if (index.unique && index.fields) {
for (const field of index.fields) {
const fieldName = typeof field === 'string' ? field : field.name || field.attribute;
if (!uniqueAttrs.includes(fieldName) && 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.includes(keys[0])) {
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 => !identityAttrs.includes(key))
.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;
}
truncateTableQuery(tableName) {
return `TRUNCATE TABLE ${this.quoteTable(tableName)}`;
}
deleteQuery(tableName, where, options = {}, model) {
const table = this.quoteTable(tableName);
let whereClause = this.getWhereConditions(where, null, model, options);
let limit = '';
if (options.limit) {
limit = ` TOP(${this.escape(options.limit)})`;
}
if (whereClause) {
whereClause = ` WHERE ${whereClause}`;
}
return `DELETE${limit} FROM ${table}${whereClause}; SELECT @@ROWCOUNT AS AFFECTEDROWS;`;
}
showIndexesQuery(tableName) {
return `EXEC sys.sp_helpindex @objname = N'${this.quoteTable(tableName)}';`;
}
showConstraintsQuery(tableName) {
return `EXEC sp_helpconstraint @objname = ${this.escape(this.quoteTable(tableName))};`;
}
removeIndexQuery(tableName, indexNameOrAttributes) {
let indexName = indexNameOrAttributes;
if (typeof indexName !== 'string') {
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
}
return `DROP INDEX ${this.quoteIdentifiers(indexName)} ON ${this.quoteIdentifiers(tableName)}`;
}
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(${attribute.values.map(value => {
return this.escape(value);
}).join(', ') }))`;
return template;
}
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()}`;
}
}
if (attribute.comment && typeof attribute.comment === 'string') {
template += ` COMMENT ${attribute.comment}`;
}
return template;
}
attributesToSQL(attributes, options) {
const result = {},
existingConstraints = [];
let key,
attribute;
for (key in attributes) {
attribute = attributes[key];
if (attribute.references) {
if (existingConstraints.includes(attribute.references.model.toString())) {
// 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');
}
/**
* Generate common SQL prefix for ForeignKeysQuery.
*
* @param {string} catalogName
* @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 {string|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 `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(foreignKey)}`;
}
getDefaultConstraintQuery(tableName, attributeName) {
const quotedTable = this.quoteTable(tableName);
return 'SELECT name FROM sys.default_constraints ' +
`WHERE PARENT_OBJECT_ID = OBJECT_ID('${quotedTable}', 'U') ` +
`AND PARENT_COLUMN_ID = (SELECT column_id FROM sys.columns WHERE NAME = ('${attributeName}') ` +
`AND object_id = OBJECT_ID('${quotedTable}', 'U'));`;
}
dropConstraintQuery(tableName, constraintName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName)};`;
}
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;
}
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 = MSSQLQueryGenerator;
+68
View File
@@ -0,0 +1,68 @@
'use strict';
/**
Returns an object that treats MSSQL's inabilities to do certain queries.
@class QueryInterface
@static
@private
*/
/**
A wrapper that fixes MSSQL's inability to cleanly remove columns from existing tables if they have a default constraint.
@param {QueryInterface} qi
@param {string} tableName The name of the table.
@param {string} attributeName The name of the attribute that we want to remove.
@param {Object} options
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@private
*/
const removeColumn = function(qi, tableName, attributeName, options) {
options = Object.assign({ raw: true }, options || {});
const findConstraintSql = qi.QueryGenerator.getDefaultConstraintQuery(tableName, attributeName);
return qi.sequelize.query(findConstraintSql, options)
.then(([results]) => {
if (!results.length) {
// No default constraint found -- we can cleanly remove the column
return;
}
const dropConstraintSql = qi.QueryGenerator.dropConstraintQuery(tableName, results[0].name);
return qi.sequelize.query(dropConstraintSql, options);
})
.then(() => {
const findForeignKeySql = qi.QueryGenerator.getForeignKeyQuery(tableName, attributeName);
return qi.sequelize.query(findForeignKeySql, options);
})
.then(([results]) => {
if (!results.length) {
// No foreign key constraints found, so we can remove the column
return;
}
const dropForeignKeySql = qi.QueryGenerator.dropForeignKeyQuery(tableName, results[0].constraint_name);
return qi.sequelize.query(dropForeignKeySql, options);
})
.then(() => {
//Check if the current column is a primaryKey
const primaryKeyConstraintSql = qi.QueryGenerator.getPrimaryKeyConstraintQuery(tableName, attributeName);
return qi.sequelize.query(primaryKeyConstraintSql, options);
})
.then(([result]) => {
if (!result.length) {
return;
}
const dropConstraintSql = qi.QueryGenerator.dropConstraintQuery(tableName, result[0].constraintName);
return qi.sequelize.query(dropConstraintSql, options);
})
.then(() => {
const removeSql = qi.QueryGenerator.removeColumnQuery(tableName, attributeName);
return qi.sequelize.query(removeSql, options);
});
};
module.exports = {
removeColumn
};
+389
View File
@@ -0,0 +1,389 @@
'use strict';
const Promise = require('../../promise');
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');
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;
//Default to a reasonable numeric precision/scale pending more sophisticated logic
paramType.typeOptions = { precision: 30, scale: 15 };
}
}
if (Buffer.isBuffer(value)) {
paramType.type = TYPES.VarBinary;
}
return paramType;
}
_run(connection, sql, parameters) {
this.sql = sql;
const { options } = this;
const complete = this._logQuery(sql, debug, parameters);
return new Promise((resolve, reject) => {
const handleTransaction = err => {
if (err) {
reject(this.formatError(err));
return;
}
resolve(this.formatResults());
};
// TRANSACTION SUPPORT
if (sql.startsWith('BEGIN TRANSACTION')) {
return connection.beginTransaction(handleTransaction, options.transaction.name, connection.lib.ISOLATION_LEVEL[options.isolationLevel]);
}
if (sql.startsWith('COMMIT TRANSACTION')) {
return connection.commitTransaction(handleTransaction);
}
if (sql.startsWith('ROLLBACK TRANSACTION')) {
return connection.rollbackTransaction(handleTransaction, options.transaction.name);
}
if (sql.startsWith('SAVE TRANSACTION')) {
return connection.saveTransaction(handleTransaction, options.transaction.name);
}
const results = [];
const request = new connection.lib.Request(sql, (err, rowCount) => {
complete();
if (err) {
err.sql = sql;
err.parameters = parameters;
reject(this.formatError(err));
} else {
resolve(this.formatResults(results, rowCount));
}
});
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 => {
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.
*
* @param {Array} data - The result of the query execution.
* @param {number} rowCount
* @private
* @example
* 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
* }
* ])
*/
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()) {
return this.handleShowTablesQuery(data);
}
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,
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})`;
}
}
}
}
if (this.isSelectQuery()) {
return this.handleSelectQuery(data);
}
if (this.isShowIndexesQuery()) {
return this.handleShowIndexesQuery(data);
}
if (this.isUpsertQuery()) {
return data[0];
}
if (this.isCallQuery()) {
return data[0];
}
if (this.isBulkUpdateQuery()) {
return data.length;
}
if (this.isBulkDeleteQuery()) {
return data[0] && data[0].AFFECTEDROWS;
}
if (this.isVersionQuery()) {
return data[0].version;
}
if (this.isForeignKeysQuery()) {
return data;
}
if (this.isInsertQuery() || this.isUpdateQuery()) {
return [result, rowCount];
}
if (this.isShowConstraintsQuery()) {
return this.handleShowConstraintsQuery(data);
}
if (this.isRawQuery()) {
// MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
return [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|PRIMARY) 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|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
});
}
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] : undefined;
let table = err.sql.match(/table \[(.+?)\]/i);
table = table ? table[1] : undefined;
return new sequelizeErrors.UnknownConstraintError({
message: match[1],
constraint,
table,
parent: err
});
}
return new sequelizeErrors.DatabaseError(err);
}
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) {
// Group by index name, and collect all fields
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: undefined,
order: column.includes('(-)') ? 'DESC' : 'ASC',
collate: undefined
});
});
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: undefined,
unique: item.index_description.toLowerCase().includes('unique'),
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 (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, 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;
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const Promise = require('../../promise');
class ResourceLock {
constructor(resource) {
this.resource = resource;
this.previous = Promise.resolve(resource);
}
unwrap() {
return this.resource;
}
lock() {
const lock = this.previous;
let resolve;
this.previous = new Promise(r => {
resolve = r;
});
return lock.disposer(resolve);
}
}
module.exports = ResourceLock;