不要怂,就是干,撸起袖子干!

Commit e9699b8c by Matt Broadstone

refactor createTableQuery

Major overhaul of the createTableQuery to bring it closer in line
with the mysql dialect. This closes a number of failed cases where
things were being created in a strange manner, as well as let's us
do away with SqlGenerator once and for all
1 parent e1052290
......@@ -14,6 +14,7 @@ var MssqlDialect = function(sequelize) {
MssqlDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), {
'RETURNING': false,
'OUTPUT': true,
'DEFAULT': true,
'DEFAULT VALUES': true,
'LIMIT ON UPDATE': true,
lock: false,
......
......@@ -2,7 +2,6 @@
var Utils = require('../../utils')
, DataTypes = require('./data-types')
, SqlGenerator = require('./sql-generator')
, Model = require('../../model')
, _ = require('lodash')
, util = require('util')
......@@ -24,32 +23,74 @@ module.exports = (function() {
},
showSchemasQuery: function() {
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(' ');
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(' ');
},
createTableQuery: function(tableName, attributes, options) {
var query = "IF OBJECT_ID('[<%= escapedTable %>]', 'U') IS NULL CREATE TABLE <%= table %> (<%= attributes%>)";
var attrStr = []
, self = this
, primaryKeys = Utils._.keys(Utils._.pick(attributes, function(dataType){
return dataType.indexOf('PRIMARY KEY') >= 0;
}));
var query = "IF OBJECT_ID('[<%= escapedTable %>]', 'U') IS NULL CREATE TABLE <%= table %> (<%= attributes %>)"
, primaryKeys = []
, foreignKeys = {}
, attrStr = []
, self = this;
for (var attr in attributes) {
var dataType = mssqlDataTypeMapping(tableName, attr, attributes[attr]);
attrStr.push(this.quoteIdentifier(attr) + " " + dataType);
if (attributes.hasOwnProperty(attr)) {
var dataType = attributes[attr]
, match;
if (Utils._.includes(dataType, 'PRIMARY KEY')) {
primaryKeys.push(attr);
if (Utils._.includes(dataType, 'REFERENCES')) {
// MySQL 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 (Utils._.includes(dataType, 'REFERENCES')) {
// MySQL 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);
}
}
}
var values = {
escapedTable: this.quoteTable(tableName).replace(/"/g, ''),
table: this.quoteTable(tableName),
attributes: attrStr.join(", ")
};
attributes: attrStr.join(', '),
}
, pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', ');
return Utils._.template(query)(values).trim() + ";";
if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns, indexName) {
if (!Utils._.isString(indexName)) {
indexName = 'uniq_' + tableName + '_' + columns.fields.join('_');
}
values.attributes += ', CONSTRAINT ' + indexName + ' UNIQUE (' + Utils._.map(columns.fields, self.quoteIdentifier).join(', ') + ')';
});
}
if (pkString.length > 0) {
values.attributes += ', PRIMARY KEY (' + pkString + ')';
}
for (var fkey in foreignKeys) {
if (foreignKeys.hasOwnProperty(fkey)) {
values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey];
}
}
return Utils._.template(query)(values).trim() + ';';
},
describeTableQuery: function(tableName, schema, schemaDelimiter) {
......@@ -137,16 +178,14 @@ module.exports = (function() {
},
renameColumnQuery: function(tableName, attrBefore, attributes) {
var newColumnName;
for (var attrName in attributes) {
newColumnName = attrName;
}
var query = "EXEC sp_rename '<%= tableName %>.<%= before %>', '<%= after %>', 'COLUMN';"
, newName = Object.keys(attributes)[0];
var query = [
SqlGenerator.renameColumnSql(tableName, attrBefore, newColumnName),
this.changeColumnQuery(tableName, attributes)
].join(' ');
return query;
return Utils._.template(query)({
tableName: tableName,
before: attrBefore,
after: newName
});
},
bulkInsertQuery: function(tableName, attrValueHashes, options, attributes) {
......@@ -268,7 +307,75 @@ module.exports = (function() {
},
attributeToSQL: function(attribute, options) {
return SqlGenerator.attributeToSQL(attribute, options);
if (!Utils._.isPlainObject(attribute)) {
attribute = {
type: attribute
};
}
// handle self referential constraints
if (attribute.Model && attribute.Model.tableName === attribute.references) {
this.sequelize.log('MSSQL does not support self referencial constraints, '
+ 'we will remove it but we recommend restructuring your query');
attribute.onDelete = '';
attribute.onUpdate = '';
}
var template;
if (attribute.type.toString() === DataTypes.ENUM.toString()) {
// enums are a special case
template = 'VARCHAR(10) NULL' /* + (attribute.allowNull ? 'NULL' : 'NOT NULL') */;
template += ' CHECK (' + attribute.field + ' IN(' + Utils._.map(attribute.values, function(value) {
return this.escape(value);
}.bind(this)).join(', ') + '))';
return template;
} else {
template = mssqlDataTypeMapping(null, null, 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);
if (attribute.referencesKey) {
template += ' (' + this.quoteIdentifier(attribute.referencesKey) + ')';
} 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: function(attributes, options) {
......@@ -311,7 +418,7 @@ module.exports = (function() {
for (key in attributes) {
attribute = attributes[key];
if (key && !attribute.field) attribute.field = key;
result[attribute.field || key] = SqlGenerator.attributeToSQL(attribute, options);
result[attribute.field || key] = this.attributeToSQL(attribute, options);
}
return result;
......@@ -368,9 +475,9 @@ module.exports = (function() {
'constraint_name = C.CONSTRAINT_NAME',
'FROM',
'INFORMATION_SCHEMA.TABLE_CONSTRAINTS C',
"WHERE C.CONSTRAINT_TYPE != 'PRIMARY KEY'",
'AND C.TABLE_NAME = ', wrapSingleQuote(tableName)
].join(" ");
"WHERE C.CONSTRAINT_TYPE = 'FOREIGN KEY'",
'AND C.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
},
dropForeignKeyQuery: function(tableName, foreignKey) {
......@@ -395,7 +502,7 @@ module.exports = (function() {
startTransactionQuery: function(transaction, options) {
if (options.parent) {
return 'SAVE TRANSACTION ' + SqlGenerator.quoteIdentifier(transaction.name) + ';';
return 'SAVE TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
}
return 'BEGIN TRANSACTION;';
......@@ -507,10 +614,18 @@ module.exports = (function() {
}
function mssqlDataTypeMapping(tableName, attr, dataType) {
if (Utils._.includes(dataType, 'TINYINT(1)')) {
dataType = dataType.replace(/TINYINT\(1\)/, 'BIT');
}
if (Utils._.includes(dataType, 'DATETIME')) {
dataType = dataType.replace(/DATETIME/, 'DATETIME2');
}
if (Utils._.includes(dataType, 'BLOB')) {
dataType = dataType.replace(/BLOB/, 'VARBINARY(MAX)');
}
return dataType;
}
......@@ -519,7 +634,5 @@ module.exports = (function() {
throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.');
};
// TODO: get rid of this
SqlGenerator.options = QueryGenerator.options;
return Utils._.extend(Utils._.clone(AbstractQueryGenerator), QueryGenerator);
})();
'use strict';
var Utils = require('../../utils')
, SqlString = require('../../sql-string')
, DataTypes = require('./data-types')
, _ = require('lodash')
, _dialect = 'mssql'
, _sequelize
, _options;
/*
Escape a value (e.g. a string, number or date)
*/
var attributeMap = {
notNull:"NOT NULL",
allowNull:"NULL",
autoIncrement:"IDENTITY(1,1)",
defaultValue:"DEFAULT",
unique:"UNIQUE",
primaryKey:"PRIMARY KEY",
foreignKey:"FOREIGN KEY",
comment:"COMMENT",
references:"REFERENCES",
onDelete:"ON DELETE",
onUpdate:"ON UPDATE",
default:"DEFAULT"
};
function escape(value, field) {
if (value && value._isSequelizeMethod) {
return value.toString();
} else {
return SqlString.escape(value, false, _options.timezone, _dialect, field);
}
}
function quoteIdentifier(identifier, force) {
if (identifier === '*') return identifier;
return Utils.addTicks(identifier, '"');
}
/*
Split an identifier into .-separated tokens and quote each part
*/
function quoteIdentifiers(identifiers, force) {
if (identifiers.indexOf('.') !== -1) {
identifiers = identifiers.split('.');
return quoteIdentifier(identifiers.slice(0, identifiers.length - 1).join('.'))
+ '.' + quoteIdentifier(identifiers[identifiers.length - 1]);
} else {
return quoteIdentifier(identifiers);
}
}
function wrapSingleQuote(identifier){
return Utils.addTicks(identifier, "'");
}
module.exports = {
get options(){
return _options;
},
set options(opt) {
_options = opt;
},
get dialect(){
return _dialect;
},
set dialect(dial) {
_dialect = dial;
},
get sequelize(){
return _sequelize;
},
set sequelize(seq) {
_sequelize = seq;
},
quoteIdentifier: function(val){
return quoteIdentifier(val);
},
quoteIdentifiers: function(val, force){
return quoteIdentifiers(val, force);
},
escape: function(value, field) {
return escape(value,field);
},
quoteTable: function(param, as) {
var table = '';
if (as === true) {
as = param.as || param.name || param;
}
if (_.isObject(param)) {
if (param.schema) {
table += param.schema + (param.delimiter || '.');
}
table += param.tableName;
table = quoteIdentifier(table);
} else {
table = quoteIdentifier(param);
}
if (as) {
table += ' AS ' + quoteIdentifier(as);
}
return table;
},
renameColumnSql: function(tableName, attrBefore, newColumnName){
var query = 'EXEC SP_RENAME \'<%= tableName %>.<%= before %>\', \'<%= after %>\';';
var attrString = [];
var values = {
tableName: tableName
, before: attrBefore
, after: newColumnName
};
return Utils._.template(query)(values);
},
attributeToSQL: function(attribute, options) {
if (!Utils._.isPlainObject(attribute)) {
attribute = {
type: attribute
};
}
var template = [];
//special enum query
if (attribute.type.toString() === DataTypes.ENUM.toString()) {
template.push('VARCHAR(10)');
if(attribute.allowNull === false){
template.push(attributeMap.notNull);
//not nullable
}else{
template.push(attributeMap.allowNull);
}
template.push('CHECK ("'
+ attribute.field + '" IN('
+ Utils._.map(attribute.values, function(value) {
return escape(value);
}.bind(this)).join(', ') + '))');
} else {
//the everything else
if (attribute.type === 'TINYINT(1)') {
attribute.type = DataTypes.BOOLEAN;
}else if(attribute.type === 'DATETIME'){
attribute.type = DataTypes.DATE;
}else if(attribute.type.toString() === 'BLOB'){
attribute.type = DataTypes.BLOB;
}
template.push(attribute.type.toString());
//a primary key
if(attribute.primaryKey){
if (!attribute.references) {
template.push(attributeMap.primaryKey);
}else{
template.push(attributeMap.foreignKey);
}
//allow null
}else if(attribute.allowNull === false){
template.push(attributeMap.notNull);
//not nullable
}else{
template.push(attributeMap.allowNull);
}
}
//auto increment
if (attribute.autoIncrement) {
template.push(attributeMap.autoIncrement);
}
// Blobs/texts cannot have a defaultValue
if (attribute.type !== 'TEXT'
&& attribute.type._binary === false
&& Utils.defaultValueSchemable(attribute.defaultValue)) {
if(options && escape(attribute.defaultValue)){
template.push(attributeMap.default + wrapSingleQuote(attribute.defaultValue));
}
}
if (!attribute.primaryKey && attribute.unique) {
template.push(attributeMap.unique);
}
if (attribute.references) {
template.push(attributeMap.references);
template.push(this.quoteTable(attribute.references));
if (attribute.referencesKey) {
template.push('(' + quoteIdentifier(attribute.referencesKey) + ')');
} else {
template.push('(' + quoteIdentifier('id') + ')');
}
//PROBLEM WITH THIS IS MSSQL DOES NOT ALLOW MULTIPLE PER KEY
if (attribute.onDelete) {
if(attribute.onDelete.toUpperCase() !== 'RESTRICT'){
template.push(attributeMap.onDelete);
template.push(attribute.onDelete.toUpperCase());
}
}
if (attribute.onUpdate && !attribute.onDelete) {
template.push(attributeMap.onUpdate);
template.push(attribute.onUpdate.toUpperCase());
}
}
return template.join(' ');
}
};
......@@ -313,9 +313,10 @@ describe(Support.getTestDialectTeaser("Model"), function () {
})
User.sync({ force: true }).on('sql', _.after(2, _.once(function(sql) {
if(dialect === 'mssql'){
expect(sql).to.contain('"username" NVARCHAR(255) NULL UNIQUE, "email" NVARCHAR(255) NULL UNIQUE, "aCol" NVARCHAR(255) NULL UNIQUE, "bCol" NVARCHAR(255) NULL UNIQUE');
}else{
if (dialect === 'mssql') {
expect(sql).to.match(/CONSTRAINT\s*(user_and_email)?\s*UNIQUE\s*\([`"]?username[`"]?, [`"]?email[`"]?\)/);
expect(sql).to.match(/CONSTRAINT\s*(a_and_b)?\s*UNIQUE\s*\([`"]?aCol[`"]?, [`"]?bCol[`"]?\)/);
} else {
expect(sql).to.match(/UNIQUE\s*(user_and_email)?\s*\([`"]?username[`"]?, [`"]?email[`"]?\)/)
expect(sql).to.match(/UNIQUE\s*(a_and_b)?\s*\([`"]?aCol[`"]?, [`"]?bCol[`"]?\)/)
}
......@@ -362,8 +363,6 @@ describe(Support.getTestDialectTeaser("Model"), function () {
, User = this.sequelize.define('UserWithUniqueUsername', {
username: { type: Sequelize.STRING, unique: { name: 'user_and_email', msg: 'User and email must be unique' }},
email: { type: Sequelize.STRING, unique: 'user_and_email' },
aCol: { type: Sequelize.STRING, unique: 'a_and_b' },
bCol: { type: Sequelize.STRING, unique: 'a_and_b' }
})
return User.sync({ force: true }).bind(this).then(function() {
......@@ -2119,8 +2118,8 @@ describe(Support.getTestDialectTeaser("Model"), function () {
} else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
} else if (dialect == 'mssql') {
expect(sql).to.match(/"authorId" INTEGER NULL REFERENCES "authors" \("id"\)/)
}else {
expect(sql).to.match(/FOREIGN KEY \("authorId"\) REFERENCES "authors" \("id"\)/)
} else {
throw new Error('Undefined dialect!')
}
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!