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

Commit a92fb9ce by Mick Hansen

Merge pull request #3837 from BridgeAR/fix-indentation

Remove unnecessary indentation
2 parents 073f1777 33ff3ad5
var Association = function() { var Association = function () {};
};
module.exports = Association; module.exports = Association;
\ No newline at end of file
...@@ -7,701 +7,699 @@ var Utils = require('./../utils') ...@@ -7,701 +7,699 @@ var Utils = require('./../utils')
, CounterCache = require('../plugins/counter-cache') , CounterCache = require('../plugins/counter-cache')
, util = require('util'); , util = require('util');
module.exports = (function() { var BelongsToMany = function(source, target, options) {
var BelongsToMany = function(source, target, options) { Association.call(this);
Association.call(this);
this.associationType = 'BelongsToMany';
this.associationType = 'BelongsToMany'; this.source = source;
this.source = source; this.target = target;
this.target = target; this.targetAssociation = null;
this.targetAssociation = null; this.options = options || {};
this.options = options || {}; this.sequelize = source.modelManager.sequelize;
this.sequelize = source.modelManager.sequelize; this.through = options.through;
this.through = options.through; this.scope = options.scope;
this.scope = options.scope; this.isMultiAssociation = true;
this.isMultiAssociation = true; this.isSelfAssociation = this.source === this.target;
this.isSelfAssociation = this.source === this.target; this.doubleLinked = false;
this.doubleLinked = false; this.as = this.options.as;
this.as = this.options.as; this.combinedTableName = Utils.combineTableNames(
this.combinedTableName = Utils.combineTableNames( this.source.tableName,
this.source.tableName, this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName );
);
if (this.through === undefined || this.through === true || this.through === null) {
if (this.through === undefined || this.through === true || this.through === null) { throw new Error('belongsToMany must be given a through option, either a string or a model');
throw new Error('belongsToMany must be given a through option, either a string or a model'); }
if (!this.through.model) {
this.through = {
model: this.through
};
}
/*
* If self association, this is the target association - Unless we find a pairing association
*/
if (this.isSelfAssociation) {
if (!this.as) {
throw new Error('\'as\' must be defined for many-to-many self-associations');
} }
if (!this.through.model) { this.targetAssociation = this;
this.through = { }
model: this.through
}; /*
* Default/generated foreign/other keys
*/
if (Utils._.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
if (!this.options.foreignKey) {
this.foreignKeyDefault = true;
} }
/* this.foreignKeyAttribute = {};
* If self association, this is the target association - Unless we find a pairing association this.foreignKey = this.options.foreignKey || Utils._.camelizeIf(
*/ [
if (this.isSelfAssociation) { Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
if (!this.as) { this.source.primaryKeyAttribute
throw new Error('\'as\' must be defined for many-to-many self-associations'); ].join('_'),
} !this.source.options.underscored
);
this.targetAssociation = this; }
if (Utils._.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
if (!this.options.otherKey) {
this.otherKeyDefault = true;
} }
/* this.otherKeyAttribute = {};
* Default/generated foreign/other keys this.otherKey = this.options.otherKey || Utils._.camelizeIf(
*/ [
if (Utils._.isObject(this.options.foreignKey)) { Utils._.underscoredIf(
this.foreignKeyAttribute = this.options.foreignKey; this.isSelfAssociation ?
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; Utils.singularize(this.as) :
} else { this.target.options.name.singular,
if (!this.options.foreignKey) { this.target.options.underscored
this.foreignKeyDefault = true; ),
} this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
);
}
this.foreignKeyAttribute = {}; /*
this.foreignKey = this.options.foreignKey || Utils._.camelizeIf( * Find paired association (if exists)
[ */
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored), _.each(this.target.associations, function(association) {
this.source.primaryKeyAttribute if (association.associationType !== 'BelongsToMany') return;
].join('_'), if (association.target !== this.source) return;
!this.source.options.underscored
);
}
if (Utils._.isObject(this.options.otherKey)) { if (this.options.through.model === association.options.through.model) {
this.otherKeyAttribute = this.options.otherKey; this.paired = association;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName; }
}, this);
if (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model,
indexes: {}, //we dont want indexes here (as referenced in #2416)
paranoid: false // A paranoid join table does not make sense
}));
} else { } else {
if (!this.options.otherKey) { this.through.model = this.sequelize.model(this.through.model);
this.otherKeyDefault = true;
}
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils._.camelizeIf(
[
Utils._.underscoredIf(
this.isSelfAssociation ?
Utils.singularize(this.as) :
this.target.options.name.singular,
this.target.options.underscored
),
this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
);
} }
}
/* if (this.paired) {
* Find paired association (if exists) if (this.otherKeyDefault) {
*/ this.otherKey = this.paired.foreignKey;
_.each(this.target.associations, function(association) {
if (association.associationType !== 'BelongsToMany') return;
if (association.target !== this.source) return;
if (this.options.through.model === association.options.through.model) {
this.paired = association;
}
}, this);
if (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model,
indexes: {}, //we dont want indexes here (as referenced in #2416)
paranoid: false // A paranoid join table does not make sense
}));
} else {
this.through.model = this.sequelize.model(this.through.model);
}
} }
if (this.paired.otherKeyDefault) {
if (this.paired) { // If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey
if (this.otherKeyDefault) { if (this.paired.otherKey !== this.foreignKey) {
this.otherKey = this.paired.foreignKey; delete this.through.model.rawAttributes[this.paired.otherKey];
}
if (this.paired.otherKeyDefault) {
// If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey
if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
}
this.paired.otherKey = this.foreignKey;
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
} }
this.paired.otherKey = this.foreignKey;
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
} }
}
if (this.through) { if (this.through) {
this.throughModel = this.through.model; this.throughModel = this.through.model;
} }
this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model); this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model);
if (this.as) { if (this.as) {
this.isAliased = true; this.isAliased = true;
if (Utils._.isPlainObject(this.as)) { if (Utils._.isPlainObject(this.as)) {
this.options.name = this.as; this.options.name = this.as;
this.as = this.as.plural; this.as = this.as.plural;
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
}
} else { } else {
this.as = this.target.options.name.plural; this.options.name = {
this.options.name = this.target.options.name; plural: this.as,
} singular: Utils.singularize(this.as)
};
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
var plural = Utils.uppercaseFirst(this.options.name.plural)
, singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
get: 'get' + plural,
set: 'set' + plural,
addMultiple: 'add' + plural,
add: 'add' + singular,
create: 'create' + singular,
remove: 'remove' + singular,
removeMultiple: 'remove' + plural,
hasSingle: 'has' + singular,
hasAll: 'has' + plural
};
if (this.options.counterCache) {
new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {});
} }
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
var plural = Utils.uppercaseFirst(this.options.name.plural)
, singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
get: 'get' + plural,
set: 'set' + plural,
addMultiple: 'add' + plural,
add: 'add' + singular,
create: 'create' + singular,
remove: 'remove' + singular,
removeMultiple: 'remove' + plural,
hasSingle: 'has' + singular,
hasAll: 'has' + plural
}; };
util.inherits(BelongsToMany, Association); if (this.options.counterCache) {
new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {});
}
};
// the id is in the target table util.inherits(BelongsToMany, Association);
// or in an extra table which connects two tables
BelongsToMany.prototype.injectAttributes = function() {
var self = this;
this.identifier = this.foreignKey; // the id is in the target table
this.foreignIdentifier = this.otherKey; // or in an extra table which connects two tables
BelongsToMany.prototype.injectAttributes = function() {
var self = this;
// remove any PKs previously defined by sequelize this.identifier = this.foreignKey;
Utils._.each(this.through.model.rawAttributes, function(attribute, attributeName) { this.foreignIdentifier = this.otherKey;
if (attribute.primaryKey === true && attribute._autoGenerated === true) {
delete self.through.model.rawAttributes[attributeName];
self.primaryKeyDeleted = true;
}
});
var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute] // remove any PKs previously defined by sequelize
, sourceKeyType = sourceKey.type Utils._.each(this.through.model.rawAttributes, function(attribute, attributeName) {
, sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute if (attribute.primaryKey === true && attribute._autoGenerated === true) {
, targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute] delete self.through.model.rawAttributes[attributeName];
, targetKeyType = targetKey.type self.primaryKeyDeleted = true;
, targetKeyField = targetKey.field || this.target.primaryKeyAttribute
, sourceAttribute = Utils._.defaults(this.foreignKeyAttribute, { type: sourceKeyType })
, targetAttribute = Utils._.defaults(this.otherKeyAttribute, { type: targetKeyType });
if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) {
var uniqueKey = [this.through.model.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey;
}
if (!this.through.model.rawAttributes[this.identifier]) {
this.through.model.rawAttributes[this.identifier] = {
_autoGenerated: true
};
}
if (!this.through.model.rawAttributes[this.foreignIdentifier]) {
this.through.model.rawAttributes[this.foreignIdentifier] = {
_autoGenerated: true
};
} }
});
var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute]
, sourceKeyType = sourceKey.type
, sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute
, targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute]
, targetKeyType = targetKey.type
, targetKeyField = targetKey.field || this.target.primaryKeyAttribute
, sourceAttribute = Utils._.defaults(this.foreignKeyAttribute, { type: sourceKeyType })
, targetAttribute = Utils._.defaults(this.otherKeyAttribute, { type: targetKeyType });
if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) {
var uniqueKey = [this.through.model.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey;
}
if (!this.through.model.rawAttributes[this.identifier]) {
this.through.model.rawAttributes[this.identifier] = {
_autoGenerated: true
};
}
if (this.options.constraints !== false) { if (!this.through.model.rawAttributes[this.foreignIdentifier]) {
sourceAttribute.references = { this.through.model.rawAttributes[this.foreignIdentifier] = {
model: this.source.getTableName(), _autoGenerated: true
key: sourceKeyField };
}; }
// For the source attribute the passed option is the priority
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.identifier].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.identifier].onUpdate;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE'; if (this.options.constraints !== false) {
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE'; sourceAttribute.references = {
model: this.source.getTableName(),
key: sourceKeyField
};
// For the source attribute the passed option is the priority
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.identifier].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.identifier].onUpdate;
targetAttribute.references = { if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
model: this.target.getTableName(), if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
key: targetKeyField
};
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
targetAttribute.onDelete = this.through.model.rawAttributes[this.foreignIdentifier].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.foreignIdentifier].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE'; targetAttribute.references = {
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE'; model: this.target.getTableName(),
} key: targetKeyField
};
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
targetAttribute.onDelete = this.through.model.rawAttributes[this.foreignIdentifier].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.foreignIdentifier].onUpdate || this.options.onUpdate;
this.through.model.rawAttributes[this.identifier] = Utils._.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute); if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
this.through.model.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute); if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
}
this.identifierField = this.through.model.rawAttributes[this.identifier].field || this.identifier; this.through.model.rawAttributes[this.identifier] = Utils._.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute);
this.foreignIdentifierField = this.through.model.rawAttributes[this.foreignIdentifier].field || this.foreignIdentifier; this.through.model.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
if (this.paired && !this.paired.foreignIdentifierField) { this.identifierField = this.through.model.rawAttributes[this.identifier].field || this.identifier;
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.foreignIdentifier].field || this.paired.foreignIdentifier; this.foreignIdentifierField = this.through.model.rawAttributes[this.foreignIdentifier].field || this.foreignIdentifier;
}
this.through.model.init(this.through.model.modelManager); if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.foreignIdentifier].field || this.paired.foreignIdentifier;
}
Helpers.checkNamingCollision(this); this.through.model.init(this.through.model.modelManager);
return this; Helpers.checkNamingCollision(this);
};
BelongsToMany.prototype.injectGetter = function(obj) { return this;
var association = this; };
obj[this.accessors.get] = function(options) { BelongsToMany.prototype.injectGetter = function(obj) {
options = association.target.__optClone(options) || {}; var association = this;
var instance = this obj[this.accessors.get] = function(options) {
, through = association.through options = association.target.__optClone(options) || {};
, scopeWhere
, throughWhere;
if (association.scope) { var instance = this
scopeWhere = _.clone(association.scope); , through = association.through
} , scopeWhere
, throughWhere;
options.where = { if (association.scope) {
$and: [ scopeWhere = _.clone(association.scope);
scopeWhere, }
options.where
]
};
if (Object(through.model) === through.model) { options.where = {
throughWhere = {}; $and: [
throughWhere[association.identifier] = instance.get(association.source.primaryKeyAttribute); scopeWhere,
options.where
]
};
if (through && through.scope) { if (Object(through.model) === through.model) {
Object.keys(through.scope).forEach(function (attribute) { throughWhere = {};
throughWhere[attribute] = through.scope[attribute]; throughWhere[association.identifier] = instance.get(association.source.primaryKeyAttribute);
}.bind(this));
}
options.include = options.include || []; if (through && through.scope) {
options.include.push({ Object.keys(through.scope).forEach(function (attribute) {
model: through.model, throughWhere[attribute] = through.scope[attribute];
as: through.model.name, }.bind(this));
attributes: options.joinTableAttributes,
association: {
isSingleAssociation: true,
source: association.target,
target: association.source,
identifier: association.foreignIdentifier,
identifierField: association.foreignIdentifierField
},
required: true,
where: throughWhere,
_pseudo: true
});
} }
var model = association.target; options.include = options.include || [];
if (options.hasOwnProperty('scope')) { options.include.push({
if (!options.scope) { model: through.model,
model = model.unscoped(); as: through.model.name,
} else { attributes: options.joinTableAttributes,
model = model.scope(options.scope); association: {
} isSingleAssociation: true,
} source: association.target,
target: association.source,
identifier: association.foreignIdentifier,
identifierField: association.foreignIdentifierField
},
required: true,
where: throughWhere,
_pseudo: true
});
}
return model.findAll(options); var model = association.target;
}; if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) { return model.findAll(options);
var where = {}; };
if (!Array.isArray(instances)) { obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
instances = [instances]; var where = {};
}
options = options || {}; if (!Array.isArray(instances)) {
options.scope = false; instances = [instances];
}
_.defaults(options, { options = options || {};
raw: true options.scope = false;
});
where.$or = instances.map(function (instance) { _.defaults(options, {
if (instance instanceof association.target.Instance) { raw: true
return instance.where(); });
} else {
var $where = {};
$where[association.target.primaryKeyAttribute] = instance;
return $where;
}
});
options.where = { where.$or = instances.map(function (instance) {
$and: [ if (instance instanceof association.target.Instance) {
where, return instance.where();
options.where } else {
] var $where = {};
}; $where[association.target.primaryKeyAttribute] = instance;
return $where;
}
});
return this[association.accessors.get](options).then(function(associatedObjects) { options.where = {
return associatedObjects.length === instances.length; $and: [
}); where,
options.where
]
}; };
return this; return this[association.accessors.get](options).then(function(associatedObjects) {
return associatedObjects.length === instances.length;
});
}; };
BelongsToMany.prototype.injectSetter = function(obj) { return this;
var association = this };
, primaryKeyAttribute = association.target.primaryKeyAttribute;
BelongsToMany.prototype.injectSetter = function(obj) {
obj[this.accessors.set] = function(newAssociatedObjects, options) { var association = this
options = options || {}; , primaryKeyAttribute = association.target.primaryKeyAttribute;
var instance = this;
obj[this.accessors.set] = function(newAssociatedObjects, options) {
return instance[association.accessors.get]({ options = options || {};
scope: false, var instance = this;
transaction: options.transaction,
logging: options.logging return instance[association.accessors.get]({
}).then(function(oldAssociatedObjects) { scope: false,
var foreignIdentifier = association.foreignIdentifier transaction: options.transaction,
, sourceKeys = Object.keys(association.source.primaryKeys) logging: options.logging
, targetKeys = Object.keys(association.target.primaryKeys) }).then(function(oldAssociatedObjects) {
, obsoleteAssociations = [] var foreignIdentifier = association.foreignIdentifier
, changedAssociations = [] , sourceKeys = Object.keys(association.source.primaryKeys)
, defaultAttributes = options , targetKeys = Object.keys(association.target.primaryKeys)
, promises = [] , obsoleteAssociations = []
, unassociatedObjects; , changedAssociations = []
, defaultAttributes = options
if (newAssociatedObjects === null) { , promises = []
newAssociatedObjects = []; , unassociatedObjects;
} else {
if (!Array.isArray(newAssociatedObjects)) { if (newAssociatedObjects === null) {
newAssociatedObjects = [newAssociatedObjects]; newAssociatedObjects = [];
} } else {
newAssociatedObjects = newAssociatedObjects.map(function(newAssociatedObject) { if (!Array.isArray(newAssociatedObjects)) {
if (!(newAssociatedObject instanceof association.target.Instance)) { newAssociatedObjects = [newAssociatedObjects];
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newAssociatedObject;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newAssociatedObject;
});
} }
newAssociatedObjects = newAssociatedObjects.map(function(newAssociatedObject) {
if (!(newAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newAssociatedObject;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newAssociatedObject;
});
}
if (options.remove) { if (options.remove) {
oldAssociatedObjects = newAssociatedObjects; oldAssociatedObjects = newAssociatedObjects;
newAssociatedObjects = []; newAssociatedObjects = [];
} }
// Don't try to insert the transaction as an attribute in the through table // Don't try to insert the transaction as an attribute in the through table
defaultAttributes = Utils._.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']); defaultAttributes = Utils._.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
unassociatedObjects = newAssociatedObjects.filter(function(obj) { unassociatedObjects = newAssociatedObjects.filter(function(obj) {
return !Utils._.find(oldAssociatedObjects, function(old) { return !Utils._.find(oldAssociatedObjects, function(old) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)); return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
});
}); });
});
oldAssociatedObjects.forEach(function(old) { oldAssociatedObjects.forEach(function(old) {
var newObj = Utils._.find(newAssociatedObjects, function(obj) { var newObj = Utils._.find(newAssociatedObjects, function(obj) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)); return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
}); });
if (!newObj) { if (!newObj) {
obsoleteAssociations.push(old); obsoleteAssociations.push(old);
} else { } else {
var throughAttributes = newObj[association.through.model.name]; var throughAttributes = newObj[association.through.model.name];
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object) // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model.Instance) { if (throughAttributes instanceof association.through.model.Instance) {
throughAttributes = {}; throughAttributes = {};
}
var changedAssociation = {
where: {},
attributes: Utils._.defaults({}, throughAttributes, defaultAttributes)
};
changedAssociation.where[association.identifier] = instance[sourceKeys[0]] || instance.id;
changedAssociation.where[foreignIdentifier] = newObj[targetKeys[0]] || newObj.id;
if (Object.keys(changedAssociation.attributes).length) {
changedAssociations.push(changedAssociation);
}
} }
});
if (obsoleteAssociations.length > 0) { var changedAssociation = {
var foreignIds = obsoleteAssociations.map(function(associatedObject) { where: {},
return ((targetKeys.length === 1) ? associatedObject[targetKeys[0]] : associatedObject.id); attributes: Utils._.defaults({}, throughAttributes, defaultAttributes)
}); };
var where = {}; changedAssociation.where[association.identifier] = instance[sourceKeys[0]] || instance.id;
where[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id); changedAssociation.where[foreignIdentifier] = newObj[targetKeys[0]] || newObj.id;
where[foreignIdentifier] = foreignIds;
promises.push(association.through.model.destroy(Utils._.extend(options, { if (Object.keys(changedAssociation.attributes).length) {
where: where changedAssociations.push(changedAssociation);
}))); }
} }
});
if (unassociatedObjects.length > 0) { if (obsoleteAssociations.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) { var foreignIds = obsoleteAssociations.map(function(associatedObject) {
var attributes = {}; return ((targetKeys.length === 1) ? associatedObject[targetKeys[0]] : associatedObject.id);
});
attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id); var where = {};
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id); where[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
where[foreignIdentifier] = foreignIds;
attributes = Utils._.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes); promises.push(association.through.model.destroy(Utils._.extend(options, {
where: where
})));
}
if (association.through.scope) { if (unassociatedObjects.length > 0) {
Object.keys(association.through.scope).forEach(function (attribute) { var bulk = unassociatedObjects.map(function(unassociatedObject) {
attributes[attribute] = association.through.scope[attribute]; var attributes = {};
});
}
return attributes; attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
}.bind(this)); attributes[foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id);
promises.push(association.through.model.bulkCreate(bulk, options)); attributes = Utils._.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);
}
if (changedAssociations.length > 0) { if (association.through.scope) {
changedAssociations.forEach(function(assoc) { Object.keys(association.through.scope).forEach(function (attribute) {
promises.push(association.through.model.update(assoc.attributes, Utils._.extend(options, { attributes[attribute] = association.through.scope[attribute];
where: assoc.where });
}))); }
});
}
return Utils.Promise.all(promises); return attributes;
}); }.bind(this));
};
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstance, additionalAttributes) { promises.push(association.through.model.bulkCreate(bulk, options));
// If newInstance is null or undefined, no-op }
if (!newInstance) return Utils.Promise.resolve();
if (association.through && association.through.scope) { if (changedAssociations.length > 0) {
_.assign(additionalAttributes, association.through.scope); changedAssociations.forEach(function(assoc) {
promises.push(association.through.model.update(assoc.attributes, Utils._.extend(options, {
where: assoc.where
})));
});
} }
var instance = this return Utils.Promise.all(promises);
, primaryKeyAttribute = association.target.primaryKeyAttribute });
, options = additionalAttributes = additionalAttributes || {}; };
if (Array.isArray(newInstance)) { obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstance, additionalAttributes) {
var newInstances = newInstance.map(function(newInstance) { // If newInstance is null or undefined, no-op
if (!(newInstance instanceof association.target.Instance)) { if (!newInstance) return Utils.Promise.resolve();
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
return association.target.build(tmpInstance, {
isNewRecord: false
});
}
return newInstance;
});
var foreignIdentifier = association.foreignIdentifier if (association.through && association.through.scope) {
, sourceKeys = Object.keys(association.source.primaryKeys) _.assign(additionalAttributes, association.through.scope);
, targetKeys = Object.keys(association.target.primaryKeys) }
, obsoleteAssociations = []
, changedAssociations = []
, defaultAttributes = additionalAttributes
, promises = []
, oldAssociations = []
, unassociatedObjects;
// Don't try to insert the transaction as an attribute in the through table var instance = this
defaultAttributes = Utils._.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']); , primaryKeyAttribute = association.target.primaryKeyAttribute
, options = additionalAttributes = additionalAttributes || {};
unassociatedObjects = newInstances.filter(function(obj) { if (Array.isArray(newInstance)) {
return !Utils._.find(oldAssociations, function(old) { var newInstances = newInstance.map(function(newInstance) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)); if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
return association.target.build(tmpInstance, {
isNewRecord: false
}); });
}
return newInstance;
});
var foreignIdentifier = association.foreignIdentifier
, sourceKeys = Object.keys(association.source.primaryKeys)
, targetKeys = Object.keys(association.target.primaryKeys)
, obsoleteAssociations = []
, changedAssociations = []
, defaultAttributes = additionalAttributes
, promises = []
, oldAssociations = []
, unassociatedObjects;
// Don't try to insert the transaction as an attribute in the through table
defaultAttributes = Utils._.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
unassociatedObjects = newInstances.filter(function(obj) {
return !Utils._.find(oldAssociations, function(old) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
}); });
});
oldAssociations.forEach(function(old) { oldAssociations.forEach(function(old) {
var newObj = Utils._.find(newInstances, function(obj) { var newObj = Utils._.find(newInstances, function(obj) {
return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id)); return (!!obj[foreignIdentifier] ? obj[foreignIdentifier] === old[foreignIdentifier] : (!!obj[targetKeys[0]] ? obj[targetKeys[0]] === old[targetKeys[0]] : obj.id === old.id));
}); });
if (!newObj) { if (!newObj) {
obsoleteAssociations.push(old); obsoleteAssociations.push(old);
} else if (Object(association.through.model) === association.through.model) { } else if (Object(association.through.model) === association.through.model) {
var throughAttributes = newObj[association.through.model.name]; var throughAttributes = newObj[association.through.model.name];
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object) // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model.Instance) { if (throughAttributes instanceof association.through.model.Instance) {
throughAttributes = {}; throughAttributes = {};
}
var changedAssociation = {
where: {},
attributes: Utils._.defaults({}, throughAttributes, defaultAttributes)
};
changedAssociation.where[association.identifier] = instance[sourceKeys[0]] || instance.id;
changedAssociation.where[foreignIdentifier] = newObj[targetKeys[0]] || newObj.id;
if (Object.keys(changedAssociation.attributes).length) {
changedAssociations.push(changedAssociation);
}
} }
});
if (obsoleteAssociations.length > 0) { var changedAssociation = {
var foreignIds = obsoleteAssociations.map(function(associatedObject) { where: {},
return ((targetKeys.length === 1) ? associatedObject[targetKeys[0]] : associatedObject.id); attributes: Utils._.defaults({}, throughAttributes, defaultAttributes)
}); };
var where = {}; changedAssociation.where[association.identifier] = instance[sourceKeys[0]] || instance.id;
where[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id); changedAssociation.where[foreignIdentifier] = newObj[targetKeys[0]] || newObj.id;
where[association.foreignIdentifier] = foreignIds;
promises.push(association.through.model.destroy(Utils._.extend(options, { if (Object.keys(changedAssociation.attributes).length) {
where: where changedAssociations.push(changedAssociation);
}))); }
} }
});
if (unassociatedObjects.length > 0) { if (obsoleteAssociations.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) { var foreignIds = obsoleteAssociations.map(function(associatedObject) {
var attributes = {}; return ((targetKeys.length === 1) ? associatedObject[targetKeys[0]] : associatedObject.id);
});
attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id); var where = {};
attributes[association.foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id); where[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
where[association.foreignIdentifier] = foreignIds;
if (Object(association.through.model) === association.through.model) { promises.push(association.through.model.destroy(Utils._.extend(options, {
attributes = Utils._.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes); where: where
} })));
}
if (association.through.scope) { if (unassociatedObjects.length > 0) {
_.assign(attributes, association.through.scope); var bulk = unassociatedObjects.map(function(unassociatedObject) {
} var attributes = {};
return attributes; attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
}.bind(this)); attributes[association.foreignIdentifier] = ((targetKeys.length === 1) ? unassociatedObject[targetKeys[0]] : unassociatedObject.id);
promises.push(association.through.model.bulkCreate(bulk, options)); if (Object(association.through.model) === association.through.model) {
} attributes = Utils._.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);
}
if (changedAssociations.length > 0) { if (association.through.scope) {
changedAssociations.forEach(function(assoc) { _.assign(attributes, association.through.scope);
promises.push(association.through.model.update(assoc.attributes, Utils._.extend(options, { }
where: assoc.where
})));
});
}
return Utils.Promise.all(promises); return attributes;
} else { }.bind(this));
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
newInstance = association.target.build(tmpInstance, {
isNewRecord: false
});
}
return instance[association.accessors.get]({ promises.push(association.through.model.bulkCreate(bulk, options));
scope: false, }
where: newInstance.where(),
transaction: (additionalAttributes || {}).transaction,
logging: options.logging
}).then(function(currentAssociatedObjects) {
var attributes = {} if (changedAssociations.length > 0) {
, foreignIdentifier = association.foreignIdentifier; changedAssociations.forEach(function(assoc) {
promises.push(association.through.model.update(assoc.attributes, Utils._.extend(options, {
where: assoc.where
})));
});
}
return Utils.Promise.all(promises);
} else {
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
newInstance = association.target.build(tmpInstance, {
isNewRecord: false
});
}
return instance[association.accessors.get]({
scope: false,
where: newInstance.where(),
transaction: (additionalAttributes || {}).transaction,
logging: options.logging
}).then(function(currentAssociatedObjects) {
var sourceKeys = Object.keys(association.source.primaryKeys); var attributes = {}
var targetKeys = Object.keys(association.target.primaryKeys); , foreignIdentifier = association.foreignIdentifier;
// Don't try to insert the transaction as an attribute in the through table var sourceKeys = Object.keys(association.source.primaryKeys);
additionalAttributes = Utils._.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']); var targetKeys = Object.keys(association.target.primaryKeys);
attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id); // Don't try to insert the transaction as an attribute in the through table
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newInstance[targetKeys[0]] : newInstance.id); additionalAttributes = Utils._.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
if (!!currentAssociatedObjects.length) {
var where = attributes;
attributes = Utils._.defaults({}, newInstance[association.through.model.name], additionalAttributes);
if (Object.keys(attributes).length) {
return association.through.model.update(attributes, Utils._.extend(options, {
where: where
}));
} else {
return Utils.Promise.resolve();
}
} else {
attributes = Utils._.defaults(attributes, newInstance[association.through.model.name], additionalAttributes);
if (association.through.scope) {
_.assign(attributes, association.through.scope);
}
return association.through.model.create(attributes, options); attributes[association.identifier] = ((sourceKeys.length === 1) ? instance[sourceKeys[0]] : instance.id);
attributes[foreignIdentifier] = ((targetKeys.length === 1) ? newInstance[targetKeys[0]] : newInstance.id);
if (!!currentAssociatedObjects.length) {
var where = attributes;
attributes = Utils._.defaults({}, newInstance[association.through.model.name], additionalAttributes);
if (Object.keys(attributes).length) {
return association.through.model.update(attributes, Utils._.extend(options, {
where: where
}));
} else {
return Utils.Promise.resolve();
}
} else {
attributes = Utils._.defaults(attributes, newInstance[association.through.model.name], additionalAttributes);
if (association.through.scope) {
_.assign(attributes, association.through.scope);
} }
});
}
};
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObject, options) { return association.through.model.create(attributes, options);
options = options || {}; }
options.remove = true; });
return this[association.accessors.set](oldAssociatedObject, options); }
}; };
return this; obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObject, options) {
options = options || {};
options.remove = true;
return this[association.accessors.set](oldAssociatedObject, options);
}; };
BelongsToMany.prototype.injectCreator = function(obj) { return this;
var association = this; };
obj[this.accessors.create] = function(values, options) { BelongsToMany.prototype.injectCreator = function(obj) {
var instance = this; var association = this;
options = options || {};
if (Array.isArray(options)) { obj[this.accessors.create] = function(values, options) {
options = { var instance = this;
fields: options options = options || {};
};
}
if (values === undefined) { if (Array.isArray(options)) {
values = {}; options = {
} fields: options
};
}
if (association.scope) { if (values === undefined) {
Object.keys(association.scope).forEach(function (attribute) { values = {};
values[attribute] = association.scope[attribute]; }
if (options.fields) options.fields.push(attribute);
});
}
// Create the related model instance if (association.scope) {
return association.target.create(values, options).then(function(newAssociatedObject) { Object.keys(association.scope).forEach(function (attribute) {
return instance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject); values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
}); });
}; }
return this; // Create the related model instance
return association.target.create(values, options).then(function(newAssociatedObject) {
return instance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject);
});
}; };
return BelongsToMany; return this;
})(); };
module.exports = BelongsToMany;
...@@ -6,176 +6,174 @@ var Utils = require('./../utils') ...@@ -6,176 +6,174 @@ var Utils = require('./../utils')
, Association = require('./base') , Association = require('./base')
, util = require('util'); , util = require('util');
module.exports = (function() { var BelongsTo = function(source, target, options) {
var BelongsTo = function(source, target, options) { Association.call(this);
Association.call(this);
this.associationType = 'BelongsTo';
this.associationType = 'BelongsTo'; this.source = source;
this.source = source; this.target = target;
this.target = target; this.options = options;
this.options = options; this.scope = options.scope;
this.scope = options.scope; this.isSingleAssociation = true;
this.isSingleAssociation = true; this.isSelfAssociation = (this.source === this.target);
this.isSelfAssociation = (this.source === this.target); this.as = this.options.as;
this.as = this.options.as;
if (Utils._.isObject(this.options.foreignKey)) {
if (Utils._.isObject(this.options.foreignKey)) { this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; } else {
} else { this.foreignKeyAttribute = {};
this.foreignKeyAttribute = {}; this.foreignKey = this.options.foreignKey;
this.foreignKey = this.options.foreignKey; }
}
if (this.as) {
if (this.as) { this.isAliased = true;
this.isAliased = true; this.options.name = {
this.options.name = { singular: this.as
singular: this.as };
}; } else {
} else { this.as = this.target.options.name.singular;
this.as = this.target.options.name.singular; this.options.name = this.target.options.name;
this.options.name = this.target.options.name; }
}
if (!this.options.foreignKey) {
this.options.foreignKey = Utils._.camelizeIf(
[
Utils._.underscoredIf(this.as, this.source.options.underscored),
this.target.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
this.identifier = this.foreignKey || Utils._.camelizeIf( if (!this.options.foreignKey) {
this.options.foreignKey = Utils._.camelizeIf(
[ [
Utils._.underscoredIf(this.options.name.singular, this.target.options.underscored), Utils._.underscoredIf(this.as, this.source.options.underscored),
this.target.primaryKeyAttribute this.target.primaryKeyAttribute
].join('_'), ].join('_'),
!this.target.options.underscored !this.source.options.underscored
); );
}
this.targetIdentifier = this.target.primaryKeyAttribute;
this.associationAccessor = this.as; this.identifier = this.foreignKey || Utils._.camelizeIf(
this.options.useHooks = options.useHooks; [
Utils._.underscoredIf(this.options.name.singular, this.target.options.underscored),
// Get singular name, trying to uppercase the first letter, unless the model forbids it this.target.primaryKeyAttribute
var singular = Utils.uppercaseFirst(this.options.name.singular); ].join('_'),
!this.target.options.underscored
this.accessors = { );
get: 'get' + singular,
set: 'set' + singular, this.targetIdentifier = this.target.primaryKeyAttribute;
create: 'create' + singular this.associationAccessor = this.as;
}; this.options.useHooks = options.useHooks;
// Get singular name, trying to uppercase the first letter, unless the model forbids it
var singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
get: 'get' + singular,
set: 'set' + singular,
create: 'create' + singular
}; };
};
util.inherits(BelongsTo, Association); util.inherits(BelongsTo, Association);
// the id is in the source table // the id is in the source table
BelongsTo.prototype.injectAttributes = function() { BelongsTo.prototype.injectAttributes = function() {
var newAttributes = {}; var newAttributes = {};
newAttributes[this.identifier] = Utils._.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.target.rawAttributes[this.targetIdentifier].type }); newAttributes[this.identifier] = Utils._.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.target.rawAttributes[this.targetIdentifier].type });
if (this.options.constraints !== false) { if (this.options.constraints !== false) {
this.options.onDelete = this.options.onDelete || 'SET NULL'; this.options.onDelete = this.options.onDelete || 'SET NULL';
this.options.onUpdate = this.options.onUpdate || 'CASCADE'; this.options.onUpdate = this.options.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.target, this.source, this.options); Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.target, this.source, this.options);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes); Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier; this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
this.source.refreshAttributes(); this.source.refreshAttributes();
Helpers.checkNamingCollision(this); Helpers.checkNamingCollision(this);
return this; return this;
}; };
// Add getAssociation method to the prototype of the model instance // Add getAssociation method to the prototype of the model instance
BelongsTo.prototype.injectGetter = function(instancePrototype) { BelongsTo.prototype.injectGetter = function(instancePrototype) {
var association = this; var association = this;
instancePrototype[this.accessors.get] = function(options) { instancePrototype[this.accessors.get] = function(options) {
var where = {}; var where = {};
where[association.targetIdentifier] = this.get(association.identifier); where[association.targetIdentifier] = this.get(association.identifier);
options = association.target.__optClone(options) || {}; options = association.target.__optClone(options) || {};
options.where = { options.where = {
$and: [ $and: [
options.where, options.where,
where where
] ]
}; };
if (options.limit === undefined) options.limit = null; if (options.limit === undefined) options.limit = null;
var model = association.target; var model = association.target;
if (options.hasOwnProperty('scope')) { if (options.hasOwnProperty('scope')) {
if (!options.scope) { if (!options.scope) {
model = model.unscoped(); model = model.unscoped();
} else { } else {
model = model.scope(options.scope); model = model.scope(options.scope);
}
} }
}
return model.find(options); return model.find(options);
};
return this;
}; };
// Add setAssociaton method to the prototype of the model instance return this;
BelongsTo.prototype.injectSetter = function(instancePrototype) { };
var association = this;
instancePrototype[this.accessors.set] = function(associatedInstance, options) { // Add setAssociaton method to the prototype of the model instance
options = options || {}; BelongsTo.prototype.injectSetter = function(instancePrototype) {
var association = this;
var value = associatedInstance; instancePrototype[this.accessors.set] = function(associatedInstance, options) {
if (associatedInstance instanceof association.target.Instance) { options = options || {};
value = associatedInstance[association.targetIdentifier];
}
this.set(association.identifier, value); var value = associatedInstance;
if (associatedInstance instanceof association.target.Instance) {
value = associatedInstance[association.targetIdentifier];
}
if (options.save === false) return; this.set(association.identifier, value);
options = Utils._.extend({ if (options.save === false) return;
fields: [association.identifier],
allowNull: [association.identifier],
association: true
}, options);
options = Utils._.extend({
fields: [association.identifier],
allowNull: [association.identifier],
association: true
}, options);
// passes the changed field to save, so only that field get updated.
return this.save(options);
};
return this; // passes the changed field to save, so only that field get updated.
return this.save(options);
}; };
// Add createAssociation method to the prototype of the model instance return this;
BelongsTo.prototype.injectCreator = function(instancePrototype) { };
var association = this;
instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) { // Add createAssociation method to the prototype of the model instance
var instance = this BelongsTo.prototype.injectCreator = function(instancePrototype) {
, options = {}; var association = this;
if ((fieldsOrOptions || {}).transaction instanceof Transaction) { instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) {
options.transaction = fieldsOrOptions.transaction; var instance = this
} , options = {};
options.logging = (fieldsOrOptions || {}).logging;
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) { if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
return instance[association.accessors.set](newAssociatedObject, options); options.transaction = fieldsOrOptions.transaction;
}); }
}; options.logging = (fieldsOrOptions || {}).logging;
return this; return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[association.accessors.set](newAssociatedObject, options);
});
}; };
return BelongsTo; return this;
})(); };
module.exports = BelongsTo;
...@@ -3,135 +3,133 @@ ...@@ -3,135 +3,133 @@
var Utils = require('./../utils') var Utils = require('./../utils')
, _ = require('lodash'); , _ = require('lodash');
module.exports = (function() { var HasManySingleLinked = function(association, instance) {
var HasManySingleLinked = function(association, instance) { this.association = association;
this.association = association; this.instance = instance;
this.instance = instance; this.target = this.association.target;
this.target = this.association.target; this.source = this.association.source;
this.source = this.association.source; };
HasManySingleLinked.prototype.injectGetter = function(options) {
var scopeWhere = this.association.scope ? {} : null;
if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) {
scopeWhere[attribute] = this.association.scope[attribute];
}.bind(this));
}
options.where = {
$and: [
new Utils.where(
this.target.rawAttributes[this.association.identifier],
this.instance[this.source.primaryKeyAttribute]
),
scopeWhere,
options.where
]
}; };
HasManySingleLinked.prototype.injectGetter = function(options) { var model = this.association.target;
var scopeWhere = this.association.scope ? {} : null; if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return model.all(options);
};
HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
var self = this
, primaryKeys
, primaryKey
, updateWhere
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {Model: {primaryKeys: {}}}).Model.primaryKeys || {})
, associationKey = (associationKeys.length === 1) ? associationKeys[0] : 'id'
, options = defaultAttributes
, promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !Utils._.find(newAssociations, function(obj) {
return obj[associationKey] === old[associationKey];
});
})
, unassociatedObjects = newAssociations.filter(function(obj) {
return !Utils._.find(oldAssociations, function(old) {
return obj[associationKey] === old[associationKey];
});
})
, update;
if (obsoleteAssociations.length > 0) {
// clear the old associations
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
associatedObject[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance.id);
return associatedObject[associationKey];
});
update = {};
update[self.association.identifier] = null;
primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {};
updateWhere[primaryKey] = obsoleteIds;
promises.push(this.association.target.unscoped().update(
update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
where: updateWhere
})
));
}
if (unassociatedObjects.length > 0) {
// For the self.instance
var pkeys = Object.keys(self.instance.Model.primaryKeys)
, pkey = pkeys.length === 1 ? pkeys[0] : 'id';
primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {};
// set the new associations
var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
associatedObject[self.association.identifier] = self.instance[pkey] || self.instance.id;
return associatedObject[associationKey];
});
update = {};
update[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id);
if (this.association.scope) { if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) { _.assign(update, this.association.scope);
scopeWhere[attribute] = this.association.scope[attribute];
}.bind(this));
} }
options.where = { updateWhere[primaryKey] = unassociatedIds;
$and: [
new Utils.where(
this.target.rawAttributes[this.association.identifier],
this.instance[this.source.primaryKeyAttribute]
),
scopeWhere,
options.where
]
};
var model = this.association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
return model.all(options); promises.push(this.association.target.unscoped().update(
}; update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
where: updateWhere
})
));
}
HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) { return Utils.Promise.all(promises);
var self = this };
, primaryKeys
, primaryKey
, updateWhere
, associationKeys = Object.keys((oldAssociations[0] || newAssociations[0] || {Model: {primaryKeys: {}}}).Model.primaryKeys || {})
, associationKey = (associationKeys.length === 1) ? associationKeys[0] : 'id'
, options = defaultAttributes
, promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !Utils._.find(newAssociations, function(obj) {
return obj[associationKey] === old[associationKey];
});
})
, unassociatedObjects = newAssociations.filter(function(obj) {
return !Utils._.find(oldAssociations, function(old) {
return obj[associationKey] === old[associationKey];
});
})
, update;
if (obsoleteAssociations.length > 0) {
// clear the old associations
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
associatedObject[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance.id);
return associatedObject[associationKey];
});
update = {};
update[self.association.identifier] = null;
primaryKeys = Object.keys(this.association.target.primaryKeys);
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id';
updateWhere = {};
updateWhere[primaryKey] = obsoleteIds;
promises.push(this.association.target.unscoped().update(
update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
where: updateWhere
})
));
}
if (unassociatedObjects.length > 0) { HasManySingleLinked.prototype.injectAdder = function(newAssociation, options) {
// For the self.instance newAssociation.set(this.association.identifier, this.instance.get(this.instance.Model.primaryKeyAttribute));
var pkeys = Object.keys(self.instance.Model.primaryKeys) if (this.association.scope) {
, pkey = pkeys.length === 1 ? pkeys[0] : 'id'; Object.keys(this.association.scope).forEach(function (attribute) {
newAssociation.set(attribute, this.association.scope[attribute]);
primaryKeys = Object.keys(this.association.target.primaryKeys); }.bind(this));
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'; }
updateWhere = {};
// set the new associations
var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
associatedObject[self.association.identifier] = self.instance[pkey] || self.instance.id;
return associatedObject[associationKey];
});
update = {};
update[self.association.identifier] = (newAssociations.length < 1 ? null : self.instance[pkey] || self.instance.id);
if (this.association.scope) {
_.assign(update, this.association.scope);
}
updateWhere[primaryKey] = unassociatedIds;
promises.push(this.association.target.unscoped().update(
update,
Utils._.extend(options, {
allowNull: [self.association.identifier],
where: updateWhere
})
));
}
return Utils.Promise.all(promises); return newAssociation.save(options);
}; };
HasManySingleLinked.prototype.injectAdder = function(newAssociation, options) {
newAssociation.set(this.association.identifier, this.instance.get(this.instance.Model.primaryKeyAttribute));
if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) {
newAssociation.set(attribute, this.association.scope[attribute]);
}.bind(this));
}
return newAssociation.save(options);
};
return HasManySingleLinked; module.exports = HasManySingleLinked;
})();
...@@ -8,282 +8,308 @@ var Utils = require('./../utils') ...@@ -8,282 +8,308 @@ var Utils = require('./../utils')
, util = require('util') , util = require('util')
, HasManySingleLinked = require('./has-many-single-linked'); , HasManySingleLinked = require('./has-many-single-linked');
module.exports = (function() { var HasMany = function(source, target, options) {
var HasMany = function(source, target, options) { Association.call(this);
Association.call(this);
this.associationType = 'HasMany';
this.associationType = 'HasMany'; this.source = source;
this.source = source; this.target = target;
this.target = target; this.targetAssociation = null;
this.targetAssociation = null; this.options = options || {};
this.options = options || {}; this.sequelize = source.modelManager.sequelize;
this.sequelize = source.modelManager.sequelize; this.through = options.through;
this.through = options.through; this.scope = options.scope;
this.scope = options.scope; this.isMultiAssociation = true;
this.isMultiAssociation = true; this.isSelfAssociation = this.source === this.target;
this.isSelfAssociation = this.source === this.target; this.as = this.options.as;
this.as = this.options.as;
if (this.options.through) {
if (this.options.through) { throw new Error('N:M associations are not supported with hasMany. Use belongsToMany instead');
throw new Error('N:M associations are not supported with hasMany. Use belongsToMany instead'); }
}
if (Utils._.isObject(this.options.foreignKey)) {
if (Utils._.isObject(this.options.foreignKey)) { this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; } else {
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey;
}
/*
* If self association, this is the target association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
if (this.as) {
this.isAliased = true;
if (Utils._.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else { } else {
this.foreignKeyAttribute = {}; this.options.name = {
this.foreignKey = this.options.foreignKey; plural: this.as,
singular: Utils.singularize(this.as)
};
} }
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
var plural = Utils.uppercaseFirst(this.options.name.plural)
, singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
get: 'get' + plural,
set: 'set' + plural,
addMultiple: 'add' + plural,
add: 'add' + singular,
create: 'create' + singular,
remove: 'remove' + singular,
removeMultiple: 'remove' + plural,
hasSingle: 'has' + singular,
hasAll: 'has' + plural
};
/* if (this.options.counterCache) {
* If self association, this is the target association new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {});
*/ }
if (this.isSelfAssociation) { };
this.targetAssociation = this;
}
if (this.as) { util.inherits(HasMany, Association);
this.isAliased = true;
if (Utils._.isPlainObject(this.as)) { // the id is in the target table
this.options.name = this.as; // or in an extra table which connects two tables
this.as = this.as.plural; HasMany.prototype.injectAttributes = function() {
} else { this.identifier = this.foreignKey || Utils._.camelizeIf(
this.options.name = { [
plural: this.as, Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
singular: Utils.singularize(this.as) this.source.primaryKeyAttribute
}; ].join('_'),
} !this.source.options.underscored
} else { );
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.associationAccessor = this.as; var newAttributes = {};
var constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it newAttributes[this.identifier] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.source.rawAttributes[this.source.primaryKeyAttribute].type });
var plural = Utils.uppercaseFirst(this.options.name.plural)
, singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
get: 'get' + plural,
set: 'set' + plural,
addMultiple: 'add' + plural,
add: 'add' + singular,
create: 'create' + singular,
remove: 'remove' + singular,
removeMultiple: 'remove' + plural,
hasSingle: 'has' + singular,
hasAll: 'has' + plural
};
if (this.options.counterCache) { if (this.options.constraints !== false) {
new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {}); constraintOptions.onDelete = constraintOptions.onDelete || 'SET NULL';
} constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}; }
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, constraintOptions);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
util.inherits(HasMany, Association); this.identifierField = this.target.rawAttributes[this.identifier].field || this.identifier;
// the id is in the target table
// or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() {
this.identifier = this.foreignKey || Utils._.camelizeIf(
[
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
var newAttributes = {};
var constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
newAttributes[this.identifier] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.source.rawAttributes[this.source.primaryKeyAttribute].type });
if (this.options.constraints !== false) {
constraintOptions.onDelete = constraintOptions.onDelete || 'SET NULL';
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, constraintOptions);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.identifier].field || this.identifier; this.target.refreshAttributes();
this.source.refreshAttributes();
this.target.refreshAttributes(); Helpers.checkNamingCollision(this);
this.source.refreshAttributes();
Helpers.checkNamingCollision(this); return this;
};
return this; HasMany.prototype.injectGetter = function(obj) {
}; var association = this;
obj[this.accessors.get] = function(options) {
var scopeWhere = association.scope ? {} : null
, Model = association.target;
HasMany.prototype.injectGetter = function(obj) { options = association.target.__optClone(options) || {};
var association = this;
obj[this.accessors.get] = function(options) { if (association.scope) {
var scopeWhere = association.scope ? {} : null _.assign(scopeWhere, association.scope);
, Model = association.target; }
options = association.target.__optClone(options) || {}; options.where = {
$and: [
new Utils.where(
association.target.rawAttributes[association.identifier],
this.get(association.source.primaryKeyAttribute, {raw: true})
),
scopeWhere,
options.where
]
};
if (association.scope) { if (options.hasOwnProperty('scope')) {
_.assign(scopeWhere, association.scope); if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
} }
}
options.where = { return Model.all(options);
$and: [ };
new Utils.where(
association.target.rawAttributes[association.identifier],
this.get(association.source.primaryKeyAttribute, {raw: true})
),
scopeWhere,
options.where
]
};
if (options.hasOwnProperty('scope')) { obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
if (!options.scope) { var where = {};
Model = Model.unscoped();
} else { if (!Array.isArray(instances)) {
Model = Model.scope(options.scope); instances = [instances];
} }
options = options || {};
options.scope = false;
where.$or = instances.map(function (instance) {
if (instance instanceof association.target.Instance) {
return instance.where();
} else {
var _where = {};
_where[association.target.primaryKeyAttribute] = instance;
return _where;
} }
});
return Model.all(options); options.where = {
$and: [
where,
options.where
]
}; };
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) { return this[association.accessors.get](
var where = {}; options,
{ raw: true }
).then(function(associatedObjects) {
return associatedObjects.length === instances.length;
});
};
if (!Array.isArray(instances)) { return this;
instances = [instances]; };
}
options = options || {}; HasMany.prototype.injectSetter = function(obj) {
options.scope = false; var association = this
, primaryKeyAttribute = association.target.primaryKeyAttribute;
where.$or = instances.map(function (instance) { obj[this.accessors.set] = function(newAssociatedObjects, additionalAttributes) {
if (instance instanceof association.target.Instance) { additionalAttributes = additionalAttributes || {};
return instance.where();
} else { if (newAssociatedObjects === null) {
var _where = {}; newAssociatedObjects = [];
_where[association.target.primaryKeyAttribute] = instance; } else {
return _where; newAssociatedObjects = newAssociatedObjects.map(function(newAssociatedObject) {
if (!(newAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newAssociatedObject;
return association.target.build(tmpInstance, {
isNewRecord: false
});
} }
return newAssociatedObject;
}); });
}
options.where = { var instance = this;
$and: [
where,
options.where
]
};
return this[association.accessors.get](
options,
{ raw: true }
).then(function(associatedObjects) {
return associatedObjects.length === instances.length;
});
};
return this; return instance[association.accessors.get]({
scope: false,
transaction: (additionalAttributes || {}).transaction,
logging: (additionalAttributes || {}).logging
}).then(function(oldAssociatedObjects) {
return new HasManySingleLinked(association, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes);
});
}; };
HasMany.prototype.injectSetter = function(obj) { obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstance, options) {
var association = this // If newInstance is null or undefined, no-op
if (!newInstance) return Utils.Promise.resolve();
var instance = this
, primaryKeyAttribute = association.target.primaryKeyAttribute; , primaryKeyAttribute = association.target.primaryKeyAttribute;
obj[this.accessors.set] = function(newAssociatedObjects, additionalAttributes) { options = options || {};
additionalAttributes = additionalAttributes || {};
if (newAssociatedObjects === null) { if (Array.isArray(newInstance)) {
newAssociatedObjects = []; var newInstances = newInstance.map(function(newInstance) {
} else { if (!(newInstance instanceof association.target.Instance)) {
newAssociatedObjects = newAssociatedObjects.map(function(newAssociatedObject) { var tmpInstance = {};
if (!(newAssociatedObject instanceof association.target.Instance)) { tmpInstance[primaryKeyAttribute] = newInstance;
var tmpInstance = {}; return association.target.build(tmpInstance, {
tmpInstance[primaryKeyAttribute] = newAssociatedObject; isNewRecord: false
return association.target.build(tmpInstance, { });
isNewRecord: false }
}); return newInstance;
} });
return newAssociatedObject;
return new HasManySingleLinked(association, this).injectSetter([], newInstances, options);
} else {
if (!(newInstance instanceof association.target.Instance)) {
var values = {};
values[primaryKeyAttribute] = newInstance;
newInstance = association.target.build(values, {
isNewRecord: false
}); });
} }
var instance = this;
return instance[association.accessors.get]({ return instance[association.accessors.get]({
where: newInstance.where(),
scope: false, scope: false,
transaction: (additionalAttributes || {}).transaction, transaction: options.transaction,
logging: (additionalAttributes || {}).logging logging: options.logging
}).then(function(oldAssociatedObjects) { }).bind(this).then(function(currentAssociatedObjects) {
return new HasManySingleLinked(association, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes); if (currentAssociatedObjects.length === 0) {
}); newInstance.set(association.identifier, instance.get(instance.Model.primaryKeyAttribute));
};
if (association.scope) {
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstance, options) { Object.keys(association.scope).forEach(function (attribute) {
// If newInstance is null or undefined, no-op newInstance.set(attribute, association.scope[attribute]);
if (!newInstance) return Utils.Promise.resolve();
var instance = this
, primaryKeyAttribute = association.target.primaryKeyAttribute;
options = options || {};
if (Array.isArray(newInstance)) {
var newInstances = newInstance.map(function(newInstance) {
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = newInstance;
return association.target.build(tmpInstance, {
isNewRecord: false
}); });
} }
return newInstance;
});
return new HasManySingleLinked(association, this).injectSetter([], newInstances, options); return newInstance.save(options);
} else { } else {
if (!(newInstance instanceof association.target.Instance)) { return Utils.Promise.resolve(currentAssociatedObjects[0]);
var values = {};
values[primaryKeyAttribute] = newInstance;
newInstance = association.target.build(values, {
isNewRecord: false
});
} }
});
}
};
return instance[association.accessors.get]({ obj[this.accessors.remove] = function(oldAssociatedObject, options) {
where: newInstance.where(), var instance = this;
scope: false, return instance[association.accessors.get]({
transaction: options.transaction, scope: false
logging: options.logging }, options).then(function(currentAssociatedObjects) {
}).bind(this).then(function(currentAssociatedObjects) { var newAssociations = [];
if (currentAssociatedObjects.length === 0) {
newInstance.set(association.identifier, instance.get(instance.Model.primaryKeyAttribute)); if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
if (association.scope) { tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
Object.keys(association.scope).forEach(function (attribute) { oldAssociatedObject = association.target.build(tmpInstance, {
newInstance.set(attribute, association.scope[attribute]); isNewRecord: false
});
}
return newInstance.save(options);
} else {
return Utils.Promise.resolve(currentAssociatedObjects[0]);
}
}); });
} }
};
obj[this.accessors.remove] = function(oldAssociatedObject, options) { currentAssociatedObjects.forEach(function(association) {
var instance = this; if (!Utils._.isEqual(oldAssociatedObject.where(), association.where())) {
return instance[association.accessors.get]({ newAssociations.push(association);
scope: false }
}, options).then(function(currentAssociatedObjects) { });
var newAssociations = [];
return instance[association.accessors.set](newAssociations, options);
});
};
obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) {
var instance = this;
return instance[association.accessors.get]({
scope: false
}, options).then(function(currentAssociatedObjects) {
var newAssociations = [];
// Ensure the oldAssociatedObjects array is an array of target instances
oldAssociatedObjects = oldAssociatedObjects.map(function(oldAssociatedObject) {
if (!(oldAssociatedObject instanceof association.target.Instance)) { if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {}; var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = oldAssociatedObject; tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
...@@ -291,88 +317,60 @@ module.exports = (function() { ...@@ -291,88 +317,60 @@ module.exports = (function() {
isNewRecord: false isNewRecord: false
}); });
} }
return oldAssociatedObject;
currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.where(), association.where())) {
newAssociations.push(association);
}
});
return instance[association.accessors.set](newAssociations, options);
}); });
};
obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) { currentAssociatedObjects.forEach(function(association) {
var instance = this;
return instance[association.accessors.get]({
scope: false
}, options).then(function(currentAssociatedObjects) {
var newAssociations = [];
// Ensure the oldAssociatedObjects array is an array of target instances
oldAssociatedObjects = oldAssociatedObjects.map(function(oldAssociatedObject) {
if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
oldAssociatedObject = association.target.build(tmpInstance, {
isNewRecord: false
});
}
return oldAssociatedObject;
});
currentAssociatedObjects.forEach(function(association) {
// Determine is this is an association we want to remove
var obj = Utils._.find(oldAssociatedObjects, function(oldAssociatedObject) {
return Utils._.isEqual(oldAssociatedObject.where(), association.where());
});
// This is not an association we want to remove. Add it back // Determine is this is an association we want to remove
// to the set of associations we will associate our instance with var obj = Utils._.find(oldAssociatedObjects, function(oldAssociatedObject) {
if (!obj) { return Utils._.isEqual(oldAssociatedObject.where(), association.where());
newAssociations.push(association);
}
}); });
return instance[association.accessors.set](newAssociations, options); // This is not an association we want to remove. Add it back
// to the set of associations we will associate our instance with
if (!obj) {
newAssociations.push(association);
}
}); });
};
return this; return instance[association.accessors.set](newAssociations, options);
});
}; };
HasMany.prototype.injectCreator = function(obj) { return this;
var association = this; };
obj[this.accessors.create] = function(values, options) { HasMany.prototype.injectCreator = function(obj) {
var instance = this; var association = this;
options = options || {};
if (Array.isArray(options)) { obj[this.accessors.create] = function(values, options) {
options = { var instance = this;
fields: options options = options || {};
};
}
if (values === undefined) { if (Array.isArray(options)) {
values = {}; options = {
} fields: options
};
}
if (association.scope) { if (values === undefined) {
Object.keys(association.scope).forEach(function (attribute) { values = {};
values[attribute] = association.scope[attribute]; }
if (options.fields) options.fields.push(attribute);
});
}
values[association.identifier] = instance.get(association.source.primaryKeyAttribute); if (association.scope) {
if (options.fields) options.fields.push(association.identifier); Object.keys(association.scope).forEach(function (attribute) {
return association.target.create(values, options); values[attribute] = association.scope[attribute];
}; if (options.fields) options.fields.push(attribute);
});
}
return this; values[association.identifier] = instance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.identifier);
return association.target.create(values, options);
}; };
return HasMany; return this;
})(); };
module.exports = HasMany;
...@@ -5,178 +5,176 @@ var Utils = require('./../utils') ...@@ -5,178 +5,176 @@ var Utils = require('./../utils')
, Association = require('./base') , Association = require('./base')
, util = require('util'); , util = require('util');
module.exports = (function() { var HasOne = function(srcModel, targetModel, options) {
var HasOne = function(srcModel, targetModel, options) { Association.call(this);
Association.call(this);
this.associationType = 'HasOne';
this.associationType = 'HasOne'; this.source = srcModel;
this.source = srcModel; this.target = targetModel;
this.target = targetModel; this.options = options;
this.options = options; this.isSingleAssociation = true;
this.isSingleAssociation = true; this.isSelfAssociation = (this.source === this.target);
this.isSelfAssociation = (this.source === this.target); this.as = this.options.as;
this.as = this.options.as;
if (Utils._.isObject(this.options.foreignKey)) {
if (Utils._.isObject(this.options.foreignKey)) { this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; } else {
} else { this.foreignKeyAttribute = {};
this.foreignKeyAttribute = {}; this.foreignKey = this.options.foreignKey;
this.foreignKey = this.options.foreignKey; }
}
if (this.as) {
if (this.as) { this.isAliased = true;
this.isAliased = true; this.options.name = {
this.options.name = { singular: this.as
singular: this.as };
}; } else {
} else { this.as = this.target.options.name.singular;
this.as = this.target.options.name.singular; this.options.name = this.target.options.name;
this.options.name = this.target.options.name; }
}
if (!this.options.foreignKey) {
this.options.foreignKey = Utils._.camelizeIf(
[
Utils._.underscoredIf(Utils.singularize(this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
this.identifier = this.foreignKey || Utils._.camelizeIf( if (!this.options.foreignKey) {
this.options.foreignKey = Utils._.camelizeIf(
[ [
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored), Utils._.underscoredIf(Utils.singularize(this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute this.source.primaryKeyAttribute
].join('_'), ].join('_'),
!this.source.options.underscored !this.source.options.underscored
); );
}
this.sourceIdentifier = this.source.primaryKeyAttribute;
this.associationAccessor = this.as; this.identifier = this.foreignKey || Utils._.camelizeIf(
this.options.useHooks = options.useHooks; [
Utils._.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
// Get singular name, trying to uppercase the first letter, unless the model forbids it this.source.primaryKeyAttribute
var singular = Utils.uppercaseFirst(this.options.name.singular); ].join('_'),
!this.source.options.underscored
this.accessors = { );
get: 'get' + singular,
set: 'set' + singular, this.sourceIdentifier = this.source.primaryKeyAttribute;
create: 'create' + singular this.associationAccessor = this.as;
}; this.options.useHooks = options.useHooks;
// Get singular name, trying to uppercase the first letter, unless the model forbids it
var singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
get: 'get' + singular,
set: 'set' + singular,
create: 'create' + singular
}; };
};
util.inherits(HasOne, Association); util.inherits(HasOne, Association);
// the id is in the target table // the id is in the target table
HasOne.prototype.injectAttributes = function() { HasOne.prototype.injectAttributes = function() {
var newAttributes = {} var newAttributes = {}
, keyType = this.source.rawAttributes[this.sourceIdentifier].type; , keyType = this.source.rawAttributes[this.sourceIdentifier].type;
newAttributes[this.identifier] = Utils._.defaults(this.foreignKeyAttribute, { type: this.options.keyType || keyType }); newAttributes[this.identifier] = Utils._.defaults(this.foreignKeyAttribute, { type: this.options.keyType || keyType });
Utils.mergeDefaults(this.target.rawAttributes, newAttributes); Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.identifier].field || this.identifier; this.identifierField = this.target.rawAttributes[this.identifier].field || this.identifier;
if (this.options.constraints !== false) { if (this.options.constraints !== false) {
this.options.onDelete = this.options.onDelete || 'SET NULL'; this.options.onDelete = this.options.onDelete || 'SET NULL';
this.options.onUpdate = this.options.onUpdate || 'CASCADE'; this.options.onUpdate = this.options.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.identifier], this.source, this.target, this.options); Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.identifier], this.source, this.target, this.options);
// Sync attributes and setters/getters to Model prototype // Sync attributes and setters/getters to Model prototype
this.target.refreshAttributes(); this.target.refreshAttributes();
Helpers.checkNamingCollision(this); Helpers.checkNamingCollision(this);
return this; return this;
}; };
HasOne.prototype.injectGetter = function(instancePrototype) { HasOne.prototype.injectGetter = function(instancePrototype) {
var association = this; var association = this;
instancePrototype[this.accessors.get] = function(options) { instancePrototype[this.accessors.get] = function(options) {
var where = {}; var where = {};
where[association.identifier] = this.get(association.sourceIdentifier); where[association.identifier] = this.get(association.sourceIdentifier);
options = association.target.__optClone(options) || {}; options = association.target.__optClone(options) || {};
options.where = { options.where = {
$and: [ $and: [
options.where, options.where,
where where
] ]
}; };
if (options.limit === undefined) options.limit = null; if (options.limit === undefined) options.limit = null;
var model = association.target; var model = association.target;
if (options.hasOwnProperty('scope')) { if (options.hasOwnProperty('scope')) {
if (!options.scope) { if (!options.scope) {
model = model.unscoped(); model = model.unscoped();
} else { } else {
model = model.scope(options.scope); model = model.scope(options.scope);
}
} }
}
return model.find(options); return model.find(options);
};
return this;
}; };
HasOne.prototype.injectSetter = function(instancePrototype) { return this;
var association = this; };
instancePrototype[this.accessors.set] = function(associatedInstance, options) { HasOne.prototype.injectSetter = function(instancePrototype) {
var instance = this; var association = this;
options = options || {}; instancePrototype[this.accessors.set] = function(associatedInstance, options) {
options.scope = false; var instance = this;
return instance[association.accessors.get](options).then(function(oldInstance) {
if (oldInstance) { options = options || {};
oldInstance[association.identifier] = null; options.scope = false;
return oldInstance.save(Utils._.extend({}, options, { return instance[association.accessors.get](options).then(function(oldInstance) {
fields: [association.identifier], if (oldInstance) {
allowNull: [association.identifier], oldInstance[association.identifier] = null;
association: true return oldInstance.save(Utils._.extend({}, options, {
})); fields: [association.identifier],
} allowNull: [association.identifier],
}).then(function() { association: true
if (associatedInstance) { }));
if (!(associatedInstance instanceof association.target.Instance)) { }
var tmpInstance = {}; }).then(function() {
tmpInstance[association.target.primaryKeyAttribute] = associatedInstance; if (associatedInstance) {
associatedInstance = association.target.build(tmpInstance, { if (!(associatedInstance instanceof association.target.Instance)) {
isNewRecord: false var tmpInstance = {};
}); tmpInstance[association.target.primaryKeyAttribute] = associatedInstance;
} associatedInstance = association.target.build(tmpInstance, {
associatedInstance.set(association.identifier, instance.get(association.sourceIdentifier)); isNewRecord: false
return associatedInstance.save(options); });
} }
return null; associatedInstance.set(association.identifier, instance.get(association.sourceIdentifier));
}); return associatedInstance.save(options);
}; }
return null;
return this; });
}; };
HasOne.prototype.injectCreator = function(instancePrototype) { return this;
var association = this; };
instancePrototype[this.accessors.create] = function(values, options) { HasOne.prototype.injectCreator = function(instancePrototype) {
var instance = this; var association = this;
values = values || {};
options = options || {};
values[association.identifier] = instance.get(association.sourceIdentifier); instancePrototype[this.accessors.create] = function(values, options) {
if (options.fields) options.fields.push(association.identifier); var instance = this;
return association.target.create(values, options); values = values || {};
}; options = options || {};
return this; values[association.identifier] = instance.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.identifier);
return association.target.create(values, options);
}; };
return HasOne; return this;
})(); };
module.exports = HasOne;
...@@ -2,48 +2,50 @@ ...@@ -2,48 +2,50 @@
var Utils = require('./../utils'); var Utils = require('./../utils');
module.exports = { function checkNamingCollision (association) {
checkNamingCollision: function (association) { if (association.source.rawAttributes.hasOwnProperty(association.as)) {
if (association.source.rawAttributes.hasOwnProperty(association.as)) { throw new Error(
throw new Error( 'Naming collision between attribute \'' + association.as +
'Naming collision between attribute \'' + association.as + '\' and association \'' + association.as + '\' on model ' + association.source.name +
'\' and association \'' + association.as + '\' on model ' + association.source.name + '. To remedy this, change either foreignKey or as in your association definition'
'. To remedy this, change either foreignKey or as in your association definition' );
); }
} }
},
function addForeignKeyConstraints (newAttribute, source, target, options) {
addForeignKeyConstraints: function(newAttribute, source, target, options) { // FK constraints are opt-in: users must either set `foreignKeyConstraints`
// FK constraints are opt-in: users must either set `foreignKeyConstraints` // on the association, or request an `onDelete` or `onUpdate` behaviour
// on the association, or request an `onDelete` or `onUpdate` behaviour
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
// Find primary keys: composite keys not supported with this approach var primaryKeys = Utils._.chain(source.rawAttributes).keys()
var primaryKeys = Utils._.chain(source.rawAttributes).keys() .filter(function(key) { return source.rawAttributes[key].primaryKey; })
.filter(function(key) { return source.rawAttributes[key].primaryKey; }) .map(function(key) { return source.rawAttributes[key].field || key; }).value();
.map(function(key) { return source.rawAttributes[key].field || key; }).value();
if (primaryKeys.length === 1) {
if (primaryKeys.length === 1) { if (!!source.options.schema) {
if (!!source.options.schema) { newAttribute.references = {
newAttribute.references = { model: source.modelManager.sequelize.queryInterface.QueryGenerator.addSchema({
model: source.modelManager.sequelize.queryInterface.QueryGenerator.addSchema({ tableName: source.tableName,
tableName: source.tableName, options: {
options: { schema: source.options.schema,
schema: source.options.schema, schemaDelimiter: source.options.schemaDelimiter
schemaDelimiter: source.options.schemaDelimiter }
} })
}) };
}; } else {
} else { newAttribute.references = { model: source.tableName };
newAttribute.references = { model: source.tableName };
}
newAttribute.references.key = primaryKeys[0];
newAttribute.onDelete = options.onDelete;
newAttribute.onUpdate = options.onUpdate;
} }
newAttribute.references.key = primaryKeys[0];
newAttribute.onDelete = options.onDelete;
newAttribute.onUpdate = options.onUpdate;
} }
} }
}
module.exports = {
checkNamingCollision: checkNamingCollision,
addForeignKeyConstraints: addForeignKeyConstraints
}; };
This diff could not be displayed because it is too large.
...@@ -4,574 +4,571 @@ var Utils = require('../../utils') ...@@ -4,574 +4,571 @@ var Utils = require('../../utils')
, Dot = require('dottie') , Dot = require('dottie')
, QueryTypes = require('../../query-types'); , QueryTypes = require('../../query-types');
module.exports = (function() { var AbstractQuery = function(database, sequelize, options) {};
var AbstractQuery = function(database, sequelize, options) {};
/**
/** The function takes the result of the query execution and groups
* Execute the passed sql query. the associated data by the callee.
*
* Examples: Example:
* groupJoinData([
* query.run('SELECT 1') {
* some: 'data',
* @param {String} sql - The SQL query which should be executed. id: 1,
* @api public association: { foo: 'bar', id: 1 }
*/ }, {
AbstractQuery.prototype.run = function() { some: 'data',
throw new Error('The run method wasn\'t overwritten!'); id: 1,
}; association: { foo: 'bar', id: 2 }
}, {
/** some: 'data',
* Check the logging option of the instance and print deprecation warnings. id: 1,
* association: { foo: 'bar', id: 3 }
* @return {void} }
*/ ])
AbstractQuery.prototype.checkLoggingOption = function() {
if (this.options.logging === true) { Result:
console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log'); Something like this:
this.options.logging = console.log;
[
{
some: 'data',
id: 1,
association: [
{ foo: 'bar', id: 1 },
{ foo: 'bar', id: 2 },
{ foo: 'bar', id: 3 }
]
}
]
*/
/*
* Assumptions
* ID is not necessarily the first field
* All fields for a level is grouped in the same set (i.e. Panel.id, Task.id, Panel.title is not possible)
* Parent keys will be seen before any include/child keys
* Previous set won't necessarily be parent set (one parent could have two children, one child would then be previous set for the other)
*/
/*
* Author (MH) comment: This code is an unreadable mess, but its performant.
* groupJoinData is a performance critical function so we prioritize perf over readability.
*/
var groupJoinData = function(rows, includeOptions, options) {
if (!rows.length) {
return [];
}
var
// Generic looping
i
, length
, $i
, $length
// Row specific looping
, rowsI
, rowsLength = rows.length
, row
// Key specific looping
, keys
, key
, keyI
, keyLength
, prevKey
, values
, topValues
, topExists
, checkExisting = options.checkExisting
// If we don't have to deduplicate we can pre-allocate the resulting array
, results = checkExisting ? [] : new Array(rowsLength)
, resultMap = {}
, includeMap = {}
, itemHash
, parentHash
, topHash
// Result variables for the respective functions
, $keyPrefix
, $keyPrefixString
, $prevKeyPrefixString
, $prevKeyPrefix
, $lastKeyPrefix
, $current
, $parent
// Map each key to an include option
, previousPiece
, buildIncludeMap = function (piece) {
if ($current.includeMap[piece]) {
includeMap[key] = $current = $current.includeMap[piece];
if (previousPiece) {
previousPiece = previousPiece+'.'+piece;
} else {
previousPiece = piece;
}
includeMap[previousPiece] = $current;
}
} }
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
if (this.options.logging === console.log) { , lastKeyPrefixMemo = {}
// using just console.log will break in node < 0.6 , lastKeyPrefix = function (key) {
this.options.logging = function(s) { console.log(s); }; if (!lastKeyPrefixMemo[key]) {
var prefix = keyPrefix(key)
, length = prefix.length;
lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
}
return lastKeyPrefixMemo[key];
} }
}; // Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
, keyPrefixStringMemo = {}
/** , keyPrefixString = function (key, memo) {
* Get the attributes of an insert query, which contains the just inserted id. if (!memo[key]) {
* memo[key] = key.substr(0, key.lastIndexOf('.'));
* @return {String} The field name. }
*/ return memo[key];
AbstractQuery.prototype.getInsertIdField = function() {
return 'insertId';
};
/**
* Iterate over all known tables and search their names inside the sql query.
* This method will also check association aliases ('as' option).
*
* @param {String} attribute An attribute of a SQL query. (?)
* @return {String} The found tableName / alias.
*/
AbstractQuery.prototype.findTableNameInAttribute = function(attribute) {
if (!this.options.include) {
return null;
} }
if (!this.options.includeNames) { // Removes the prefix from a key ('id' for 'User.Results.id')
this.options.includeNames = this.options.include.map(function(include) { , removeKeyPrefixMemo = {}
return include.as; , removeKeyPrefix = function (key) {
}); if (!removeKeyPrefixMemo[key]) {
var index = key.lastIndexOf('.');
removeKeyPrefixMemo[key] = key.substr(index === -1 ? 0 : index + 1);
}
return removeKeyPrefixMemo[key];
} }
// Calculates the array prefix of a key (['User', 'Results'] for 'User.Results.id')
var tableNames = this.options.includeNames.filter(function(include) { , keyPrefixMemo = {}
return attribute.indexOf(include + '.') === 0; , keyPrefix = function (key) {
}); // We use a double memo and keyPrefixString so that different keys with the same prefix will receive the same array instead of differnet arrays with equal values
if (!keyPrefixMemo[key]) {
if (tableNames.length === 1) { var prefixString = keyPrefixString(key, keyPrefixStringMemo);
return tableNames[0]; if (!keyPrefixMemo[prefixString]) {
} else { keyPrefixMemo[prefixString] = prefixString ? prefixString.split('.') : [];
return null; }
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
}
return keyPrefixMemo[key];
} }
}; , primaryKeyAttributes
, prefix;
AbstractQuery.prototype.isRawQuery = function () {
return this.options.type === QueryTypes.RAW;
};
AbstractQuery.prototype.isVersionQuery = function () { for (rowsI = 0; rowsI < rowsLength; rowsI++) {
return this.options.type === QueryTypes.VERSION; row = rows[rowsI];
};
AbstractQuery.prototype.isUpsertQuery = function () { // Keys are the same for all rows, so only need to compute them on the first row
return this.options.type === QueryTypes.UPSERT; if (rowsI === 0) {
}; keys = Object.keys(row);
keyLength = keys.length;
}
AbstractQuery.prototype.isInsertQuery = function(results, metaData) { if (checkExisting) {
var result = true; topExists = false;
if (this.options.type === QueryTypes.INSERT) { // Compute top level hash key (this is usually just the primary key values)
return true; $length = includeOptions.model.primaryKeyAttributes.length;
if ($length === 1) {
topHash = row[includeOptions.model.primaryKeyAttributes[0]];
} else {
topHash = '';
for ($i = 0; $i < $length; $i++) {
topHash += row[includeOptions.model.primaryKeyAttributes[$i]];
}
}
} }
// is insert query if sql contains insert into topValues = values = {};
result = result && (this.sql.toLowerCase().indexOf('insert into') === 0); $prevKeyPrefix = undefined;
for (keyI = 0; keyI < keyLength; keyI++) {
key = keys[keyI];
// The string prefix isn't actualy needed
// We use it so keyPrefix for different keys will resolve to the same array if they have the same prefix
// TODO: Find a better way?
$keyPrefixString = keyPrefixString(key, keyPrefixStringMemo);
$keyPrefix = keyPrefix(key);
// On the first row we compute the includeMap
if (rowsI === 0 && includeMap[key] === undefined) {
if (!$keyPrefix.length) {
includeMap[key] = includeMap[''] = includeOptions;
} else {
$current = includeOptions;
previousPiece = undefined;
$keyPrefix.forEach(buildIncludeMap);
}
}
// is insert query if no results are passed or if the result has the inserted id // End of key set
result = result && (!results || results.hasOwnProperty(this.getInsertIdField())); if ($prevKeyPrefix !== undefined && $prevKeyPrefix !== $keyPrefix) {
if (checkExisting) {
// Compute hash key for this set instance
// TODO: Optimize
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? $parent+'.'+$prevKeyPrefix[i] : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$i]];
}
}
if (!parentHash) {
parentHash = topHash;
}
// is insert query if no metadata are passed or if the metadata has the inserted id itemHash = parentHash + itemHash;
result = result && (!metaData || metaData.hasOwnProperty(this.getInsertIdField())); $parent = prefix;
if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
return result; if (itemHash === topHash) {
}; if (!resultMap[itemHash]) {
resultMap[itemHash] = values;
} else {
topExists = true;
}
} else {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
AbstractQuery.prototype.handleInsertQuery = function(results, metaData) { if (includeMap[prevKey].association.isSingleAssociation) {
if (this.instance) { $parent[$lastKeyPrefix] = resultMap[itemHash] = values;
// add the inserted row id to the instance } else {
var autoIncrementField = this.model.autoIncrementField if (!$parent[$lastKeyPrefix]) {
, id = null; $parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
}
id = id || (results && results[this.getInsertIdField()]); // Reset values
id = id || (metaData && metaData[this.getInsertIdField()]); values = {};
} else {
// If checkExisting is false it's because there's only 1:1 associations in this query
// However we still need to map onto the appropriate parent
// For 1:1 we map forward, initializing the value object on the parent to be filled in the next iterations of the loop
$current = topValues;
length = $keyPrefix.length;
if (length) {
for (i = 0; i < length; i++) {
if (i === length -1) {
values = $current[$keyPrefix[i]] = {};
}
$current = $current[$keyPrefix[i]];
}
}
}
}
this.instance[autoIncrementField] = id; // End of iteration, set value and set prev values (for next iteration)
values[removeKeyPrefix(key)] = row[key];
prevKey = key;
$prevKeyPrefix = $keyPrefix;
$prevKeyPrefixString = $keyPrefixString;
} }
};
AbstractQuery.prototype.isShowTablesQuery = function() { if (checkExisting) {
return this.options.type === QueryTypes.SHOWTABLES; length = $prevKeyPrefix.length;
}; $parent = null;
parentHash = null;
AbstractQuery.prototype.handleShowTablesQuery = function(results) {
return Utils._.flatten(results.map(function(resultSet) { if (length) {
return Utils._.values(resultSet); for (i = 0; i < length; i++) {
})); prefix = $parent ? $parent+'.'+$prevKeyPrefix[i] : $prevKeyPrefix[i];
}; primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$i]];
}
}
if (!parentHash) {
parentHash = topHash;
}
AbstractQuery.prototype.isShowIndexesQuery = function () { itemHash = parentHash + itemHash;
return this.options.type === QueryTypes.SHOWINDEXES; $parent = prefix;
}; if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
AbstractQuery.prototype.isDescribeQuery = function () { if (itemHash === topHash) {
return this.options.type === QueryTypes.DESCRIBE; if (!resultMap[itemHash]) {
}; resultMap[itemHash] = values;
} else {
topExists = true;
}
} else {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
AbstractQuery.prototype.isSelectQuery = function() { if (includeMap[prevKey].association.isSingleAssociation) {
return this.options.type === QueryTypes.SELECT; $parent[$lastKeyPrefix] = resultMap[itemHash] = values;
}; } else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
}
if (!topExists) {
results.push(topValues);
}
} else {
results[rowsI] = topValues;
}
}
return results;
};
/**
* Execute the passed sql query.
*
* Examples:
*
* query.run('SELECT 1')
*
* @param {String} sql - The SQL query which should be executed.
* @api public
*/
AbstractQuery.prototype.run = function() {
throw new Error('The run method wasn\'t overwritten!');
};
/**
* Check the logging option of the instance and print deprecation warnings.
*
* @return {void}
*/
AbstractQuery.prototype.checkLoggingOption = function() {
if (this.options.logging === true) {
console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log');
this.options.logging = console.log;
}
if (this.options.logging === console.log) {
// using just console.log will break in node < 0.6
this.options.logging = function(s) { console.log(s); };
}
};
/**
* Get the attributes of an insert query, which contains the just inserted id.
*
* @return {String} The field name.
*/
AbstractQuery.prototype.getInsertIdField = function() {
return 'insertId';
};
/**
* Iterate over all known tables and search their names inside the sql query.
* This method will also check association aliases ('as' option).
*
* @param {String} attribute An attribute of a SQL query. (?)
* @return {String} The found tableName / alias.
*/
AbstractQuery.prototype.findTableNameInAttribute = function(attribute) {
if (!this.options.include) {
return null;
}
if (!this.options.includeNames) {
this.options.includeNames = this.options.include.map(function(include) {
return include.as;
});
}
AbstractQuery.prototype.isBulkUpdateQuery = function() { var tableNames = this.options.includeNames.filter(function(include) {
return this.options.type === QueryTypes.BULKUPDATE; return attribute.indexOf(include + '.') === 0;
}; });
AbstractQuery.prototype.isBulkDeleteQuery = function() { if (tableNames.length === 1) {
return this.options.type === QueryTypes.BULKDELETE; return tableNames[0];
}; } else {
return null;
}
};
AbstractQuery.prototype.isForeignKeysQuery = function() { AbstractQuery.prototype.isRawQuery = function () {
return this.options.type === QueryTypes.FOREIGNKEYS; return this.options.type === QueryTypes.RAW;
}; };
AbstractQuery.prototype.isUpdateQuery = function() { AbstractQuery.prototype.isVersionQuery = function () {
return this.options.type === QueryTypes.UPDATE; return this.options.type === QueryTypes.VERSION;
}; };
AbstractQuery.prototype.handleSelectQuery = function(results) { AbstractQuery.prototype.isUpsertQuery = function () {
var result = null; return this.options.type === QueryTypes.UPSERT;
};
// Raw queries AbstractQuery.prototype.isInsertQuery = function(results, metaData) {
if (this.options.raw) { var result = true;
result = results.map(function(result) {
var o = {};
for (var key in result) { if (this.options.type === QueryTypes.INSERT) {
if (result.hasOwnProperty(key)) { return true;
o[key] = result[key]; }
}
}
if (this.options.nest) { // is insert query if sql contains insert into
o = Dot.transform(o); result = result && (this.sql.toLowerCase().indexOf('insert into') === 0);
}
return o; // is insert query if no results are passed or if the result has the inserted id
}, this); result = result && (!results || results.hasOwnProperty(this.getInsertIdField()));
// Queries with include
} else if (this.options.hasJoin === true) {
results = groupJoinData(results, {
model: this.model,
includeMap: this.options.includeMap,
includeNames: this.options.includeNames
}, {
checkExisting: this.options.hasMultiAssociation
});
result = this.model.bulkBuild(results, {
isNewRecord: false,
include: this.options.include,
includeNames: this.options.includeNames,
includeMap: this.options.includeMap,
includeValidated: true,
attributes: this.options.originalAttributes || this.options.attributes,
raw: true
});
// Regular queries
} else {
result = this.model.bulkBuild(results, {
isNewRecord: false,
raw: true,
attributes: this.options.attributes
});
}
// return the first real model instance if options.plain is set (e.g. Model.find) // is insert query if no metadata are passed or if the metadata has the inserted id
if (this.options.plain) { result = result && (!metaData || metaData.hasOwnProperty(this.getInsertIdField()));
result = (result.length === 0) ? null : result[0];
}
return result; return result;
}; };
AbstractQuery.prototype.isShowOrDescribeQuery = function() { AbstractQuery.prototype.handleInsertQuery = function(results, metaData) {
var result = false; if (this.instance) {
// add the inserted row id to the instance
var autoIncrementField = this.model.autoIncrementField
, id = null;
result = result || (this.sql.toLowerCase().indexOf('show') === 0); id = id || (results && results[this.getInsertIdField()]);
result = result || (this.sql.toLowerCase().indexOf('describe') === 0); id = id || (metaData && metaData[this.getInsertIdField()]);
return result; this.instance[autoIncrementField] = id;
}; }
};
AbstractQuery.prototype.isCallQuery = function() { AbstractQuery.prototype.isShowTablesQuery = function() {
var result = false; return this.options.type === QueryTypes.SHOWTABLES;
};
result = result || (this.sql.toLowerCase().indexOf('call') === 0); AbstractQuery.prototype.handleShowTablesQuery = function(results) {
return Utils._.flatten(results.map(function(resultSet) {
return Utils._.values(resultSet);
}));
};
return result; AbstractQuery.prototype.isShowIndexesQuery = function () {
}; return this.options.type === QueryTypes.SHOWINDEXES;
};
AbstractQuery.prototype.isDescribeQuery = function () {
return this.options.type === QueryTypes.DESCRIBE;
};
/** AbstractQuery.prototype.isSelectQuery = function() {
The function takes the result of the query execution and groups return this.options.type === QueryTypes.SELECT;
the associated data by the callee. };
Example: AbstractQuery.prototype.isBulkUpdateQuery = function() {
groupJoinData([ return this.options.type === QueryTypes.BULKUPDATE;
{ };
some: 'data',
id: 1,
association: { foo: 'bar', id: 1 }
}, {
some: 'data',
id: 1,
association: { foo: 'bar', id: 2 }
}, {
some: 'data',
id: 1,
association: { foo: 'bar', id: 3 }
}
])
Result:
Something like this:
[
{
some: 'data',
id: 1,
association: [
{ foo: 'bar', id: 1 },
{ foo: 'bar', id: 2 },
{ foo: 'bar', id: 3 }
]
}
]
*/
/*
* Assumptions
* ID is not necessarily the first field
* All fields for a level is grouped in the same set (i.e. Panel.id, Task.id, Panel.title is not possible)
* Parent keys will be seen before any include/child keys
* Previous set won't necessarily be parent set (one parent could have two children, one child would then be previous set for the other)
*/
/*
* Author (MH) comment: This code is an unreadable mess, but its performant.
* groupJoinData is a performance critical function so we prioritize perf over readability.
*/
var groupJoinData = function(rows, includeOptions, options) {
if (!rows.length) {
return [];
}
var AbstractQuery.prototype.isBulkDeleteQuery = function() {
// Generic looping return this.options.type === QueryTypes.BULKDELETE;
i };
, length
, $i
, $length
// Row specific looping
, rowsI
, rowsLength = rows.length
, row
// Key specific looping
, keys
, key
, keyI
, keyLength
, prevKey
, values
, topValues
, topExists
, checkExisting = options.checkExisting
// If we don't have to deduplicate we can pre-allocate the resulting array
, results = checkExisting ? [] : new Array(rowsLength)
, resultMap = {}
, includeMap = {}
, itemHash
, parentHash
, topHash
// Result variables for the respective functions
, $keyPrefix
, $keyPrefixString
, $prevKeyPrefixString
, $prevKeyPrefix
, $lastKeyPrefix
, $current
, $parent
// Map each key to an include option
, previousPiece
, buildIncludeMap = function (piece) {
if ($current.includeMap[piece]) {
includeMap[key] = $current = $current.includeMap[piece];
if (previousPiece) {
previousPiece = previousPiece+'.'+piece;
} else {
previousPiece = piece;
}
includeMap[previousPiece] = $current;
}
}
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
, lastKeyPrefixMemo = {}
, lastKeyPrefix = function (key) {
if (!lastKeyPrefixMemo[key]) {
var prefix = keyPrefix(key)
, length = prefix.length;
lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
}
return lastKeyPrefixMemo[key];
}
// Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
, keyPrefixStringMemo = {}
, keyPrefixString = function (key, memo) {
if (!memo[key]) {
memo[key] = key.substr(0, key.lastIndexOf('.'));
}
return memo[key];
}
// Removes the prefix from a key ('id' for 'User.Results.id')
, removeKeyPrefixMemo = {}
, removeKeyPrefix = function (key) {
if (!removeKeyPrefixMemo[key]) {
var index = key.lastIndexOf('.');
removeKeyPrefixMemo[key] = key.substr(index === -1 ? 0 : index + 1);
}
return removeKeyPrefixMemo[key];
}
// Calculates the array prefix of a key (['User', 'Results'] for 'User.Results.id')
, keyPrefixMemo = {}
, keyPrefix = function (key) {
// We use a double memo and keyPrefixString so that different keys with the same prefix will receive the same array instead of differnet arrays with equal values
if (!keyPrefixMemo[key]) {
var prefixString = keyPrefixString(key, keyPrefixStringMemo);
if (!keyPrefixMemo[prefixString]) {
keyPrefixMemo[prefixString] = prefixString ? prefixString.split('.') : [];
}
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
}
return keyPrefixMemo[key];
}
, primaryKeyAttributes
, prefix;
for (rowsI = 0; rowsI < rowsLength; rowsI++) { AbstractQuery.prototype.isForeignKeysQuery = function() {
row = rows[rowsI]; return this.options.type === QueryTypes.FOREIGNKEYS;
};
// Keys are the same for all rows, so only need to compute them on the first row AbstractQuery.prototype.isUpdateQuery = function() {
if (rowsI === 0) { return this.options.type === QueryTypes.UPDATE;
keys = Object.keys(row); };
keyLength = keys.length;
}
if (checkExisting) { AbstractQuery.prototype.handleSelectQuery = function(results) {
topExists = false; var result = null;
// Compute top level hash key (this is usually just the primary key values) // Raw queries
$length = includeOptions.model.primaryKeyAttributes.length; if (this.options.raw) {
if ($length === 1) { result = results.map(function(result) {
topHash = row[includeOptions.model.primaryKeyAttributes[0]]; var o = {};
} else {
topHash = ''; for (var key in result) {
for ($i = 0; $i < $length; $i++) { if (result.hasOwnProperty(key)) {
topHash += row[includeOptions.model.primaryKeyAttributes[$i]]; o[key] = result[key];
}
} }
} }
topValues = values = {}; if (this.options.nest) {
$prevKeyPrefix = undefined; o = Dot.transform(o);
for (keyI = 0; keyI < keyLength; keyI++) { }
key = keys[keyI];
// The string prefix isn't actualy needed
// We use it so keyPrefix for different keys will resolve to the same array if they have the same prefix
// TODO: Find a better way?
$keyPrefixString = keyPrefixString(key, keyPrefixStringMemo);
$keyPrefix = keyPrefix(key);
// On the first row we compute the includeMap
if (rowsI === 0 && includeMap[key] === undefined) {
if (!$keyPrefix.length) {
includeMap[key] = includeMap[''] = includeOptions;
} else {
$current = includeOptions;
previousPiece = undefined;
$keyPrefix.forEach(buildIncludeMap);
}
}
// End of key set return o;
if ($prevKeyPrefix !== undefined && $prevKeyPrefix !== $keyPrefix) { }, this);
if (checkExisting) { // Queries with include
// Compute hash key for this set instance } else if (this.options.hasJoin === true) {
// TODO: Optimize results = groupJoinData(results, {
length = $prevKeyPrefix.length; model: this.model,
$parent = null; includeMap: this.options.includeMap,
parentHash = null; includeNames: this.options.includeNames
}, {
if (length) { checkExisting: this.options.hasMultiAssociation
for (i = 0; i < length; i++) { });
prefix = $parent ? $parent+'.'+$prevKeyPrefix[i] : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$i]];
}
}
if (!parentHash) {
parentHash = topHash;
}
itemHash = parentHash + itemHash; result = this.model.bulkBuild(results, {
$parent = prefix; isNewRecord: false,
if (i < length - 1) { include: this.options.include,
parentHash = itemHash; includeNames: this.options.includeNames,
} includeMap: this.options.includeMap,
} includeValidated: true,
} else { attributes: this.options.originalAttributes || this.options.attributes,
itemHash = topHash; raw: true
} });
// Regular queries
} else {
result = this.model.bulkBuild(results, {
isNewRecord: false,
raw: true,
attributes: this.options.attributes
});
}
if (itemHash === topHash) { // return the first real model instance if options.plain is set (e.g. Model.find)
if (!resultMap[itemHash]) { if (this.options.plain) {
resultMap[itemHash] = values; result = (result.length === 0) ? null : result[0];
} else { }
topExists = true;
}
} else {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
}
// Reset values return result;
values = {}; };
} else {
// If checkExisting is false it's because there's only 1:1 associations in this query
// However we still need to map onto the appropriate parent
// For 1:1 we map forward, initializing the value object on the parent to be filled in the next iterations of the loop
$current = topValues;
length = $keyPrefix.length;
if (length) {
for (i = 0; i < length; i++) {
if (i === length -1) {
values = $current[$keyPrefix[i]] = {};
}
$current = $current[$keyPrefix[i]];
}
}
}
}
// End of iteration, set value and set prev values (for next iteration) AbstractQuery.prototype.isShowOrDescribeQuery = function() {
values[removeKeyPrefix(key)] = row[key]; var result = false;
prevKey = key;
$prevKeyPrefix = $keyPrefix;
$prevKeyPrefixString = $keyPrefixString;
}
if (checkExisting) { result = result || (this.sql.toLowerCase().indexOf('show') === 0);
length = $prevKeyPrefix.length; result = result || (this.sql.toLowerCase().indexOf('describe') === 0);
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? $parent+'.'+$prevKeyPrefix[i] : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$i]];
}
}
if (!parentHash) {
parentHash = topHash;
}
itemHash = parentHash + itemHash; return result;
$parent = prefix; };
if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
if (itemHash === topHash) { AbstractQuery.prototype.isCallQuery = function() {
if (!resultMap[itemHash]) { var result = false;
resultMap[itemHash] = values;
} else {
topExists = true;
}
} else {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
if (includeMap[prevKey].association.isSingleAssociation) { result = result || (this.sql.toLowerCase().indexOf('call') === 0);
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
}
if (!topExists) {
results.push(topValues);
}
} else {
results[rowsI] = topValues;
}
}
return results; return result;
}; };
AbstractQuery.$groupJoinData = groupJoinData; AbstractQuery.$groupJoinData = groupJoinData;
return AbstractQuery; module.exports = AbstractQuery;
})();
...@@ -6,606 +6,604 @@ var Utils = require('../../utils') ...@@ -6,606 +6,604 @@ var Utils = require('../../utils')
, Model = require('../../model') , Model = require('../../model')
, AbstractQueryGenerator = require('../abstract/query-generator'); , AbstractQueryGenerator = require('../abstract/query-generator');
module.exports = (function() { /* istanbul ignore next */
var QueryGenerator = { var throwMethodUndefined = function(methodName) {
options: {}, throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.');
dialect: 'mssql', };
createSchema: function(schema) { var QueryGenerator = {
return [ options: {},
'IF NOT EXISTS (SELECT schema_name', dialect: 'mssql',
'FROM information_schema.schemata',
'WHERE schema_name =', wrapSingleQuote(schema), ')', createSchema: function(schema) {
'BEGIN', return [
"EXEC sp_executesql N'CREATE SCHEMA", 'IF NOT EXISTS (SELECT schema_name',
this.quoteIdentifier(schema), 'FROM information_schema.schemata',
";'", 'WHERE schema_name =', wrapSingleQuote(schema), ')',
'END;' 'BEGIN',
].join(' '); "EXEC sp_executesql N'CREATE SCHEMA",
}, this.quoteIdentifier(schema),
";'",
showSchemasQuery: function() { 'END;'
return [ ].join(' ');
'SELECT "name" as "schema_name" FROM sys.schemas as s', },
'WHERE "s"."name" NOT IN (',
"'INFORMATION_SCHEMA', 'dbo', 'guest', 'sys', 'archive'", showSchemasQuery: function() {
')', 'AND', '"s"."name" NOT LIKE', "'db_%'" return [
].join(' '); 'SELECT "name" as "schema_name" FROM sys.schemas as s',
}, 'WHERE "s"."name" NOT IN (',
"'INFORMATION_SCHEMA', 'dbo', 'guest', 'sys', 'archive'",
versionQuery: function() { ')', 'AND', '"s"."name" NOT LIKE', "'db_%'"
return "SELECT @@VERSION as 'version'"; ].join(' ');
}, },
createTableQuery: function(tableName, attributes, options) { versionQuery: function() {
var query = "IF OBJECT_ID('<%= table %>', 'U') IS NULL CREATE TABLE <%= table %> (<%= attributes %>)" return "SELECT @@VERSION as 'version'";
, primaryKeys = [] },
, foreignKeys = {}
, attrStr = [] createTableQuery: function(tableName, attributes, options) {
, self = this; var query = "IF OBJECT_ID('<%= table %>', 'U') IS NULL CREATE TABLE <%= table %> (<%= attributes %>)"
, primaryKeys = []
for (var attr in attributes) { , foreignKeys = {}
if (attributes.hasOwnProperty(attr)) { , attrStr = []
var dataType = attributes[attr] , self = this;
, match;
for (var attr in attributes) {
if (Utils._.includes(dataType, 'PRIMARY KEY')) { if (attributes.hasOwnProperty(attr)) {
primaryKeys.push(attr); var dataType = attributes[attr]
, match;
if (Utils._.includes(dataType, 'REFERENCES')) {
// MSSQL doesn't support inline REFERENCES declarations: move to the end if (Utils._.includes(dataType, 'PRIMARY KEY')) {
match = dataType.match(/^(.+) (REFERENCES.*)$/); primaryKeys.push(attr);
attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1].replace(/PRIMARY KEY/, ''));
foreignKeys[attr] = match[2]; if (Utils._.includes(dataType, 'REFERENCES')) {
} else { // MSSQL doesn't support inline REFERENCES declarations: move to the end
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType.replace(/PRIMARY KEY/, ''));
}
} else if (Utils._.includes(dataType, 'REFERENCES')) {
// MSSQL doesn't support inline REFERENCES declarations: move to the end
match = dataType.match(/^(.+) (REFERENCES.*)$/); match = dataType.match(/^(.+) (REFERENCES.*)$/);
attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1]); attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1].replace(/PRIMARY KEY/, ''));
foreignKeys[attr] = match[2]; foreignKeys[attr] = match[2];
} else { } else {
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType); attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType.replace(/PRIMARY KEY/, ''));
} }
} else if (Utils._.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);
} }
} }
}
var values = { var values = {
table: this.quoteTable(tableName), table: this.quoteTable(tableName),
attributes: attrStr.join(', '), attributes: attrStr.join(', '),
} }
, pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', '); , pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', ');
if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns, indexName) {
if (!Utils._.isString(indexName)) {
indexName = 'uniq_' + tableName + '_' + columns.fields.join('_');
}
values.attributes += ', CONSTRAINT ' + self.quoteIdentifier(indexName) + ' UNIQUE (' + Utils._.map(columns.fields, self.quoteIdentifier).join(', ') + ')';
});
}
if (pkString.length > 0) {
values.attributes += ', PRIMARY KEY (' + pkString + ')';
}
for (var fkey in foreignKeys) { if (!!options.uniqueKeys) {
if (foreignKeys.hasOwnProperty(fkey)) { Utils._.each(options.uniqueKeys, function(columns, indexName) {
values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey]; if (!Utils._.isString(indexName)) {
indexName = 'uniq_' + tableName + '_' + columns.fields.join('_');
} }
} values.attributes += ', CONSTRAINT ' + self.quoteIdentifier(indexName) + ' UNIQUE (' + Utils._.map(columns.fields, self.quoteIdentifier).join(', ') + ')';
return Utils._.template(query)(values).trim() + ';';
},
describeTableQuery: function(tableName, schema) {
var sql = [
'SELECT',
"c.COLUMN_NAME AS 'Name',",
"c.DATA_TYPE AS 'Type',",
"c.IS_NULLABLE as 'IsNull',",
"COLUMN_DEFAULT AS 'Default'",
'FROM',
'INFORMATION_SCHEMA.TABLES t',
'INNER JOIN',
'INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME',
'WHERE t.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
if (schema) {
sql += 'AND t.TABLE_SCHEMA =' + wrapSingleQuote(schema);
}
return sql;
},
renameTableQuery: function(before, after) {
var query = 'EXEC sp_rename <%= before %>, <%= after %>;';
return Utils._.template(query)({
before: this.quoteTable(before),
after: this.quoteTable(after)
}); });
}, }
showTablesQuery: function () { if (pkString.length > 0) {
return 'SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES;'; values.attributes += ', PRIMARY KEY (' + pkString + ')';
}, }
dropTableQuery: function(tableName) { for (var fkey in foreignKeys) {
var query = "IF OBJECT_ID('<%= table %>', 'U') IS NOT NULL DROP TABLE <%= table %>"; if (foreignKeys.hasOwnProperty(fkey)) {
var values = { values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey];
table: this.quoteTable(tableName) }
}; }
return Utils._.template(query)(values).trim() + ';'; return Utils._.template(query)(values).trim() + ';';
}, },
addColumnQuery: function(table, key, dataType) { describeTableQuery: function(tableName, schema) {
// FIXME: attributeToSQL SHOULD be using attributes in addColumnQuery var sql = [
// but instead we need to pass the key along as the field here 'SELECT',
dataType.field = key; "c.COLUMN_NAME AS 'Name',",
"c.DATA_TYPE AS 'Type',",
var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;' "c.IS_NULLABLE as 'IsNull',",
, attribute = Utils._.template('<%= key %> <%= definition %>')({ "COLUMN_DEFAULT AS 'Default'",
key: this.quoteIdentifier(key), 'FROM',
definition: this.attributeToSQL(dataType, { 'INFORMATION_SCHEMA.TABLES t',
context: 'addColumn' 'INNER JOIN',
}) 'INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME',
}); 'WHERE t.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
return Utils._.template(query)({
table: this.quoteTable(table), if (schema) {
attribute: attribute sql += 'AND t.TABLE_SCHEMA =' + wrapSingleQuote(schema);
}); }
},
removeColumnQuery: function(tableName, attributeName) { return sql;
var query = 'ALTER TABLE <%= tableName %> DROP <%= attributeName %>;'; },
return Utils._.template(query)({
tableName: this.quoteTable(tableName), renameTableQuery: function(before, after) {
attributeName: this.quoteIdentifier(attributeName) var query = 'EXEC sp_rename <%= before %>, <%= after %>;';
return Utils._.template(query)({
before: this.quoteTable(before),
after: this.quoteTable(after)
});
},
showTablesQuery: function () {
return 'SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES;';
},
dropTableQuery: function(tableName) {
var query = "IF OBJECT_ID('<%= table %>', 'U') IS NOT NULL DROP TABLE <%= table %>";
var values = {
table: this.quoteTable(tableName)
};
return Utils._.template(query)(values).trim() + ';';
},
addColumnQuery: function(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;
var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'
, attribute = Utils._.template('<%= key %> <%= definition %>')({
key: this.quoteIdentifier(key),
definition: this.attributeToSQL(dataType, {
context: 'addColumn'
})
}); });
},
changeColumnQuery: function(tableName, attributes) { return Utils._.template(query)({
var query = 'ALTER TABLE <%= tableName %> ALTER COLUMN <%= attributes %>;'; table: this.quoteTable(table),
var attrString = []; attribute: attribute
});
},
removeColumnQuery: function(tableName, attributeName) {
var query = 'ALTER TABLE <%= tableName %> DROP <%= attributeName %>;';
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
attributeName: this.quoteIdentifier(attributeName)
});
},
changeColumnQuery: function(tableName, attributes) {
var query = 'ALTER TABLE <%= tableName %> ALTER COLUMN <%= attributes %>;';
var attrString = [];
for (var attrName in attributes) {
var definition = attributes[attrName];
attrString.push(Utils._.template('<%= attrName %> <%= definition %>')({
attrName: this.quoteIdentifier(attrName),
definition: definition
}));
}
for (var attrName in attributes) { return Utils._.template(query)({
var definition = attributes[attrName]; tableName: this.quoteTable(tableName),
attributes: attrString.join(', ')
});
},
renameColumnQuery: function(tableName, attrBefore, attributes) {
var query = "EXEC sp_rename '<%= tableName %>.<%= before %>', '<%= after %>', 'COLUMN';"
, newName = Object.keys(attributes)[0];
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
before: attrBefore,
after: newName
});
},
bulkInsertQuery: function(tableName, attrValueHashes, options, attributes) {
var query = 'INSERT INTO <%= table %> (<%= attributes %>)<%= output %> VALUES <%= tuples %>;'
, emptyQuery = 'INSERT INTO <%= table %><%= output %> DEFAULT VALUES'
, tuples = []
, allAttributes = []
, needIdentityInsertWrapper = false
, allQueries = []
, outputFragment;
if (options.returning) {
outputFragment = ' OUTPUT INSERTED.*';
}
attrString.push(Utils._.template('<%= attrName %> <%= definition %>')({ Utils._.forEach(attrValueHashes, function(attrValueHash) {
attrName: this.quoteIdentifier(attrName), // special case for empty objects with primary keys
definition: definition var fields = Object.keys(attrValueHash);
})); if (fields.length === 1 && attributes[fields[0]].autoIncrement && attrValueHash[fields[0]] === null) {
allQueries.push(emptyQuery);
return;
} }
return Utils._.template(query)({ // normal case
tableName: this.quoteTable(tableName), Utils._.forOwn(attrValueHash, function(value, key) {
attributes: attrString.join(', ') if (value !== null && attributes[key].autoIncrement) {
}); needIdentityInsertWrapper = true;
}, }
renameColumnQuery: function(tableName, attrBefore, attributes) { if (allAttributes.indexOf(key) === -1) {
var query = "EXEC sp_rename '<%= tableName %>.<%= before %>', '<%= after %>', 'COLUMN';" if (value === null && attributes[key].autoIncrement)
, newName = Object.keys(attributes)[0]; return;
return Utils._.template(query)({ allAttributes.push(key);
tableName: this.quoteTable(tableName), }
before: attrBefore,
after: newName
}); });
}, });
bulkInsertQuery: function(tableName, attrValueHashes, options, attributes) {
var query = 'INSERT INTO <%= table %> (<%= attributes %>)<%= output %> VALUES <%= tuples %>;'
, emptyQuery = 'INSERT INTO <%= table %><%= output %> DEFAULT VALUES'
, tuples = []
, allAttributes = []
, needIdentityInsertWrapper = false
, allQueries = []
, outputFragment;
if (options.returning) {
outputFragment = ' OUTPUT INSERTED.*';
}
if (allAttributes.length > 0) {
Utils._.forEach(attrValueHashes, function(attrValueHash) { Utils._.forEach(attrValueHashes, function(attrValueHash) {
// special case for empty objects with primary keys tuples.push('(' +
var fields = Object.keys(attrValueHash); allAttributes.map(function(key) {
if (fields.length === 1 && attributes[fields[0]].autoIncrement && attrValueHash[fields[0]] === null) { return this.escape(attrValueHash[key]);
allQueries.push(emptyQuery); }.bind(this)).join(',') +
return; ')');
} }.bind(this));
// normal case allQueries.push(query);
Utils._.forOwn(attrValueHash, function(value, key) { }
if (value !== null && attributes[key].autoIncrement) {
needIdentityInsertWrapper = true;
}
if (allAttributes.indexOf(key) === -1) { var replacements = {
if (value === null && attributes[key].autoIncrement) table: this.quoteTable(tableName),
return; attributes: allAttributes.map(function(attr) {
return this.quoteIdentifier(attr);
}.bind(this)).join(','),
tuples: tuples,
output: outputFragment
};
var generatedQuery = Utils._.template(allQueries.join(';'))(replacements);
if (needIdentityInsertWrapper) {
generatedQuery = [
'SET IDENTITY_INSERT', this.quoteTable(tableName), 'ON;',
generatedQuery,
'SET IDENTITY_INSERT', this.quoteTable(tableName), 'OFF;',
].join(' ');
}
allAttributes.push(key); return generatedQuery;
} },
});
});
if (allAttributes.length > 0) { deleteQuery: function(tableName, where, options) {
Utils._.forEach(attrValueHashes, function(attrValueHash) { options = options || {};
tuples.push('(' +
allAttributes.map(function(key) {
return this.escape(attrValueHash[key]);
}.bind(this)).join(',') +
')');
}.bind(this));
allQueries.push(query); var table = this.quoteTable(tableName);
} if (options.truncate === true) {
// Truncate does not allow LIMIT and WHERE
return 'TRUNCATE TABLE ' + table;
}
var replacements = { where = this.getWhereConditions(where);
table: this.quoteTable(tableName), var limit = ''
attributes: allAttributes.map(function(attr) { , query = 'DELETE<%= limit %> FROM <%= table %><%= where %>; ' +
return this.quoteIdentifier(attr); 'SELECT @@ROWCOUNT AS AFFECTEDROWS;';
}.bind(this)).join(','),
tuples: tuples,
output: outputFragment
};
var generatedQuery = Utils._.template(allQueries.join(';'))(replacements); if (Utils._.isUndefined(options.limit)) {
if (needIdentityInsertWrapper) { options.limit = 1;
generatedQuery = [ }
'SET IDENTITY_INSERT', this.quoteTable(tableName), 'ON;',
generatedQuery,
'SET IDENTITY_INSERT', this.quoteTable(tableName), 'OFF;',
].join(' ');
}
return generatedQuery; if (!!options.limit) {
}, limit = ' TOP(' + this.escape(options.limit) + ')';
}
deleteQuery: function(tableName, where, options) { var replacements = {
options = options || {}; limit: limit,
table: table,
where: where,
};
var table = this.quoteTable(tableName); if (replacements.where) {
if (options.truncate === true) { replacements.where = ' WHERE ' + replacements.where;
// Truncate does not allow LIMIT and WHERE }
return 'TRUNCATE TABLE ' + table;
}
where = this.getWhereConditions(where); return Utils._.template(query)(replacements);
var limit = '' },
, query = 'DELETE<%= limit %> FROM <%= table %><%= where %>; ' +
'SELECT @@ROWCOUNT AS AFFECTEDROWS;';
if (Utils._.isUndefined(options.limit)) { showIndexesQuery: function(tableName) {
options.limit = 1; var sql = "EXEC sys.sp_helpindex @objname = N'<%= tableName %>';";
} return Utils._.template(sql)({
tableName: this.quoteTable(tableName)
});
},
if (!!options.limit) { removeIndexQuery: function(tableName, indexNameOrAttributes) {
limit = ' TOP(' + this.escape(options.limit) + ')'; var sql = 'DROP INDEX <%= indexName %> ON <%= tableName %>'
} , indexName = indexNameOrAttributes;
var replacements = { if (typeof indexName !== 'string') {
limit: limit, indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
table: table, }
where: where,
};
if (replacements.where) { var values = {
replacements.where = ' WHERE ' + replacements.where; tableName: this.quoteIdentifiers(tableName),
} indexName: indexName
};
return Utils._.template(query)(replacements); return Utils._.template(sql)(values);
}, },
showIndexesQuery: function(tableName) { attributeToSQL: function(attribute) {
var sql = "EXEC sys.sp_helpindex @objname = N'<%= tableName %>';"; if (!Utils._.isPlainObject(attribute)) {
return Utils._.template(sql)({ attribute = {
tableName: this.quoteTable(tableName) type: attribute
}); };
}, }
removeIndexQuery: function(tableName, indexNameOrAttributes) { // handle self referential constraints
var sql = 'DROP INDEX <%= indexName %> ON <%= tableName %>' if (attribute.references) {
, indexName = indexNameOrAttributes; attribute = Utils.formatReferences(attribute);
if (typeof indexName !== 'string') { if (attribute.Model && attribute.Model.tableName === attribute.references.model) {
indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_')); 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 values = { var template;
tableName: this.quoteIdentifiers(tableName),
indexName: indexName
};
return Utils._.template(sql)(values); if (attribute.type instanceof DataTypes.ENUM) {
}, if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
attributeToSQL: function(attribute) { // enums are a special case
if (!Utils._.isPlainObject(attribute)) { template = 'VARCHAR(10) NULL' /* + (attribute.allowNull ? 'NULL' : 'NOT NULL') */;
attribute = { template += ' CHECK (' + attribute.field + ' IN(' + Utils._.map(attribute.values, function(value) {
type: attribute return this.escape(value);
}; }.bind(this)).join(', ') + '))';
} return template;
} else {
template = attribute.type.toString();
}
// handle self referential constraints if (attribute.allowNull === false) {
if (attribute.references) { template += ' NOT NULL';
attribute = Utils.formatReferences(attribute); } else if (!attribute.primaryKey && !Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ' NULL';
}
if (attribute.Model && attribute.Model.tableName === attribute.references.model) { if (attribute.autoIncrement) {
this.sequelize.log('MSSQL does not support self referencial constraints, ' template += ' IDENTITY(1,1)';
+ 'we will remove it but we recommend restructuring your query'); }
attribute.onDelete = '';
attribute.onUpdate = ''; // Blobs/texts cannot have a defaultValue
} if (attribute.type !== 'TEXT' && attribute.type._binary !== true &&
} Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ' DEFAULT ' + this.escape(attribute.defaultValue);
}
var template; if (attribute.unique === true) {
template += ' UNIQUE';
}
if (attribute.primaryKey) {
template += ' PRIMARY KEY';
}
if (attribute.type instanceof DataTypes.ENUM) { if (attribute.references) {
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values; template += ' REFERENCES ' + this.quoteTable(attribute.references.model);
// enums are a special case if (attribute.references.key) {
template = 'VARCHAR(10) NULL' /* + (attribute.allowNull ? 'NULL' : 'NOT NULL') */; template += ' (' + this.quoteIdentifier(attribute.references.key) + ')';
template += ' CHECK (' + attribute.field + ' IN(' + Utils._.map(attribute.values, function(value) {
return this.escape(value);
}.bind(this)).join(', ') + '))';
return template;
} else { } else {
template = attribute.type.toString(); template += ' (' + this.quoteIdentifier('id') + ')';
} }
if (attribute.allowNull === false) { if (attribute.onDelete) {
template += ' NOT NULL'; template += ' ON DELETE ' + attribute.onDelete.toUpperCase();
} else if (!attribute.primaryKey && !Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ' NULL';
} }
if (attribute.autoIncrement) { if (attribute.onUpdate) {
template += ' IDENTITY(1,1)'; template += ' ON UPDATE ' + attribute.onUpdate.toUpperCase();
} }
}
// Blobs/texts cannot have a defaultValue return template;
if (attribute.type !== 'TEXT' && attribute.type._binary !== true && },
Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ' DEFAULT ' + this.escape(attribute.defaultValue);
}
if (attribute.unique === true) { attributesToSQL: function(attributes, options) {
template += ' UNIQUE'; var result = {}
} , key
, attribute
, existingConstraints = [];
if (attribute.primaryKey) { for (key in attributes) {
template += ' PRIMARY KEY'; attribute = attributes[key];
}
if (attribute.references) { if (attribute.references) {
template += ' REFERENCES ' + this.quoteTable(attribute.references.model); attribute = Utils.formatReferences(attributes[key]);
if (attribute.references.key) { if (existingConstraints.indexOf(attribute.references.model.toString()) !== -1) {
template += ' (' + this.quoteIdentifier(attribute.references.key) + ')'; // no cascading constraints to a table more than once
attribute.onDelete = '';
attribute.onUpdate = '';
} else { } else {
template += ' (' + this.quoteIdentifier('id') + ')'; existingConstraints.push(attribute.references.model.toString());
}
if (attribute.onDelete) { // NOTE: this really just disables cascading updates for all
template += ' ON DELETE ' + attribute.onDelete.toUpperCase(); // definitions. Can be made more robust to support the
// few cases where MSSQL actually supports them
attribute.onUpdate = '';
} }
if (attribute.onUpdate) {
template += ' ON UPDATE ' + attribute.onUpdate.toUpperCase();
}
} }
return template; if (key && !attribute.field) attribute.field = key;
}, result[attribute.field || key] = this.attributeToSQL(attribute, options);
}
attributesToSQL: function(attributes, options) {
var result = {}
, key
, attribute
, existingConstraints = [];
for (key in attributes) {
attribute = attributes[key];
if (attribute.references) {
attribute = Utils.formatReferences(attributes[key]);
if (existingConstraints.indexOf(attribute.references.model.toString()) !== -1) { return result;
// 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 findAutoIncrementField: function(factory) {
// definitions. Can be made more robust to support the var fields = [];
// few cases where MSSQL actually supports them for (var name in factory.attributes) {
attribute.onUpdate = ''; if (factory.attributes.hasOwnProperty(name)) {
} var definition = factory.attributes[name];
if (definition && definition.autoIncrement) {
fields.push(name);
} }
if (key && !attribute.field) attribute.field = key;
result[attribute.field || key] = this.attributeToSQL(attribute, options);
} }
}
return result; return fields;
}, },
findAutoIncrementField: function(factory) { createTrigger: function() {
var fields = []; throwMethodUndefined('createTrigger');
for (var name in factory.attributes) { },
if (factory.attributes.hasOwnProperty(name)) {
var definition = factory.attributes[name]; dropTrigger: function() {
throwMethodUndefined('dropTrigger');
},
renameTrigger: function() {
throwMethodUndefined('renameTrigger');
},
createFunction: function() {
throwMethodUndefined('createFunction');
},
dropFunction: function() {
throwMethodUndefined('dropFunction');
},
renameFunction: function() {
throwMethodUndefined('renameFunction');
},
quoteIdentifier: function(identifier, force) {
if (identifier === '*') return identifier;
return '[' + identifier.replace(/[\[\]']+/g,'') + ']';
},
getForeignKeysQuery: function(table, databaseName) {
var tableName = table.tableName || table;
var sql = [
'SELECT',
'constraint_name = C.CONSTRAINT_NAME',
'FROM',
'INFORMATION_SCHEMA.TABLE_CONSTRAINTS C',
"WHERE C.CONSTRAINT_TYPE = 'FOREIGN KEY'",
'AND C.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
if (table.schema) {
sql += ' AND C.TABLE_SCHEMA =' + wrapSingleQuote(table.schema);
}
if (definition && definition.autoIncrement) { return sql;
fields.push(name); },
}
}
}
return fields; dropForeignKeyQuery: function(tableName, foreignKey) {
}, return Utils._.template('ALTER TABLE <%= table %> DROP <%= key %>')({
table: this.quoteTable(tableName),
createTrigger: function() { key: this.quoteIdentifier(foreignKey)
throwMethodUndefined('createTrigger'); });
}, },
dropTrigger: function() {
throwMethodUndefined('dropTrigger');
},
renameTrigger: function() {
throwMethodUndefined('renameTrigger');
},
createFunction: function() {
throwMethodUndefined('createFunction');
},
dropFunction: function() {
throwMethodUndefined('dropFunction');
},
renameFunction: function() {
throwMethodUndefined('renameFunction');
},
quoteIdentifier: function(identifier, force) {
if (identifier === '*') return identifier;
return '[' + identifier.replace(/[\[\]']+/g,'') + ']';
},
getForeignKeysQuery: function(table, databaseName) {
var tableName = table.tableName || table;
var sql = [
'SELECT',
'constraint_name = C.CONSTRAINT_NAME',
'FROM',
'INFORMATION_SCHEMA.TABLE_CONSTRAINTS C',
"WHERE C.CONSTRAINT_TYPE = 'FOREIGN KEY'",
'AND C.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
if (table.schema) { setAutocommitQuery: function(value) {
sql += ' AND C.TABLE_SCHEMA =' + wrapSingleQuote(table.schema); return '';
} // return 'SET IMPLICIT_TRANSACTIONS ' + (!!value ? 'OFF' : 'ON') + ';';
},
return sql; setIsolationLevelQuery: function(value, options) {
}, if (options.parent) {
return;
}
dropForeignKeyQuery: function(tableName, foreignKey) { return 'SET TRANSACTION ISOLATION LEVEL ' + value + ';';
return Utils._.template('ALTER TABLE <%= table %> DROP <%= key %>')({ },
table: this.quoteTable(tableName),
key: this.quoteIdentifier(foreignKey)
});
},
setAutocommitQuery: function(value) { startTransactionQuery: function(transaction, options) {
return ''; if (options.parent) {
// return 'SET IMPLICIT_TRANSACTIONS ' + (!!value ? 'OFF' : 'ON') + ';'; return 'SAVE TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
}, }
setIsolationLevelQuery: function(value, options) { return 'BEGIN TRANSACTION;';
if (options.parent) { },
return;
}
return 'SET TRANSACTION ISOLATION LEVEL ' + value + ';'; commitTransactionQuery: function(options) {
}, if (options.parent) {
return;
}
startTransactionQuery: function(transaction, options) { return 'COMMIT TRANSACTION;';
if (options.parent) { },
return 'SAVE TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
}
return 'BEGIN TRANSACTION;'; rollbackTransactionQuery: function(transaction, options) {
}, if (options.parent) {
return 'ROLLBACK TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
}
commitTransactionQuery: function(options) { return 'ROLLBACK TRANSACTION;';
if (options.parent) { },
return;
addLimitAndOffset: function(options, model) {
var fragment = '';
var offset = options.offset || 0
, isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation;
// FIXME: This is ripped from selectQuery to determine whether there is already
// an ORDER BY added for a subquery. Should be refactored so we dont' need
// the duplication. Also consider moving this logic inside the options.order
// check, so that we aren't compiling this twice for every invocation.
var mainQueryOrder = [];
var subQueryOrder = [];
if (options.order) {
if (Array.isArray(options.order)) {
options.order.forEach(function(t) {
if (!Array.isArray(t)) {
if (isSubQuery && !(t instanceof Model) && !(t.model instanceof Model)) {
subQueryOrder.push(this.quote(t, model));
}
} else {
if (isSubQuery && !(t[0] instanceof Model) && !(t[0].model instanceof Model)) {
subQueryOrder.push(this.quote(t, model));
}
mainQueryOrder.push(this.quote(t, model));
}
}.bind(this));
} else {
mainQueryOrder.push(options.order);
} }
}
return 'COMMIT TRANSACTION;'; if (options.limit || options.offset) {
}, if (!options.order || (options.include && !subQueryOrder.length)) {
fragment += (options.order && !isSubQuery) ? ', ' : ' ORDER BY ';
rollbackTransactionQuery: function(transaction, options) { fragment += this.quoteIdentifier(model.primaryKeyAttribute);
if (options.parent) {
return 'ROLLBACK TRANSACTION ' + this.quoteIdentifier(transaction.name) + ';';
} }
return 'ROLLBACK TRANSACTION;'; if (options.offset || options.limit) {
}, fragment += ' OFFSET ' + offset + ' ROWS';
addLimitAndOffset: function(options, model) {
var fragment = '';
var offset = options.offset || 0
, isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation;
// FIXME: This is ripped from selectQuery to determine whether there is already
// an ORDER BY added for a subquery. Should be refactored so we dont' need
// the duplication. Also consider moving this logic inside the options.order
// check, so that we aren't compiling this twice for every invocation.
var mainQueryOrder = [];
var subQueryOrder = [];
if (options.order) {
if (Array.isArray(options.order)) {
options.order.forEach(function(t) {
if (!Array.isArray(t)) {
if (isSubQuery && !(t instanceof Model) && !(t.model instanceof Model)) {
subQueryOrder.push(this.quote(t, model));
}
} else {
if (isSubQuery && !(t[0] instanceof Model) && !(t[0].model instanceof Model)) {
subQueryOrder.push(this.quote(t, model));
}
mainQueryOrder.push(this.quote(t, model));
}
}.bind(this));
} else {
mainQueryOrder.push(options.order);
}
} }
if (options.limit || options.offset) { if (options.limit) {
if (!options.order || (options.include && !subQueryOrder.length)) { fragment += ' FETCH NEXT ' + options.limit + ' ROWS ONLY';
fragment += (options.order && !isSubQuery) ? ', ' : ' ORDER BY ';
fragment += this.quoteIdentifier(model.primaryKeyAttribute);
}
if (options.offset || options.limit) {
fragment += ' OFFSET ' + offset + ' ROWS';
}
if (options.limit) {
fragment += ' FETCH NEXT ' + options.limit + ' ROWS ONLY';
}
} }
return fragment;
},
booleanValue: function(value) {
return !!value ? 1 : 0;
} }
};
// private methods return fragment;
function wrapSingleQuote(identifier){ },
return Utils.addTicks(identifier, "'");
booleanValue: function(value) {
return !!value ? 1 : 0;
} }
};
/* istanbul ignore next */ // private methods
var throwMethodUndefined = function(methodName) { function wrapSingleQuote(identifier){
throw new Error('The method "' + methodName + '" is not defined! Please add it to your sql dialect.'); return Utils.addTicks(identifier, "'");
}; }
return Utils._.extend(Utils._.clone(AbstractQueryGenerator), QueryGenerator); module.exports = Utils._.extend(Utils._.clone(AbstractQueryGenerator), QueryGenerator);
})();
...@@ -4,284 +4,282 @@ var Utils = require('../../utils') ...@@ -4,284 +4,282 @@ var Utils = require('../../utils')
, AbstractQuery = require('../abstract/query') , AbstractQuery = require('../abstract/query')
, sequelizeErrors = require('../../errors.js'); , sequelizeErrors = require('../../errors.js');
module.exports = (function() { var Query = function(connection, sequelize, options) {
var Query = function(connection, sequelize, options) { this.connection = connection;
this.connection = connection; this.instance = options.instance;
this.instance = options.instance; this.model = options.model;
this.model = options.model; this.sequelize = sequelize;
this.sequelize = sequelize; this.options = Utils._.extend({
this.options = Utils._.extend({ logging: console.log,
logging: console.log, plain: false,
plain: false, raw: false
raw: false }, options || {});
}, options || {});
this.checkLoggingOption();
this.checkLoggingOption(); };
};
Utils.inherit(Query, AbstractQuery);
Utils.inherit(Query, AbstractQuery);
Query.prototype.getInsertIdField = function() {
Query.prototype.getInsertIdField = function() { return 'id';
return 'id'; };
};
Query.prototype.run = function(sql) {
Query.prototype.run = function(sql) { var self = this;
var self = this; this.sql = sql;
this.sql = sql;
this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
var promise = new Utils.Promise(function(resolve, reject) {
var promise = new Utils.Promise(function(resolve, reject) { // TRANSACTION SUPPORT
// TRANSACTION SUPPORT if (Utils._.contains(self.sql, 'BEGIN TRANSACTION')) {
if (Utils._.contains(self.sql, 'BEGIN TRANSACTION')) { self.connection.beginTransaction(function(err) {
self.connection.beginTransaction(function(err) { if (!!err) {
if (!!err) { reject(self.formatError(err));
reject(self.formatError(err)); } else {
} else { resolve(self.formatResults());
resolve(self.formatResults()); }
} } /* name, isolation_level */);
} /* name, isolation_level */); } else if (Utils._.contains(self.sql, 'COMMIT TRANSACTION')) {
} else if (Utils._.contains(self.sql, 'COMMIT TRANSACTION')) { self.connection.commitTransaction(function(err) {
self.connection.commitTransaction(function(err) { if (!!err) {
if (!!err) { reject(self.formatError(err));
reject(self.formatError(err)); } else {
} else { resolve(self.formatResults());
resolve(self.formatResults()); }
} });
}); } else if (Utils._.contains(self.sql, 'ROLLBACK TRANSACTION')) {
} else if (Utils._.contains(self.sql, 'ROLLBACK TRANSACTION')) { self.connection.rollbackTransaction(function(err) {
self.connection.rollbackTransaction(function(err) { if (!!err) {
if (!!err) { reject(self.formatError(err));
reject(self.formatError(err)); } else {
} else { resolve(self.formatResults());
resolve(self.formatResults()); }
} });
}); } else {
} else { // QUERY SUPPORT
// QUERY SUPPORT var results = [];
var results = [];
var request = new self.connection.lib.Request(self.sql, function(err) {
var request = new self.connection.lib.Request(self.sql, function(err) { if (err) {
if (err) { err.sql = sql;
err.sql = sql; reject(self.formatError(err));
reject(self.formatError(err)); } else {
} else { resolve(self.formatResults(results));
resolve(self.formatResults(results)); }
} });
});
request.on('row', function(columns) {
var row = {};
columns.forEach(function(column) {
row[column.metadata.colName] = column.value;
});
results.push(row); request.on('row', function(columns) {
var row = {};
columns.forEach(function(column) {
row[column.metadata.colName] = column.value;
}); });
self.connection.execSql(request); results.push(row);
} });
});
return promise; self.connection.execSql(request);
}; }
});
/**
* High level function that handles the results of a query execution. return promise;
* };
*
* Example: /**
* query.formatResults([ * High level function that handles the results of a query execution.
* { *
* id: 1, // this is from the main table *
* attr2: 'snafu', // this is from the main table * Example:
* Tasks.id: 1, // this is from the associated table * query.formatResults([
* Tasks.title: 'task' // this is from the associated table * {
* } * 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
* @param {Array} data - The result of the query execution. * Tasks.title: 'task' // this is from the associated table
*/ * }
Query.prototype.formatResults = function(data) { * ])
var result = this.instance; *
if (this.isInsertQuery(data)) { * @param {Array} data - The result of the query execution.
this.handleInsertQuery(data); */
Query.prototype.formatResults = function(data) {
if (!this.instance) { var result = this.instance;
if (this.options.plain) { if (this.isInsertQuery(data)) {
// NOTE: super contrived. This just passes the newly added query-interface this.handleInsertQuery(data);
// 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 if (!this.instance) {
// because there was no calling Model. if (this.options.plain) {
var record = data[0]; // NOTE: super contrived. This just passes the newly added query-interface
result = record[Object.keys(record)[0]]; // test returning only the PK. There isn't a way in MSSQL to identify
} else { // that a given return value is the PK, and we have no schema information
result = data; // because there was no calling Model.
} var record = data[0];
result = record[Object.keys(record)[0]];
} else {
result = data;
} }
} }
}
if (this.isShowTablesQuery()) {
result = this.handleShowTablesQuery(data); if (this.isShowTablesQuery()) {
} else if (this.isDescribeQuery()) { result = this.handleShowTablesQuery(data);
result = {}; } else if (this.isDescribeQuery()) {
data.forEach(function(_result) { result = {};
if (_result.Default) data.forEach(function(_result) {
_result.Default = _result.Default.replace("('",'').replace("')",'').replace(/'/g,''); /* jshint ignore: line */ if (_result.Default)
_result.Default = _result.Default.replace("('",'').replace("')",'').replace(/'/g,''); /* jshint ignore: line */
result[_result.Name] = {
type: _result.Type.toUpperCase(), result[_result.Name] = {
allowNull: (_result.IsNull === 'YES' ? true : false), type: _result.Type.toUpperCase(),
defaultValue: _result.Default allowNull: (_result.IsNull === 'YES' ? true : false),
}; defaultValue: _result.Default
});
} else if (this.isShowIndexesQuery()) {
result = this.handleShowIndexesQuery(data);
} else if (this.isSelectQuery()) {
result = this.handleSelectQuery(data);
} else if (this.isCallQuery()) {
result = data[0];
} else if (this.isBulkUpdateQuery()) {
result = data.length;
} else if (this.isBulkDeleteQuery()){
result = data[0] && data[0].AFFECTEDROWS;
} else if (this.isVersionQuery()) {
result = data[0].version;
} else if (this.isForeignKeysQuery()) {
result = data;
} else if (this.isRawQuery()) {
// MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
result = [data, data];
}
return result;
};
Query.prototype.handleShowTablesQuery = function(results) {
return results.map(function(resultSet) {
return {
tableName: resultSet.TABLE_NAME,
schema: resultSet.TABLE_SCHEMA
}; };
}); });
}; } else if (this.isShowIndexesQuery()) {
result = this.handleShowIndexesQuery(data);
Query.prototype.formatError = function (err) { } else if (this.isSelectQuery()) {
var match; result = this.handleSelectQuery(data);
match = err.message.match(/Violation of UNIQUE KEY constraint '((.|\s)*)'. Cannot insert duplicate key in object '.*'. The duplicate key value is \((.*)\)./); } else if (this.isCallQuery()) {
match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/); result = data[0];
if (match && match.length > 1) { } else if (this.isBulkUpdateQuery()) {
var fields = {} result = data.length;
, message = 'Validation error' } else if (this.isBulkDeleteQuery()){
, uniqueKey = this.model.uniqueKeys[match[1]]; result = data[0] && data[0].AFFECTEDROWS;
} else if (this.isVersionQuery()) {
if (!!uniqueKey.msg) message = uniqueKey.msg; result = data[0].version;
if (!!match[2]) { } else if (this.isForeignKeysQuery()) {
var values = match[2].split(',').map(Function.prototype.call, String.prototype.trim); result = data;
if (!!uniqueKey) { } else if (this.isRawQuery()) {
fields = Utils._.zipObject(uniqueKey.fields, values); // MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
} else { result = [data, data];
fields[match[1]] = match[2]; }
}
return result;
};
Query.prototype.handleShowTablesQuery = function(results) {
return results.map(function(resultSet) {
return {
tableName: resultSet.TABLE_NAME,
schema: resultSet.TABLE_SCHEMA
};
});
};
Query.prototype.formatError = function (err) {
var match;
match = err.message.match(/Violation of UNIQUE 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) {
var fields = {}
, message = 'Validation error'
, uniqueKey = this.model.uniqueKeys[match[1]];
if (!!uniqueKey.msg) message = uniqueKey.msg;
if (!!match[2]) {
var values = match[2].split(',').map(Function.prototype.call, String.prototype.trim);
if (!!uniqueKey) {
fields = Utils._.zipObject(uniqueKey.fields, values);
} else {
fields[match[1]] = match[2];
} }
}
var errors = []; var errors = [];
Utils._.forOwn(fields, function(value, field) { Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem( errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value)); field + ' must be unique', 'unique violation', field, value));
}); });
return new sequelizeErrors.UniqueConstraintError({ return new sequelizeErrors.UniqueConstraintError({
message: message, message: message,
errors: errors, errors: errors,
parent: err, parent: err,
fields: fields fields: fields
}); });
} }
match = err.message.match(/Failed on step '(.*)'.Could not create constraint. See previous errors./);
match = err.message.match(/The DELETE statement conflicted with the REFERENCE 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(/Failed on step '(.*)'.Could not create constraint. See previous errors./); return new sequelizeErrors.DatabaseError(err);
match = err.message.match(/The DELETE statement conflicted with the REFERENCE constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./); };
if (match && match.length > 0) {
return new sequelizeErrors.ForeignKeyConstraintError({
fields: null,
index: match[1],
parent: err
});
}
return new sequelizeErrors.DatabaseError(err); Query.prototype.isShowOrDescribeQuery = function() {
}; var result = false;
Query.prototype.isShowOrDescribeQuery = function() { result = result || (this.sql.toLowerCase().indexOf("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'") === 0); /* jshint ignore: line */
var result = false; result = result || (this.sql.toLowerCase().indexOf('select tablename = t.name, name = ind.name,') === 0);
result = result || (this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0);
result = result || (this.sql.toLowerCase().indexOf("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'") === 0); /* jshint ignore: line */ return result;
result = result || (this.sql.toLowerCase().indexOf('select tablename = t.name, name = ind.name,') === 0); };
result = result || (this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0);
return result; Query.prototype.isShowIndexesQuery = function () {
}; return this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0;
};
Query.prototype.isShowIndexesQuery = function () { Query.prototype.handleShowIndexesQuery = function (data) {
return this.sql.toLowerCase().indexOf('exec sys.sp_helpindex @objname') === 0; // Group by index name, and collect all fields
}; data = Utils._.foldl(data, function (acc, item) {
if (!(item.index_name in acc)) {
acc[item.index_name] = item;
item.fields = [];
}
Query.prototype.handleShowIndexesQuery = function (data) { Utils._.forEach(item.index_keys.split(','), function(column) {
// Group by index name, and collect all fields var columnName = column.trim();
data = Utils._.foldl(data, function (acc, item) { if (columnName.indexOf('(-)') !== -1) {
if (!(item.index_name in acc)) { columnName = columnName.replace('(-)','');
acc[item.index_name] = item;
item.fields = [];
} }
Utils._.forEach(item.index_keys.split(','), function(column) { acc[item.index_name].fields.push({
var columnName = column.trim(); attribute: columnName,
if (columnName.indexOf('(-)') !== -1) { length: undefined,
columnName = columnName.replace('(-)',''); order: (column.indexOf('(-)') !== -1 ? 'DESC' : 'ASC'),
} collate: undefined
acc[item.index_name].fields.push({
attribute: columnName,
length: undefined,
order: (column.indexOf('(-)') !== -1 ? 'DESC' : 'ASC'),
collate: undefined
});
}); });
delete item.index_keys;
return acc;
}, {});
return Utils._.map(data, function(item) {
return {
primary: (item.index_name.toLowerCase().indexOf('pk') === 0),
fields: item.fields,
name: item.index_name,
tableName: undefined,
unique: (item.index_description.toLowerCase().indexOf('unique') !== -1),
type: undefined,
};
}); });
}; delete item.index_keys;
return acc;
Query.prototype.handleInsertQuery = function(results, metaData) { }, {});
if (this.instance) {
// add the inserted row id to the instance return Utils._.map(data, function(item) {
var autoIncrementField = this.model.autoIncrementField return {
, autoIncrementFieldAlias = null primary: (item.index_name.toLowerCase().indexOf('pk') === 0),
, id = null; fields: item.fields,
name: item.index_name,
if (this.model.rawAttributes.hasOwnProperty(autoIncrementField) && tableName: undefined,
this.model.rawAttributes[autoIncrementField].field !== undefined) unique: (item.index_description.toLowerCase().indexOf('unique') !== -1),
autoIncrementFieldAlias = this.model.rawAttributes[autoIncrementField].field ; type: undefined,
};
id = id || (results && results[0][this.getInsertIdField()]); });
id = id || (metaData && metaData[this.getInsertIdField()]); };
id = id || (results && results[0][autoIncrementField]);
id = id || (autoIncrementFieldAlias && results && results[0][autoIncrementFieldAlias]); Query.prototype.handleInsertQuery = function(results, metaData) {
if (this.instance) {
this.instance[autoIncrementField] = id; // add the inserted row id to the instance
} var autoIncrementField = this.model.autoIncrementField
}; , autoIncrementFieldAlias = null
, id = null;
return Query;
})(); if (this.model.rawAttributes.hasOwnProperty(autoIncrementField) &&
this.model.rawAttributes[autoIncrementField].field !== undefined)
autoIncrementFieldAlias = this.model.rawAttributes[autoIncrementField].field ;
id = id || (results && results[0][this.getInsertIdField()]);
id = id || (metaData && metaData[this.getInsertIdField()]);
id = id || (results && results[0][autoIncrementField]);
id = id || (autoIncrementFieldAlias && results && results[0][autoIncrementFieldAlias]);
this.instance[autoIncrementField] = id;
}
};
module.exports = Query;
...@@ -3,376 +3,374 @@ ...@@ -3,376 +3,374 @@
var Utils = require('../../utils') var Utils = require('../../utils')
, DataTypes = require('../../data-types'); , DataTypes = require('../../data-types');
module.exports = (function() { var QueryGenerator = {
var QueryGenerator = { dialect: 'mysql',
dialect: 'mysql',
createSchema: function() {
createSchema: function() { var query = 'SHOW TABLES';
var query = 'SHOW TABLES'; return Utils._.template(query)({});
return Utils._.template(query)({}); },
},
showSchemasQuery: function() {
showSchemasQuery: function() { return 'SHOW TABLES';
return 'SHOW TABLES'; },
},
versionQuery: function() {
versionQuery: function() { return 'SELECT VERSION() as `version`';
return 'SELECT VERSION() as `version`'; },
},
createTableQuery: function(tableName, attributes, options) {
createTableQuery: function(tableName, attributes, options) { options = Utils._.extend({
options = Utils._.extend({ engine: 'InnoDB',
engine: 'InnoDB', charset: null
charset: null }, options || {});
}, options || {});
var self = this;
var self = this;
var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>) ENGINE=<%= engine %><%= comment %><%= charset %><%= collation %><%= initialAutoIncrement %>'
var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>) ENGINE=<%= engine %><%= comment %><%= charset %><%= collation %><%= initialAutoIncrement %>' , primaryKeys = []
, primaryKeys = [] , foreignKeys = {}
, foreignKeys = {} , attrStr = [];
, attrStr = [];
for (var attr in attributes) {
for (var attr in attributes) { if (attributes.hasOwnProperty(attr)) {
if (attributes.hasOwnProperty(attr)) { var dataType = attributes[attr]
var dataType = attributes[attr] , match;
, match;
if (Utils._.includes(dataType, 'PRIMARY KEY')) {
if (Utils._.includes(dataType, 'PRIMARY KEY')) { primaryKeys.push(attr);
primaryKeys.push(attr);
if (Utils._.includes(dataType, 'REFERENCES')) {
if (Utils._.includes(dataType, 'REFERENCES')) { // MySQL doesn't support inline REFERENCES declarations: move to the end
// 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.*)$/); match = dataType.match(/^(.+) (REFERENCES.*)$/);
attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1]); attrStr.push(this.quoteIdentifier(attr) + ' ' + match[1].replace(/PRIMARY KEY/, ''));
foreignKeys[attr] = match[2]; foreignKeys[attr] = match[2];
} else { } else {
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType); 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 = { var values = {
table: this.quoteTable(tableName), table: this.quoteTable(tableName),
attributes: attrStr.join(', '), attributes: attrStr.join(', '),
comment: options.comment && Utils._.isString(options.comment) ? ' COMMENT ' + this.escape(options.comment) : '', comment: options.comment && Utils._.isString(options.comment) ? ' COMMENT ' + this.escape(options.comment) : '',
engine: options.engine, engine: options.engine,
charset: (options.charset ? ' DEFAULT CHARSET=' + options.charset : ''), charset: (options.charset ? ' DEFAULT CHARSET=' + options.charset : ''),
collation: (options.collate ? ' COLLATE ' + options.collate : ''), collation: (options.collate ? ' COLLATE ' + options.collate : ''),
initialAutoIncrement: (options.initialAutoIncrement ? ' AUTO_INCREMENT=' + options.initialAutoIncrement : '') initialAutoIncrement: (options.initialAutoIncrement ? ' AUTO_INCREMENT=' + options.initialAutoIncrement : '')
} }
, pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', '); , pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', ');
if (!!options.uniqueKeys) { if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns, indexName) { Utils._.each(options.uniqueKeys, function(columns, indexName) {
if (!columns.singleField) { // If it's a single field its handled in column def, not as an index if (!columns.singleField) { // If it's a single field its handled in column def, not as an index
if (!Utils._.isString(indexName)) { if (!Utils._.isString(indexName)) {
indexName = 'uniq_' + tableName + '_' + columns.fields.join('_'); indexName = 'uniq_' + tableName + '_' + columns.fields.join('_');
}
values.attributes += ', UNIQUE ' + self.quoteIdentifier(indexName) + ' (' + Utils._.map(columns.fields, self.quoteIdentifier).join(', ') + ')';
} }
}); values.attributes += ', UNIQUE ' + self.quoteIdentifier(indexName) + ' (' + Utils._.map(columns.fields, self.quoteIdentifier).join(', ') + ')';
} }
});
}
if (pkString.length > 0) { if (pkString.length > 0) {
values.attributes += ', PRIMARY KEY (' + pkString + ')'; values.attributes += ', PRIMARY KEY (' + pkString + ')';
} }
for (var fkey in foreignKeys) { for (var fkey in foreignKeys) {
if (foreignKeys.hasOwnProperty(fkey)) { if (foreignKeys.hasOwnProperty(fkey)) {
values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey]; values.attributes += ', FOREIGN KEY (' + this.quoteIdentifier(fkey) + ') ' + foreignKeys[fkey];
}
} }
}
return Utils._.template(query)(values).trim() + ';'; return Utils._.template(query)(values).trim() + ';';
}, },
showTablesQuery: function() {
return 'SHOW TABLES;';
},
addColumnQuery: function(table, key, dataType) {
var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'
, attribute = Utils._.template('<%= key %> <%= definition %>')({
key: this.quoteIdentifier(key),
definition: this.attributeToSQL(dataType, {
context: 'addColumn'
})
});
return Utils._.template(query)({
table: this.quoteTable(table),
attribute: attribute
});
},
removeColumnQuery: function(tableName, attributeName) {
var query = 'ALTER TABLE <%= tableName %> DROP <%= attributeName %>;';
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
attributeName: this.quoteIdentifier(attributeName)
});
},
changeColumnQuery: function(tableName, attributes) { showTablesQuery: function() {
var query = 'ALTER TABLE <%= tableName %> CHANGE <%= attributes %>;'; return 'SHOW TABLES;';
var attrString = []; },
for (var attrName in attributes) { addColumnQuery: function(table, key, dataType) {
var definition = attributes[attrName]; var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'
, attribute = Utils._.template('<%= key %> <%= definition %>')({
key: this.quoteIdentifier(key),
definition: this.attributeToSQL(dataType, {
context: 'addColumn'
})
});
attrString.push(Utils._.template('`<%= attrName %>` `<%= attrName %>` <%= definition %>')({ return Utils._.template(query)({
attrName: attrName, table: this.quoteTable(table),
definition: definition attribute: attribute
})); });
} },
removeColumnQuery: function(tableName, attributeName) {
var query = 'ALTER TABLE <%= tableName %> DROP <%= attributeName %>;';
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
attributeName: this.quoteIdentifier(attributeName)
});
},
changeColumnQuery: function(tableName, attributes) {
var query = 'ALTER TABLE <%= tableName %> CHANGE <%= attributes %>;';
var attrString = [];
for (var attrName in attributes) {
var definition = attributes[attrName];
attrString.push(Utils._.template('`<%= attrName %>` `<%= attrName %>` <%= definition %>')({
attrName: attrName,
definition: definition
}));
}
return Utils._.template(query)({ tableName: this.quoteTable(tableName), attributes: attrString.join(', ') }); return Utils._.template(query)({ tableName: this.quoteTable(tableName), attributes: attrString.join(', ') });
}, },
renameColumnQuery: function(tableName, attrBefore, attributes) { renameColumnQuery: function(tableName, attrBefore, attributes) {
var query = 'ALTER TABLE <%= tableName %> CHANGE <%= attributes %>;'; var query = 'ALTER TABLE <%= tableName %> CHANGE <%= attributes %>;';
var attrString = []; var attrString = [];
for (var attrName in attributes) { for (var attrName in attributes) {
var definition = attributes[attrName]; var definition = attributes[attrName];
attrString.push(Utils._.template('`<%= before %>` `<%= after %>` <%= definition %>')({ attrString.push(Utils._.template('`<%= before %>` `<%= after %>` <%= definition %>')({
before: attrBefore, before: attrBefore,
after: attrName, after: attrName,
definition: definition definition: definition
})); }));
} }
return Utils._.template(query)({ tableName: this.quoteTable(tableName), attributes: attrString.join(', ') }); return Utils._.template(query)({ tableName: this.quoteTable(tableName), attributes: attrString.join(', ') });
}, },
upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) { upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) {
options.onDuplicate = 'UPDATE '; options.onDuplicate = 'UPDATE ';
options.onDuplicate += Object.keys(updateValues).map(function (key) { options.onDuplicate += Object.keys(updateValues).map(function (key) {
key = this.quoteIdentifier(key); key = this.quoteIdentifier(key);
return key + '=VALUES(' + key +')'; return key + '=VALUES(' + key +')';
}, this).join(', '); }, this).join(', ');
return this.insertQuery(tableName, insertValues, rawAttributes, options); return this.insertQuery(tableName, insertValues, rawAttributes, options);
}, },
bulkInsertQuery: function(tableName, attrValueHashes, options) { bulkInsertQuery: function(tableName, attrValueHashes, options) {
var query = 'INSERT<%= ignoreDuplicates %> INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %><%= onDuplicateKeyUpdate %>;' var query = 'INSERT<%= ignoreDuplicates %> INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %><%= onDuplicateKeyUpdate %>;'
, tuples = [] , tuples = []
, allAttributes = [] , allAttributes = []
, onDuplicateKeyUpdate = ''; , onDuplicateKeyUpdate = '';
Utils._.forEach(attrValueHashes, function(attrValueHash) { Utils._.forEach(attrValueHashes, function(attrValueHash) {
Utils._.forOwn(attrValueHash, function(value, key) { Utils._.forOwn(attrValueHash, function(value, key) {
if (allAttributes.indexOf(key) === -1) allAttributes.push(key); if (allAttributes.indexOf(key) === -1) allAttributes.push(key);
});
}); });
});
Utils._.forEach(attrValueHashes, function(attrValueHash) {
tuples.push('(' +
allAttributes.map(function(key) {
return this.escape(attrValueHash[key]);
}.bind(this)).join(',') +
')');
}.bind(this));
if (options && options.updateOnDuplicate) {
onDuplicateKeyUpdate += ' ON DUPLICATE KEY UPDATE ' + options.updateOnDuplicate.map(function(attr) {
var key = this.quoteIdentifier(attr);
return key + '=VALUES(' + key + ')';
}.bind(this)).join(',');
}
Utils._.forEach(attrValueHashes, function(attrValueHash) { var replacements = {
tuples.push('(' + ignoreDuplicates: options && options.ignoreDuplicates ? ' IGNORE' : '',
allAttributes.map(function(key) { table: this.quoteTable(tableName),
return this.escape(attrValueHash[key]); attributes: allAttributes.map(function(attr) {
}.bind(this)).join(',') + return this.quoteIdentifier(attr);
')'); }.bind(this)).join(','),
}.bind(this)); tuples: tuples,
onDuplicateKeyUpdate: onDuplicateKeyUpdate
if (options && options.updateOnDuplicate) { };
onDuplicateKeyUpdate += ' ON DUPLICATE KEY UPDATE ' + options.updateOnDuplicate.map(function(attr) {
var key = this.quoteIdentifier(attr); return Utils._.template(query)(replacements);
return key + '=VALUES(' + key + ')'; },
}.bind(this)).join(',');
} deleteQuery: function(tableName, where, options) {
options = options || {};
var replacements = {
ignoreDuplicates: options && options.ignoreDuplicates ? ' IGNORE' : '', var table = this.quoteTable(tableName);
table: this.quoteTable(tableName), if (options.truncate === true) {
attributes: allAttributes.map(function(attr) { // Truncate does not allow LIMIT and WHERE
return this.quoteIdentifier(attr); return 'TRUNCATE ' + table;
}.bind(this)).join(','), }
tuples: tuples,
onDuplicateKeyUpdate: onDuplicateKeyUpdate
};
return Utils._.template(query)(replacements); where = this.getWhereConditions(where);
}, var limit = '';
deleteQuery: function(tableName, where, options) { if (Utils._.isUndefined(options.limit)) {
options = options || {}; options.limit = 1;
}
var table = this.quoteTable(tableName); if (!!options.limit) {
if (options.truncate === true) { limit = ' LIMIT ' + this.escape(options.limit);
// Truncate does not allow LIMIT and WHERE }
return 'TRUNCATE ' + table;
}
where = this.getWhereConditions(where); var query = 'DELETE FROM ' + table;
var limit = ''; if (where) query += ' WHERE ' + where;
query += limit;
if (Utils._.isUndefined(options.limit)) { return query;
options.limit = 1; },
}
if (!!options.limit) { showIndexesQuery: function(tableName, options) {
limit = ' LIMIT ' + this.escape(options.limit); var sql = 'SHOW INDEX FROM <%= tableName %><%= options %>';
} return Utils._.template(sql)({
tableName: this.quoteTable(tableName),
options: (options || {}).database ? ' FROM `' + options.database + '`' : ''
});
},
var query = 'DELETE FROM ' + table; removeIndexQuery: function(tableName, indexNameOrAttributes) {
if (where) query += ' WHERE ' + where; var sql = 'DROP INDEX <%= indexName %> ON <%= tableName %>'
query += limit; , indexName = indexNameOrAttributes;
return query; if (typeof indexName !== 'string') {
}, indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
}
showIndexesQuery: function(tableName, options) { return Utils._.template(sql)({ tableName: this.quoteIdentifiers(tableName), indexName: indexName });
var sql = 'SHOW INDEX FROM <%= tableName %><%= options %>'; },
return Utils._.template(sql)({
tableName: this.quoteTable(tableName),
options: (options || {}).database ? ' FROM `' + options.database + '`' : ''
});
},
removeIndexQuery: function(tableName, indexNameOrAttributes) { attributeToSQL: function(attribute) {
var sql = 'DROP INDEX <%= indexName %> ON <%= tableName %>' if (!Utils._.isPlainObject(attribute)) {
, indexName = indexNameOrAttributes; attribute = {
type: attribute
};
}
if (typeof indexName !== 'string') { var template;
indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
}
return Utils._.template(sql)({ tableName: this.quoteIdentifiers(tableName), indexName: indexName }); if (attribute.type instanceof DataTypes.ENUM) {
}, if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
template = 'ENUM(' + Utils._.map(attribute.values, function(value) {
return this.escape(value);
}.bind(this)).join(', ') + ')';
} else {
template = attribute.type.toString();
}
attributeToSQL: function(attribute) { if (attribute.allowNull === false) {
if (!Utils._.isPlainObject(attribute)) { template += ' NOT NULL';
attribute = { }
type: attribute
};
}
var template; if (attribute.autoIncrement) {
template += ' auto_increment';
}
if (attribute.type instanceof DataTypes.ENUM) { // Blobs/texts cannot have a defaultValue
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values; if (attribute.type !== 'TEXT' && attribute.type._binary !== true && Utils.defaultValueSchemable(attribute.defaultValue)) {
template = 'ENUM(' + Utils._.map(attribute.values, function(value) { template += ' DEFAULT ' + this.escape(attribute.defaultValue);
return this.escape(value); }
}.bind(this)).join(', ') + ')';
} else {
template = attribute.type.toString();
}
if (attribute.allowNull === false) { if (attribute.unique === true) {
template += ' NOT NULL'; template += ' UNIQUE';
} }
if (attribute.autoIncrement) { if (attribute.primaryKey) {
template += ' auto_increment'; template += ' PRIMARY KEY';
} }
// Blobs/texts cannot have a defaultValue if (attribute.after) {
if (attribute.type !== 'TEXT' && attribute.type._binary !== true && Utils.defaultValueSchemable(attribute.defaultValue)) { template += ' AFTER ' + this.quoteIdentifier(attribute.after);
template += ' DEFAULT ' + this.escape(attribute.defaultValue); }
}
if (attribute.unique === true) { if (attribute.references) {
template += ' UNIQUE'; attribute = Utils.formatReferences(attribute);
} template += ' REFERENCES ' + this.quoteTable(attribute.references.model);
if (attribute.primaryKey) { if (attribute.references.key) {
template += ' PRIMARY KEY'; template += ' (' + this.quoteIdentifier(attribute.references.key) + ')';
} else {
template += ' (' + this.quoteIdentifier('id') + ')';
} }
if (attribute.after) { if (attribute.onDelete) {
template += ' AFTER ' + this.quoteIdentifier(attribute.after); template += ' ON DELETE ' + attribute.onDelete.toUpperCase();
} }
if (attribute.references) { if (attribute.onUpdate) {
attribute = Utils.formatReferences(attribute); template += ' ON UPDATE ' + attribute.onUpdate.toUpperCase();
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; return template;
}, },
attributesToSQL: function(attributes, options) { attributesToSQL: function(attributes, options) {
var result = {} var result = {}
, key , key
, attribute; , attribute;
for (key in attributes) { for (key in attributes) {
attribute = attributes[key]; attribute = attributes[key];
result[attribute.field || key] = this.attributeToSQL(attribute, options); result[attribute.field || key] = this.attributeToSQL(attribute, options);
} }
return result; return result;
}, },
findAutoIncrementField: function(factory) { findAutoIncrementField: function(factory) {
var fields = []; var fields = [];
for (var name in factory.attributes) { for (var name in factory.attributes) {
if (factory.attributes.hasOwnProperty(name)) { if (factory.attributes.hasOwnProperty(name)) {
var definition = factory.attributes[name]; var definition = factory.attributes[name];
if (definition && definition.autoIncrement) { if (definition && definition.autoIncrement) {
fields.push(name); fields.push(name);
}
} }
} }
return fields;
},
quoteIdentifier: function(identifier) {
if (identifier === '*') return identifier;
return Utils.addTicks(identifier, '`');
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
return "SELECT CONSTRAINT_NAME as constraint_name FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '" + tableName + /* jshint ignore: line */
"' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='" + schemaName + "' AND REFERENCED_TABLE_NAME IS NOT NULL;"; /* jshint ignore: line */
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
dropForeignKeyQuery: function(tableName, foreignKey) {
return 'ALTER TABLE ' + this.quoteTable(tableName) + ' DROP FOREIGN KEY ' + this.quoteIdentifier(foreignKey) + ';';
} }
};
return Utils._.extend(Utils._.clone(require('../abstract/query-generator')), QueryGenerator); return fields;
})(); },
quoteIdentifier: function(identifier) {
if (identifier === '*') return identifier;
return Utils.addTicks(identifier, '`');
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
return "SELECT CONSTRAINT_NAME as constraint_name FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '" + tableName + /* jshint ignore: line */
"' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='" + schemaName + "' AND REFERENCED_TABLE_NAME IS NOT NULL;"; /* jshint ignore: line */
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
dropForeignKeyQuery: function(tableName, foreignKey) {
return 'ALTER TABLE ' + this.quoteTable(tableName) + ' DROP FOREIGN KEY ' + this.quoteIdentifier(foreignKey) + ';';
}
};
module.exports = Utils._.extend(Utils._.clone(require('../abstract/query-generator')), QueryGenerator);
...@@ -5,189 +5,187 @@ var Utils = require('../../utils') ...@@ -5,189 +5,187 @@ var Utils = require('../../utils')
, uuid = require('node-uuid') , uuid = require('node-uuid')
, sequelizeErrors = require('../../errors.js'); , sequelizeErrors = require('../../errors.js');
module.exports = (function() { var Query = function(connection, sequelize, options) {
var Query = function(connection, sequelize, options) { this.connection = connection;
this.connection = connection; this.instance = options.instance;
this.instance = options.instance; this.model = options.model;
this.model = options.model; this.sequelize = sequelize;
this.sequelize = sequelize; this.uuid = uuid.v4();
this.uuid = uuid.v4(); this.options = Utils._.extend({
this.options = Utils._.extend({ logging: console.log,
logging: console.log, plain: false,
plain: false, raw: false
raw: false }, options || {});
}, options || {});
this.checkLoggingOption();
this.checkLoggingOption(); };
};
Utils.inherit(Query, AbstractQuery);
Utils.inherit(Query, AbstractQuery); Query.prototype.run = function(sql) {
Query.prototype.run = function(sql) { var self = this;
var self = this; this.sql = sql;
this.sql = sql;
this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
var promise = new Utils.Promise(function(resolve, reject) {
var promise = new Utils.Promise(function(resolve, reject) { self.connection.query(self.sql, function(err, results) {
self.connection.query(self.sql, function(err, results) { if (err) {
if (err) { err.sql = sql;
err.sql = sql;
reject(self.formatError(err));
reject(self.formatError(err)); } else {
} else { resolve(self.formatResults(results));
resolve(self.formatResults(results));
}
}).setMaxListeners(100);
});
return promise;
};
/**
* High level function that handles the results of a query execution.
*
*
* 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
* }
* ])
*
* @param {Array} data - The result of the query execution.
*/
Query.prototype.formatResults = function(data) {
var result = this.instance;
if (this.isInsertQuery(data)) {
this.handleInsertQuery(data);
if (!this.instance) {
result = data[this.getInsertIdField()];
} }
}).setMaxListeners(100);
});
return promise;
};
/**
* High level function that handles the results of a query execution.
*
*
* 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
* }
* ])
*
* @param {Array} data - The result of the query execution.
*/
Query.prototype.formatResults = function(data) {
var result = this.instance;
if (this.isInsertQuery(data)) {
this.handleInsertQuery(data);
if (!this.instance) {
result = data[this.getInsertIdField()];
} }
}
if (this.isSelectQuery()) {
result = this.handleSelectQuery(data);
} else if (this.isShowTablesQuery()) {
result = this.handleShowTablesQuery(data);
} else if (this.isDescribeQuery()) {
result = {};
data.forEach(function(_result) {
result[_result.Field] = {
type: _result.Type.toUpperCase(),
allowNull: (_result.Null === 'YES'),
defaultValue: _result.Default
};
});
} else if (this.isShowIndexesQuery()) {
result = this.handleShowIndexesQuery(data);
} else if (this.isCallQuery()) {
result = data[0];
} else if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery() || this.isUpsertQuery()) {
result = data.affectedRows;
} else if (this.isVersionQuery()) {
result = data[0].version;
} else if (this.isForeignKeysQuery()) {
result = data;
} else if (this.isRawQuery()) {
// MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
result = [data, data];
}
return result;
};
Query.prototype.formatError = function (err) {
var match;
switch (err.errno || err.code) {
case 1062:
match = err.message.match(/Duplicate entry '(.*)' for key '?((.|\s)*?)'?$/);
var values = match ? match[1].split('-') : undefined
, fields = {}
, message = 'Validation error'
, uniqueKey = this.model && this.model.uniqueKeys[match[2]];
if (!!uniqueKey) {
if (!!uniqueKey.msg) message = uniqueKey.msg;
fields = Utils._.zipObject(uniqueKey.fields, values);
} else {
fields[match[2]] = match[1];
}
if (this.isSelectQuery()) { var errors = [];
result = this.handleSelectQuery(data); Utils._.forOwn(fields, function(value, field) {
} else if (this.isShowTablesQuery()) { errors.push(new sequelizeErrors.ValidationErrorItem(
result = this.handleShowTablesQuery(data); field + ' must be unique', 'unique violation', field, value));
} else if (this.isDescribeQuery()) {
result = {};
data.forEach(function(_result) {
result[_result.Field] = {
type: _result.Type.toUpperCase(),
allowNull: (_result.Null === 'YES'),
defaultValue: _result.Default
};
}); });
} else if (this.isShowIndexesQuery()) {
result = this.handleShowIndexesQuery(data);
} else if (this.isCallQuery()) {
result = data[0];
} else if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery() || this.isUpsertQuery()) {
result = data.affectedRows;
} else if (this.isVersionQuery()) {
result = data[0].version;
} else if (this.isForeignKeysQuery()) {
result = data;
} else if (this.isRawQuery()) {
// MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
result = [data, data];
}
return result; return new sequelizeErrors.UniqueConstraintError({
}; message: message,
errors: errors,
parent: err,
Query.prototype.formatError = function (err) { fields: fields
var match; });
switch (err.errno || err.code) {
case 1062:
match = err.message.match(/Duplicate entry '(.*)' for key '?((.|\s)*?)'?$/);
var values = match ? match[1].split('-') : undefined
, fields = {}
, message = 'Validation error'
, uniqueKey = this.model && this.model.uniqueKeys[match[2]];
if (!!uniqueKey) {
if (!!uniqueKey.msg) message = uniqueKey.msg;
fields = Utils._.zipObject(uniqueKey.fields, values);
} else {
fields[match[2]] = match[1];
}
var errors = [];
Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value));
});
return new sequelizeErrors.UniqueConstraintError({
message: message,
errors: errors,
parent: err,
fields: fields
});
case 1451:
match = err.message.match(/FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)(?: ON .*)?\)$/);
return new sequelizeErrors.ForeignKeyConstraintError({
fields: null,
index: match ? match[3] : undefined,
parent: err
});
case 1452:
match = err.message.match(/FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)(.*)\)$/);
return new sequelizeErrors.ForeignKeyConstraintError({
fields: null,
index: match ? match[1] : undefined,
parent: err
});
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.handleShowIndexesQuery = function (data) {
// Group by index name, and collect all fields
data = Utils._.foldl(data, function (acc, item) {
if (!(item.Key_name in acc)) {
acc[item.Key_name] = item;
item.fields = [];
}
acc[item.Key_name].fields[item.Seq_in_index - 1] = { case 1451:
attribute: item.Column_name, match = err.message.match(/FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)(?: ON .*)?\)$/);
length: item.Sub_part || undefined,
order: item.Collation === 'A' ? 'ASC' : undefined return new sequelizeErrors.ForeignKeyConstraintError({
}; fields: null,
delete item.column_name; index: match ? match[3] : undefined,
parent: err
return acc; });
}, {});
case 1452:
return Utils._.map(data, function(item) { match = err.message.match(/FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)(.*)\)$/);
return {
primary: item.Key_name === 'PRIMARY', return new sequelizeErrors.ForeignKeyConstraintError({
fields: item.fields, fields: null,
name: item.Key_name, index: match ? match[1] : undefined,
tableName: item.Table, parent: err
unique: (item.Non_unique !== 1), });
type: item.Index_type,
}; default:
}); return new sequelizeErrors.DatabaseError(err);
}; }
};
Query.prototype.handleShowIndexesQuery = function (data) {
// Group by index name, and collect all fields
data = Utils._.foldl(data, function (acc, item) {
if (!(item.Key_name in acc)) {
acc[item.Key_name] = item;
item.fields = [];
}
return Query; acc[item.Key_name].fields[item.Seq_in_index - 1] = {
})(); attribute: item.Column_name,
length: item.Sub_part || undefined,
order: item.Collation === 'A' ? 'ASC' : undefined
};
delete item.column_name;
return acc;
}, {});
return Utils._.map(data, function(item) {
return {
primary: item.Key_name === 'PRIMARY',
fields: item.fields,
name: item.Key_name,
tableName: item.Table,
unique: (item.Non_unique !== 1),
type: item.Index_type,
};
});
};
module.exports = Query;
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
var hstore = require('pg-hstore')({sanitize : true}); var hstore = require('pg-hstore')({sanitize : true});
module.exports = { function stringify (data) {
stringify: function(data) { if (data === null) return null;
if(data === null) return null;
return hstore.stringify(data); return hstore.stringify(data);
}, }
parse: function(value) {
if(value === null) return null;
function parse (value) {
if (value === null) return null;
return hstore.parse(value); return hstore.parse(value);
} }
module.exports = {
stringify: stringify,
parse: parse
}; };
...@@ -10,935 +10,933 @@ var Utils = require('../../utils') ...@@ -10,935 +10,933 @@ var Utils = require('../../utils')
, AbstractQueryGenerator = require('../abstract/query-generator') , AbstractQueryGenerator = require('../abstract/query-generator')
, primaryKeys = {}; , primaryKeys = {};
module.exports = (function() { var QueryGenerator = {
var QueryGenerator = { options: {},
options: {}, dialect: 'postgres',
dialect: 'postgres',
createSchema: function(schema) { createSchema: function(schema) {
var query = 'CREATE SCHEMA <%= schema%>;'; var query = 'CREATE SCHEMA <%= schema%>;';
return Utils._.template(query)({schema: schema}); return Utils._.template(query)({schema: schema});
}, },
dropSchema: function(schema) { dropSchema: function(schema) {
var query = 'DROP SCHEMA <%= schema%> CASCADE;'; var query = 'DROP SCHEMA <%= schema%> CASCADE;';
return Utils._.template(query)({schema: schema}); return Utils._.template(query)({schema: schema});
}, },
showSchemasQuery: function() { showSchemasQuery: function() {
return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';"; return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';";
}, },
versionQuery: function() { versionQuery: function() {
return 'SELECT VERSION() as "version"'; return 'SELECT VERSION() as "version"';
}, },
createTableQuery: function(tableName, attributes, options) { createTableQuery: function(tableName, attributes, options) {
var self = this; var self = this;
options = Utils._.extend({ options = Utils._.extend({
}, options || {}); }, options || {});
primaryKeys[tableName] = []; primaryKeys[tableName] = [];
var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)<%= comments %>' var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)<%= comments %>'
, comments = '' , comments = ''
, attrStr = [] , attrStr = []
, i; , i;
if (options.comment && Utils._.isString(options.comment)) { if (options.comment && Utils._.isString(options.comment)) {
comments += '; COMMENT ON TABLE <%= table %> IS ' + this.escape(options.comment); comments += '; COMMENT ON TABLE <%= table %> IS ' + this.escape(options.comment);
} }
for (var attr in attributes) {
if ((i = attributes[attr].indexOf('COMMENT')) !== -1) {
// Move comment to a seperate query
comments += '; ' + attributes[attr].substring(i);
attributes[attr] = attributes[attr].substring(0, i);
}
var dataType = this.pgDataTypeMapping(tableName, attr, attributes[attr]);
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType);
}
var values = {
table: this.quoteTable(tableName),
attributes: attrStr.join(', '),
comments: Utils._.template(comments)({ table: this.quoteTable(tableName)})
};
if (!!options.uniqueKeys) { for (var attr in attributes) {
Utils._.each(options.uniqueKeys, function(columns) { if ((i = attributes[attr].indexOf('COMMENT')) !== -1) {
if (!columns.singleField) { // If it's a single field its handled in column def, not as an index // Move comment to a seperate query
values.attributes += ', UNIQUE (' + columns.fields.map(function(f) { return self.quoteIdentifiers(f); }).join(', ') + ')'; comments += '; ' + attributes[attr].substring(i);
} attributes[attr] = attributes[attr].substring(0, i);
});
} }
var pks = primaryKeys[tableName].map(function(pk) { var dataType = this.pgDataTypeMapping(tableName, attr, attributes[attr]);
return this.quoteIdentifier(pk); attrStr.push(this.quoteIdentifier(attr) + ' ' + dataType);
}.bind(this)).join(','); }
if (pks.length > 0) {
values.attributes += ', PRIMARY KEY (' + pks + ')';
}
return Utils._.template(query)(values).trim() + ';'; var values = {
}, table: this.quoteTable(tableName),
attributes: attrStr.join(', '),
comments: Utils._.template(comments)({ table: this.quoteTable(tableName)})
};
dropTableQuery: function(tableName, options) { if (!!options.uniqueKeys) {
options = options || {}; Utils._.each(options.uniqueKeys, function(columns) {
var query = 'DROP TABLE IF EXISTS <%= table %><%= cascade %>;'; if (!columns.singleField) { // If it's a single field its handled in column def, not as an index
return Utils._.template(query)({ values.attributes += ', UNIQUE (' + columns.fields.map(function(f) { return self.quoteIdentifiers(f); }).join(', ') + ')';
table: this.quoteTable(tableName), }
cascade: options.cascade ? ' CASCADE' : ''
}); });
}, }
showTablesQuery: function() { var pks = primaryKeys[tableName].map(function(pk) {
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';"; return this.quoteIdentifier(pk);
}, }.bind(this)).join(',');
describeTableQuery: function(tableName, schema) { if (pks.length > 0) {
if (!schema) { values.attributes += ', PRIMARY KEY (' + pks + ')';
schema = 'public'; }
}
var query = 'SELECT c.column_name as "Field", c.column_default as "Default", c.is_nullable as "Null", ' + return Utils._.template(query)(values).trim() + ';';
"CASE WHEN c.udt_name = 'hstore' " + },
'THEN c.udt_name ELSE c.data_type END as "Type", (SELECT array_agg(e.enumlabel) ' +
'FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special" ' + dropTableQuery: function(tableName, options) {
'FROM information_schema.columns c WHERE table_name = <%= table %> AND table_schema = <%= schema %>'; options = options || {};
var query = 'DROP TABLE IF EXISTS <%= table %><%= cascade %>;';
return Utils._.template(query)({
table: this.quoteTable(tableName),
cascade: options.cascade ? ' CASCADE' : ''
});
},
showTablesQuery: function() {
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';";
},
describeTableQuery: function(tableName, schema) {
if (!schema) {
schema = 'public';
}
return Utils._.template(query)({ var query = 'SELECT c.column_name as "Field", c.column_default as "Default", c.is_nullable as "Null", ' +
table: this.escape(tableName), "CASE WHEN c.udt_name = 'hstore' " +
schema: this.escape(schema) 'THEN c.udt_name ELSE c.data_type END as "Type", (SELECT array_agg(e.enumlabel) ' +
}); 'FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special" ' +
}, 'FROM information_schema.columns c WHERE table_name = <%= table %> AND table_schema = <%= schema %>';
return Utils._.template(query)({
table: this.escape(tableName),
schema: this.escape(schema)
});
},
// A recursive parser for nested where conditions
parseConditionObject: function(_conditions, path) {
var self = this;
path = path || [];
return Utils._.reduce(_conditions, function (r, v, k) { // result, key, value
if (Utils._.isObject(v)) {
r = r.concat(self.parseConditionObject(v, path.concat(k))); // Recursively parse objects
} else {
r.push({ path: path.concat(k), value: v });
}
return r;
}, []);
},
handleSequelizeMethod: function (smth, tableName, factory, options, prepend) {
var _ = Utils._;
if (smth instanceof Utils.json) {
// Parse nested object
if (smth.conditions) {
var conditions = _.map(this.parseConditionObject(smth.conditions), function generateSql(condition) {
return util.format("%s#>>'{%s}' = '%s'",
_.first(condition.path),
_.rest(condition.path).join(','),
condition.value);
});
// A recursive parser for nested where conditions return conditions.join(' and ');
parseConditionObject: function(_conditions, path) { } else if (smth.path) {
var self = this; var str;
path = path || []; // Allow specifying conditions using the postgres json syntax
return Utils._.reduce(_conditions, function (r, v, k) { // result, key, value if (_.any(['->', '->>', '#>'], _.partial(_.contains, smth.path))) {
if (Utils._.isObject(v)) { str = smth.path;
r = r.concat(self.parseConditionObject(v, path.concat(k))); // Recursively parse objects
} else { } else {
r.push({ path: path.concat(k), value: v }); // Also support json dot notation
var path = smth.path.split('.');
str = util.format("%s#>>'{%s}'",
_.first(path),
_.rest(path).join(','));
} }
return r;
}, []);
},
handleSequelizeMethod: function (smth, tableName, factory, options, prepend) {
var _ = Utils._;
if (smth instanceof Utils.json) {
// Parse nested object
if (smth.conditions) {
var conditions = _.map(this.parseConditionObject(smth.conditions), function generateSql(condition) {
return util.format("%s#>>'{%s}' = '%s'",
_.first(condition.path),
_.rest(condition.path).join(','),
condition.value);
});
return conditions.join(' and ');
} else if (smth.path) {
var str;
// Allow specifying conditions using the postgres json syntax
if (_.any(['->', '->>', '#>'], _.partial(_.contains, smth.path))) {
str = smth.path;
} else {
// Also support json dot notation
var path = smth.path.split('.');
str = util.format("%s#>>'{%s}'",
_.first(path),
_.rest(path).join(','));
}
if (smth.value) {
str += util.format(' = %s', this.escape(smth.value));
}
return str; if (smth.value) {
str += util.format(' = %s', this.escape(smth.value));
} }
} else {
return AbstractQueryGenerator.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
}
},
addColumnQuery: function(table, key, dataType) {
var query = 'ALTER TABLE <%= table %> ADD COLUMN <%= attribute %>;'
, dbDataType = this.attributeToSQL(dataType, {context: 'addColumn'})
, attribute;
if (dataType.type && dataType.type instanceof DataTypes.ENUM || dataType instanceof DataTypes.ENUM) { return str;
query = this.pgEnum(table, key, dataType) + query;
} }
} else {
return AbstractQueryGenerator.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
}
},
attribute = Utils._.template('<%= key %> <%= definition %>')({ addColumnQuery: function(table, key, dataType) {
key: this.quoteIdentifier(key), var query = 'ALTER TABLE <%= table %> ADD COLUMN <%= attribute %>;'
definition: this.pgDataTypeMapping(table, key, dbDataType) , dbDataType = this.attributeToSQL(dataType, {context: 'addColumn'})
}); , attribute;
return Utils._.template(query)({
table: this.quoteTable(table),
attribute: attribute
});
},
removeColumnQuery: function(tableName, attributeName) {
var query = 'ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;';
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
attributeName: this.quoteIdentifier(attributeName)
});
},
changeColumnQuery: function(tableName, attributes) {
var query = 'ALTER TABLE <%= tableName %> ALTER COLUMN <%= query %>;'
, sql = [];
for (var attributeName in attributes) {
var definition = this.pgDataTypeMapping(tableName, attributeName, attributes[attributeName]);
var attrSql = '';
if (definition.indexOf('NOT NULL') > 0) {
attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' SET NOT NULL'
});
definition = definition.replace('NOT NULL', '').trim();
} else {
attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' DROP NOT NULL'
});
}
if (definition.indexOf('DEFAULT') > 0) {
attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' SET DEFAULT ' + definition.match(/DEFAULT ([^;]+)/)[1]
});
definition = definition.replace(/(DEFAULT[^;]+)/, '').trim();
} else {
attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' DROP DEFAULT'
});
}
if (attributes[attributeName].match(/^ENUM\(/)) { if (dataType.type && dataType.type instanceof DataTypes.ENUM || dataType instanceof DataTypes.ENUM) {
query = this.pgEnum(tableName, attributeName, attributes[attributeName]) + query; query = this.pgEnum(table, key, dataType) + query;
definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier('enum_' + tableName + '_' + attributeName)); }
definition += ' USING (' + this.quoteIdentifier(attributeName) + '::' + this.quoteIdentifier(definition) + ')';
}
if (definition.match(/UNIQUE;*$/)) { attribute = Utils._.template('<%= key %> <%= definition %>')({
definition = definition.replace(/UNIQUE;*$/, ''); key: this.quoteIdentifier(key),
definition: this.pgDataTypeMapping(table, key, dbDataType)
});
return Utils._.template(query)({
table: this.quoteTable(table),
attribute: attribute
});
},
removeColumnQuery: function(tableName, attributeName) {
var query = 'ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;';
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
attributeName: this.quoteIdentifier(attributeName)
});
},
changeColumnQuery: function(tableName, attributes) {
var query = 'ALTER TABLE <%= tableName %> ALTER COLUMN <%= query %>;'
, sql = [];
for (var attributeName in attributes) {
var definition = this.pgDataTypeMapping(tableName, attributeName, attributes[attributeName]);
var attrSql = '';
if (definition.indexOf('NOT NULL') > 0) {
attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' SET NOT NULL'
});
attrSql += Utils._.template(query.replace('ALTER COLUMN', ''))({ definition = definition.replace('NOT NULL', '').trim();
tableName: this.quoteTable(tableName), } else {
query: 'ADD CONSTRAINT ' + this.quoteIdentifier(attributeName + '_unique_idx') + ' UNIQUE (' + this.quoteIdentifier(attributeName) + ')' attrSql += Utils._.template(query)({
}); tableName: this.quoteTable(tableName),
} query: this.quoteIdentifier(attributeName) + ' DROP NOT NULL'
});
}
if (definition.indexOf('DEFAULT') > 0) {
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName), tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition query: this.quoteIdentifier(attributeName) + ' SET DEFAULT ' + definition.match(/DEFAULT ([^;]+)/)[1]
}); });
sql.push(attrSql); definition = definition.replace(/(DEFAULT[^;]+)/, '').trim();
} else {
attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName),
query: this.quoteIdentifier(attributeName) + ' DROP DEFAULT'
});
} }
return sql.join(''); if (attributes[attributeName].match(/^ENUM\(/)) {
}, query = this.pgEnum(tableName, attributeName, attributes[attributeName]) + query;
definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier('enum_' + tableName + '_' + attributeName));
definition += ' USING (' + this.quoteIdentifier(attributeName) + '::' + this.quoteIdentifier(definition) + ')';
}
renameColumnQuery: function(tableName, attrBefore, attributes) { if (definition.match(/UNIQUE;*$/)) {
var query = 'ALTER TABLE <%= tableName %> RENAME COLUMN <%= attributes %>;'; definition = definition.replace(/UNIQUE;*$/, '');
var attrString = [];
for (var attributeName in attributes) { attrSql += Utils._.template(query.replace('ALTER COLUMN', ''))({
attrString.push(Utils._.template('<%= before %> TO <%= after %>')({ tableName: this.quoteTable(tableName),
before: this.quoteIdentifier(attrBefore), query: 'ADD CONSTRAINT ' + this.quoteIdentifier(attributeName + '_unique_idx') + ' UNIQUE (' + this.quoteIdentifier(attributeName) + ')'
after: this.quoteIdentifier(attributeName) });
}));
} }
return Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: this.quoteTable(tableName), tableName: this.quoteTable(tableName),
attributes: attrString.join(', ') query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition
}); });
},
fn: function(fnName, tableName, body, returns, language) {
fnName = fnName || 'testfunc';
language = language || 'plpgsql';
returns = returns || 'SETOF ' + this.quoteTable(tableName);
var query = 'CREATE OR REPLACE FUNCTION pg_temp.<%= fnName %>() RETURNS <%= returns %> AS $func$ BEGIN <%= body %> END; $func$ LANGUAGE <%= language %>; SELECT * FROM pg_temp.<%= fnName %>();'; sql.push(attrSql);
}
return Utils._.template(query)({ return sql.join('');
fnName: fnName, },
returns: returns,
language: language,
body: body
});
},
exceptionFn: function(fnName, tableName, main, then, when, returns, language) { renameColumnQuery: function(tableName, attrBefore, attributes) {
when = when || 'unique_violation'; var query = 'ALTER TABLE <%= tableName %> RENAME COLUMN <%= attributes %>;';
var attrString = [];
var body = '<%= main %> EXCEPTION WHEN <%= when %> THEN <%= then %>;'; for (var attributeName in attributes) {
body = Utils._.template(body)({ attrString.push(Utils._.template('<%= before %> TO <%= after %>')({
main: main, before: this.quoteIdentifier(attrBefore),
when: when, after: this.quoteIdentifier(attributeName)
then: then }));
}); }
return this.fn(fnName, tableName, body, returns, language); return Utils._.template(query)({
}, tableName: this.quoteTable(tableName),
attributes: attrString.join(', ')
// http://www.maori.geek.nz/post/postgres_upsert_update_or_insert_in_ger_using_knex_js });
upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) { },
var insert = this.insertQuery(tableName, insertValues, rawAttributes, options);
var update = this.updateQuery(tableName, updateValues, where, options, rawAttributes); fn: function(fnName, tableName, body, returns, language) {
fnName = fnName || 'testfunc';
// The numbers here are selected to match the number of affected rows returned by MySQL language = language || 'plpgsql';
return this.exceptionFn( returns = returns || 'SETOF ' + this.quoteTable(tableName);
'sequelize_upsert',
tableName, var query = 'CREATE OR REPLACE FUNCTION pg_temp.<%= fnName %>() RETURNS <%= returns %> AS $func$ BEGIN <%= body %> END; $func$ LANGUAGE <%= language %>; SELECT * FROM pg_temp.<%= fnName %>();';
insert + ' RETURN 1;',
update + '; RETURN 2', return Utils._.template(query)({
'unique_violation', fnName: fnName,
'integer' returns: returns,
); language: language,
}, body: body
});
bulkInsertQuery: function(tableName, attrValueHashes, options, modelAttributes) { },
options = options || {};
exceptionFn: function(fnName, tableName, main, then, when, returns, language) {
var query = 'INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>' when = when || 'unique_violation';
, tuples = []
, serials = [] var body = '<%= main %> EXCEPTION WHEN <%= when %> THEN <%= then %>;';
, allAttributes = []; body = Utils._.template(body)({
main: main,
if (options.returning) { when: when,
query += ' RETURNING *'; then: then
} });
return this.fn(fnName, tableName, body, returns, language);
},
// http://www.maori.geek.nz/post/postgres_upsert_update_or_insert_in_ger_using_knex_js
upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) {
var insert = this.insertQuery(tableName, insertValues, rawAttributes, options);
var update = this.updateQuery(tableName, updateValues, where, options, rawAttributes);
// The numbers here are selected to match the number of affected rows returned by MySQL
return this.exceptionFn(
'sequelize_upsert',
tableName,
insert + ' RETURN 1;',
update + '; RETURN 2',
'unique_violation',
'integer'
);
},
bulkInsertQuery: function(tableName, attrValueHashes, options, modelAttributes) {
options = options || {};
var query = 'INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>'
, tuples = []
, serials = []
, allAttributes = [];
if (options.returning) {
query += ' RETURNING *';
}
Utils._.forEach(attrValueHashes, function(attrValueHash) { Utils._.forEach(attrValueHashes, function(attrValueHash) {
Utils._.forOwn(attrValueHash, function(value, key) { Utils._.forOwn(attrValueHash, function(value, key) {
if (allAttributes.indexOf(key) === -1) { if (allAttributes.indexOf(key) === -1) {
allAttributes.push(key); allAttributes.push(key);
} }
if (modelAttributes && modelAttributes[key] && modelAttributes[key].autoIncrement === true) { if (modelAttributes && modelAttributes[key] && modelAttributes[key].autoIncrement === true) {
serials.push(key); serials.push(key);
} }
});
}); });
});
Utils._.forEach(attrValueHashes, function(attrValueHash) { Utils._.forEach(attrValueHashes, function(attrValueHash) {
tuples.push('(' + tuples.push('(' +
allAttributes.map(function(key) { allAttributes.map(function(key) {
if (serials.indexOf(key) !== -1) { if (serials.indexOf(key) !== -1) {
return attrValueHash[key] || 'DEFAULT'; return attrValueHash[key] || 'DEFAULT';
} }
return this.escape(attrValueHash[key], modelAttributes && modelAttributes[key]); return this.escape(attrValueHash[key], modelAttributes && modelAttributes[key]);
}.bind(this)).join(',') + }.bind(this)).join(',') +
')'); ')');
}.bind(this)); }.bind(this));
var replacements = {
table: this.quoteTable(tableName)
, attributes: allAttributes.map(function(attr) {
return this.quoteIdentifier(attr);
}.bind(this)).join(',')
, tuples: tuples.join(',')
};
query = query + ';';
return Utils._.template(query)(replacements); var replacements = {
}, table: this.quoteTable(tableName)
, attributes: allAttributes.map(function(attr) {
return this.quoteIdentifier(attr);
}.bind(this)).join(',')
, tuples: tuples.join(',')
};
deleteQuery: function(tableName, where, options, model) { query = query + ';';
var query;
options = options || {}; return Utils._.template(query)(replacements);
},
tableName = Utils.removeTicks(this.quoteTable(tableName), '"'); deleteQuery: function(tableName, where, options, model) {
var query;
if (options.truncate === true) { options = options || {};
query = 'TRUNCATE ' + QueryGenerator.quoteIdentifier(tableName);
if (options.cascade) { tableName = Utils.removeTicks(this.quoteTable(tableName), '"');
query += ' CASCADE';
}
return query; if (options.truncate === true) {
} query = 'TRUNCATE ' + QueryGenerator.quoteIdentifier(tableName);
if (Utils._.isUndefined(options.limit)) { if (options.cascade) {
options.limit = 1; query += ' CASCADE';
} }
primaryKeys[tableName] = primaryKeys[tableName] || []; return query;
}
if (!!model && primaryKeys[tableName].length < 1) {
primaryKeys[tableName] = Object.keys(model.primaryKeys);
}
if (options.limit) { if (Utils._.isUndefined(options.limit)) {
query = 'DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %><%= where %><%= limit %>)'; options.limit = 1;
} else { }
query = 'DELETE FROM <%= table %><%= where %>';
}
var pks; primaryKeys[tableName] = primaryKeys[tableName] || [];
if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) {
pks = primaryKeys[tableName].map(function(pk) {
return this.quoteIdentifier(pk);
}.bind(this)).join(',');
} else {
pks = this.quoteIdentifier('id');
}
var replacements = { if (!!model && primaryKeys[tableName].length < 1) {
table: this.quoteIdentifiers(tableName), primaryKeys[tableName] = Object.keys(model.primaryKeys);
where: this.getWhereConditions(where), }
limit: !!options.limit ? ' LIMIT ' + this.escape(options.limit) : '',
primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks,
primaryKeysSelection: pks
};
if (replacements.where) { if (options.limit) {
replacements.where = ' WHERE ' + replacements.where; query = 'DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %><%= where %><%= limit %>)';
} } else {
query = 'DELETE FROM <%= table %><%= where %>';
}
return Utils._.template(query)(replacements); var pks;
}, if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) {
pks = primaryKeys[tableName].map(function(pk) {
return this.quoteIdentifier(pk);
}.bind(this)).join(',');
} else {
pks = this.quoteIdentifier('id');
}
showIndexesQuery: function(tableName) { var replacements = {
if (!Utils._.isString(tableName)) { table: this.quoteIdentifiers(tableName),
tableName = tableName.tableName; where: this.getWhereConditions(where),
} limit: !!options.limit ? ' LIMIT ' + this.escape(options.limit) : '',
primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks,
primaryKeysSelection: pks
};
// This is ARCANE! if (replacements.where) {
var query = 'SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, ' + replacements.where = ' WHERE ' + replacements.where;
'array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) ' + }
'AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a ' +
'WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND '+
"t.relkind = 'r' and t.relname = '<%= tableName %>' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;";
return Utils._.template(query)({ tableName: tableName }); return Utils._.template(query)(replacements);
}, },
removeIndexQuery: function(tableName, indexNameOrAttributes) { showIndexesQuery: function(tableName) {
var sql = 'DROP INDEX IF EXISTS <%= indexName %>' if (!Utils._.isString(tableName)) {
, indexName = indexNameOrAttributes; tableName = tableName.tableName;
}
if (typeof indexName !== 'string') { // This is ARCANE!
indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_')); var query = 'SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, ' +
} 'array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) ' +
'AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a ' +
'WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND '+
"t.relkind = 'r' and t.relname = '<%= tableName %>' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;";
return Utils._.template(sql)({ return Utils._.template(query)({ tableName: tableName });
tableName: this.quoteIdentifiers(tableName), },
indexName: this.quoteIdentifiers(indexName)
});
},
addLimitAndOffset: function(options) { removeIndexQuery: function(tableName, indexNameOrAttributes) {
var fragment = ''; var sql = 'DROP INDEX IF EXISTS <%= indexName %>'
if (options.limit) fragment += ' LIMIT ' + options.limit; , indexName = indexNameOrAttributes;
if (options.offset) fragment += ' OFFSET ' + options.offset;
return fragment; if (typeof indexName !== 'string') {
}, indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
}
attributeToSQL: function(attribute) { return Utils._.template(sql)({
if (!Utils._.isPlainObject(attribute)) { tableName: this.quoteIdentifiers(tableName),
attribute = { indexName: this.quoteIdentifiers(indexName)
type: attribute });
}; },
}
var template = '<%= type %>' addLimitAndOffset: function(options) {
, replacements = {}; var fragment = '';
if (options.limit) fragment += ' LIMIT ' + options.limit;
if (options.offset) fragment += ' OFFSET ' + options.offset;
if (attribute.type instanceof DataTypes.ENUM) { return fragment;
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values; },
if (Array.isArray(attribute.values) && (attribute.values.length > 0)) { attributeToSQL: function(attribute) {
replacements.type = 'ENUM(' + Utils._.map(attribute.values, function(value) { if (!Utils._.isPlainObject(attribute)) {
return this.escape(value); attribute = {
}.bind(this)).join(', ') + ')'; type: attribute
} else { };
throw new Error("Values for ENUM haven't been defined."); }
}
}
if (!replacements.type) { var template = '<%= type %>'
replacements.type = attribute.type; , replacements = {};
}
if (attribute.hasOwnProperty('allowNull') && (!attribute.allowNull)) { if (attribute.type instanceof DataTypes.ENUM) {
template += ' NOT NULL'; if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
}
if (attribute.autoIncrement) { if (Array.isArray(attribute.values) && (attribute.values.length > 0)) {
template += ' SERIAL'; replacements.type = 'ENUM(' + Utils._.map(attribute.values, function(value) {
return this.escape(value);
}.bind(this)).join(', ') + ')';
} else {
throw new Error("Values for ENUM haven't been defined.");
} }
}
if (Utils.defaultValueSchemable(attribute.defaultValue)) { if (!replacements.type) {
template += ' DEFAULT <%= defaultValue %>'; replacements.type = attribute.type;
replacements.defaultValue = this.escape(attribute.defaultValue, attribute); }
}
if (attribute.unique === true) { if (attribute.hasOwnProperty('allowNull') && (!attribute.allowNull)) {
template += ' UNIQUE'; template += ' NOT NULL';
} }
if (attribute.primaryKey) { if (attribute.autoIncrement) {
template += ' PRIMARY KEY'; template += ' SERIAL';
} }
if (attribute.references) { if (Utils.defaultValueSchemable(attribute.defaultValue)) {
attribute = Utils.formatReferences(attribute); template += ' DEFAULT <%= defaultValue %>';
template += ' REFERENCES <%= referencesTable %> (<%= referencesKey %>)'; replacements.defaultValue = this.escape(attribute.defaultValue, attribute);
replacements.referencesTable = this.quoteTable(attribute.references.model); }
if (attribute.references.key) { if (attribute.unique === true) {
replacements.referencesKey = this.quoteIdentifiers(attribute.references.key); template += ' UNIQUE';
} else { }
replacements.referencesKey = this.quoteIdentifier('id');
}
if (attribute.onDelete) { if (attribute.primaryKey) {
template += ' ON DELETE <%= onDeleteAction %>'; template += ' PRIMARY KEY';
replacements.onDeleteAction = attribute.onDelete.toUpperCase(); }
}
if (attribute.onUpdate) { if (attribute.references) {
template += ' ON UPDATE <%= onUpdateAction %>'; attribute = Utils.formatReferences(attribute);
replacements.onUpdateAction = attribute.onUpdate.toUpperCase(); template += ' REFERENCES <%= referencesTable %> (<%= referencesKey %>)';
} replacements.referencesTable = this.quoteTable(attribute.references.model);
if (attribute.references.deferrable) { if (attribute.references.key) {
template += ' <%= deferrable %>'; replacements.referencesKey = this.quoteIdentifiers(attribute.references.key);
replacements.deferrable = attribute.references.deferrable.toString(this); } else {
} replacements.referencesKey = this.quoteIdentifier('id');
} }
return Utils._.template(template)(replacements); if (attribute.onDelete) {
}, template += ' ON DELETE <%= onDeleteAction %>';
replacements.onDeleteAction = attribute.onDelete.toUpperCase();
deferConstraintsQuery: function (options) { }
return options.deferrable.toString(this);
},
setConstraintQuery: function (columns, type) { if (attribute.onUpdate) {
var columnFragment = 'ALL'; template += ' ON UPDATE <%= onUpdateAction %>';
replacements.onUpdateAction = attribute.onUpdate.toUpperCase();
}
if (columns) { if (attribute.references.deferrable) {
columnFragment = columns.map(function (column) { template += ' <%= deferrable %>';
return this.quoteIdentifier(column); replacements.deferrable = attribute.references.deferrable.toString(this);
}.bind(this)).join(', ');
} }
}
return 'SET CONSTRAINTS ' + columnFragment + ' ' + type; return Utils._.template(template)(replacements);
}, },
setDeferredQuery: function (columns) { deferConstraintsQuery: function (options) {
return this.setConstraintQuery(columns, 'DEFERRED'); return options.deferrable.toString(this);
}, },
setImmediateQuery: function (columns) { setConstraintQuery: function (columns, type) {
return this.setConstraintQuery(columns, 'IMMEDIATE'); var columnFragment = 'ALL';
},
attributesToSQL: function(attributes, options) { if (columns) {
var result = {} columnFragment = columns.map(function (column) {
, key return this.quoteIdentifier(column);
, attribute; }.bind(this)).join(', ');
}
for (key in attributes) { return 'SET CONSTRAINTS ' + columnFragment + ' ' + type;
attribute = attributes[key]; },
result[attribute.field || key] = this.attributeToSQL(attribute, options);
}
return result; setDeferredQuery: function (columns) {
}, return this.setConstraintQuery(columns, 'DEFERRED');
},
findAutoIncrementField: function(factory) { setImmediateQuery: function (columns) {
var fields = []; return this.setConstraintQuery(columns, 'IMMEDIATE');
},
for (var name in factory.attributes) { attributesToSQL: function(attributes, options) {
var definition = factory.attributes[name]; var result = {}
, key
, attribute;
if (definition && definition.autoIncrement) { for (key in attributes) {
fields.push(name); attribute = attributes[key];
} result[attribute.field || key] = this.attributeToSQL(attribute, options);
} }
return fields; return result;
}, },
createTrigger: function(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
var sql = [
'CREATE <%= constraintVal %>TRIGGER <%= triggerName %>'
, '<%= eventType %> <%= eventSpec %>'
, 'ON <%= tableName %>'
, '<%= optionsSpec %>'
, 'EXECUTE PROCEDURE <%= functionName %>(<%= paramList %>);'
].join('\n\t');
return Utils._.template(sql)({
constraintVal: this.triggerEventTypeIsConstraint(eventType),
triggerName: triggerName,
eventType: this.decodeTriggerEventType(eventType),
eventSpec: this.expandTriggerEventSpec(fireOnSpec),
tableName: tableName,
optionsSpec: this.expandOptions(optionsArray),
functionName: functionName,
paramList: this.expandFunctionParamList(functionParams)
});
},
dropTrigger: function(tableName, triggerName) { findAutoIncrementField: function(factory) {
var sql = 'DROP TRIGGER <%= triggerName %> ON <%= tableName %> RESTRICT;'; var fields = [];
return Utils._.template(sql)({
triggerName: triggerName,
tableName: tableName
});
},
renameTrigger: function(tableName, oldTriggerName, newTriggerName) {
var sql = 'ALTER TRIGGER <%= oldTriggerName %> ON <%= tableName %> RENAME TO <%= newTriggerName%>;';
return Utils._.template(sql)({
tableName: tableName,
oldTriggerName: oldTriggerName,
newTriggerName: newTriggerName
});
},
createFunction: function(functionName, params, returnType, language, body, options) {
var sql = ['CREATE FUNCTION <%= functionName %>(<%= paramList %>)'
, 'RETURNS <%= returnType %> AS $func$'
, 'BEGIN'
, '\t<%= body %>'
, 'END;'
, "$func$ language '<%= language %>'<%= options %>;"
].join('\n');
return Utils._.template(sql)({
functionName: functionName,
paramList: this.expandFunctionParamList(params),
returnType: returnType,
body: body.replace('\n', '\n\t'),
language: language,
options: this.expandOptions(options)
});
},
dropFunction: function(functionName, params) {
// RESTRICT is (currently, as of 9.2) default but we'll be explicit
var sql = 'DROP FUNCTION <%= functionName %>(<%= paramList %>) RESTRICT;';
return Utils._.template(sql)({
functionName: functionName,
paramList: this.expandFunctionParamList(params)
});
},
renameFunction: function(oldFunctionName, params, newFunctionName) {
var sql = 'ALTER FUNCTION <%= oldFunctionName %>(<%= paramList %>) RENAME TO <%= newFunctionName %>;';
return Utils._.template(sql)({
oldFunctionName: oldFunctionName,
paramList: this.expandFunctionParamList(params),
newFunctionName: newFunctionName
});
},
databaseConnectionUri: function(config) {
var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %><% if(ssl) { %>?ssl=<%= ssl %><% } %>';
return Utils._.template(template)({
user: config.username,
password: config.password,
database: config.database,
host: config.host,
port: config.port,
protocol: config.protocol,
ssl: config.ssl
});
},
pgEscapeAndQuote: function(val) { for (var name in factory.attributes) {
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'")); var definition = factory.attributes[name];
},
expandFunctionParamList: function expandFunctionParamList(params) { if (definition && definition.autoIncrement) {
if (Utils._.isUndefined(params) || !Utils._.isArray(params)) { fields.push(name);
throw new Error('expandFunctionParamList: function parameters array required, including an empty one for no arguments');
} }
}
var paramList = Utils._.each(params, function expandParam(curParam) { return fields;
var paramDef = []; },
if (Utils._.has(curParam, 'type')) {
if (Utils._.has(curParam, 'direction')) { paramDef.push(curParam.direction); } createTrigger: function(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
if (Utils._.has(curParam, 'name')) { paramDef.push(curParam.name); } var sql = [
paramDef.push(curParam.type); 'CREATE <%= constraintVal %>TRIGGER <%= triggerName %>'
} else { , '<%= eventType %> <%= eventSpec %>'
throw new Error('createFunction called with a parameter with no type'); , 'ON <%= tableName %>'
} , '<%= optionsSpec %>'
return paramDef.join(' '); , 'EXECUTE PROCEDURE <%= functionName %>(<%= paramList %>);'
}); ].join('\n\t');
return paramList.join(', ');
}, return Utils._.template(sql)({
constraintVal: this.triggerEventTypeIsConstraint(eventType),
expandOptions: function expandOptions(options) { triggerName: triggerName,
return Utils._.isUndefined(options) || Utils._.isEmpty(options) ? eventType: this.decodeTriggerEventType(eventType),
'' : '\n\t' + options.join('\n\t'); eventSpec: this.expandTriggerEventSpec(fireOnSpec),
}, tableName: tableName,
optionsSpec: this.expandOptions(optionsArray),
decodeTriggerEventType: function decodeTriggerEventType(eventSpecifier) { functionName: functionName,
var EVENT_DECODER = { paramList: this.expandFunctionParamList(functionParams)
'after': 'AFTER', });
'before': 'BEFORE', },
'instead_of': 'INSTEAD OF',
'after_constraint': 'AFTER' dropTrigger: function(tableName, triggerName) {
}; var sql = 'DROP TRIGGER <%= triggerName %> ON <%= tableName %> RESTRICT;';
return Utils._.template(sql)({
triggerName: triggerName,
tableName: tableName
});
},
renameTrigger: function(tableName, oldTriggerName, newTriggerName) {
var sql = 'ALTER TRIGGER <%= oldTriggerName %> ON <%= tableName %> RENAME TO <%= newTriggerName%>;';
return Utils._.template(sql)({
tableName: tableName,
oldTriggerName: oldTriggerName,
newTriggerName: newTriggerName
});
},
createFunction: function(functionName, params, returnType, language, body, options) {
var sql = ['CREATE FUNCTION <%= functionName %>(<%= paramList %>)'
, 'RETURNS <%= returnType %> AS $func$'
, 'BEGIN'
, '\t<%= body %>'
, 'END;'
, "$func$ language '<%= language %>'<%= options %>;"
].join('\n');
return Utils._.template(sql)({
functionName: functionName,
paramList: this.expandFunctionParamList(params),
returnType: returnType,
body: body.replace('\n', '\n\t'),
language: language,
options: this.expandOptions(options)
});
},
dropFunction: function(functionName, params) {
// RESTRICT is (currently, as of 9.2) default but we'll be explicit
var sql = 'DROP FUNCTION <%= functionName %>(<%= paramList %>) RESTRICT;';
return Utils._.template(sql)({
functionName: functionName,
paramList: this.expandFunctionParamList(params)
});
},
renameFunction: function(oldFunctionName, params, newFunctionName) {
var sql = 'ALTER FUNCTION <%= oldFunctionName %>(<%= paramList %>) RENAME TO <%= newFunctionName %>;';
return Utils._.template(sql)({
oldFunctionName: oldFunctionName,
paramList: this.expandFunctionParamList(params),
newFunctionName: newFunctionName
});
},
databaseConnectionUri: function(config) {
var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %><% if(ssl) { %>?ssl=<%= ssl %><% } %>';
return Utils._.template(template)({
user: config.username,
password: config.password,
database: config.database,
host: config.host,
port: config.port,
protocol: config.protocol,
ssl: config.ssl
});
},
pgEscapeAndQuote: function(val) {
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"));
},
expandFunctionParamList: function expandFunctionParamList(params) {
if (Utils._.isUndefined(params) || !Utils._.isArray(params)) {
throw new Error('expandFunctionParamList: function parameters array required, including an empty one for no arguments');
}
if (!Utils._.has(EVENT_DECODER, eventSpecifier)) { var paramList = Utils._.each(params, function expandParam(curParam) {
throw new Error('Invalid trigger event specified: ' + eventSpecifier); var paramDef = [];
} if (Utils._.has(curParam, 'type')) {
if (Utils._.has(curParam, 'direction')) { paramDef.push(curParam.direction); }
if (Utils._.has(curParam, 'name')) { paramDef.push(curParam.name); }
paramDef.push(curParam.type);
} else {
throw new Error('createFunction called with a parameter with no type');
}
return paramDef.join(' ');
});
return paramList.join(', ');
},
expandOptions: function expandOptions(options) {
return Utils._.isUndefined(options) || Utils._.isEmpty(options) ?
'' : '\n\t' + options.join('\n\t');
},
decodeTriggerEventType: function decodeTriggerEventType(eventSpecifier) {
var EVENT_DECODER = {
'after': 'AFTER',
'before': 'BEFORE',
'instead_of': 'INSTEAD OF',
'after_constraint': 'AFTER'
};
if (!Utils._.has(EVENT_DECODER, eventSpecifier)) {
throw new Error('Invalid trigger event specified: ' + eventSpecifier);
}
return EVENT_DECODER[eventSpecifier]; return EVENT_DECODER[eventSpecifier];
}, },
triggerEventTypeIsConstraint: function triggerEventTypeIsConstraint(eventSpecifier) { triggerEventTypeIsConstraint: function triggerEventTypeIsConstraint(eventSpecifier) {
return eventSpecifier === 'after_constrain' ? 'CONSTRAINT ' : ''; return eventSpecifier === 'after_constrain' ? 'CONSTRAINT ' : '';
}, },
expandTriggerEventSpec: function expandTriggerEventSpec(fireOnSpec) { expandTriggerEventSpec: function expandTriggerEventSpec(fireOnSpec) {
if (Utils._.isEmpty(fireOnSpec)) { if (Utils._.isEmpty(fireOnSpec)) {
throw new Error('no table change events specified to trigger on'); throw new Error('no table change events specified to trigger on');
} }
return Utils._.map(fireOnSpec, function parseTriggerEventSpec(fireValue, fireKey) { return Utils._.map(fireOnSpec, function parseTriggerEventSpec(fireValue, fireKey) {
var EVENT_MAP = { var EVENT_MAP = {
'insert': 'INSERT', 'insert': 'INSERT',
'update': 'UPDATE', 'update': 'UPDATE',
'delete': 'DELETE', 'delete': 'DELETE',
'truncate': 'TRUNCATE' 'truncate': 'TRUNCATE'
}; };
if (!Utils._.has(EVENT_MAP, fireKey)) { if (!Utils._.has(EVENT_MAP, fireKey)) {
throw new Error('parseTriggerEventSpec: undefined trigger event ' + fireKey); throw new Error('parseTriggerEventSpec: undefined trigger event ' + fireKey);
} }
var eventSpec = EVENT_MAP[fireKey]; var eventSpec = EVENT_MAP[fireKey];
if (eventSpec === 'UPDATE') { if (eventSpec === 'UPDATE') {
if (Utils._.isArray(fireValue) && fireValue.length > 0) { if (Utils._.isArray(fireValue) && fireValue.length > 0) {
eventSpec += ' OF ' + fireValue.join(', '); eventSpec += ' OF ' + fireValue.join(', ');
}
} }
return eventSpec;
}).join(' OR ');
},
pgListEnums: function(tableName, attrName, options) {
if (arguments.length === 1) {
options = tableName;
tableName = null;
} }
var enumName = ''; return eventSpec;
}).join(' OR ');
},
if (!!tableName && !!attrName) { pgListEnums: function(tableName, attrName, options) {
enumName = ' AND t.typname=' + this.escape('enum_' + tableName + '_' + attrName) + ' '; if (arguments.length === 1) {
} options = tableName;
tableName = null;
}
var query = 'SELECT t.typname enum_name, array_agg(e.enumlabel) enum_value FROM pg_type t ' + var enumName = '';
'JOIN pg_enum e ON t.oid = e.enumtypid ' +
'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
"WHERE n.nspname = 'public' " + enumName + ' GROUP BY 1';
return query; if (!!tableName && !!attrName) {
}, enumName = ' AND t.typname=' + this.escape('enum_' + tableName + '_' + attrName) + ' ';
}
pgEnum: function(tableName, attr, dataType, options) { var query = 'SELECT t.typname enum_name, array_agg(e.enumlabel) enum_value FROM pg_type t ' +
var enumName = this.pgEscapeAndQuote('enum_' + tableName + '_' + attr) 'JOIN pg_enum e ON t.oid = e.enumtypid ' +
, values; 'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
"WHERE n.nspname = 'public' " + enumName + ' GROUP BY 1';
if (dataType.values) { return query;
values = "ENUM('" + dataType.values.join("', '") + "')"; },
} else {
values = dataType.toString().match(/^ENUM\(.+\)/)[0];
}
var sql = 'CREATE TYPE ' + enumName + ' AS ' + values + '; '; pgEnum: function(tableName, attr, dataType, options) {
if (!!options && options.force === true) { var enumName = this.pgEscapeAndQuote('enum_' + tableName + '_' + attr)
sql = this.pgEnumDrop(tableName, attr) + sql; , values;
}
return sql;
},
pgEnumAdd: function(tableName, attr, value, options) { if (dataType.values) {
var enumName = this.pgEscapeAndQuote('enum_' + tableName + '_' + attr); values = "ENUM('" + dataType.values.join("', '") + "')";
var sql = 'ALTER TYPE ' + enumName + ' ADD VALUE ' + this.escape(value); } else {
values = dataType.toString().match(/^ENUM\(.+\)/)[0];
}
if (!!options.before) { var sql = 'CREATE TYPE ' + enumName + ' AS ' + values + '; ';
sql += ' BEFORE ' + this.escape(options.before); if (!!options && options.force === true) {
} sql = this.pgEnumDrop(tableName, attr) + sql;
else if (!!options.after) { }
sql += ' AFTER ' + this.escape(options.after); return sql;
} },
return sql; pgEnumAdd: function(tableName, attr, value, options) {
}, var enumName = this.pgEscapeAndQuote('enum_' + tableName + '_' + attr);
var sql = 'ALTER TYPE ' + enumName + ' ADD VALUE ' + this.escape(value);
pgEnumDrop: function(tableName, attr, enumName) { if (!!options.before) {
enumName = enumName || this.pgEscapeAndQuote('enum_' + tableName + '_' + attr); sql += ' BEFORE ' + this.escape(options.before);
return 'DROP TYPE IF EXISTS ' + enumName + '; '; }
}, else if (!!options.after) {
sql += ' AFTER ' + this.escape(options.after);
}
fromArray: function(text) { return sql;
text = text.replace(/^{/, '').replace(/}$/, ''); },
var matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig);
if (matches.length < 1) { pgEnumDrop: function(tableName, attr, enumName) {
return []; enumName = enumName || this.pgEscapeAndQuote('enum_' + tableName + '_' + attr);
} return 'DROP TYPE IF EXISTS ' + enumName + '; ';
},
matches = matches.map(function(m) { fromArray: function(text) {
return m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/, ''); text = text.replace(/^{/, '').replace(/}$/, '');
}); var matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig);
return matches.slice(0, -1); if (matches.length < 1) {
}, return [];
}
padInt: function(i) { matches = matches.map(function(m) {
return (i < 10) ? '0' + i.toString() : i.toString(); return m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/, '');
}, });
pgDataTypeMapping: function(tableName, attr, dataType) { return matches.slice(0, -1);
return this.dataTypeMapping(tableName, attr, dataType); },
},
dataTypeMapping: function(tableName, attr, dataType) { padInt: function(i) {
if (Utils._.includes(dataType, 'PRIMARY KEY')) { return (i < 10) ? '0' + i.toString() : i.toString();
primaryKeys[tableName].push(attr); },
dataType = dataType.replace(/PRIMARY KEY/, '');
}
if (Utils._.includes(dataType, 'SERIAL')) { pgDataTypeMapping: function(tableName, attr, dataType) {
if (Utils._.includes(dataType, 'BIGINT')) { return this.dataTypeMapping(tableName, attr, dataType);
dataType = dataType.replace(/SERIAL/, 'BIGSERIAL'); },
dataType = dataType.replace(/BIGINT/, '');
} else {
dataType = dataType.replace(/INTEGER/, '');
}
dataType = dataType.replace(/NOT NULL/, '');
}
if (dataType.match(/^ENUM\(/)) { dataTypeMapping: function(tableName, attr, dataType) {
dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEscapeAndQuote('enum_' + tableName + '_' + attr)); if (Utils._.includes(dataType, 'PRIMARY KEY')) {
} primaryKeys[tableName].push(attr);
dataType = dataType.replace(/PRIMARY KEY/, '');
}
return dataType; if (Utils._.includes(dataType, 'SERIAL')) {
}, if (Utils._.includes(dataType, 'BIGINT')) {
dataType = dataType.replace(/SERIAL/, 'BIGSERIAL');
quoteIdentifier: function(identifier, force) { dataType = dataType.replace(/BIGINT/, '');
if (identifier === '*') return identifier;
if (!force && this.options && this.options.quoteIdentifiers === false) { // default is `true`
// In Postgres, if tables or attributes are created double-quoted,
// they are also case sensitive. If they contain any uppercase
// characters, they must always be double-quoted. This makes it
// impossible to write queries in portable SQL if tables are created in
// this way. Hence, we strip quotes if we don't want case sensitivity.
return Utils.removeTicks(identifier, '"');
} else { } else {
return Utils.addTicks(identifier, '"'); dataType = dataType.replace(/INTEGER/, '');
}
},
/*
Escape a value (e.g. a string, number or date)
*/
escape: function(value, field) {
if (value && value._isSequelizeMethod) {
return this.handleSequelizeMethod(value);
} }
dataType = dataType.replace(/NOT NULL/, '');
}
if (Utils._.isObject(value) && field && (field.type instanceof DataTypes.HSTORE || DataTypes.ARRAY.is(field.type, DataTypes.HSTORE))) { if (dataType.match(/^ENUM\(/)) {
if (field.type instanceof DataTypes.HSTORE){ dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEscapeAndQuote('enum_' + tableName + '_' + attr));
return "'" + hstore.stringify(value) + "'"; }
} else if (DataTypes.ARRAY.is(field.type, DataTypes.HSTORE)) {
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + hstore.stringify(v) + "'::hstore";}).join(',') + ']::HSTORE[]';
}
} else if(Utils._.isArray(value) && field && (field.type instanceof DataTypes.RANGE || DataTypes.ARRAY.is(field.type, DataTypes.RANGE))) {
if(field.type instanceof DataTypes.RANGE) { // escape single value
return "'" + range.stringify(value) + "'";
}
else if (DataTypes.ARRAY.is(field.type, DataTypes.RANGE)) { // escape array of ranges
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + range.stringify(v) + "'";}).join(',') + ']::' + field.type.toString();
}
} else if (field && (field.type instanceof DataTypes.JSON || field.type instanceof DataTypes.JSONB)) {
value = JSON.stringify(value);
} else if (Array.isArray(value) && field && DataTypes.ARRAY.is(field.type, DataTypes.JSON)) {
return 'ARRAY[' + value.map(function (v) {
return SqlString.escape(JSON.stringify(v), false, this.options.timezone, this.dialect, field);
}, this).join(',') + ']::JSON[]';
}
return SqlString.escape(value, false, this.options.timezone, this.dialect, field); return dataType;
}, },
/** quoteIdentifier: function(identifier, force) {
* Generates an SQL query that returns all foreign keys of a table. if (identifier === '*') return identifier;
* if (!force && this.options && this.options.quoteIdentifiers === false) { // default is `true`
* @param {String} tableName The name of the table. // In Postgres, if tables or attributes are created double-quoted,
* @param {String} schemaName The name of the schema. // they are also case sensitive. If they contain any uppercase
* @return {String} The generated sql query. // characters, they must always be double-quoted. This makes it
*/ // impossible to write queries in portable SQL if tables are created in
getForeignKeysQuery: function(tableName, schemaName) { // this way. Hence, we strip quotes if we don't want case sensitivity.
return 'SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r ' + return Utils.removeTicks(identifier, '"');
"WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '" + tableName + "' LIMIT 1) AND r.contype = 'f' ORDER BY 1;"; } else {
}, return Utils.addTicks(identifier, '"');
}
/** },
* Generates an SQL query that removes a foreign key from a table.
* /*
* @param {String} tableName The name of the table. Escape a value (e.g. a string, number or date)
* @param {String} foreignKey The name of the foreign key constraint. */
* @return {String} The generated sql query. escape: function(value, field) {
*/ if (value && value._isSequelizeMethod) {
dropForeignKeyQuery: function(tableName, foreignKey) { return this.handleSequelizeMethod(value);
return 'ALTER TABLE ' + this.quoteTable(tableName) + ' DROP CONSTRAINT ' + this.quoteIdentifier(foreignKey) + ';'; }
},
setAutocommitQuery: function(value, options) {
if (options.parent) {
return;
}
// POSTGRES does not support setting AUTOCOMMIT = OFF if (Utils._.isObject(value) && field && (field.type instanceof DataTypes.HSTORE || DataTypes.ARRAY.is(field.type, DataTypes.HSTORE))) {
if (!value) { if (field.type instanceof DataTypes.HSTORE){
return; return "'" + hstore.stringify(value) + "'";
} } else if (DataTypes.ARRAY.is(field.type, DataTypes.HSTORE)) {
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + hstore.stringify(v) + "'::hstore";}).join(',') + ']::HSTORE[]';
}
} else if(Utils._.isArray(value) && field && (field.type instanceof DataTypes.RANGE || DataTypes.ARRAY.is(field.type, DataTypes.RANGE))) {
if(field.type instanceof DataTypes.RANGE) { // escape single value
return "'" + range.stringify(value) + "'";
}
else if (DataTypes.ARRAY.is(field.type, DataTypes.RANGE)) { // escape array of ranges
return 'ARRAY[' + Utils._.map(value, function(v){return "'" + range.stringify(v) + "'";}).join(',') + ']::' + field.type.toString();
}
} else if (field && (field.type instanceof DataTypes.JSON || field.type instanceof DataTypes.JSONB)) {
value = JSON.stringify(value);
} else if (Array.isArray(value) && field && DataTypes.ARRAY.is(field.type, DataTypes.JSON)) {
return 'ARRAY[' + value.map(function (v) {
return SqlString.escape(JSON.stringify(v), false, this.options.timezone, this.dialect, field);
}, this).join(',') + ']::JSON[]';
}
return AbstractQueryGenerator.setAutocommitQuery.call(this, value, options); return SqlString.escape(value, false, this.options.timezone, this.dialect, field);
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
return 'SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r ' +
"WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '" + tableName + "' LIMIT 1) AND r.contype = 'f' ORDER BY 1;";
},
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {String} tableName The name of the table.
* @param {String} foreignKey The name of the foreign key constraint.
* @return {String} The generated sql query.
*/
dropForeignKeyQuery: function(tableName, foreignKey) {
return 'ALTER TABLE ' + this.quoteTable(tableName) + ' DROP CONSTRAINT ' + this.quoteIdentifier(foreignKey) + ';';
},
setAutocommitQuery: function(value, options) {
if (options.parent) {
return;
} }
};
return Utils._.extend(Utils._.clone(AbstractQueryGenerator), QueryGenerator); // POSTGRES does not support setting AUTOCOMMIT = OFF
})(); if (!value) {
return;
}
return AbstractQueryGenerator.setAutocommitQuery.call(this, value, options);
}
};
module.exports = Utils._.extend(Utils._.clone(AbstractQueryGenerator), QueryGenerator);
...@@ -66,212 +66,236 @@ function dialectSpecificFieldDatatypeMap (options, prefix) { ...@@ -66,212 +66,236 @@ function dialectSpecificFieldDatatypeMap (options, prefix) {
return fields; return fields;
} }
module.exports = (function() { var Query = function(client, sequelize, options) {
var Query = function(client, sequelize, options) { this.client = client;
this.client = client; this.sequelize = sequelize;
this.sequelize = sequelize; this.instance = options.instance;
this.instance = options.instance; this.model = options.model;
this.model = options.model; this.options = Utils._.extend({
this.options = Utils._.extend({ logging: console.log,
logging: console.log, plain: false,
plain: false, raw: false
raw: false }, options || {});
}, options || {});
this.checkLoggingOption();
this.checkLoggingOption(); };
}; Utils.inherit(Query, AbstractQuery);
Utils.inherit(Query, AbstractQuery);
Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields;
Query.prototype.run = function(sql) {
this.sql = sql;
var self = this
, receivedError = false
, query = this.client.query(sql)
, rows = [];
this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options);
var promise = new Promise(function(resolve, reject) {
query.on('row', function(row) {
rows.push(row);
});
query.on('error', function(err) { Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields;
receivedError = true;
err.sql = sql;
reject(self.formatError(err));
});
query.on('end', function(result) { Query.prototype.run = function(sql) {
if (receivedError) { this.sql = sql;
return;
}
resolve([rows, sql, result]); var self = this
}); , receivedError = false
}).spread(function(rows, sql, result) { , query = this.client.query(sql)
var results = rows , rows = [];
, isTableNameQuery = (sql.indexOf('SELECT table_name FROM information_schema.tables') === 0)
, isRelNameQuery = (sql.indexOf('SELECT relname FROM pg_class WHERE oid IN') === 0) this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options);
, dialectSpecificFields
, isDialectSpecificField = Utils._.memoize(function (key) { return dialectSpecificFields.hasOwnProperty(key); }); var promise = new Promise(function(resolve, reject) {
query.on('row', function(row) {
if (isTableNameQuery || isRelNameQuery) { rows.push(row);
if (isRelNameQuery) { });
results = rows.map(function(row) {
return { query.on('error', function(err) {
name: row.relname, receivedError = true;
tableName: row.relname.split('_')[0] err.sql = sql;
}; reject(self.formatError(err));
}); });
} else {
results = rows.map(function(row) { return Utils._.values(row); }); query.on('end', function(result) {
} if (receivedError) {
return results; return;
} }
if (rows[0] && rows[0].sequelize_caught_exception !== undefined) { resolve([rows, sql, result]);
if (rows[0].sequelize_caught_exception !== null) { });
throw new self.formatError({ }).spread(function(rows, sql, result) {
code: '23505', var results = rows
detail: rows[0].sequelize_caught_exception , isTableNameQuery = (sql.indexOf('SELECT table_name FROM information_schema.tables') === 0)
}); , isRelNameQuery = (sql.indexOf('SELECT relname FROM pg_class WHERE oid IN') === 0)
} else { , dialectSpecificFields
rows = rows.map(function (row) { , isDialectSpecificField = Utils._.memoize(function (key) { return dialectSpecificFields.hasOwnProperty(key); });
delete row.sequelize_caught_exception;
return row; if (isTableNameQuery || isRelNameQuery) {
}); if (isRelNameQuery) {
} results = rows.map(function(row) {
return {
name: row.relname,
tableName: row.relname.split('_')[0]
};
});
} else {
results = rows.map(function(row) { return Utils._.values(row); });
} }
return results;
}
if (self.isShowIndexesQuery()) { if (rows[0] && rows[0].sequelize_caught_exception !== undefined) {
results.forEach(function (result) { if (rows[0].sequelize_caught_exception !== null) {
var attributes = /ON .*? (?:USING .*?\s)?\((.*)\)/gi.exec(result.definition)[1].split(',') throw new self.formatError({
, field code: '23505',
detail: rows[0].sequelize_caught_exception
, attribute
, columns;
// Map column index in table to column name
columns = Utils._.zipObject(
result.column_indexes,
self.sequelize.queryInterface.QueryGenerator.fromArray(result.column_names)
);
delete result.column_indexes;
delete result.column_names;
// Indkey is the order of attributes in the index, specified by a string of attribute indexes
result.fields = result.indkey.split(' ').map(function (indKey, index) {
field = columns[indKey];
attribute = attributes[index];
return {
attribute: field,
collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : undefined,
order: attribute.indexOf('DESC') !== -1 ? 'DESC' : attribute.indexOf('ASC') !== -1 ? 'ASC': undefined,
length: undefined
};
});
delete result.columns;
}); });
return results; } else {
} else if (self.isForeignKeysQuery()) { rows = rows.map(function (row) {
result = []; delete row.sequelize_caught_exception;
rows.forEach(function(row) { return row;
var defParts; });
if (row.condef !== undefined && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) { }
row.id = row.constraint_name; }
row.table = defParts[2];
row.from = defParts[1]; if (self.isShowIndexesQuery()) {
row.to = defParts[3]; results.forEach(function (result) {
var i; var attributes = /ON .*? (?:USING .*?\s)?\((.*)\)/gi.exec(result.definition)[1].split(',')
for (i=5;i<=8;i+=3) { , field
if (/(UPDATE|DELETE)/.test(defParts[i])) {
row['on_'+defParts[i].toLowerCase()] = defParts[i+1]; , attribute
} , columns;
// Map column index in table to column name
columns = Utils._.zipObject(
result.column_indexes,
self.sequelize.queryInterface.QueryGenerator.fromArray(result.column_names)
);
delete result.column_indexes;
delete result.column_names;
// Indkey is the order of attributes in the index, specified by a string of attribute indexes
result.fields = result.indkey.split(' ').map(function (indKey, index) {
field = columns[indKey];
attribute = attributes[index];
return {
attribute: field,
collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : undefined,
order: attribute.indexOf('DESC') !== -1 ? 'DESC' : attribute.indexOf('ASC') !== -1 ? 'ASC': undefined,
length: undefined
};
});
delete result.columns;
});
return results;
} else if (self.isForeignKeysQuery()) {
result = [];
rows.forEach(function(row) {
var defParts;
if (row.condef !== undefined && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) {
row.id = row.constraint_name;
row.table = defParts[2];
row.from = defParts[1];
row.to = defParts[3];
var i;
for (i=5;i<=8;i+=3) {
if (/(UPDATE|DELETE)/.test(defParts[i])) {
row['on_'+defParts[i].toLowerCase()] = defParts[i+1];
} }
} }
result.push(row);
});
return result;
} else if (self.isSelectQuery()) {
// Postgres will treat tables as case-insensitive, so fix the case
// of the returned values to match attributes
if (self.options.raw === false && self.sequelize.options.quoteIdentifiers === false) {
var attrsMap = Utils._.reduce(self.model.attributes, function(m, v, k) { m[k.toLowerCase()] = k; return m; }, {});
rows.forEach(function(row) {
Utils._.keys(row).forEach(function(key) {
var targetAttr = attrsMap[key];
if (typeof targetAttr === 'string' && targetAttr !== key) {
row[targetAttr] = row[key];
delete row[key];
}
});
});
} }
result.push(row);
if (!!self.model && rows.length > 0) { });
dialectSpecificFields = dialectSpecificFieldDatatypeMap({ return result;
callee: self.model, } else if (self.isSelectQuery()) {
include: self.options.include, // Postgres will treat tables as case-insensitive, so fix the case
types: dialectSpecificTypes // of the returned values to match attributes
if (self.options.raw === false && self.sequelize.options.quoteIdentifiers === false) {
var attrsMap = Utils._.reduce(self.model.attributes, function(m, v, k) { m[k.toLowerCase()] = k; return m; }, {});
rows.forEach(function(row) {
Utils._.keys(row).forEach(function(key) {
var targetAttr = attrsMap[key];
if (typeof targetAttr === 'string' && targetAttr !== key) {
row[targetAttr] = row[key];
delete row[key];
}
}); });
});
}
// check whether custom fields exist in responses model if (!!self.model && rows.length > 0) {
if (!Utils._.isEmpty(dialectSpecificFields)) { dialectSpecificFields = dialectSpecificFieldDatatypeMap({
rows.forEach(function (row, rowId, rows) { callee: self.model,
Utils._.each(row, function (value, key) { include: self.options.include,
if (isDialectSpecificField(key)) types: dialectSpecificTypes
rows[rowId][key] = });
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
}); // check whether custom fields exist in responses model
if (!Utils._.isEmpty(dialectSpecificFields)) {
rows.forEach(function (row, rowId, rows) {
Utils._.each(row, function (value, key) {
if (isDialectSpecificField(key))
rows[rowId][key] =
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
}); });
} });
} }
}
return self.handleSelectQuery(rows); return self.handleSelectQuery(rows);
} else if (QueryTypes.DESCRIBE === self.options.type) { } else if (QueryTypes.DESCRIBE === self.options.type) {
result = {}; result = {};
rows.forEach(function(_result) { rows.forEach(function(_result) {
result[_result.Field] = { result[_result.Field] = {
type: _result.Type.toUpperCase(), type: _result.Type.toUpperCase(),
allowNull: (_result.Null === 'YES'), allowNull: (_result.Null === 'YES'),
defaultValue: _result.Default, defaultValue: _result.Default,
special: (!!_result.special ? self.sequelize.queryInterface.QueryGenerator.fromArray(_result.special) : []) special: (!!_result.special ? self.sequelize.queryInterface.QueryGenerator.fromArray(_result.special) : [])
}; };
if (result[_result.Field].type === 'BOOLEAN') { if (result[_result.Field].type === 'BOOLEAN') {
result[_result.Field].defaultValue = { 'false': false, 'true': true }[result[_result.Field].defaultValue]; result[_result.Field].defaultValue = { 'false': false, 'true': true }[result[_result.Field].defaultValue];
if (result[_result.Field].defaultValue === undefined) { if (result[_result.Field].defaultValue === undefined) {
result[_result.Field].defaultValue = null; result[_result.Field].defaultValue = null;
}
} }
}
if (typeof result[_result.Field].defaultValue === 'string') { if (typeof result[_result.Field].defaultValue === 'string') {
result[_result.Field].defaultValue = result[_result.Field].defaultValue.replace(/'/g, ''); result[_result.Field].defaultValue = result[_result.Field].defaultValue.replace(/'/g, '');
if (result[_result.Field].defaultValue.indexOf('::') > -1) { if (result[_result.Field].defaultValue.indexOf('::') > -1) {
var split = result[_result.Field].defaultValue.split('::'); var split = result[_result.Field].defaultValue.split('::');
if (split[1].toLowerCase() !== 'regclass)') { if (split[1].toLowerCase() !== 'regclass)') {
result[_result.Field].defaultValue = split[0]; result[_result.Field].defaultValue = split[0];
}
} }
} }
}
});
return result;
} else if (self.isShowOrDescribeQuery()) {
return results;
} else if (QueryTypes.BULKUPDATE === self.options.type) {
if (!self.options.returning) {
return parseInt(result.rowCount, 10);
}
if (!!self.model && rows.length > 0) {
dialectSpecificFields = dialectSpecificFieldDatatypeMap({
callee: self.model,
types: dialectSpecificTypes
}); });
return result; // check whether custom fields exist in responses model
} else if (self.isShowOrDescribeQuery()) { if (!Utils._.isEmpty(dialectSpecificFields)) {
return results; rows.forEach(function (row, rowId, rows) {
} else if (QueryTypes.BULKUPDATE === self.options.type) { Utils._.each(row, function (value, key) {
if (!self.options.returning) { if (isDialectSpecificField(key))
return parseInt(result.rowCount, 10); rows[rowId][key] =
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
});
});
} }
}
if (!!self.model && rows.length > 0) { return self.handleSelectQuery(rows);
} else if (QueryTypes.BULKDELETE === self.options.type) {
return parseInt(result.rowCount, 10);
} else if (self.isUpsertQuery()) {
return rows[0].sequelize_upsert;
} else if (self.isInsertQuery() || self.isUpdateQuery()) {
if (self.instance && self.instance.dataValues) {
if (self.model) {
dialectSpecificFields = dialectSpecificFieldDatatypeMap({ dialectSpecificFields = dialectSpecificFieldDatatypeMap({
callee: self.model, callee: self.model,
types: dialectSpecificTypes types: dialectSpecificTypes
...@@ -279,156 +303,130 @@ module.exports = (function() { ...@@ -279,156 +303,130 @@ module.exports = (function() {
// check whether custom fields exist in responses model // check whether custom fields exist in responses model
if (!Utils._.isEmpty(dialectSpecificFields)) { if (!Utils._.isEmpty(dialectSpecificFields)) {
rows.forEach(function (row, rowId, rows) { Utils._.each(rows[0], function (value, key) {
Utils._.each(row, function (value, key) { if (isDialectSpecificField(key))
if (isDialectSpecificField(key)) rows[0][key] =
rows[rowId][key] = parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
});
}); });
} }
} }
return self.handleSelectQuery(rows); for (var key in rows[0]) {
} else if (QueryTypes.BULKDELETE === self.options.type) { if (rows[0].hasOwnProperty(key)) {
return parseInt(result.rowCount, 10); var record = rows[0][key];
} else if (self.isUpsertQuery()) {
return rows[0].sequelize_upsert;
} else if (self.isInsertQuery() || self.isUpdateQuery()) {
if (self.instance && self.instance.dataValues) {
if (self.model) {
dialectSpecificFields = dialectSpecificFieldDatatypeMap({
callee: self.model,
types: dialectSpecificTypes
});
// check whether custom fields exist in responses model
if (!Utils._.isEmpty(dialectSpecificFields)) {
Utils._.each(rows[0], function (value, key) {
if (isDialectSpecificField(key))
rows[0][key] =
parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
});
}
}
for (var key in rows[0]) { var attr = Utils._.find(self.model.rawAttributes, function (attribute) {
if (rows[0].hasOwnProperty(key)) { return attribute.fieldName === key || attribute.field === key;
var record = rows[0][key]; });
var attr = Utils._.find(self.model.rawAttributes, function (attribute) {
return attribute.fieldName === key || attribute.field === key;
});
self.instance.dataValues[attr && attr.fieldName || key] = record; self.instance.dataValues[attr && attr.fieldName || key] = record;
}
} }
} }
return self.instance || (rows && ((self.options.plain && rows[0]) || rows)) || undefined;
} else if (self.isVersionQuery()) {
return results[0].version;
} else if (self.isRawQuery()) {
return [rows, result];
} else {
return results;
} }
});
return promise; return self.instance || (rows && ((self.options.plain && rows[0]) || rows)) || undefined;
}; } else if (self.isVersionQuery()) {
return results[0].version;
Query.prototype.formatError = function (err) { } else if (self.isRawQuery()) {
var match return [rows, result];
, table } else {
, index return results;
, fields }
, errors });
, message;
var code = err.code || err.sqlState
, errMessage = err.message || err.messagePrimary
, errDetail = err.detail || err.messageDetail;
switch (code) {
case '23503':
index = errMessage.match(/violates foreign key constraint \"(.+?)\"/);
index = index ? index[1] : undefined;
table = errMessage.match(/on table \"(.+?)\"/);
table = table ? table[1] : undefined;
return new sequelizeErrors.ForeignKeyConstraintError({
fields: null,
index: index,
table: table,
parent: err
});
case '23505':
// there are multiple different formats of error messages for this error code
// this regex should check at least two
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
if (match) {
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
errors = [];
message = 'Validation error';
Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value));
});
if (this.model && this.model.uniqueKeys) {
Utils._.forOwn(this.model.uniqueKeys, function(constraint) {
if (Utils._.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
message = constraint.msg;
return false;
}
});
}
return new sequelizeErrors.UniqueConstraintError({ return promise;
message: message, };
errors: errors,
parent: err,
fields: fields
});
} else {
return new sequelizeErrors.UniqueConstraintError({
message: errMessage,
parent: err
});
}
break; Query.prototype.formatError = function (err) {
case '23P01': var match
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/); , table
, index
, fields
, errors
, message;
var code = err.code || err.sqlState
, errMessage = err.message || err.messagePrimary
, errDetail = err.detail || err.messageDetail;
switch (code) {
case '23503':
index = errMessage.match(/violates foreign key constraint \"(.+?)\"/);
index = index ? index[1] : undefined;
table = errMessage.match(/on table \"(.+?)\"/);
table = table ? table[1] : undefined;
return new sequelizeErrors.ForeignKeyConstraintError({
fields: null,
index: index,
table: table,
parent: err
});
case '23505':
// there are multiple different formats of error messages for this error code
// this regex should check at least two
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
if (match) {
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
errors = [];
message = 'Validation error';
Utils._.forOwn(fields, function(value, field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, value));
});
if (match) { if (this.model && this.model.uniqueKeys) {
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', ')); Utils._.forOwn(this.model.uniqueKeys, function(constraint) {
if (Utils._.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
message = constraint.msg;
return false;
}
});
} }
message = 'Exclusion constraint error';
return new sequelizeErrors.ExclusionConstraintError({ return new sequelizeErrors.UniqueConstraintError({
message: message, message: message,
constraint: err.constraint, errors: errors,
fields: fields, parent: err,
table: err.table, fields: fields
});
} else {
return new sequelizeErrors.UniqueConstraintError({
message: errMessage,
parent: err parent: err
}); });
}
default: break;
return new sequelizeErrors.DatabaseError(err); case '23P01':
} match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
};
if (match) {
fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
}
message = 'Exclusion constraint error';
return new sequelizeErrors.ExclusionConstraintError({
message: message,
constraint: err.constraint,
fields: fields,
table: err.table,
parent: err
});
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.isForeignKeysQuery = function() { Query.prototype.isForeignKeysQuery = function() {
return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql); return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql);
}; };
Query.prototype.getInsertIdField = function() { Query.prototype.getInsertIdField = function() {
return 'id'; return 'id';
}; };
return Query; module.exports = Query;
})();
...@@ -3,60 +3,64 @@ ...@@ -3,60 +3,64 @@
var Utils = require('../../utils'), var Utils = require('../../utils'),
moment = require('moment'); moment = require('moment');
module.exports = { function stringify (data) {
stringify: function (data) { if (data === null) return null;
if (data === null) return null;
if (!Utils._.isArray(data) || data.length !== 2) return '';
if (!Utils._.isArray(data) || data.length !== 2) return ''; if (Utils._.any(data, Utils._.isNull)) return '';
if (Utils._.any(data, Utils._.isNull)) return ''; if (data.hasOwnProperty('inclusive')) {
if (!data.inclusive) data.inclusive = [false, false];
else if (data.inclusive === true) data.inclusive = [true, true];
} else {
data.inclusive = [false, false];
}
if (data.hasOwnProperty('inclusive')) { Utils._.each(data, function (value, index) {
if (!data.inclusive) data.inclusive = [false, false]; if (Utils._.isObject(value)) {
else if (data.inclusive === true) data.inclusive = [true, true]; if (value.hasOwnProperty('inclusive')) data.inclusive[index] = !!value.inclusive;
} else { if (value.hasOwnProperty('value')) data[index] = value.value;
data.inclusive = [false, false];
} }
});
return (data.inclusive[0] ? '[' : '(') + JSON.stringify(data[0]) + ',' + JSON.stringify(data[1]) + (data.inclusive[1] ? ']' : ')');
}
function parse (value, AttributeType) {
if (value === null) return null;
if(typeof AttributeType === 'function') AttributeType = new AttributeType();
AttributeType = AttributeType || ''; // if attribute is not defined, assign empty string in order to prevent
// AttributeType.toString() to fail with uncaught exception later in the code
var result = value
.substring(1, value.length - 1)
.split(',', 2);
Utils._.each(data, function (value, index) { if (result.length !== 2) return value;
if (Utils._.isObject(value)) {
if (value.hasOwnProperty('inclusive')) data.inclusive[index] = !!value.inclusive; result = result
if (value.hasOwnProperty('value')) data[index] = value.value; .map(function (value) {
switch (AttributeType.toString()) {
case 'int4range':
return parseInt(value, 10);
case 'numrange':
return parseFloat(value);
case 'daterange':
case 'tsrange':
case 'tstzrange':
return moment(value).toDate();
} }
return value;
}); });
return (data.inclusive[0] ? '[' : '(') + JSON.stringify(data[0]) + ',' + JSON.stringify(data[1]) + (data.inclusive[1] ? ']' : ')'); result.inclusive = [(value[0] === '['), (value[value.length - 1] === ']')];
},
parse: function (value, AttributeType) { return result;
if (value === null) return null; }
if(typeof AttributeType === 'function') AttributeType = new AttributeType(); module.exports = {
AttributeType = AttributeType || ''; // if attribute is not defined, assign empty string in order to prevent stringify: stringify,
// AttributeType.toString() to fail with uncaught exception later in the code parse: parse
var result = value
.substring(1, value.length - 1)
.split(',', 2);
if (result.length !== 2) return value;
result = result
.map(function (value) {
switch (AttributeType.toString()) {
case 'int4range':
return parseInt(value, 10);
case 'numrange':
return parseFloat(value);
case 'daterange':
case 'tsrange':
case 'tstzrange':
return moment(value).toDate();
}
return value;
});
result.inclusive = [(value[0] === '['), (value[value.length - 1] === ']')];
return result;
}
}; };
...@@ -11,433 +11,431 @@ var MySqlQueryGenerator = Utils._.extend( ...@@ -11,433 +11,431 @@ var MySqlQueryGenerator = Utils._.extend(
Utils._.clone(require('../mysql/query-generator')) Utils._.clone(require('../mysql/query-generator'))
); );
module.exports = (function() { var QueryGenerator = {
var QueryGenerator = { options: {},
options: {}, dialect: 'sqlite',
dialect: 'sqlite',
createSchema: function() {
createSchema: function() { var query = "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
var query = "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';"; return Utils._.template(query)({});
return Utils._.template(query)({}); },
},
showSchemasQuery: function() {
showSchemasQuery: function() { return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';"; },
},
versionQuery: function() {
versionQuery: function() { return 'SELECT sqlite_version() as `version`';
return 'SELECT sqlite_version() as `version`'; },
},
createTableQuery: function(tableName, attributes, options) {
createTableQuery: function(tableName, attributes, options) { options = options || {};
options = options || {};
var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)'
var query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)' , primaryKeys = []
, primaryKeys = [] , needsMultiplePrimaryKeys = (Utils._.values(attributes).filter(function(definition) {
, needsMultiplePrimaryKeys = (Utils._.values(attributes).filter(function(definition) { return Utils._.includes(definition, 'PRIMARY KEY');
return Utils._.includes(definition, 'PRIMARY KEY'); }).length > 1)
}).length > 1) , attrStr = [];
, attrStr = [];
for (var attr in attributes) {
for (var attr in attributes) { if (attributes.hasOwnProperty(attr)) {
if (attributes.hasOwnProperty(attr)) { var dataType = attributes[attr];
var dataType = attributes[attr]; var containsAutoIncrement = Utils._.includes(dataType, 'AUTOINCREMENT');
var containsAutoIncrement = Utils._.includes(dataType, 'AUTOINCREMENT');
if (containsAutoIncrement) {
if (containsAutoIncrement) { dataType = dataType.replace(/BIGINT/, 'INTEGER');
dataType = dataType.replace(/BIGINT/, 'INTEGER'); }
}
var dataTypeString = dataType; var dataTypeString = dataType;
if (Utils._.includes(dataType, 'PRIMARY KEY')) { if (Utils._.includes(dataType, 'PRIMARY KEY')) {
if (Utils._.includes(dataType, 'INTEGER')) { // Only INTEGER is allowed for primary key, see https://github.com/sequelize/sequelize/issues/969 (no lenght, unsigned etc) if (Utils._.includes(dataType, 'INTEGER')) { // Only INTEGER is allowed for primary key, see https://github.com/sequelize/sequelize/issues/969 (no lenght, unsigned etc)
dataTypeString = containsAutoIncrement ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INTEGER PRIMARY KEY'; dataTypeString = containsAutoIncrement ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INTEGER PRIMARY KEY';
} }
if (needsMultiplePrimaryKeys) { if (needsMultiplePrimaryKeys) {
primaryKeys.push(attr); primaryKeys.push(attr);
dataTypeString = dataType.replace(/PRIMARY KEY/, 'NOT NULL'); dataTypeString = dataType.replace(/PRIMARY KEY/, 'NOT NULL');
}
} }
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataTypeString);
} }
attrStr.push(this.quoteIdentifier(attr) + ' ' + dataTypeString);
} }
}
var values = { var values = {
table: this.quoteTable(tableName), table: this.quoteTable(tableName),
attributes: attrStr.join(', '), attributes: attrStr.join(', '),
charset: (options.charset ? 'DEFAULT CHARSET=' + options.charset : '') charset: (options.charset ? 'DEFAULT CHARSET=' + options.charset : '')
} }
, pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', '); , pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk); }.bind(this)).join(', ');
if (!!options.uniqueKeys) { if (!!options.uniqueKeys) {
Utils._.each(options.uniqueKeys, function(columns) { Utils._.each(options.uniqueKeys, function(columns) {
if (!columns.singleField) { // If it's a single field its handled in column def, not as an index if (!columns.singleField) { // If it's a single field its handled in column def, not as an index
values.attributes += ', UNIQUE (' + columns.fields.join(', ') + ')'; values.attributes += ', UNIQUE (' + columns.fields.join(', ') + ')';
} }
}); });
} }
if (pkString.length > 0) { if (pkString.length > 0) {
values.attributes += ', PRIMARY KEY (' + pkString + ')'; values.attributes += ', PRIMARY KEY (' + pkString + ')';
} }
var sql = Utils._.template(query)(values).trim() + ';'; var sql = Utils._.template(query)(values).trim() + ';';
return this.replaceBooleanDefaults(sql); return this.replaceBooleanDefaults(sql);
}, },
booleanValue: function(value){ booleanValue: function(value){
return !!value ? 1 : 0; return !!value ? 1 : 0;
}, },
addLimitAndOffset: function(options){ addLimitAndOffset: function(options){
var fragment = ''; var fragment = '';
if (options.offset && !options.limit) { if (options.offset && !options.limit) {
fragment += ' LIMIT ' + options.offset + ', ' + 10000000000000; fragment += ' LIMIT ' + options.offset + ', ' + 10000000000000;
} else if (options.limit) { } else if (options.limit) {
if (options.offset) { if (options.offset) {
fragment += ' LIMIT ' + options.offset + ', ' + options.limit; fragment += ' LIMIT ' + options.offset + ', ' + options.limit;
} else { } else {
fragment += ' LIMIT ' + options.limit; fragment += ' LIMIT ' + options.limit;
}
} }
}
return fragment; return fragment;
}, },
addColumnQuery: function(table, key, dataType) { addColumnQuery: function(table, key, dataType) {
var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;' var query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'
, attributes = {}; , attributes = {};
attributes[key] = dataType; attributes[key] = dataType;
var fields = this.attributesToSQL(attributes, { var fields = this.attributesToSQL(attributes, {
context: 'addColumn' context: 'addColumn'
}); });
var attribute = Utils._.template('<%= key %> <%= definition %>')({ var attribute = Utils._.template('<%= key %> <%= definition %>')({
key: this.quoteIdentifier(key), key: this.quoteIdentifier(key),
definition: fields[key] definition: fields[key]
});
var sql = Utils._.template(query)({
table: this.quoteTable(table),
attribute: attribute
}); });
return this.replaceBooleanDefaults(sql); var sql = Utils._.template(query)({
}, table: this.quoteTable(table),
attribute: attribute
});
showTablesQuery: function() { return this.replaceBooleanDefaults(sql);
return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';"; },
},
upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) { showTablesQuery: function() {
options.ignore = true; return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
var sql = this.insertQuery(tableName, insertValues, rawAttributes, options) + ' ' + this.updateQuery(tableName, updateValues, where, options, rawAttributes); },
return sql; upsertQuery: function (tableName, insertValues, updateValues, where, rawAttributes, options) {
}, options.ignore = true;
var sql = this.insertQuery(tableName, insertValues, rawAttributes, options) + ' ' + this.updateQuery(tableName, updateValues, where, options, rawAttributes);
bulkInsertQuery: function(tableName, attrValueHashes, options) { return sql;
var query = 'INSERT<%= ignoreDuplicates %> INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>;' },
, tuples = []
, allAttributes = [];
Utils._.forEach(attrValueHashes, function(attrValueHash) { bulkInsertQuery: function(tableName, attrValueHashes, options) {
Utils._.forOwn(attrValueHash, function(value, key) { var query = 'INSERT<%= ignoreDuplicates %> INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>;'
if (allAttributes.indexOf(key) === -1) allAttributes.push(key); , tuples = []
}); , allAttributes = [];
});
Utils._.forEach(attrValueHashes, function(attrValueHash) {
tuples.push('(' +
allAttributes.map(function (key) {
return this.escape(attrValueHash[key]);
}.bind(this)).join(',') +
')');
}.bind(this));
var replacements = {
ignoreDuplicates: options && options.ignoreDuplicates ? ' OR IGNORE' : '',
table: this.quoteTable(tableName),
attributes: allAttributes.map(function(attr) {
return this.quoteIdentifier(attr);
}.bind(this)).join(','),
tuples: tuples
};
return Utils._.template(query)(replacements); Utils._.forEach(attrValueHashes, function(attrValueHash) {
}, Utils._.forOwn(attrValueHash, function(value, key) {
if (allAttributes.indexOf(key) === -1) allAttributes.push(key);
});
});
Utils._.forEach(attrValueHashes, function(attrValueHash) {
tuples.push('(' +
allAttributes.map(function (key) {
return this.escape(attrValueHash[key]);
}.bind(this)).join(',') +
')');
}.bind(this));
var replacements = {
ignoreDuplicates: options && options.ignoreDuplicates ? ' OR IGNORE' : '',
table: this.quoteTable(tableName),
attributes: allAttributes.map(function(attr) {
return this.quoteIdentifier(attr);
}.bind(this)).join(','),
tuples: tuples
};
return Utils._.template(query)(replacements);
},
updateQuery: function(tableName, attrValueHash, where, options) {
options = options || {};
_.defaults(options, this.options);
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, options.omitNull, options);
var query = 'UPDATE <%= table %> SET <%= values %> <%= where %>'
, values = [];
for (var key in attrValueHash) {
var value = attrValueHash[key];
values.push(this.quoteIdentifier(key) + '=' + this.escape(value));
}
updateQuery: function(tableName, attrValueHash, where, options) { var replacements = {
options = options || {}; table: this.quoteTable(tableName),
_.defaults(options, this.options); values: values.join(','),
where: this.whereQuery(where)
};
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, options.omitNull, options); return Utils._.template(query)(replacements).trim();
},
var query = 'UPDATE <%= table %> SET <%= values %> <%= where %>' deleteQuery: function(tableName, where, options) {
, values = []; options = options || {};
for (var key in attrValueHash) { var query = 'DELETE FROM <%= table %><%= where %>';
var value = attrValueHash[key]; var replacements = {
values.push(this.quoteIdentifier(key) + '=' + this.escape(value)); table: this.quoteTable(tableName),
} where: this.getWhereConditions(where)
};
var replacements = { if (replacements.where) {
table: this.quoteTable(tableName), replacements.where = ' WHERE ' + replacements.where;
values: values.join(','), }
where: this.whereQuery(where)
};
return Utils._.template(query)(replacements).trim(); return Utils._.template(query)(replacements);
}, },
deleteQuery: function(tableName, where, options) { attributesToSQL: function(attributes) {
options = options || {}; var result = {};
var query = 'DELETE FROM <%= table %><%= where %>'; for (var name in attributes) {
var replacements = { var dataType = attributes[name];
table: this.quoteTable(tableName), var fieldName = dataType.field || name;
where: this.getWhereConditions(where)
};
if (replacements.where) { if (Utils._.isObject(dataType)) {
replacements.where = ' WHERE ' + replacements.where; var template = '<%= type %>'
} , replacements = { type: dataType.type };
return Utils._.template(query)(replacements); if (dataType.type instanceof DataTypes.ENUM) {
}, replacements.type = 'TEXT';
attributesToSQL: function(attributes) { if (!(Array.isArray(dataType.values) && (dataType.values.length > 0))) {
var result = {}; throw new Error("Values for ENUM haven't been defined.");
}
}
for (var name in attributes) { if (dataType.hasOwnProperty('allowNull') && !dataType.allowNull) {
var dataType = attributes[name]; template += ' NOT NULL';
var fieldName = dataType.field || name; }
if (Utils._.isObject(dataType)) { if (Utils.defaultValueSchemable(dataType.defaultValue)) {
var template = '<%= type %>' // TODO thoroughly check that DataTypes.NOW will properly
, replacements = { type: dataType.type }; // get populated on all databases as DEFAULT value
// i.e. mysql requires: DEFAULT CURRENT_TIMESTAMP
template += ' DEFAULT <%= defaultValue %>';
replacements.defaultValue = this.escape(dataType.defaultValue);
}
if (dataType.type instanceof DataTypes.ENUM) { if (dataType.unique === true) {
replacements.type = 'TEXT'; template += ' UNIQUE';
}
if (!(Array.isArray(dataType.values) && (dataType.values.length > 0))) { if (dataType.primaryKey) {
throw new Error("Values for ENUM haven't been defined."); template += ' PRIMARY KEY';
}
}
if (dataType.hasOwnProperty('allowNull') && !dataType.allowNull) { if (dataType.autoIncrement) {
template += ' NOT NULL'; template += ' AUTOINCREMENT';
} }
}
if (Utils.defaultValueSchemable(dataType.defaultValue)) { if(dataType.references) {
// TODO thoroughly check that DataTypes.NOW will properly dataType = Utils.formatReferences(dataType);
// get populated on all databases as DEFAULT value template += ' REFERENCES <%= referencesTable %> (<%= referencesKey %>)';
// i.e. mysql requires: DEFAULT CURRENT_TIMESTAMP replacements.referencesTable = this.quoteTable(dataType.references.model);
template += ' DEFAULT <%= defaultValue %>';
replacements.defaultValue = this.escape(dataType.defaultValue);
}
if (dataType.unique === true) { if(dataType.references.key) {
template += ' UNIQUE'; replacements.referencesKey = this.quoteIdentifier(dataType.references.key);
} else {
replacements.referencesKey = this.quoteIdentifier('id');
} }
if (dataType.primaryKey) { if(dataType.onDelete) {
template += ' PRIMARY KEY'; template += ' ON DELETE <%= onDeleteAction %>';
replacements.onDeleteAction = dataType.onDelete.toUpperCase();
if (dataType.autoIncrement) {
template += ' AUTOINCREMENT';
}
} }
if(dataType.references) { if(dataType.onUpdate) {
dataType = Utils.formatReferences(dataType); template += ' ON UPDATE <%= onUpdateAction %>';
template += ' REFERENCES <%= referencesTable %> (<%= referencesKey %>)'; replacements.onUpdateAction = dataType.onUpdate.toUpperCase();
replacements.referencesTable = this.quoteTable(dataType.references.model);
if(dataType.references.key) {
replacements.referencesKey = this.quoteIdentifier(dataType.references.key);
} else {
replacements.referencesKey = this.quoteIdentifier('id');
}
if(dataType.onDelete) {
template += ' ON DELETE <%= onDeleteAction %>';
replacements.onDeleteAction = dataType.onDelete.toUpperCase();
}
if(dataType.onUpdate) {
template += ' ON UPDATE <%= onUpdateAction %>';
replacements.onUpdateAction = dataType.onUpdate.toUpperCase();
}
} }
result[fieldName] = Utils._.template(template)(replacements);
} else {
result[fieldName] = dataType;
} }
}
return result; result[fieldName] = Utils._.template(template)(replacements);
}, } else {
result[fieldName] = dataType;
findAutoIncrementField: function(factory) {
var fields = [];
for (var name in factory.attributes) {
if (factory.attributes.hasOwnProperty(name)) {
var definition = factory.attributes[name];
if (definition && definition.autoIncrement) {
fields.push(name);
}
}
} }
}
return fields; return result;
}, },
showIndexesQuery: function(tableName) {
var sql = 'PRAGMA INDEX_LIST(<%= tableName %>)';
return Utils._.template(sql)({ tableName: this.quoteTable(tableName) });
},
removeIndexQuery: function(tableName, indexNameOrAttributes) { findAutoIncrementField: function(factory) {
var sql = 'DROP INDEX IF EXISTS <%= indexName %>' var fields = [];
, indexName = indexNameOrAttributes;
if (typeof indexName !== 'string') { for (var name in factory.attributes) {
indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_')); if (factory.attributes.hasOwnProperty(name)) {
var definition = factory.attributes[name];
if (definition && definition.autoIncrement) {
fields.push(name);
}
} }
}
return Utils._.template(sql)( { tableName: this.quoteIdentifiers(tableName), indexName: indexName }); return fields;
}, },
describeTableQuery: function(tableName, schema, schemaDelimiter) {
var options = {};
options.schema = schema;
options.schemaDelimiter = schemaDelimiter;
options.quoted = false;
var sql = 'PRAGMA TABLE_INFO(<%= tableName %>);'; showIndexesQuery: function(tableName) {
return Utils._.template(sql)({ tableName: this.addSchema({tableName: this.quoteIdentifiers(tableName), options: options})}); var sql = 'PRAGMA INDEX_LIST(<%= tableName %>)';
}, return Utils._.template(sql)({ tableName: this.quoteTable(tableName) });
},
removeColumnQuery: function(tableName, attributes) { removeIndexQuery: function(tableName, indexNameOrAttributes) {
var backupTableName var sql = 'DROP INDEX IF EXISTS <%= indexName %>'
, query; , indexName = indexNameOrAttributes;
attributes = this.attributesToSQL(attributes); if (typeof indexName !== 'string') {
indexName = Utils.inflection.underscore(tableName + '_' + indexNameOrAttributes.join('_'));
}
if (typeof tableName === 'object') { return Utils._.template(sql)( { tableName: this.quoteIdentifiers(tableName), indexName: indexName });
backupTableName = { },
tableName: tableName.tableName + '_backup',
schema: tableName.schema
};
} else {
backupTableName = tableName + '_backup';
}
query = [ describeTableQuery: function(tableName, schema, schemaDelimiter) {
this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'), var options = {};
'INSERT INTO <%= backupTableName %> SELECT <%= attributeNames %> FROM <%= tableName %>;', options.schema = schema;
'DROP TABLE <%= tableName %>;', options.schemaDelimiter = schemaDelimiter;
this.createTableQuery(tableName, attributes), options.quoted = false;
'INSERT INTO <%= tableName %> SELECT <%= attributeNames %> FROM <%= backupTableName %>;',
'DROP TABLE <%= backupTableName %>;'
].join('');
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
backupTableName: this.quoteTable(backupTableName),
attributeNames: Utils._.keys(attributes).join(', ')
});
},
renameColumnQuery: function(tableName, attrNameBefore, attrNameAfter, attributes) { var sql = 'PRAGMA TABLE_INFO(<%= tableName %>);';
var backupTableName return Utils._.template(sql)({ tableName: this.addSchema({tableName: this.quoteIdentifiers(tableName), options: options})});
, query; },
attributes = this.attributesToSQL(attributes); removeColumnQuery: function(tableName, attributes) {
var backupTableName
, query;
if (typeof tableName === 'object') { attributes = this.attributesToSQL(attributes);
backupTableName = {
tableName: tableName.tableName + '_backup',
schema: tableName.schema
};
} else {
backupTableName = tableName + '_backup';
}
query = [ if (typeof tableName === 'object') {
this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'), backupTableName = {
'INSERT INTO <%= backupTableName %> SELECT <%= attributeNamesImport %> FROM <%= tableName %>;', tableName: tableName.tableName + '_backup',
'DROP TABLE <%= tableName %>;', schema: tableName.schema
this.createTableQuery(tableName, attributes), };
'INSERT INTO <%= tableName %> SELECT <%= attributeNamesExport %> FROM <%= backupTableName %>;', } else {
'DROP TABLE <%= backupTableName %>;' backupTableName = tableName + '_backup';
].join(''); }
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
backupTableName: this.quoteTable(backupTableName),
attributeNamesImport: Utils._.keys(attributes).map(function(attr) {
return (attrNameAfter === attr) ? this.quoteIdentifier(attrNameBefore) + ' AS ' + this.quoteIdentifier(attr) : this.quoteIdentifier(attr);
}.bind(this)).join(', '),
attributeNamesExport: Utils._.keys(attributes).map(function(attr) {
return this.quoteIdentifier(attr);
}.bind(this)).join(', ')
});
},
startTransactionQuery: function(transaction, options) { query = [
if (options.parent) { this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'),
return 'SAVEPOINT ' + this.quoteIdentifier(transaction.name) + ';'; 'INSERT INTO <%= backupTableName %> SELECT <%= attributeNames %> FROM <%= tableName %>;',
} 'DROP TABLE <%= tableName %>;',
this.createTableQuery(tableName, attributes),
'INSERT INTO <%= tableName %> SELECT <%= attributeNames %> FROM <%= backupTableName %>;',
'DROP TABLE <%= backupTableName %>;'
].join('');
return Utils._.template(query)({
tableName: this.quoteTable(tableName),
backupTableName: this.quoteTable(backupTableName),
attributeNames: Utils._.keys(attributes).join(', ')
});
},
renameColumnQuery: function(tableName, attrNameBefore, attrNameAfter, attributes) {
var backupTableName
, query;
attributes = this.attributesToSQL(attributes);
if (typeof tableName === 'object') {
backupTableName = {
tableName: tableName.tableName + '_backup',
schema: tableName.schema
};
} else {
backupTableName = tableName + '_backup';
}
return 'BEGIN TRANSACTION;'; query = [
}, this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'),
'INSERT INTO <%= backupTableName %> SELECT <%= attributeNamesImport %> FROM <%= tableName %>;',
setAutocommitQuery: function() { 'DROP TABLE <%= tableName %>;',
return '-- SQLite does not support SET autocommit.'; this.createTableQuery(tableName, attributes),
}, 'INSERT INTO <%= tableName %> SELECT <%= attributeNamesExport %> FROM <%= backupTableName %>;',
'DROP TABLE <%= backupTableName %>;'
setIsolationLevelQuery: function(value) { ].join('');
switch (value) {
case Transaction.ISOLATION_LEVELS.REPEATABLE_READ: return Utils._.template(query)({
return '-- SQLite is not able to choose the isolation level REPEATABLE READ.'; tableName: this.quoteTable(tableName),
case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED: backupTableName: this.quoteTable(backupTableName),
return 'PRAGMA read_uncommitted = ON;'; attributeNamesImport: Utils._.keys(attributes).map(function(attr) {
case Transaction.ISOLATION_LEVELS.READ_COMMITTED: return (attrNameAfter === attr) ? this.quoteIdentifier(attrNameBefore) + ' AS ' + this.quoteIdentifier(attr) : this.quoteIdentifier(attr);
return 'PRAGMA read_uncommitted = OFF;'; }.bind(this)).join(', '),
case Transaction.ISOLATION_LEVELS.SERIALIZABLE: attributeNamesExport: Utils._.keys(attributes).map(function(attr) {
return "-- SQLite's default isolation level is SERIALIZABLE. Nothing to do."; return this.quoteIdentifier(attr);
default: }.bind(this)).join(', ')
throw new Error('Unknown isolation level: ' + value); });
} },
},
startTransactionQuery: function(transaction, options) {
replaceBooleanDefaults: function(sql) { if (options.parent) {
return sql.replace(/DEFAULT '?false'?/g, 'DEFAULT 0').replace(/DEFAULT '?true'?/g, 'DEFAULT 1'); return 'SAVEPOINT ' + this.quoteIdentifier(transaction.name) + ';';
},
quoteIdentifier: function(identifier) {
if (identifier === '*') return identifier;
return Utils.addTicks(identifier, '`');
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
var sql = 'PRAGMA foreign_key_list(<%= tableName %>)';
return Utils._.template(sql)({ tableName: tableName });
} }
};
return Utils._.extend({}, MySqlQueryGenerator, QueryGenerator); return 'BEGIN TRANSACTION;';
})(); },
setAutocommitQuery: function() {
return '-- SQLite does not support SET autocommit.';
},
setIsolationLevelQuery: function(value) {
switch (value) {
case Transaction.ISOLATION_LEVELS.REPEATABLE_READ:
return '-- SQLite is not able to choose the isolation level REPEATABLE READ.';
case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED:
return 'PRAGMA read_uncommitted = ON;';
case Transaction.ISOLATION_LEVELS.READ_COMMITTED:
return 'PRAGMA read_uncommitted = OFF;';
case Transaction.ISOLATION_LEVELS.SERIALIZABLE:
return "-- SQLite's default isolation level is SERIALIZABLE. Nothing to do.";
default:
throw new Error('Unknown isolation level: ' + value);
}
},
replaceBooleanDefaults: function(sql) {
return sql.replace(/DEFAULT '?false'?/g, 'DEFAULT 0').replace(/DEFAULT '?true'?/g, 'DEFAULT 1');
},
quoteIdentifier: function(identifier) {
if (identifier === '*') return identifier;
return Utils.addTicks(identifier, '`');
},
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {String} tableName The name of the table.
* @param {String} schemaName The name of the schema.
* @return {String} The generated sql query.
*/
getForeignKeysQuery: function(tableName, schemaName) {
var sql = 'PRAGMA foreign_key_list(<%= tableName %>)';
return Utils._.template(sql)({ tableName: tableName });
}
};
module.exports = Utils._.extend({}, MySqlQueryGenerator, QueryGenerator);
...@@ -9,106 +9,111 @@ var Utils = require('../../utils') ...@@ -9,106 +9,111 @@ var Utils = require('../../utils')
@class QueryInterface @class QueryInterface
@static @static
*/ */
module.exports = {
/** /**
A wrapper that fixes SQLite's inability to remove columns from existing tables. A wrapper that fixes SQLite's inability to remove columns from existing tables.
It will create a backup of the table, drop the table afterwards and create a It will create a backup of the table, drop the table afterwards and create a
new table with the same name but without the obsolete column. new table with the same name but without the obsolete column.
@method removeColumn @method removeColumn
@for QueryInterface @for QueryInterface
@param {String} tableName The name of the table. @param {String} tableName The name of the table.
@param {String} attributeName The name of the attribute that we want to remove. @param {String} attributeName The name of the attribute that we want to remove.
@param {Object} options @param {Object} options
@param {Boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries @param {Boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@param {CustomEventEmitter} emitter The EventEmitter from outside. @param {CustomEventEmitter} emitter The EventEmitter from outside.
@param {Function} queryAndEmit The function from outside that triggers some events to get triggered. @param {Function} queryAndEmit The function from outside that triggers some events to get triggered.
@since 1.6.0 @since 1.6.0
*/ */
removeColumn: function(tableName, attributeName, options) { var removeColumn = function(tableName, attributeName, options) {
var self = this; var self = this;
options = options || {}; options = options || {};
return this.describeTable(tableName).then(function(fields) { return this.describeTable(tableName).then(function(fields) {
delete fields[attributeName]; delete fields[attributeName];
var sql = self.QueryGenerator.removeColumnQuery(tableName, fields) var sql = self.QueryGenerator.removeColumnQuery(tableName, fields)
, subQueries = sql.split(';').filter(function(q) { return q !== ''; }); , subQueries = sql.split(';').filter(function(q) { return q !== ''; });
return Promise.each(subQueries, function(subQuery) { return Promise.each(subQueries, function(subQuery) {
return self.sequelize.query(subQuery + ';', { raw: true, logging: options.logging }); return self.sequelize.query(subQuery + ';', { raw: true, logging: options.logging });
});
}); });
}, });
};
/**
A wrapper that fixes SQLite's inability to change columns from existing tables. /**
It will create a backup of the table, drop the table afterwards and create a A wrapper that fixes SQLite's inability to change columns from existing tables.
new table with the same name but with a modified version of the respective column. It will create a backup of the table, drop the table afterwards and create a
new table with the same name but with a modified version of the respective column.
@method changeColumn
@for QueryInterface @method changeColumn
@for QueryInterface
@param {String} tableName The name of the table.
@param {Object} attributes An object with the attribute's name as key and it's options as value object. @param {String} tableName The name of the table.
@param {Object} options @param {Object} attributes An object with the attribute's name as key and it's options as value object.
@param {Boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries @param {Object} options
@param {CustomEventEmitter} emitter The EventEmitter from outside. @param {Boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@param {Function} queryAndEmit The function from outside that triggers some events to get triggered. @param {CustomEventEmitter} emitter The EventEmitter from outside.
@param {Function} queryAndEmit The function from outside that triggers some events to get triggered.
@since 1.6.0
*/ @since 1.6.0
changeColumn: function(tableName, attributes, options) { */
var attributeName = Utils._.keys(attributes)[0] var changeColumn = function(tableName, attributes, options) {
, self = this; var attributeName = Utils._.keys(attributes)[0]
options = options || {}; , self = this;
options = options || {};
return this.describeTable(tableName).then(function(fields) {
fields[attributeName] = attributes[attributeName]; return this.describeTable(tableName).then(function(fields) {
fields[attributeName] = attributes[attributeName];
var sql = self.QueryGenerator.removeColumnQuery(tableName, fields)
, subQueries = sql.split(';').filter(function(q) { return q !== ''; }); var sql = self.QueryGenerator.removeColumnQuery(tableName, fields)
, subQueries = sql.split(';').filter(function(q) { return q !== ''; });
return Promise.each(subQueries, function(subQuery) {
return self.sequelize.query(subQuery + ';', { raw: true, logging: options.logging }); return Promise.each(subQueries, function(subQuery) {
}); return self.sequelize.query(subQuery + ';', { raw: true, logging: options.logging });
}); });
}, });
};
/**
A wrapper that fixes SQLite's inability to rename columns from existing tables. /**
It will create a backup of the table, drop the table afterwards and create a A wrapper that fixes SQLite's inability to rename columns from existing tables.
new table with the same name but with a renamed version of the respective column. It will create a backup of the table, drop the table afterwards and create a
new table with the same name but with a renamed version of the respective column.
@method renameColumn
@for QueryInterface @method renameColumn
@for QueryInterface
@param {String} tableName The name of the table.
@param {String} attrNameBefore The name of the attribute before it was renamed. @param {String} tableName The name of the table.
@param {String} attrNameAfter The name of the attribute after it was renamed. @param {String} attrNameBefore The name of the attribute before it was renamed.
@param {Object} options @param {String} attrNameAfter The name of the attribute after it was renamed.
@param {Boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries @param {Object} options
@param {CustomEventEmitter} emitter The EventEmitter from outside. @param {Boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@param {Function} queryAndEmit The function from outside that triggers some events to get triggered. @param {CustomEventEmitter} emitter The EventEmitter from outside.
@param {Function} queryAndEmit The function from outside that triggers some events to get triggered.
@since 1.6.0
*/ @since 1.6.0
renameColumn: function(tableName, attrNameBefore, attrNameAfter, options) { */
var self = this; var renameColumn = function(tableName, attrNameBefore, attrNameAfter, options) {
options = options || {}; var self = this;
options = options || {};
return this.describeTable(tableName).then(function(fields) {
fields[attrNameAfter] = Utils._.clone(fields[attrNameBefore]); return this.describeTable(tableName).then(function(fields) {
delete fields[attrNameBefore]; fields[attrNameAfter] = Utils._.clone(fields[attrNameBefore]);
delete fields[attrNameBefore];
var sql = self.QueryGenerator.renameColumnQuery(tableName, attrNameBefore, attrNameAfter, fields)
, subQueries = sql.split(';').filter(function(q) { return q !== ''; }); var sql = self.QueryGenerator.renameColumnQuery(tableName, attrNameBefore, attrNameAfter, fields)
, subQueries = sql.split(';').filter(function(q) { return q !== ''; });
return Promise.each(subQueries, function(subQuery) {
return self.sequelize.query(subQuery + ';', { raw: true, logging: options.logging }); return Promise.each(subQueries, function(subQuery) {
}); return self.sequelize.query(subQuery + ';', { raw: true, logging: options.logging });
}); });
} });
}; };
module.exports = {
removeColumn: removeColumn,
changeColumn: changeColumn,
renameColumn: renameColumn
};
\ No newline at end of file
...@@ -5,276 +5,274 @@ var Utils = require('../../utils') ...@@ -5,276 +5,274 @@ var Utils = require('../../utils')
, QueryTypes = require('../../query-types') , QueryTypes = require('../../query-types')
, sequelizeErrors = require('../../errors.js'); , sequelizeErrors = require('../../errors.js');
module.exports = (function() { var Query = function(database, sequelize, options) {
var Query = function(database, sequelize, options) { this.database = database;
this.database = database; this.sequelize = sequelize;
this.sequelize = sequelize; this.instance = options.instance;
this.instance = options.instance; this.model = options.model;
this.model = options.model; this.options = Utils._.extend({
this.options = Utils._.extend({ logging: console.log,
logging: console.log, plain: false,
plain: false, raw: false
raw: false }, options || {});
}, options || {});
this.checkLoggingOption();
this.checkLoggingOption(); };
}; Utils.inherit(Query, AbstractQuery);
Utils.inherit(Query, AbstractQuery);
Query.prototype.getInsertIdField = function() {
Query.prototype.getInsertIdField = function() { return 'lastID';
return 'lastID'; };
};
Query.prototype.run = function(sql) {
Query.prototype.run = function(sql) { var self = this
var self = this , promise;
, promise;
this.sql = sql;
this.sql = sql;
this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options);
this.sequelize.log('Executing (' + (this.database.uuid || 'default') + '): ' + this.sql, this.options);
promise = new Utils.Promise(function(resolve) {
promise = new Utils.Promise(function(resolve) { var columnTypes = {};
var columnTypes = {};
self.database.serialize(function() {
self.database.serialize(function() { var executeSql = function() {
var executeSql = function() { if (self.sql.indexOf('-- ') === 0) {
if (self.sql.indexOf('-- ') === 0) { return resolve();
return resolve(); } else {
} else { resolve(new Utils.Promise(function(resolve, reject) {
resolve(new Utils.Promise(function(resolve, reject) { self.database[self.getDatabaseMethod()](self.sql, function(err, results) {
self.database[self.getDatabaseMethod()](self.sql, function(err, results) { if (err) {
if (err) { err.sql = self.sql;
err.sql = self.sql; reject(self.formatError(err));
reject(self.formatError(err)); } else {
} else { var metaData = this;
var metaData = this; metaData.columnTypes = columnTypes;
metaData.columnTypes = columnTypes;
var result = self.instance;
var result = self.instance;
// add the inserted row id to the instance
// add the inserted row id to the instance if (self.isInsertQuery(results, metaData)) {
if (self.isInsertQuery(results, metaData)) { self.handleInsertQuery(results, metaData);
self.handleInsertQuery(results, metaData);
if (!self.instance) {
if (!self.instance) { result = metaData[self.getInsertIdField()];
result = metaData[self.getInsertIdField()];
}
} }
}
if (self.sql.indexOf('sqlite_master') !== -1) { if (self.sql.indexOf('sqlite_master') !== -1) {
result = results.map(function(resultSet) { return resultSet.name; }); result = results.map(function(resultSet) { return resultSet.name; });
} else if (self.isSelectQuery()) { } else if (self.isSelectQuery()) {
if (!self.options.raw) { if (!self.options.raw) {
results = results.map(function(result) { results = results.map(function(result) {
for (var name in result) { for (var name in result) {
if (result.hasOwnProperty(name) && metaData.columnTypes[name]) { if (result.hasOwnProperty(name) && metaData.columnTypes[name]) {
if (metaData.columnTypes[name] === 'DATETIME') { if (metaData.columnTypes[name] === 'DATETIME') {
// we need to convert the timestamps into actual date objects // we need to convert the timestamps into actual date objects
var val = result[name]; var val = result[name];
if (val !== null) { if (val !== null) {
if (val.indexOf('+') === -1) { if (val.indexOf('+') === -1) {
// For backwards compat. Dates inserted by sequelize < 2.0dev12 will not have a timestamp set // For backwards compat. Dates inserted by sequelize < 2.0dev12 will not have a timestamp set
result[name] = new Date(val + self.sequelize.options.timezone); result[name] = new Date(val + self.sequelize.options.timezone);
} else { } else {
result[name] = new Date(val); // We already have a timezone stored in the string result[name] = new Date(val); // We already have a timezone stored in the string
}
}
} else if (metaData.columnTypes[name].lastIndexOf('BLOB') !== -1) {
if (result[name]) {
result[name] = new Buffer(result[name]);
} }
} }
} else if (metaData.columnTypes[name].lastIndexOf('BLOB') !== -1) {
if (result[name]) {
result[name] = new Buffer(result[name]);
}
} }
} }
return result;
});
}
result = self.handleSelectQuery(results);
} else if (self.isShowOrDescribeQuery()) {
result = results;
} else if (self.sql.indexOf('PRAGMA INDEX_LIST') !== -1) {
result = self.handleShowIndexesQuery(results);
} else if (self.sql.indexOf('PRAGMA INDEX_INFO') !== -1) {
result = results;
} else if (self.sql.indexOf('PRAGMA TABLE_INFO') !== -1) {
// this is the sqlite way of getting the metadata of a table
result = {};
results.forEach(function(_result) {
result[_result.name] = {
type: _result.type,
allowNull: (_result.notnull === 0),
defaultValue: _result.dflt_value,
primaryKey : (_result.pk === 1)
};
if (result[_result.name].type === 'TINYINT(1)') {
result[_result.name].defaultValue = { '0': false, '1': true }[result[_result.name].defaultValue];
}
if (result[_result.name].defaultValue === undefined) {
result[_result.name].defaultValue = null;
}
if (typeof result[_result.name].defaultValue === 'string') {
result[_result.name].defaultValue = result[_result.name].defaultValue.replace(/'/g, '');
} }
return result;
}); });
} else if (self.sql.indexOf('PRAGMA foreign_keys;') !== -1) {
result = results[0];
} else if (self.sql.indexOf('PRAGMA foreign_keys') !== -1) {
result = results;
} else if (self.sql.indexOf('PRAGMA foreign_key_list') !== -1) {
result = results;
} else if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].indexOf(self.options.type) !== -1) {
result = metaData.changes;
} else if (self.options.type === QueryTypes.UPSERT) {
result = undefined;
} else if (self.options.type === QueryTypes.VERSION) {
result = results[0].version;
} else if (self.options.type === QueryTypes.RAW) {
result = [results, metaData];
} }
resolve(result); result = self.handleSelectQuery(results);
} } else if (self.isShowOrDescribeQuery()) {
}); result = results;
})); } else if (self.sql.indexOf('PRAGMA INDEX_LIST') !== -1) {
} result = self.handleShowIndexesQuery(results);
}; } else if (self.sql.indexOf('PRAGMA INDEX_INFO') !== -1) {
result = results;
} else if (self.sql.indexOf('PRAGMA TABLE_INFO') !== -1) {
// this is the sqlite way of getting the metadata of a table
result = {};
results.forEach(function(_result) {
result[_result.name] = {
type: _result.type,
allowNull: (_result.notnull === 0),
defaultValue: _result.dflt_value,
primaryKey : (_result.pk === 1)
};
if (result[_result.name].type === 'TINYINT(1)') {
result[_result.name].defaultValue = { '0': false, '1': true }[result[_result.name].defaultValue];
}
if ((self.getDatabaseMethod() === 'all')) { if (result[_result.name].defaultValue === undefined) {
var tableNames = []; result[_result.name].defaultValue = null;
if (self.options && self.options.tableNames) { }
tableNames = self.options.tableNames;
} else if (/FROM `(.*?)`/i.exec(self.sql)) {
tableNames.push(/FROM `(.*?)`/i.exec(self.sql)[1]);
}
if (!tableNames.length) { if (typeof result[_result.name].defaultValue === 'string') {
return executeSql(); result[_result.name].defaultValue = result[_result.name].defaultValue.replace(/'/g, '');
} else {
return Utils.Promise.map(tableNames, function(tableName) {
if (tableName !== 'sqlite_master') {
return new Utils.Promise(function(resolve) {
// get the column types
self.database.all('PRAGMA table_info(`' + tableName + '`)', function(err, results) {
if (!err) {
for (var i = 0, l = results.length; i < l; i++) {
columnTypes[tableName + '.' + results[i].name] = columnTypes[results[i].name] = results[i].type;
}
} }
resolve();
}); });
}); } else if (self.sql.indexOf('PRAGMA foreign_keys;') !== -1) {
result = results[0];
} else if (self.sql.indexOf('PRAGMA foreign_keys') !== -1) {
result = results;
} else if (self.sql.indexOf('PRAGMA foreign_key_list') !== -1) {
result = results;
} else if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].indexOf(self.options.type) !== -1) {
result = metaData.changes;
} else if (self.options.type === QueryTypes.UPSERT) {
result = undefined;
} else if (self.options.type === QueryTypes.VERSION) {
result = results[0].version;
} else if (self.options.type === QueryTypes.RAW) {
result = [results, metaData];
}
resolve(result);
} }
}).then(executeSql); });
} }));
} else { }
};
if ((self.getDatabaseMethod() === 'all')) {
var tableNames = [];
if (self.options && self.options.tableNames) {
tableNames = self.options.tableNames;
} else if (/FROM `(.*?)`/i.exec(self.sql)) {
tableNames.push(/FROM `(.*?)`/i.exec(self.sql)[1]);
}
if (!tableNames.length) {
return executeSql(); return executeSql();
} else {
return Utils.Promise.map(tableNames, function(tableName) {
if (tableName !== 'sqlite_master') {
return new Utils.Promise(function(resolve) {
// get the column types
self.database.all('PRAGMA table_info(`' + tableName + '`)', function(err, results) {
if (!err) {
for (var i = 0, l = results.length; i < l; i++) {
columnTypes[tableName + '.' + results[i].name] = columnTypes[results[i].name] = results[i].type;
}
}
resolve();
});
});
}
}).then(executeSql);
} }
}); } else {
return executeSql();
}
}); });
});
return promise; return promise;
}; };
Query.prototype.formatError = function (err) { Query.prototype.formatError = function (err) {
var match; var match;
switch (err.code) { switch (err.code) {
case 'SQLITE_CONSTRAINT': case 'SQLITE_CONSTRAINT':
match = err.message.match(/FOREIGN KEY constraint failed/); match = err.message.match(/FOREIGN KEY constraint failed/);
if (match !== null) { if (match !== null) {
return new sequelizeErrors.ForeignKeyConstraintError({ return new sequelizeErrors.ForeignKeyConstraintError({
parent :err parent :err
}); });
} }
var fields = []; var fields = [];
// Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique // Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
match = err.message.match(/columns (.*?) are/); match = err.message.match(/columns (.*?) are/);
if (match !== null && match.length >= 2) { if (match !== null && match.length >= 2) {
fields = match[1].split(', '); fields = match[1].split(', ');
} else { } else {
// Sqlite post 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y // Sqlite post 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
match = err.message.match(/UNIQUE constraint failed: (.*)/); match = err.message.match(/UNIQUE constraint failed: (.*)/);
if (match !== null && match.length >= 2) { if (match !== null && match.length >= 2) {
fields = match[1].split(', ').map(function (columnWithTable) { fields = match[1].split(', ').map(function (columnWithTable) {
return columnWithTable.split('.')[1]; return columnWithTable.split('.')[1];
}); });
}
} }
}
var errors = [] var errors = []
, self = this , self = this
, message = 'Validation error'; , message = 'Validation error';
fields.forEach(function(field) {
errors.push(new sequelizeErrors.ValidationErrorItem(
field + ' must be unique', 'unique violation', field, self.instance && self.instance[field]));
});
if (this.model) { fields.forEach(function(field) {
Utils._.forOwn(this.model.uniqueKeys, function(constraint) { errors.push(new sequelizeErrors.ValidationErrorItem(
if (Utils._.isEqual(constraint.fields, fields) && !!constraint.msg) { field + ' must be unique', 'unique violation', field, self.instance && self.instance[field]));
message = constraint.msg; });
return false;
}
});
}
return new sequelizeErrors.UniqueConstraintError({ if (this.model) {
message: message, Utils._.forOwn(this.model.uniqueKeys, function(constraint) {
errors: errors, if (Utils._.isEqual(constraint.fields, fields) && !!constraint.msg) {
parent: err, message = constraint.msg;
fields: fields return false;
}
}); });
}
case 'SQLITE_BUSY': return new sequelizeErrors.UniqueConstraintError({
return new sequelizeErrors.TimeoutError(err); message: message,
errors: errors,
default: parent: err,
return new sequelizeErrors.DatabaseError(err); fields: fields
} });
};
Query.prototype.handleShowIndexesQuery = function (data) {
var self = this;
// Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
return this.sequelize.Promise.map(data.reverse(), function (item) {
item.fields = [];
item.primary = false;
item.unique = !!item.unique;
return self.run('PRAGMA INDEX_INFO(`' + item.name + '`)').then(function (columns) {
columns.forEach(function (column) {
item.fields[column.seqno] = {
attribute: column.name,
length: undefined,
order: undefined,
};
});
return item; case 'SQLITE_BUSY':
return new sequelizeErrors.TimeoutError(err);
default:
return new sequelizeErrors.DatabaseError(err);
}
};
Query.prototype.handleShowIndexesQuery = function (data) {
var self = this;
// Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
return this.sequelize.Promise.map(data.reverse(), function (item) {
item.fields = [];
item.primary = false;
item.unique = !!item.unique;
return self.run('PRAGMA INDEX_INFO(`' + item.name + '`)').then(function (columns) {
columns.forEach(function (column) {
item.fields[column.seqno] = {
attribute: column.name,
length: undefined,
order: undefined,
};
}); });
return item;
}); });
}; });
};
Query.prototype.getDatabaseMethod = function() {
if (this.isUpsertQuery()) { Query.prototype.getDatabaseMethod = function() {
return 'exec'; // Needed to run multiple queries in one if (this.isUpsertQuery()) {
} else if (this.isInsertQuery() || this.isUpdateQuery() || this.isBulkUpdateQuery() || (this.sql.toLowerCase().indexOf('CREATE TEMPORARY TABLE'.toLowerCase()) !== -1) || this.options.type === QueryTypes.BULKDELETE) { return 'exec'; // Needed to run multiple queries in one
return 'run'; } else if (this.isInsertQuery() || this.isUpdateQuery() || this.isBulkUpdateQuery() || (this.sql.toLowerCase().indexOf('CREATE TEMPORARY TABLE'.toLowerCase()) !== -1) || this.options.type === QueryTypes.BULKDELETE) {
} else { return 'run';
return 'all'; } else {
} return 'all';
}; }
};
return Query;
})(); module.exports = Query;
...@@ -11,1030 +11,1028 @@ var Utils = require('./utils') ...@@ -11,1030 +11,1028 @@ var Utils = require('./utils')
, primitives = ['string', 'number', 'boolean'] , primitives = ['string', 'number', 'boolean']
, defaultsOptions = { raw: true }; , defaultsOptions = { raw: true };
module.exports = (function() { // private
/** var initValues = function(values, options) {
* This class represents an single instance, a database row. You might see it referred to as both Instance and instance. You should not var defaults
* instantiate the Instance class directly, instead you access it using the finder and creation methods on the model. , key;
*
* Instance instances operate with the concept of a `dataValues` property, which stores the actual values represented by the instance.
* By default, the values from dataValues can also be accessed directly from the Instance, that is:
* ```js
* instance.field
* // is the same as
* instance.get('field')
* // is the same as
* instance.getDataValue('field')
* ```
* However, if getters and/or setters are defined for `field` they will be invoked, instead of returning the value from `dataValues`.
* Accessing properties directly or using `get` is preferred for regular use, `getDataValue` should only be used for custom getters.
*
* @see {Sequelize#define} for more information about getters and setters
* @class Instance
*/
var Instance = function(values, options) {
this.dataValues = {};
this._previousDataValues = {};
this._changed = {};
this.__options = this.Model.options;
this.options = options || {};
this.hasPrimaryKeys = this.Model.options.hasPrimaryKeys;
this.__eagerlyLoadedAssociations = [];
/**
* Returns true if this instance has not yet been persisted to the database
* @property isNewRecord
* @return {Boolean}
*/
this.isNewRecord = options.isNewRecord;
/**
* Returns the Model the instance was created from.
* @see {Model}
* @property Model
* @return {Model}
*/
initValues.call(this, values, options);
};
/** values = values && _.clone(values) || {};
* A reference to the sequelize instance
* @see {Sequelize}
* @property sequelize
* @return {Sequelize}
*/
Object.defineProperty(Instance.prototype, 'sequelize', {
get: function() { return this.Model.modelManager.sequelize; }
});
/** if (options.isNewRecord) {
* Get an object representing the query for this instance, use with `options.where` defaults = {};
*
* @property where
* @return {Object}
*/
Instance.prototype.where = function() {
var where;
where = this.Model.primaryKeyAttributes.reduce(function (result, attribute) { if (this.Model._hasDefaultValues) {
result[attribute] = this.get(attribute, {raw: true}); Utils._.each(this.Model._defaultValues, function(valueFn, key) {
return result; if (defaults[key] === undefined) {
}.bind(this), {}); defaults[key] = valueFn();
}
});
}
if (_.size(where) === 0) { // set id to null if not passed as value, a newly created dao has no id
return this.__options.whereCollection; // removing this breaks bulkCreate
// do after default values since it might have UUID as a default value
if (!defaults.hasOwnProperty(this.Model.primaryKeyAttribute)) {
defaults[this.Model.primaryKeyAttribute] = null;
} }
return where;
};
Instance.prototype.toString = function () { if (this.Model._timestampAttributes.createdAt && defaults[this.Model._timestampAttributes.createdAt]) {
return '[object SequelizeInstance:'+this.Model.name+']'; this.dataValues[this.Model._timestampAttributes.createdAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.createdAt]);
}; delete defaults[this.Model._timestampAttributes.createdAt];
}
/** if (this.Model._timestampAttributes.updatedAt && defaults[this.Model._timestampAttributes.updatedAt]) {
* Get the value of the underlying data value this.dataValues[this.Model._timestampAttributes.updatedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.updatedAt]);
* delete defaults[this.Model._timestampAttributes.updatedAt];
* @param {String} key }
* @return {any}
*/
Instance.prototype.getDataValue = function(key) {
return this.dataValues[key];
};
/** if (this.Model._timestampAttributes.deletedAt && defaults[this.Model._timestampAttributes.deletedAt]) {
* Update the underlying data value this.dataValues[this.Model._timestampAttributes.deletedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.deletedAt]);
* delete defaults[this.Model._timestampAttributes.deletedAt];
* @param {String} key
* @param {any} value
*/
Instance.prototype.setDataValue = function(key, value) {
var originalValue = this._previousDataValues[key];
if (primitives.indexOf(typeof value) === -1 || value !== originalValue) {
this.changed(key, true);
} }
this.dataValues[key] = value; if (Object.keys(defaults).length) {
}; for (key in defaults) {
if (values[key] === undefined) {
this.set(key, Utils.toDefaultValue(defaults[key]), defaultsOptions);
}
}
}
}
this.set(values, options);
};
/**
* This class represents an single instance, a database row. You might see it referred to as both Instance and instance. You should not
* instantiate the Instance class directly, instead you access it using the finder and creation methods on the model.
*
* Instance instances operate with the concept of a `dataValues` property, which stores the actual values represented by the instance.
* By default, the values from dataValues can also be accessed directly from the Instance, that is:
* ```js
* instance.field
* // is the same as
* instance.get('field')
* // is the same as
* instance.getDataValue('field')
* ```
* However, if getters and/or setters are defined for `field` they will be invoked, instead of returning the value from `dataValues`.
* Accessing properties directly or using `get` is preferred for regular use, `getDataValue` should only be used for custom getters.
*
* @see {Sequelize#define} for more information about getters and setters
* @class Instance
*/
var Instance = function(values, options) {
this.dataValues = {};
this._previousDataValues = {};
this._changed = {};
this.__options = this.Model.options;
this.options = options || {};
this.hasPrimaryKeys = this.Model.options.hasPrimaryKeys;
this.__eagerlyLoadedAssociations = [];
/**
* Returns true if this instance has not yet been persisted to the database
* @property isNewRecord
* @return {Boolean}
*/
this.isNewRecord = options.isNewRecord;
/** /**
* If no key is given, returns all values of the instance, also invoking virtual getters. * Returns the Model the instance was created from.
* * @see {Model}
* If key is given and a field or virtual getter is present for the key it will call that getter - else it will return the value for key. * @property Model
* * @return {Model}
* @param {String} [key]
* @param {Object} [options]
* @param {Boolean} [options.plain=false] If set to true, included instances will be returned as plain objects
* @return {Object|any}
*/ */
Instance.prototype.get = function(key, options) {
if (options === undefined && typeof key === 'object') {
options = key;
key = undefined;
}
if (key) { initValues.call(this, values, options);
if (this._customGetters[key]) { };
return this._customGetters[key].call(this, key);
} /**
if (options && options.plain && this.options.include && this.options.includeNames.indexOf(key) !== -1) { * A reference to the sequelize instance
if (Array.isArray(this.dataValues[key])) { * @see {Sequelize}
return this.dataValues[key].map(function (instance) { * @property sequelize
return instance.get({plain: options.plain}); * @return {Sequelize}
}); */
} else if (this.dataValues[key] instanceof Instance) { Object.defineProperty(Instance.prototype, 'sequelize', {
return this.dataValues[key].get({plain: options.plain}); get: function() { return this.Model.modelManager.sequelize; }
} else { });
return this.dataValues[key];
} /**
* Get an object representing the query for this instance, use with `options.where`
*
* @property where
* @return {Object}
*/
Instance.prototype.where = function() {
var where;
where = this.Model.primaryKeyAttributes.reduce(function (result, attribute) {
result[attribute] = this.get(attribute, {raw: true});
return result;
}.bind(this), {});
if (_.size(where) === 0) {
return this.__options.whereCollection;
}
return where;
};
Instance.prototype.toString = function () {
return '[object SequelizeInstance:'+this.Model.name+']';
};
/**
* Get the value of the underlying data value
*
* @param {String} key
* @return {any}
*/
Instance.prototype.getDataValue = function(key) {
return this.dataValues[key];
};
/**
* Update the underlying data value
*
* @param {String} key
* @param {any} value
*/
Instance.prototype.setDataValue = function(key, value) {
var originalValue = this._previousDataValues[key];
if (primitives.indexOf(typeof value) === -1 || value !== originalValue) {
this.changed(key, true);
}
this.dataValues[key] = value;
};
/**
* If no key is given, returns all values of the instance, also invoking virtual getters.
*
* If key is given and a field or virtual getter is present for the key it will call that getter - else it will return the value for key.
*
* @param {String} [key]
* @param {Object} [options]
* @param {Boolean} [options.plain=false] If set to true, included instances will be returned as plain objects
* @return {Object|any}
*/
Instance.prototype.get = function(key, options) {
if (options === undefined && typeof key === 'object') {
options = key;
key = undefined;
}
if (key) {
if (this._customGetters[key]) {
return this._customGetters[key].call(this, key);
}
if (options && options.plain && this.options.include && this.options.includeNames.indexOf(key) !== -1) {
if (Array.isArray(this.dataValues[key])) {
return this.dataValues[key].map(function (instance) {
return instance.get({plain: options.plain});
});
} else if (this.dataValues[key] instanceof Instance) {
return this.dataValues[key].get({plain: options.plain});
} else {
return this.dataValues[key];
} }
return this.dataValues[key];
} }
return this.dataValues[key];
}
if (this._hasCustomGetters || (options && options.plain && this.options.include) || (options && options.clone)) { if (this._hasCustomGetters || (options && options.plain && this.options.include) || (options && options.clone)) {
var values = {} var values = {}
, _key; , _key;
if (this._hasCustomGetters) { if (this._hasCustomGetters) {
for (_key in this._customGetters) { for (_key in this._customGetters) {
if (this._customGetters.hasOwnProperty(_key)) { if (this._customGetters.hasOwnProperty(_key)) {
values[_key] = this.get(_key, options); values[_key] = this.get(_key, options);
}
} }
} }
}
for (_key in this.dataValues) { for (_key in this.dataValues) {
if (!values.hasOwnProperty(_key) && this.dataValues.hasOwnProperty(_key)) { if (!values.hasOwnProperty(_key) && this.dataValues.hasOwnProperty(_key)) {
if (options && options.plain && this.options.include && this.options.includeNames.indexOf(_key) !== -1) { if (options && options.plain && this.options.include && this.options.includeNames.indexOf(_key) !== -1) {
if (Array.isArray(this.dataValues[_key])) { if (Array.isArray(this.dataValues[_key])) {
values[_key] = this.dataValues[_key].map(function (instance) { values[_key] = this.dataValues[_key].map(function (instance) {
return instance.get({plain: options.plain}); return instance.get({plain: options.plain});
}); });
} else if (this.dataValues[_key] instanceof Instance) { } else if (this.dataValues[_key] instanceof Instance) {
values[_key] = this.dataValues[_key].get({plain: options.plain}); values[_key] = this.dataValues[_key].get({plain: options.plain});
} else {
values[_key] = this.dataValues[_key];
}
} else { } else {
values[_key] = this.dataValues[_key]; values[_key] = this.dataValues[_key];
} }
} else {
values[_key] = this.dataValues[_key];
} }
} }
return values;
} }
return this.dataValues; return values;
}; }
return this.dataValues;
};
/**
* Set is used to update values on the instance (the sequelize representation of the instance that is, remember that nothing will be persisted before you actually call `save`).
* In its most basic form `set` will update a value stored in the underlying `dataValues` object. However, if a custom setter function is defined for the key, that function
* will be called instead. To bypass the setter, you can pass `raw: true` in the options object.
*
* If set is called with an object, it will loop over the object, and call set recursively for each key, value pair. If you set raw to true, the underlying dataValues will either be
* set directly to the object passed, or used to extend dataValues, if dataValues already contain values.
*
* When set is called, the previous value of the field is stored and sets a changed flag(see `changed`).
*
* Set can also be used to build instances for associations, if you have values for those.
* When using set with associations you need to make sure the property key matches the alias of the association
* while also making sure that the proper include options have been set (from .build() or .find())
*
* If called with a dot.seperated key on a JSON/JSONB attribute it will set the value nested and flag the entire object as changed.
*
* @see {Model#find} for more information about includes
* @param {String|Object} key
* @param {any} value
* @param {Object} [options]
* @param {Boolean} [options.raw=false] If set to true, field and virtual setters will be ignored
* @param {Boolean} [options.reset=false] Clear all previously set data values
* @alias setAttributes
*/
Instance.prototype.set = function(key, value, options) {
var values
, originalValue
, keys
, i
, length;
if (typeof key === 'object' && key !== null) {
values = key;
options = value || {};
if (options.reset) {
this.dataValues = {};
}
/** // If raw, and we're not dealing with includes or special attributes, just set it straight on the dataValues object
* Set is used to update values on the instance (the sequelize representation of the instance that is, remember that nothing will be persisted before you actually call `save`). if (options.raw && !(this.options && this.options.include) && !(options && options.attributes) && !this.Model._hasBooleanAttributes && !this.Model._hasDateAttributes) {
* In its most basic form `set` will update a value stored in the underlying `dataValues` object. However, if a custom setter function is defined for the key, that function if (Object.keys(this.dataValues).length) {
* will be called instead. To bypass the setter, you can pass `raw: true` in the options object. this.dataValues = _.extend(this.dataValues, values);
* } else {
* If set is called with an object, it will loop over the object, and call set recursively for each key, value pair. If you set raw to true, the underlying dataValues will either be this.dataValues = values;
* set directly to the object passed, or used to extend dataValues, if dataValues already contain values.
*
* When set is called, the previous value of the field is stored and sets a changed flag(see `changed`).
*
* Set can also be used to build instances for associations, if you have values for those.
* When using set with associations you need to make sure the property key matches the alias of the association
* while also making sure that the proper include options have been set (from .build() or .find())
*
* If called with a dot.seperated key on a JSON/JSONB attribute it will set the value nested and flag the entire object as changed.
*
* @see {Model#find} for more information about includes
* @param {String|Object} key
* @param {any} value
* @param {Object} [options]
* @param {Boolean} [options.raw=false] If set to true, field and virtual setters will be ignored
* @param {Boolean} [options.reset=false] Clear all previously set data values
* @alias setAttributes
*/
Instance.prototype.set = function(key, value, options) {
var values
, originalValue
, keys
, i
, length;
if (typeof key === 'object' && key !== null) {
values = key;
options = value || {};
if (options.reset) {
this.dataValues = {};
} }
// If raw, .changed() shouldn't be true
this._previousDataValues = _.clone(this.dataValues);
} else {
// Loop and call set
// If raw, and we're not dealing with includes or special attributes, just set it straight on the dataValues object if (options.attributes) {
if (options.raw && !(this.options && this.options.include) && !(options && options.attributes) && !this.Model._hasBooleanAttributes && !this.Model._hasDateAttributes) { keys = options.attributes;
if (Object.keys(this.dataValues).length) { if (this.Model._hasVirtualAttributes) {
this.dataValues = _.extend(this.dataValues, values); keys = keys.concat(this.Model._virtualAttributes);
} else {
this.dataValues = values;
} }
// If raw, .changed() shouldn't be true
this._previousDataValues = _.clone(this.dataValues);
} else {
// Loop and call set
if (options.attributes) { if (this.options.includeNames) {
keys = options.attributes; keys = keys.concat(this.options.includeNames);
if (this.Model._hasVirtualAttributes) { }
keys = keys.concat(this.Model._virtualAttributes);
}
if (this.options.includeNames) {
keys = keys.concat(this.options.includeNames);
}
for (i = 0, length = keys.length; i < length; i++) { for (i = 0, length = keys.length; i < length; i++) {
if (values[keys[i]] !== undefined) { if (values[keys[i]] !== undefined) {
this.set(keys[i], values[keys[i]], options); this.set(keys[i], values[keys[i]], options);
}
}
} else {
for (key in values) {
this.set(key, values[key], options);
} }
} }
} else {
if (options.raw) { for (key in values) {
// If raw, .changed() shouldn't be true this.set(key, values[key], options);
this._previousDataValues = _.clone(this.dataValues);
} }
} }
} else {
if (!options) if (options.raw) {
options = {}; // If raw, .changed() shouldn't be true
if (!options.raw) { this._previousDataValues = _.clone(this.dataValues);
originalValue = this.dataValues[key];
} }
}
} else {
if (!options)
options = {};
if (!options.raw) {
originalValue = this.dataValues[key];
}
// If not raw, and there's a customer setter // If not raw, and there's a customer setter
if (!options.raw && this._customSetters[key]) { if (!options.raw && this._customSetters[key]) {
this._customSetters[key].call(this, value, key); this._customSetters[key].call(this, value, key);
} else { } else {
// Check if we have included models, and if this key matches the include model names/aliases // Check if we have included models, and if this key matches the include model names/aliases
if (this.options && this.options.include && this.options.includeNames.indexOf(key) !== -1 && value) { if (this.options && this.options.include && this.options.includeNames.indexOf(key) !== -1 && value) {
// Pass it on to the include handler // Pass it on to the include handler
this._setInclude(key, value, options); this._setInclude(key, value, options);
return; return;
} else { } else {
// Bunch of stuff we won't do when its raw // Bunch of stuff we won't do when its raw
if (!options.raw) { if (!options.raw) {
// If attribute is not in model definition, return // If attribute is not in model definition, return
if (!this._isAttribute(key)) { if (!this._isAttribute(key)) {
if (key.indexOf('.') > -1 && this.Model._isJsonAttribute(key.split('.')[0])) { if (key.indexOf('.') > -1 && this.Model._isJsonAttribute(key.split('.')[0])) {
Dottie.set(this.dataValues, key, value); Dottie.set(this.dataValues, key, value);
this.changed(key, true); this.changed(key, true);
}
return;
} }
return;
}
// If attempting to set primary key and primary key is already defined, return // If attempting to set primary key and primary key is already defined, return
if (this.Model._hasPrimaryKeys && originalValue && this.Model._isPrimaryKey(key)) { if (this.Model._hasPrimaryKeys && originalValue && this.Model._isPrimaryKey(key)) {
return; return;
} }
// If attempting to set read only attributes, return // If attempting to set read only attributes, return
if (!this.isNewRecord && this.Model._hasReadOnlyAttributes && this.Model._isReadOnlyAttribute(key)) { if (!this.isNewRecord && this.Model._hasReadOnlyAttributes && this.Model._isReadOnlyAttribute(key)) {
return; return;
} }
// Convert date fields to real date objects // Convert date fields to real date objects
if (this.Model._hasDateAttributes && this.Model._isDateAttribute(key) && value !== null && value !== undefined && !(value instanceof Date) && !value._isSequelizeMethod) { if (this.Model._hasDateAttributes && this.Model._isDateAttribute(key) && value !== null && value !== undefined && !(value instanceof Date) && !value._isSequelizeMethod) {
value = new Date(value); value = new Date(value);
}
} }
}
// Convert boolean-ish values to booleans // Convert boolean-ish values to booleans
if (this.Model._hasBooleanAttributes && this.Model._isBooleanAttribute(key) && value !== null && value !== undefined && !value._isSequelizeMethod) { if (this.Model._hasBooleanAttributes && this.Model._isBooleanAttribute(key) && value !== null && value !== undefined && !value._isSequelizeMethod) {
if (Buffer.isBuffer(value) && value.length === 1) { if (Buffer.isBuffer(value) && value.length === 1) {
// Bit fields are returned as buffers // Bit fields are returned as buffers
value = value[0]; value = value[0];
} }
if (_.isString(value)) { if (_.isString(value)) {
// Only take action on valid boolean strings. // Only take action on valid boolean strings.
value = (value === 'true') ? true : (value === 'false') ? false : value; value = (value === 'true') ? true : (value === 'false') ? false : value;
} else if (_.isNumber(value)) { } else if (_.isNumber(value)) {
// Only take action on valid boolean integers. // Only take action on valid boolean integers.
value = (value === 1) ? true : (value === 0) ? false : value; value = (value === 1) ? true : (value === 0) ? false : value;
}
} }
if (!options.raw && ((primitives.indexOf(typeof value) === -1 && value !== null) || value !== originalValue)) {
this._previousDataValues[key] = originalValue;
this.changed(key, true);
}
this.dataValues[key] = value;
} }
if (!options.raw && ((primitives.indexOf(typeof value) === -1 && value !== null) || value !== originalValue)) {
this._previousDataValues[key] = originalValue;
this.changed(key, true);
}
this.dataValues[key] = value;
} }
} }
}
return this;
}; return this;
};
Instance.prototype.setAttributes = function(updates) {
return this.set(updates); Instance.prototype.setAttributes = function(updates) {
}; return this.set(updates);
};
/**
* If changed is called with a string it will return a boolean indicating whether the value of that key in `dataValues` is different from the value in `_previousDataValues`. /**
* * If changed is called with a string it will return a boolean indicating whether the value of that key in `dataValues` is different from the value in `_previousDataValues`.
* If changed is called without an argument, it will return an array of keys that have changed. *
* * If changed is called without an argument, it will return an array of keys that have changed.
* If changed is called without an argument and no keys have changed, it will return `false`. *
* * If changed is called without an argument and no keys have changed, it will return `false`.
* @param {String} [key] *
* @return {Boolean|Array} * @param {String} [key]
*/ * @return {Boolean|Array}
Instance.prototype.changed = function(key, value) { */
if (key) { Instance.prototype.changed = function(key, value) {
if (value !== undefined) { if (key) {
this._changed[key] = value; if (value !== undefined) {
return this; this._changed[key] = value;
} return this;
return this._changed[key] || false;
} }
return this._changed[key] || false;
}
var changed = Object.keys(this.dataValues).filter(function(key) {
return this.changed(key);
}.bind(this));
return changed.length ? changed : false;
};
/**
* Returns the previous value for key from `_previousDataValues`.
* @param {String} key
* @return {any}
*/
Instance.prototype.previous = function(key) {
return this._previousDataValues[key];
};
Instance.prototype._setInclude = function(key, value, options) {
if (!Array.isArray(value)) value = [value];
if (value[0] instanceof Instance) {
value = value.map(function(instance) {
return instance.dataValues;
});
}
var include = this.options.includeMap[key]
, association = include.association
, self = this
, accessor = key
, childOptions
, primaryKeyAttribute = include.model.primaryKeyAttribute
, isEmpty;
if (!isEmpty) {
childOptions = {
isNewRecord: this.isNewRecord,
include: include.include,
includeNames: include.includeNames,
includeMap: include.includeMap,
includeValidated: true,
raw: options.raw,
attributes: include.originalAttributes
};
}
if (include.originalAttributes === undefined || include.originalAttributes.length) {
if (association.isSingleAssociation) {
if (Array.isArray(value)) {
value = value[0];
}
var changed = Object.keys(this.dataValues).filter(function(key) { isEmpty = value && value[primaryKeyAttribute] === null;
return this.changed(key); self[accessor] = self.dataValues[accessor] = isEmpty ? null : include.model.build(value, childOptions);
}.bind(this)); } else {
isEmpty = value[0] && value[0][primaryKeyAttribute] === null;
return changed.length ? changed : false; self[accessor] = self.dataValues[accessor] = isEmpty ? [] : include.model.bulkBuild(value, childOptions);
};
/**
* Returns the previous value for key from `_previousDataValues`.
* @param {String} key
* @return {any}
*/
Instance.prototype.previous = function(key) {
return this._previousDataValues[key];
};
Instance.prototype._setInclude = function(key, value, options) {
if (!Array.isArray(value)) value = [value];
if (value[0] instanceof Instance) {
value = value.map(function(instance) {
return instance.dataValues;
});
} }
}
};
/**
* Validate this instance, and if the validation passes, persist it to the database.
*
* On success, the callback will be called with this instance. On validation error, the callback will be called with an instance of `Sequelize.ValidationError`.
* This error will have a property for each of the fields for which validation failed, with the error message for that field.
*
* @param {Object} [options]
* @param {Object} [options.fields] An optional array of strings, representing database columns. If fields is provided, only those columns will be validated and saved.
* @param {Boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
* @param {Boolean} [options.validate=true] If false, validations won't be run.
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<this|Errors.ValidationError>}
*/
Instance.prototype.save = function(options) {
if (arguments.length > 1) {
throw new Error('The second argument was removed in favor of the options object.');
}
options = _.defaults(options || {}, {
hooks: true,
validate: true
});
var include = this.options.includeMap[key] if (!options.fields) {
, association = include.association if (this.isNewRecord) {
, self = this options.fields = Object.keys(this.Model.attributes);
, accessor = key } else {
, childOptions options.fields = _.intersection(this.changed(), Object.keys(this.Model.attributes));
, primaryKeyAttribute = include.model.primaryKeyAttribute
, isEmpty;
if (!isEmpty) {
childOptions = {
isNewRecord: this.isNewRecord,
include: include.include,
includeNames: include.includeNames,
includeMap: include.includeMap,
includeValidated: true,
raw: options.raw,
attributes: include.originalAttributes
};
} }
if (include.originalAttributes === undefined || include.originalAttributes.length) {
if (association.isSingleAssociation) {
if (Array.isArray(value)) {
value = value[0];
}
isEmpty = value && value[primaryKeyAttribute] === null; options.defaultFields = options.fields;
self[accessor] = self.dataValues[accessor] = isEmpty ? null : include.model.build(value, childOptions); }
} else {
isEmpty = value[0] && value[0][primaryKeyAttribute] === null;
self[accessor] = self.dataValues[accessor] = isEmpty ? [] : include.model.bulkBuild(value, childOptions);
}
}
};
/** if (options.returning === undefined) {
* Validate this instance, and if the validation passes, persist it to the database. if (options.association) {
* options.returning = false;
* On success, the callback will be called with this instance. On validation error, the callback will be called with an instance of `Sequelize.ValidationError`. } else if (this.isNewRecord) {
* This error will have a property for each of the fields for which validation failed, with the error message for that field. options.returning = true;
*
* @param {Object} [options]
* @param {Object} [options.fields] An optional array of strings, representing database columns. If fields is provided, only those columns will be validated and saved.
* @param {Boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
* @param {Boolean} [options.validate=true] If false, validations won't be run.
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<this|Errors.ValidationError>}
*/
Instance.prototype.save = function(options) {
if (arguments.length > 1) {
throw new Error('The second argument was removed in favor of the options object.');
} }
options = _.defaults(options || {}, { }
hooks: true,
validate: true var self = this
, primaryKeyName = this.Model.primaryKeyAttribute
, primaryKeyAttribute = primaryKeyName && this.Model.rawAttributes[primaryKeyName]
, updatedAtAttr = this.Model._timestampAttributes.updatedAt
, createdAtAttr = this.Model._timestampAttributes.createdAt
, hook = self.isNewRecord ? 'Create' : 'Update'
, wasNewRecord = this.isNewRecord;
if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) === -1) {
options.fields.push(updatedAtAttr);
}
if (options.silent === true && !(this.isNewRecord && this.get(updatedAtAttr, {raw: true}))) {
// UpdateAtAttr might have been added as a result of Object.keys(Model.attributes). In that case we have to remove it again
Utils._.remove(options.fields, function(val) {
return val === updatedAtAttr;
}); });
updatedAtAttr = false;
}
if (!options.fields) { if (this.isNewRecord === true) {
if (this.isNewRecord) { if (createdAtAttr && options.fields.indexOf(createdAtAttr) === -1) {
options.fields = Object.keys(this.Model.attributes); options.fields.push(createdAtAttr);
} else {
options.fields = _.intersection(this.changed(), Object.keys(this.Model.attributes));
}
options.defaultFields = options.fields;
} }
if (options.returning === undefined) { if (primaryKeyAttribute && primaryKeyAttribute.defaultValue && options.fields.indexOf(primaryKeyName) < 0) {
if (options.association) { options.fields.unshift(primaryKeyName);
options.returning = false;
} else if (this.isNewRecord) {
options.returning = true;
}
} }
}
var self = this if (this.isNewRecord === false) {
, primaryKeyName = this.Model.primaryKeyAttribute if (primaryKeyName && !this.get(primaryKeyName, {raw: true})) {
, primaryKeyAttribute = primaryKeyName && this.Model.rawAttributes[primaryKeyName] throw new Error('You attempted to save an instance with no primary key, this is not allowed since it would result in a global update');
, updatedAtAttr = this.Model._timestampAttributes.updatedAt
, createdAtAttr = this.Model._timestampAttributes.createdAt
, hook = self.isNewRecord ? 'Create' : 'Update'
, wasNewRecord = this.isNewRecord;
if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) === -1) {
options.fields.push(updatedAtAttr);
} }
}
if (options.silent === true && !(this.isNewRecord && this.get(updatedAtAttr, {raw: true}))) { return Promise.bind(this).then(function() {
// UpdateAtAttr might have been added as a result of Object.keys(Model.attributes). In that case we have to remove it again // Validate
Utils._.remove(options.fields, function(val) { if (options.validate) {
return val === updatedAtAttr; return Promise.bind(this).then(function () {
// hookValidate rejects with errors, validate returns with errors
if (options.hooks) return this.hookValidate(options);
return this.validate(options).then(function (err) {
if (err) throw err;
});
}); });
updatedAtAttr = false;
} }
}).then(function() {
return Promise.bind(this).then(function() {
// Run before hook
if (options.hooks) {
var beforeHookValues = _.pick(this.dataValues, options.fields)
, afterHookValues
, hookChanged
, ignoreChanged = _.difference(this.changed(), options.fields); // In case of update where it's only supposed to update the passed values and the hook values
if (this.isNewRecord === true) { if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) !== -1) {
if (createdAtAttr && options.fields.indexOf(createdAtAttr) === -1) { ignoreChanged = _.without(ignoreChanged, updatedAtAttr);
options.fields.push(createdAtAttr); }
}
if (primaryKeyAttribute && primaryKeyAttribute.defaultValue && options.fields.indexOf(primaryKeyName) < 0) { return this.Model.runHooks('before' + hook, this, options).bind(this).then(function() {
options.fields.unshift(primaryKeyName); if (options.defaultFields && !this.isNewRecord) {
} afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged));
}
if (this.isNewRecord === false) { hookChanged = [];
if (primaryKeyName && !this.get(primaryKeyName, {raw: true})) { Object.keys(afterHookValues).forEach(function (key) {
throw new Error('You attempted to save an instance with no primary key, this is not allowed since it would result in a global update'); if (afterHookValues[key] !== beforeHookValues[key]) {
} hookChanged.push(key);
} }
});
return Promise.bind(this).then(function() { options.fields = _.unique(options.fields.concat(hookChanged));
// Validate
if (options.validate) {
return Promise.bind(this).then(function () {
// hookValidate rejects with errors, validate returns with errors
if (options.hooks) return this.hookValidate(options);
return this.validate(options).then(function (err) {
if (err) throw err;
});
});
}
}).then(function() {
return Promise.bind(this).then(function() {
// Run before hook
if (options.hooks) {
var beforeHookValues = _.pick(this.dataValues, options.fields)
, afterHookValues
, hookChanged
, ignoreChanged = _.difference(this.changed(), options.fields); // In case of update where it's only supposed to update the passed values and the hook values
if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) !== -1) {
ignoreChanged = _.without(ignoreChanged, updatedAtAttr);
} }
return this.Model.runHooks('before' + hook, this, options).bind(this).then(function() { if (hookChanged) {
if (options.defaultFields && !this.isNewRecord) { if (options.validate) {
afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged)); // Validate again
hookChanged = [];
Object.keys(afterHookValues).forEach(function (key) {
if (afterHookValues[key] !== beforeHookValues[key]) {
hookChanged.push(key);
}
});
options.fields = _.unique(options.fields.concat(hookChanged));
}
if (hookChanged) {
if (options.validate) {
// Validate again
options.skip = _.difference(Object.keys(this.Model.rawAttributes), hookChanged); options.skip = _.difference(Object.keys(this.Model.rawAttributes), hookChanged);
return Promise.bind(this).then(function () { return Promise.bind(this).then(function () {
// hookValidate rejects with errors, validate returns with errors // hookValidate rejects with errors, validate returns with errors
if (options.hooks) return this.hookValidate(options); if (options.hooks) return this.hookValidate(options);
return this.validate(options).then(function (err) { return this.validate(options).then(function (err) {
if (err) throw err; if (err) throw err;
});
}).then(function() {
delete options.skip;
}); });
} }).then(function() {
delete options.skip;
});
} }
}); }
}
}).then(function() {
if (!options.fields.length) return this;
if (!this.isNewRecord) return this;
if (!this.options.include || !this.options.include.length) return this;
// Nested creation for BelongsTo relations
return Promise.map(this.options.include.filter(function (include) {
return include.association instanceof BelongsTo;
}), function (include) {
var instance = self.get(include.as);
if (!instance) return Promise.resolve();
return instance.save({
transaction: options.transaction,
logging: options.logging
}).then(function () {
return self[include.association.accessors.set](instance, {save: false});
});
}); });
}) }
.then(function() { }).then(function() {
if (!options.fields.length) return this; if (!options.fields.length) return this;
if (!this.isNewRecord) return this;
if (!this.options.include || !this.options.include.length) return this;
// Nested creation for BelongsTo relations
return Promise.map(this.options.include.filter(function (include) {
return include.association instanceof BelongsTo;
}), function (include) {
var instance = self.get(include.as);
if (!instance) return Promise.resolve();
return instance.save({
transaction: options.transaction,
logging: options.logging
}).then(function () {
return self[include.association.accessors.set](instance, {save: false});
});
});
})
.then(function() {
if (!options.fields.length) return this;
var values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.Model) var values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.Model)
, query = null , query = null
, args = [] , args = []
, now = Utils.now(this.sequelize.options.dialect); , now = Utils.now(this.sequelize.options.dialect);
if (updatedAtAttr && !options.silent) { if (updatedAtAttr && !options.silent) {
self.dataValues[updatedAtAttr] = values[self.Model.rawAttributes[updatedAtAttr].field || updatedAtAttr] = self.Model.$getDefaultTimestamp(updatedAtAttr) || now; self.dataValues[updatedAtAttr] = values[self.Model.rawAttributes[updatedAtAttr].field || updatedAtAttr] = self.Model.$getDefaultTimestamp(updatedAtAttr) || now;
} }
if (self.isNewRecord && createdAtAttr && !values[createdAtAttr]) { if (self.isNewRecord && createdAtAttr && !values[createdAtAttr]) {
self.dataValues[createdAtAttr] = values[self.Model.rawAttributes[createdAtAttr].field || createdAtAttr] = self.Model.$getDefaultTimestamp(createdAtAttr) || now; self.dataValues[createdAtAttr] = values[self.Model.rawAttributes[createdAtAttr].field || createdAtAttr] = self.Model.$getDefaultTimestamp(createdAtAttr) || now;
} }
if (self.isNewRecord) { if (self.isNewRecord) {
query = 'insert'; query = 'insert';
args = [self, self.Model.getTableName(options), values, options]; args = [self, self.Model.getTableName(options), values, options];
} else { } else {
var where = this.where(); var where = this.where();
where = Utils.mapValueFieldNames(where, Object.keys(where), this.Model); where = Utils.mapValueFieldNames(where, Object.keys(where), this.Model);
query = 'update'; query = 'update';
args = [self, self.Model.getTableName(options), values, where, options]; args = [self, self.Model.getTableName(options), values, where, options];
} }
return self.sequelize.getQueryInterface()[query].apply(self.sequelize.getQueryInterface(), args) return self.sequelize.getQueryInterface()[query].apply(self.sequelize.getQueryInterface(), args)
.then(function(result) { .then(function(result) {
// Transfer database generated values (defaults, autoincrement, etc) // Transfer database generated values (defaults, autoincrement, etc)
Object.keys(self.Model.rawAttributes).forEach(function (attr) { Object.keys(self.Model.rawAttributes).forEach(function (attr) {
if (self.Model.rawAttributes[attr].field && if (self.Model.rawAttributes[attr].field &&
values[self.Model.rawAttributes[attr].field] !== undefined && values[self.Model.rawAttributes[attr].field] !== undefined &&
self.Model.rawAttributes[attr].field !== attr self.Model.rawAttributes[attr].field !== attr
) { ) {
values[attr] = values[self.Model.rawAttributes[attr].field]; values[attr] = values[self.Model.rawAttributes[attr].field];
delete values[self.Model.rawAttributes[attr].field]; delete values[self.Model.rawAttributes[attr].field];
}
});
values = _.extend(values, result.dataValues);
result.dataValues = _.extend(result.dataValues, values);
return result;
})
.tap(function(result) {
// Run after hook
if (options.hooks) {
return self.Model.runHooks('after' + hook, result, options);
} }
}) });
.then(function(result) { values = _.extend(values, result.dataValues);
options.fields.forEach(function (field) {
result._previousDataValues[field] = result.dataValues[field]; result.dataValues = _.extend(result.dataValues, values);
self.changed(field, false); return result;
}); })
self.isNewRecord = false; .tap(function(result) {
return result; // Run after hook
}) if (options.hooks) {
.tap(function() { return self.Model.runHooks('after' + hook, result, options);
if (!wasNewRecord) return; }
if (!self.options.include || !self.options.include.length) return; })
.then(function(result) {
// Nested creation for HasOne/HasMany/BelongsToMany relations options.fields.forEach(function (field) {
return Promise.map(self.options.include.filter(function (include) { result._previousDataValues[field] = result.dataValues[field];
return !(include.association instanceof BelongsTo); self.changed(field, false);
}), function (include) { });
var instances = self.get(include.as); self.isNewRecord = false;
return result;
if (!instances) return Promise.resolve(); })
if (!Array.isArray(instances)) instances = [instances]; .tap(function() {
if (!instances.length) return Promise.resolve(); if (!wasNewRecord) return;
if (!self.options.include || !self.options.include.length) return;
// Instances will be updated in place so we can safely treat HasOne like a HasMany
return Promise.map(instances, function (instance) { // Nested creation for HasOne/HasMany/BelongsToMany relations
if (include.association instanceof BelongsToMany) { return Promise.map(self.options.include.filter(function (include) {
return instance.save({transaction: options.transaction, logging: options.logging}).then(function () { return !(include.association instanceof BelongsTo);
var values = {}; }), function (include) {
values[include.association.foreignKey] = self.get(self.Model.primaryKeyAttribute, {raw: true}); var instances = self.get(include.as);
values[include.association.otherKey] = instance.get(instance.Model.primaryKeyAttribute, {raw: true});
return include.association.throughModel.create(values, {transaction: options.transaction, logging: options.logging}); if (!instances) return Promise.resolve();
}); if (!Array.isArray(instances)) instances = [instances];
} else { if (!instances.length) return Promise.resolve();
instance.set(include.association.identifier, self.get(self.Model.primaryKeyAttribute, {raw: true}));
return instance.save({transaction: options.transaction, logging: options.logging}); // Instances will be updated in place so we can safely treat HasOne like a HasMany
} return Promise.map(instances, function (instance) {
}); if (include.association instanceof BelongsToMany) {
return instance.save({transaction: options.transaction, logging: options.logging}).then(function () {
var values = {};
values[include.association.foreignKey] = self.get(self.Model.primaryKeyAttribute, {raw: true});
values[include.association.otherKey] = instance.get(instance.Model.primaryKeyAttribute, {raw: true});
return include.association.throughModel.create(values, {transaction: options.transaction, logging: options.logging});
});
} else {
instance.set(include.association.identifier, self.get(self.Model.primaryKeyAttribute, {raw: true}));
return instance.save({transaction: options.transaction, logging: options.logging});
}
}); });
}); });
}); });
});
};
/*
* Refresh the current instance in-place, i.e. update the object with current data from the DB and return the same object.
* This is different from doing a `find(Instance.id)`, because that would create and return a new instance. With this method,
* all references to the Instance are updated with the new data and no new objects are created.
*
* @see {Model#find}
* @param {Object} [options] Options that are passed on to `Model.find`
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @return {Promise<this>}
*/
Instance.prototype.reload = function(options) {
var self = this
, where = [
this.sequelize.getQueryInterface().quoteTable(this.Model.name) + '.' + this.sequelize.getQueryInterface().quoteIdentifier(this.Model.primaryKeyField) + '=?',
this.get(this.Model.primaryKeyAttribute, {raw: true})
];
options = _.defaults(options || {}, {
where: where,
limit: 1,
include: this.options.include || null
}); });
});
};
/*
* Refresh the current instance in-place, i.e. update the object with current data from the DB and return the same object.
* This is different from doing a `find(Instance.id)`, because that would create and return a new instance. With this method,
* all references to the Instance are updated with the new data and no new objects are created.
*
* @see {Model#find}
* @param {Object} [options] Options that are passed on to `Model.find`
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @return {Promise<this>}
*/
Instance.prototype.reload = function(options) {
var self = this
, where = [
this.sequelize.getQueryInterface().quoteTable(this.Model.name) + '.' + this.sequelize.getQueryInterface().quoteIdentifier(this.Model.primaryKeyField) + '=?',
this.get(this.Model.primaryKeyAttribute, {raw: true})
];
options = _.defaults(options || {}, {
where: where,
limit: 1,
include: this.options.include || null
});
return this.Model.findOne(options).then(function(reload) { return this.Model.findOne(options).then(function(reload) {
self.set(reload.dataValues, {raw: true, reset: true}); self.set(reload.dataValues, {raw: true, reset: true});
}).return(self); }).return(self);
}; };
/* /*
* Validate the attribute of this instance according to validation rules set in the model definition. * Validate the attribute of this instance according to validation rules set in the model definition.
* *
* Emits null if and only if validation successful; otherwise an Error instance containing { field name : [error msgs] } entries. * Emits null if and only if validation successful; otherwise an Error instance containing { field name : [error msgs] } entries.
* *
* @param {Object} [options] Options that are passed to the validator * @param {Object} [options] Options that are passed to the validator
* @param {Array} [options.skip] An array of strings. All properties that are in this array will not be validated * @param {Array} [options.skip] An array of strings. All properties that are in this array will not be validated
* @see {InstanceValidator} * @see {InstanceValidator}
* *
* @return {Promise<undefined|Errors.ValidationError>} * @return {Promise<undefined|Errors.ValidationError>}
*/ */
Instance.prototype.validate = function(options) { Instance.prototype.validate = function(options) {
return new InstanceValidator(this, options).validate(); return new InstanceValidator(this, options).validate();
}; };
Instance.prototype.hookValidate = function(options) { Instance.prototype.hookValidate = function(options) {
return new InstanceValidator(this, options).hookValidate(); return new InstanceValidator(this, options).hookValidate();
}; };
/** /**
* This is the same as calling `set` and then calling `save`. * This is the same as calling `set` and then calling `save`.
* *
* @see {Instance#set} * @see {Instance#set}
* @see {Instance#save} * @see {Instance#save}
* @param {Object} updates See `set` * @param {Object} updates See `set`
* @param {Object} options See `save` * @param {Object} options See `save`
* *
* @return {Promise<this>} * @return {Promise<this>}
* @alias updateAttributes * @alias updateAttributes
*/ */
Instance.prototype.update = function(values, options) { Instance.prototype.update = function(values, options) {
var changedBefore = this.changed() || [] var changedBefore = this.changed() || []
, sideEffects , sideEffects
, fields; , fields;
options = options || {}; options = options || {};
if (Array.isArray(options)) options = {fields: options}; if (Array.isArray(options)) options = {fields: options};
this.set(values, {attributes: options.fields}); this.set(values, {attributes: options.fields});
// Now we need to figure out which fields were actually affected by the setter. // Now we need to figure out which fields were actually affected by the setter.
sideEffects = _.without.apply(this, [this.changed() || []].concat(changedBefore)); sideEffects = _.without.apply(this, [this.changed() || []].concat(changedBefore));
fields = _.union(Object.keys(values), sideEffects); fields = _.union(Object.keys(values), sideEffects);
if (!options.fields) { if (!options.fields) {
options.fields = _.intersection(fields, this.changed()); options.fields = _.intersection(fields, this.changed());
options.defaultFields = options.fields; options.defaultFields = options.fields;
}
return this.save(options);
};
Instance.prototype.updateAttributes = Instance.prototype.update;
/**
* Destroy the row corresponding to this instance. Depending on your setting for paranoid, the row will either be completely deleted, or have its deletedAt timestamp set to the current time.
*
* @param {Object} [options={}]
* @param {Boolean} [options.force=false] If set to true, paranoid models will actually be deleted
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<undefined>}
*/
Instance.prototype.destroy = function(options) {
options = Utils._.extend({
hooks: true,
force: false
}, options || {});
return Promise.bind(this).then(function() {
// Run before hook
if (options.hooks) {
return this.Model.runHooks('beforeDestroy', this, options);
} }
}).then(function() {
var where;
return this.save(options); if (this.Model._timestampAttributes.deletedAt && options.force === false) {
}; this.setDataValue(this.Model._timestampAttributes.deletedAt, new Date());
Instance.prototype.updateAttributes = Instance.prototype.update; return this.save(_.extend(_.clone(options), {hooks : false}));
} else {
/** where = {};
* Destroy the row corresponding to this instance. Depending on your setting for paranoid, the row will either be completely deleted, or have its deletedAt timestamp set to the current time. var primaryKeys = this.Model.primaryKeyAttributes;
* for(var i = 0; i < primaryKeys.length; i++) {
* @param {Object} [options={}] where[this.Model.rawAttributes[primaryKeys[i]].field] = this.get(primaryKeys[i], { raw: true });
* @param {Boolean} [options.force=false] If set to true, paranoid models will actually be deleted
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<undefined>}
*/
Instance.prototype.destroy = function(options) {
options = Utils._.extend({
hooks: true,
force: false
}, options || {});
return Promise.bind(this).then(function() {
// Run before hook
if (options.hooks) {
return this.Model.runHooks('beforeDestroy', this, options);
}
}).then(function() {
var where;
if (this.Model._timestampAttributes.deletedAt && options.force === false) {
this.setDataValue(this.Model._timestampAttributes.deletedAt, new Date());
return this.save(_.extend(_.clone(options), {hooks : false}));
} else {
where = {};
var primaryKeys = this.Model.primaryKeyAttributes;
for(var i = 0; i < primaryKeys.length; i++) {
where[this.Model.rawAttributes[primaryKeys[i]].field] = this.get(primaryKeys[i], { raw: true });
}
return this.sequelize.getQueryInterface().delete(this, this.Model.getTableName(options), where, _.defaults(options, { type: QueryTypes.DELETE,limit: null}));
}
}).tap(function() {
// Run after hook
if (options.hooks) {
return this.Model.runHooks('afterDestroy', this, options);
}
}).then(function(result) {
return result;
});
};
/**
* Restore the row corresponding to this instance. Only available for paranoid models.
*
* @param {Object} [options={}]
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<undefined>}
*/
Instance.prototype.restore = function(options) {
if (!this.Model._timestampAttributes.deletedAt) throw new Error('Model is not paranoid');
options = Utils._.extend({
hooks: true,
force: false
}, options || {});
return Promise.bind(this).then(function() {
// Run before hook
if (options.hooks) {
return this.Model.runHooks('beforeRestore', this, options);
}
}).then(function() {
this.setDataValue(this.Model._timestampAttributes.deletedAt, null);
return this.save(_.extend(_.clone(options), {hooks : false, omitNull : false}));
}).tap(function() {
// Run after hook
if (options.hooks) {
return this.Model.runHooks('afterRestore', this, options);
}
});
};
/**
* Increment the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The increment is done using a
* ```sql
* SET column = column + X
* ```
* query. To get the correct value after an increment into the Instance you should do a reload.
*
*```js
* instance.increment('number') // increment number by 1
* instance.increment(['number', 'count'], { by: 2 }) // increment number and count by 2
* instance.increment({ answer: 42, tries: 1}, { by: 2 }) // increment answer by 42, and tries by 1.
* // `by` is ignored, since each column has its own value
* ```
*
* @see {Instance#reload}
* @param {String|Array|Object} fields If a string is provided, that column is incremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is incremented by the value given.
* @param {Object} [options]
* @param {Integer} [options.by=1] The number to increment by
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<this>}
*/
Instance.prototype.increment = function(fields, options) {
var identifier = this.where()
, updatedAtAttr = this.Model._timestampAttributes.updatedAt
, values = {}
, where;
if (identifier) {
for (var attrName in identifier) {
// Field name mapping
var rawAttribute = this.Model.rawAttributes[attrName];
if (rawAttribute.field && rawAttribute.field !== rawAttribute.fieldName) {
identifier[this.Model.rawAttributes[attrName].field] = identifier[attrName];
delete identifier[attrName];
}
} }
return this.sequelize.getQueryInterface().delete(this, this.Model.getTableName(options), where, _.defaults(options, { type: QueryTypes.DELETE,limit: null}));
} }
}).tap(function() {
options = _.defaults(options || {}, { // Run after hook
by: 1, if (options.hooks) {
attributes: {}, return this.Model.runHooks('afterDestroy', this, options);
where: {}
});
where = _.extend(options.where || {}, identifier);
if (Utils._.isString(fields)) {
values[fields] = options.by;
} else if (Utils._.isArray(fields)) {
Utils._.each(fields, function(field) {
values[field] = options.by;
});
} else { // Assume fields is key-value pairs
values = fields;
} }
}).then(function(result) {
if (updatedAtAttr && !values[updatedAtAttr]) { return result;
options.attributes[updatedAtAttr] = this.Model.$getDefaultTimestamp(updatedAtAttr) || Utils.now(this.sequelize.options.dialect); });
};
/**
* Restore the row corresponding to this instance. Only available for paranoid models.
*
* @param {Object} [options={}]
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<undefined>}
*/
Instance.prototype.restore = function(options) {
if (!this.Model._timestampAttributes.deletedAt) throw new Error('Model is not paranoid');
options = Utils._.extend({
hooks: true,
force: false
}, options || {});
return Promise.bind(this).then(function() {
// Run before hook
if (options.hooks) {
return this.Model.runHooks('beforeRestore', this, options);
} }
}).then(function() {
Object.keys(values).forEach(function(attr) { this.setDataValue(this.Model._timestampAttributes.deletedAt, null);
return this.save(_.extend(_.clone(options), {hooks : false, omitNull : false}));
}).tap(function() {
// Run after hook
if (options.hooks) {
return this.Model.runHooks('afterRestore', this, options);
}
});
};
/**
* Increment the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The increment is done using a
* ```sql
* SET column = column + X
* ```
* query. To get the correct value after an increment into the Instance you should do a reload.
*
*```js
* instance.increment('number') // increment number by 1
* instance.increment(['number', 'count'], { by: 2 }) // increment number and count by 2
* instance.increment({ answer: 42, tries: 1}, { by: 2 }) // increment answer by 42, and tries by 1.
* // `by` is ignored, since each column has its own value
* ```
*
* @see {Instance#reload}
* @param {String|Array|Object} fields If a string is provided, that column is incremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is incremented by the value given.
* @param {Object} [options]
* @param {Integer} [options.by=1] The number to increment by
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise<this>}
*/
Instance.prototype.increment = function(fields, options) {
var identifier = this.where()
, updatedAtAttr = this.Model._timestampAttributes.updatedAt
, values = {}
, where;
if (identifier) {
for (var attrName in identifier) {
// Field name mapping // Field name mapping
if (this.Model.rawAttributes[attr] && this.Model.rawAttributes[attr].field && this.Model.rawAttributes[attr].field !== attr) { var rawAttribute = this.Model.rawAttributes[attrName];
values[this.Model.rawAttributes[attr].field] = values[attr]; if (rawAttribute.field && rawAttribute.field !== rawAttribute.fieldName) {
delete values[attr]; identifier[this.Model.rawAttributes[attrName].field] = identifier[attrName];
delete identifier[attrName];
} }
}, this);
return this.sequelize.getQueryInterface().increment(this, this.Model.getTableName(options), values, where, options).return(this);
};
/**
* Decrement the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The decrement is done using a
* ```sql
* SET column = column - X
* ```
* query. To get the correct value after an decrement into the Instance you should do a reload.
*
* ```js
* instance.decrement('number') // decrement number by 1
* instance.decrement(['number', 'count'], { by: 2 }) // decrement number and count by 2
* instance.decrement({ answer: 42, tries: 1}, { by: 2 }) // decrement answer by 42, and tries by 1.
* // `by` is ignored, since each column has its own value
* ```
*
* @see {Instance#reload}
* @param {String|Array|Object} fields If a string is provided, that column is decremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is decremented by the value given
* @param {Object} [options]
* @param {Integer} [options.by=1] The number to decrement by
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise}
*/
Instance.prototype.decrement = function(fields, options) {
options = _.defaults(options || {}, {
by: 1
});
if (!Utils._.isString(fields) && !Utils._.isArray(fields)) { // Assume fields is key-value pairs
Utils._.each(fields, function(value, field) {
fields[field] = -value;
});
} }
}
options.by = 0 - options.by; options = _.defaults(options || {}, {
by: 1,
return this.increment(fields, options); attributes: {},
}; where: {}
});
/**
* Check whether all values of this and `other` Instance are the same
*
* @param {Instance} other
* @return {Boolean}
*/
Instance.prototype.equals = function(other) {
var result = true;
Utils._.each(this.dataValues, function(value, key) {
if (Utils._.isDate(value) && Utils._.isDate(other[key])) {
result = result && (value.getTime() === other[key].getTime());
} else {
result = result && (value === other[key]);
}
});
return result;
};
/** where = _.extend(options.where || {}, identifier);
* Check if this is eqaul to one of `others` by calling equals
*
* @param {Array} others
* @return {Boolean}
*/
Instance.prototype.equalsOneOf = function(others) {
var self = this;
return _.any(others, function(other) { if (Utils._.isString(fields)) {
return self.equals(other); values[fields] = options.by;
} else if (Utils._.isArray(fields)) {
Utils._.each(fields, function(field) {
values[field] = options.by;
}); });
}; } else { // Assume fields is key-value pairs
values = fields;
Instance.prototype.setValidators = function(attribute, validators) { }
this.validators[attribute] = validators;
}; if (updatedAtAttr && !values[updatedAtAttr]) {
options.attributes[updatedAtAttr] = this.Model.$getDefaultTimestamp(updatedAtAttr) || Utils.now(this.sequelize.options.dialect);
}
Object.keys(values).forEach(function(attr) {
// Field name mapping
if (this.Model.rawAttributes[attr] && this.Model.rawAttributes[attr].field && this.Model.rawAttributes[attr].field !== attr) {
values[this.Model.rawAttributes[attr].field] = values[attr];
delete values[attr];
}
}, this);
return this.sequelize.getQueryInterface().increment(this, this.Model.getTableName(options), values, where, options).return(this);
};
/**
* Decrement the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The decrement is done using a
* ```sql
* SET column = column - X
* ```
* query. To get the correct value after an decrement into the Instance you should do a reload.
*
* ```js
* instance.decrement('number') // decrement number by 1
* instance.decrement(['number', 'count'], { by: 2 }) // decrement number and count by 2
* instance.decrement({ answer: 42, tries: 1}, { by: 2 }) // decrement answer by 42, and tries by 1.
* // `by` is ignored, since each column has its own value
* ```
*
* @see {Instance#reload}
* @param {String|Array|Object} fields If a string is provided, that column is decremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is decremented by the value given
* @param {Object} [options]
* @param {Integer} [options.by=1] The number to decrement by
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction]
*
* @return {Promise}
*/
Instance.prototype.decrement = function(fields, options) {
options = _.defaults(options || {}, {
by: 1
});
/** if (!Utils._.isString(fields) && !Utils._.isArray(fields)) { // Assume fields is key-value pairs
* Convert the instance to a JSON representation. Proxies to calling `get` with no keys. This means get all values gotten from the DB, and apply all custom getters. Utils._.each(fields, function(value, field) {
* fields[field] = -value;
* @see {Instance#get}
* @return {object}
*/
Instance.prototype.toJSON = function() {
return this.get({
plain: true
}); });
}; }
// private options.by = 0 - options.by;
var initValues = function(values, options) {
var defaults
, key;
values = values && _.clone(values) || {}; return this.increment(fields, options);
};
if (options.isNewRecord) { /**
defaults = {}; * Check whether all values of this and `other` Instance are the same
*
* @param {Instance} other
* @return {Boolean}
*/
Instance.prototype.equals = function(other) {
var result = true;
if (this.Model._hasDefaultValues) { Utils._.each(this.dataValues, function(value, key) {
Utils._.each(this.Model._defaultValues, function(valueFn, key) { if (Utils._.isDate(value) && Utils._.isDate(other[key])) {
if (defaults[key] === undefined) { result = result && (value.getTime() === other[key].getTime());
defaults[key] = valueFn(); } else {
} result = result && (value === other[key]);
}); }
} });
// set id to null if not passed as value, a newly created dao has no id
// removing this breaks bulkCreate
// do after default values since it might have UUID as a default value
if (!defaults.hasOwnProperty(this.Model.primaryKeyAttribute)) {
defaults[this.Model.primaryKeyAttribute] = null;
}
if (this.Model._timestampAttributes.createdAt && defaults[this.Model._timestampAttributes.createdAt]) {
this.dataValues[this.Model._timestampAttributes.createdAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.createdAt]);
delete defaults[this.Model._timestampAttributes.createdAt];
}
if (this.Model._timestampAttributes.updatedAt && defaults[this.Model._timestampAttributes.updatedAt]) {
this.dataValues[this.Model._timestampAttributes.updatedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.updatedAt]);
delete defaults[this.Model._timestampAttributes.updatedAt];
}
if (this.Model._timestampAttributes.deletedAt && defaults[this.Model._timestampAttributes.deletedAt]) { return result;
this.dataValues[this.Model._timestampAttributes.deletedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.deletedAt]); };
delete defaults[this.Model._timestampAttributes.deletedAt];
}
if (Object.keys(defaults).length) { /**
for (key in defaults) { * Check if this is eqaul to one of `others` by calling equals
if (values[key] === undefined) { *
this.set(key, Utils.toDefaultValue(defaults[key]), defaultsOptions); * @param {Array} others
} * @return {Boolean}
} */
} Instance.prototype.equalsOneOf = function(others) {
} var self = this;
this.set(values, options); return _.any(others, function(other) {
}; return self.equals(other);
});
};
Instance.prototype.setValidators = function(attribute, validators) {
this.validators[attribute] = validators;
};
/**
* Convert the instance to a JSON representation. Proxies to calling `get` with no keys. This means get all values gotten from the DB, and apply all custom getters.
*
* @see {Instance#get}
* @return {object}
*/
Instance.prototype.toJSON = function() {
return this.get({
plain: true
});
};
return Instance; module.exports = Instance;
})();
...@@ -4,100 +4,98 @@ var Toposort = require('toposort-class') ...@@ -4,100 +4,98 @@ var Toposort = require('toposort-class')
, Utils = require('./utils') , Utils = require('./utils')
, _ = require('lodash'); , _ = require('lodash');
module.exports = (function() { var ModelManager = function(sequelize) {
var ModelManager = function(sequelize) { this.models = [];
this.models = []; this.sequelize = sequelize;
this.sequelize = sequelize; };
};
ModelManager.prototype.addModel = function(model) {
this.models.push(model);
this.sequelize.models[model.name] = model;
return model;
};
ModelManager.prototype.removeModel = function(model) {
this.models = this.models.filter(function($model) {
return $model.name !== model.name;
});
delete this.sequelize.models[model.name]; ModelManager.prototype.addModel = function(model) {
}; this.models.push(model);
this.sequelize.models[model.name] = model;
ModelManager.prototype.getModel = function(against, options) { return model;
options = _.defaults(options || {}, { };
attribute: 'name'
});
var model = this.models.filter(function(model) { ModelManager.prototype.removeModel = function(model) {
return model[options.attribute] === against; this.models = this.models.filter(function($model) {
}); return $model.name !== model.name;
});
return !!model ? model[0] : null; delete this.sequelize.models[model.name];
}; };
ModelManager.prototype.__defineGetter__('all', function() { ModelManager.prototype.getModel = function(against, options) {
return this.models; options = _.defaults(options || {}, {
attribute: 'name'
}); });
/** var model = this.models.filter(function(model) {
* Iterate over Models in an order suitable for e.g. creating tables. Will return model[options.attribute] === against;
* take foreign key constraints into account so that dependencies are visited });
* before dependents.
*/
ModelManager.prototype.forEachModel = function(iterator, options) {
var models = {}
, sorter = new Toposort()
, sorted
, dep;
options = _.defaults(options || {}, {
reverse: true
});
this.models.forEach(function(model) { return !!model ? model[0] : null;
var deps = [] };
, tableName = model.getTableName();
ModelManager.prototype.__defineGetter__('all', function() {
return this.models;
});
/**
* Iterate over Models in an order suitable for e.g. creating tables. Will
* take foreign key constraints into account so that dependencies are visited
* before dependents.
*/
ModelManager.prototype.forEachModel = function(iterator, options) {
var models = {}
, sorter = new Toposort()
, sorted
, dep;
options = _.defaults(options || {}, {
reverse: true
});
if (_.isObject(tableName)) { this.models.forEach(function(model) {
tableName = tableName.schema + '.' + tableName.tableName; var deps = []
} , tableName = model.getTableName();
models[tableName] = model; if (_.isObject(tableName)) {
tableName = tableName.schema + '.' + tableName.tableName;
}
for (var attrName in model.rawAttributes) { models[tableName] = model;
if (model.rawAttributes.hasOwnProperty(attrName)) {
var attribute = model.rawAttributes[attrName];
if (attribute.references) { for (var attrName in model.rawAttributes) {
attribute = Utils.formatReferences(attribute); if (model.rawAttributes.hasOwnProperty(attrName)) {
dep = attribute.references.model; var attribute = model.rawAttributes[attrName];
if (_.isObject(dep)) { if (attribute.references) {
dep = dep.schema + '.' + dep.tableName; attribute = Utils.formatReferences(attribute);
} dep = attribute.references.model;
deps.push(dep); if (_.isObject(dep)) {
dep = dep.schema + '.' + dep.tableName;
} }
deps.push(dep);
} }
} }
}
deps = deps.filter(function(dep) { deps = deps.filter(function(dep) {
return tableName !== dep; return tableName !== dep;
});
sorter.add(tableName, deps);
}); });
sorted = sorter.sort(); sorter.add(tableName, deps);
if (options.reverse) { });
sorted = sorted.reverse();
} sorted = sorter.sort();
sorted.forEach(function(name) { if (options.reverse) {
iterator(models[name], name); sorted = sorted.reverse();
}); }
}; sorted.forEach(function(name) {
iterator(models[name], name);
});
};
return ModelManager; module.exports = ModelManager;
})();
This diff could not be displayed because it is too large.
'use strict'; 'use strict';
module.exports = (function() { var Attribute = function(options) {
var Attribute = function(options) {
if (options.type === undefined) options = {type: options}; if (options.type === undefined) options = {type: options};
this.type = options.type; this.type = options.type;
}; };
return Attribute; module.exports = Attribute;
})();
...@@ -5,175 +5,173 @@ var Utils = require('./../utils') ...@@ -5,175 +5,173 @@ var Utils = require('./../utils')
, DataTypes = require('../data-types') , DataTypes = require('../data-types')
, Promise = require('bluebird'); , Promise = require('bluebird');
module.exports = (function() { var CounterCache = function(association, options) {
var CounterCache = function(association, options) { this.association = association;
this.association = association; this.source = association.source;
this.source = association.source; this.target = association.target;
this.target = association.target; this.options = options || {};
this.options = options || {};
this.sequelize = this.source.modelManager.sequelize;
this.sequelize = this.source.modelManager.sequelize; this.as = this.options.as;
this.as = this.options.as;
if (association.associationType !== 'HasMany') {
if (association.associationType !== 'HasMany') { throw new Error('Can only have CounterCache on HasMany association');
throw new Error('Can only have CounterCache on HasMany association'); }
}
if (this.as) {
this.isAliased = true;
this.columnName = this.as;
} else {
this.as = 'count_' + this.target.options.name.plural;
this.columnName = Utils._.camelizeIf(
this.as,
!this.source.options.underscored
);
}
this.injectAttributes();
this.injectHooks();
};
// Add countAssociation attribute to source model
CounterCache.prototype.injectAttributes = function() {
// Do not try to use a column that's already taken
Helpers.checkNamingCollision(this);
var newAttributes = {};
newAttributes[this.columnName] = {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: Utils._.partial(
Utils.toDefaultValue,
0
)
};
if (this.as) { Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.isAliased = true;
this.columnName = this.as;
} else {
this.as = 'count_' + this.target.options.name.plural;
this.columnName = Utils._.camelizeIf(
this.as,
!this.source.options.underscored
);
}
this.injectAttributes(); // Sync attributes and setters/getters to DAO prototype
this.injectHooks(); this.source.refreshAttributes();
}; };
// Add setAssociaton method to the prototype of the model instance
CounterCache.prototype.injectHooks = function() {
var association = this.association,
counterCacheInstance = this,
CounterUtil,
fullUpdateHook,
atomicHooks,
previousTargetId;
CounterUtil = {
update: function (targetId) {
var query = CounterUtil._targetQuery(targetId);
return association.target.count({ where: query }).then(function (count) {
var newValues = {};
// Add countAssociation attribute to source model query = CounterUtil._sourceQuery(targetId);
CounterCache.prototype.injectAttributes = function() {
// Do not try to use a column that's already taken
Helpers.checkNamingCollision(this);
var newAttributes = {}; newValues[counterCacheInstance.columnName] = count;
newAttributes[this.columnName] = { return association.source.update(newValues, { where: query });
type: DataTypes.INTEGER, });
allowNull: false, },
defaultValue: Utils._.partial( increment: function (targetId) {
Utils.toDefaultValue, var query = CounterUtil._sourceQuery(targetId);
0
)
};
Utils.mergeDefaults(this.source.rawAttributes, newAttributes); return association.source.find({ where: query }).then(function (instance) {
return instance.increment(counterCacheInstance.columnName, { by: 1 });
});
},
decrement: function (targetId) {
var query = CounterUtil._sourceQuery(targetId);
// Sync attributes and setters/getters to DAO prototype return association.source.find({ where: query }).then(function (instance) {
this.source.refreshAttributes(); return instance.decrement(counterCacheInstance.columnName, { by: 1 });
});
},
// helpers
_targetQuery: function (id) {
var query = {};
query[association.identifier] = id;
return query;
},
_sourceQuery: function (id) {
var query = {};
query[association.source.primaryKeyAttribute] = id;
return query;
}
}; };
// Add setAssociaton method to the prototype of the model instance fullUpdateHook = function (target) {
CounterCache.prototype.injectHooks = function() { var targetId = target.get(association.identifier)
var association = this.association, , promises = [];
counterCacheInstance = this,
CounterUtil,
fullUpdateHook,
atomicHooks,
previousTargetId;
CounterUtil = {
update: function (targetId) {
var query = CounterUtil._targetQuery(targetId);
return association.target.count({ where: query }).then(function (count) {
var newValues = {};
query = CounterUtil._sourceQuery(targetId);
newValues[counterCacheInstance.columnName] = count;
return association.source.update(newValues, { where: query });
});
},
increment: function (targetId) {
var query = CounterUtil._sourceQuery(targetId);
return association.source.find({ where: query }).then(function (instance) {
return instance.increment(counterCacheInstance.columnName, { by: 1 });
});
},
decrement: function (targetId) {
var query = CounterUtil._sourceQuery(targetId);
return association.source.find({ where: query }).then(function (instance) {
return instance.decrement(counterCacheInstance.columnName, { by: 1 });
});
},
// helpers
_targetQuery: function (id) {
var query = {};
query[association.identifier] = id;
return query;
},
_sourceQuery: function (id) {
var query = {};
query[association.source.primaryKeyAttribute] = id;
return query;
}
};
fullUpdateHook = function (target) { if (targetId) {
var targetId = target.get(association.identifier) promises.push(CounterUtil.update(targetId));
, promises = []; }
if (previousTargetId && previousTargetId !== targetId) {
promises.push(CounterUtil.update(previousTargetId));
}
return Promise.all(promises).return(undefined);
};
atomicHooks = {
create: function (target) {
var targetId = target.get(association.identifier);
if (targetId) { if (targetId) {
promises.push(CounterUtil.update(targetId)); return CounterUtil.increment(targetId);
} }
},
update: function (target) {
var targetId = target.get(association.identifier)
, promises = [];
if (previousTargetId && previousTargetId !== targetId) { if (targetId && !previousTargetId) {
promises.push(CounterUtil.update(previousTargetId)); promises.push(CounterUtil.increment(targetId));
}
if (!targetId && previousTargetId) {
promises.push(CounterUtil.decrement(targetId));
} }
if (previousTargetId && targetId && previousTargetId !== targetId) {
promises.push(CounterUtil.increment(targetId));
promises.push(CounterUtil.decrement(previousTargetId));
}
return Promise.all(promises);
},
destroy: function (target) {
var targetId = target.get(association.identifier);
return Promise.all(promises).return(undefined); if (targetId) {
}; return CounterUtil.decrement(targetId);
atomicHooks = {
create: function (target) {
var targetId = target.get(association.identifier);
if (targetId) {
return CounterUtil.increment(targetId);
}
},
update: function (target) {
var targetId = target.get(association.identifier)
, promises = [];
if (targetId && !previousTargetId) {
promises.push(CounterUtil.increment(targetId));
}
if (!targetId && previousTargetId) {
promises.push(CounterUtil.decrement(targetId));
}
if (previousTargetId && targetId && previousTargetId !== targetId) {
promises.push(CounterUtil.increment(targetId));
promises.push(CounterUtil.decrement(previousTargetId));
}
return Promise.all(promises);
},
destroy: function (target) {
var targetId = target.get(association.identifier);
if (targetId) {
return CounterUtil.decrement(targetId);
}
} }
};
// previousDataValues are cleared before afterUpdate, so we need to save this here
association.target.addHook('beforeUpdate', function (target) {
previousTargetId = target.previous(association.identifier);
});
if (this.options.atomic === false) {
association.target.addHook('afterCreate', fullUpdateHook);
association.target.addHook('afterUpdate', fullUpdateHook);
association.target.addHook('afterDestroy', fullUpdateHook);
} else {
association.target.addHook('afterCreate', atomicHooks.create);
association.target.addHook('afterUpdate', atomicHooks.update);
association.target.addHook('afterDestroy', atomicHooks.destroy);
} }
}; };
return CounterCache; // previousDataValues are cleared before afterUpdate, so we need to save this here
})(); association.target.addHook('beforeUpdate', function (target) {
previousTargetId = target.previous(association.identifier);
});
if (this.options.atomic === false) {
association.target.addHook('afterCreate', fullUpdateHook);
association.target.addHook('afterUpdate', fullUpdateHook);
association.target.addHook('afterDestroy', fullUpdateHook);
} else {
association.target.addHook('afterCreate', atomicHooks.create);
association.target.addHook('afterUpdate', atomicHooks.update);
association.target.addHook('afterDestroy', atomicHooks.destroy);
}
};
module.exports = CounterCache;
...@@ -8,176 +8,158 @@ var Utils = require('./utils') ...@@ -8,176 +8,158 @@ var Utils = require('./utils')
, Promise = require('./promise') , Promise = require('./promise')
, QueryTypes = require('./query-types'); , QueryTypes = require('./query-types');
module.exports = (function() { /*
/* * The interface that Sequelize uses to talk to all databases
* The interface that Sequelize uses to talk to all databases * @class QueryInterface
* @class QueryInterface **/
**/ var QueryInterface = function(sequelize) {
var QueryInterface = function(sequelize) { this.sequelize = sequelize;
this.sequelize = sequelize; this.QueryGenerator = this.sequelize.dialect.QueryGenerator;
this.QueryGenerator = this.sequelize.dialect.QueryGenerator; };
};
QueryInterface.prototype.createSchema = function(schema, options) {
QueryInterface.prototype.createSchema = function(schema, options) { options = options || {};
options = options || {}; var sql = this.QueryGenerator.createSchema(schema);
var sql = this.QueryGenerator.createSchema(schema); return this.sequelize.query(sql, {logging: options.logging});
return this.sequelize.query(sql, {logging: options.logging}); };
};
QueryInterface.prototype.dropSchema = function(schema, options) {
QueryInterface.prototype.dropSchema = function(schema, options) { options = options || {};
options = options || {}; var sql = this.QueryGenerator.dropSchema(schema);
var sql = this.QueryGenerator.dropSchema(schema); return this.sequelize.query(sql, {logging: options.logging});
return this.sequelize.query(sql, {logging: options.logging}); };
};
QueryInterface.prototype.dropAllSchemas = function(options) {
QueryInterface.prototype.dropAllSchemas = function(options) { options = options || {};
options = options || {};
var self = this;
var self = this; if (!this.QueryGenerator._dialect.supports.schemas) {
if (!this.QueryGenerator._dialect.supports.schemas) { return this.sequelize.drop({ logging: options.logging });
return this.sequelize.drop({ logging: options.logging }); } else {
} else { return this.showAllSchemas(options).map(function(schemaName) {
return this.showAllSchemas(options).map(function(schemaName) { return self.dropSchema(schemaName, { logging: options.logging });
return self.dropSchema(schemaName, { logging: options.logging });
});
}
};
QueryInterface.prototype.showAllSchemas = function(options) {
var self = this;
options = Utils._.extend({
raw: true,
type: this.sequelize.QueryTypes.SELECT,
logging: false
}, options || {});
var showSchemasSql = self.QueryGenerator.showSchemasQuery();
return this.sequelize.query(showSchemasSql, options).then(function(schemaNames) {
return Utils._.flatten(
Utils._.map(schemaNames, function(value) {
return (!!value.schema_name ? value.schema_name : value);
})
);
}); });
}; }
};
QueryInterface.prototype.databaseVersion = function(options) { QueryInterface.prototype.showAllSchemas = function(options) {
options = options || {}; var self = this;
return this.sequelize.query(this.QueryGenerator.versionQuery(), {
raw: true,
type: QueryTypes.VERSION,
logging: options.logging
});
};
QueryInterface.prototype.createTable = function(tableName, attributes, options) { options = Utils._.extend({
var keys = Object.keys(attributes) raw: true,
, keyLen = keys.length type: this.sequelize.QueryTypes.SELECT,
, self = this logging: false
, sql = '' }, options || {});
, i = 0;
options = options || {}; var showSchemasSql = self.QueryGenerator.showSchemasQuery();
attributes = Utils._.mapValues(attributes, function(attribute) { return this.sequelize.query(showSchemasSql, options).then(function(schemaNames) {
if (!Utils._.isPlainObject(attribute)) { return Utils._.flatten(
attribute = { type: attribute, allowNull: true }; Utils._.map(schemaNames, function(value) {
return (!!value.schema_name ? value.schema_name : value);
})
);
});
};
QueryInterface.prototype.databaseVersion = function(options) {
options = options || {};
return this.sequelize.query(this.QueryGenerator.versionQuery(), {
raw: true,
type: QueryTypes.VERSION,
logging: options.logging
});
};
QueryInterface.prototype.createTable = function(tableName, attributes, options) {
var keys = Object.keys(attributes)
, keyLen = keys.length
, self = this
, sql = ''
, i = 0;
options = options || {};
attributes = Utils._.mapValues(attributes, function(attribute) {
if (!Utils._.isPlainObject(attribute)) {
attribute = { type: attribute, allowNull: true };
}
attribute.type = self.sequelize.normalizeDataType(attribute.type);
if (attribute.hasOwnProperty('defaultValue')) {
if (typeof attribute.defaultValue === 'function' && (
attribute.defaultValue === DataTypes.NOW ||
attribute.defaultValue === DataTypes.UUIDV4 ||
attribute.defaultValue === DataTypes.UUIDV4
)) {
attribute.defaultValue = new attribute.defaultValue();
} }
}
attribute.type = self.sequelize.normalizeDataType(attribute.type); if (attribute.type instanceof DataTypes.ENUM) {
// The ENUM is a special case where the type is an object containing the values
attribute.values = attribute.values || attribute.type.values || [];
if (attribute.hasOwnProperty('defaultValue')) { if (!attribute.values.length) {
if (typeof attribute.defaultValue === 'function' && ( throw new Error('Values for ENUM haven\'t been defined.');
attribute.defaultValue === DataTypes.NOW ||
attribute.defaultValue === DataTypes.UUIDV4 ||
attribute.defaultValue === DataTypes.UUIDV4
)) {
attribute.defaultValue = new attribute.defaultValue();
}
} }
}
if (attribute.type instanceof DataTypes.ENUM) { return attribute;
// The ENUM is a special case where the type is an object containing the values });
attribute.values = attribute.values || attribute.type.values || [];
if (!attribute.values.length) { // Postgres requires a special SQL command for enums
throw new Error('Values for ENUM haven\'t been defined.'); if (self.sequelize.options.dialect === 'postgres') {
} var promises = []
} // For backwards-compatibility, public schemas don't need to
// explicitly state their schema when creating a new enum type
, getTableName = (!options || !options.schema || options.schema === 'public' ? '' : options.schema + '_') + tableName;
return attribute; for (i = 0; i < keyLen; i++) {
}); if (attributes[keys[i]].type instanceof DataTypes.ENUM) {
sql = self.QueryGenerator.pgListEnums(getTableName, attributes[keys[i]].field || keys[i], options);
promises.push(self.sequelize.query(sql, { plain: true, raw: true, type: QueryTypes.SELECT, logging: options.logging }));
}
}
// Postgres requires a special SQL command for enums return Promise.all(promises).then(function(results) {
if (self.sequelize.options.dialect === 'postgres') {
var promises = [] var promises = []
// For backwards-compatibility, public schemas don't need to , instanceTable = self.sequelize.modelManager.models.filter(function(instance) { return instance.tableName === tableName; })
// explicitly state their schema when creating a new enum type , enumIdx = 0;
, getTableName = (!options || !options.schema || options.schema === 'public' ? '' : options.schema + '_') + tableName;
instanceTable = instanceTable.length > 0 ? instanceTable[0] : null;
for (i = 0; i < keyLen; i++) { for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].type instanceof DataTypes.ENUM) { if (attributes[keys[i]].type instanceof DataTypes.ENUM) {
sql = self.QueryGenerator.pgListEnums(getTableName, attributes[keys[i]].field || keys[i], options); // If the enum type doesn't exist then create it
promises.push(self.sequelize.query(sql, { plain: true, raw: true, type: QueryTypes.SELECT, logging: options.logging })); if (!results[enumIdx]) {
} sql = self.QueryGenerator.pgEnum(getTableName, attributes[keys[i]].field || keys[i], attributes[keys[i]], options);
} promises.push(self.sequelize.query(sql, { raw: true, logging: options.logging }));
} else if (!!results[enumIdx] && !!instanceTable) {
return Promise.all(promises).then(function(results) { var enumVals = self.QueryGenerator.fromArray(results[enumIdx].enum_value)
var promises = [] , vals = instanceTable.rawAttributes[keys[i]].values;
, instanceTable = self.sequelize.modelManager.models.filter(function(instance) { return instance.tableName === tableName; })
, enumIdx = 0; vals.forEach(function(value, idx) {
// reset out after/before options since it's for every enum value
instanceTable = instanceTable.length > 0 ? instanceTable[0] : null; options.before = null;
options.after = null;
for (i = 0; i < keyLen; i++) {
if (attributes[keys[i]].type instanceof DataTypes.ENUM) { if (enumVals.indexOf(value) === -1) {
// If the enum type doesn't exist then create it if (!!vals[idx + 1]) {
if (!results[enumIdx]) { options.before = vals[idx + 1];
sql = self.QueryGenerator.pgEnum(getTableName, attributes[keys[i]].field || keys[i], attributes[keys[i]], options); }
promises.push(self.sequelize.query(sql, { raw: true, logging: options.logging })); else if (!!vals[idx - 1]) {
} else if (!!results[enumIdx] && !!instanceTable) { options.after = vals[idx - 1];
var enumVals = self.QueryGenerator.fromArray(results[enumIdx].enum_value)
, vals = instanceTable.rawAttributes[keys[i]].values;
vals.forEach(function(value, idx) {
// reset out after/before options since it's for every enum value
options.before = null;
options.after = null;
if (enumVals.indexOf(value) === -1) {
if (!!vals[idx + 1]) {
options.before = vals[idx + 1];
}
else if (!!vals[idx - 1]) {
options.after = vals[idx - 1];
}
promises.push(self.sequelize.query(self.QueryGenerator.pgEnumAdd(getTableName, keys[i], value, options), options));
} }
}); promises.push(self.sequelize.query(self.QueryGenerator.pgEnumAdd(getTableName, keys[i], value, options), options));
enumIdx++; }
} });
enumIdx++;
} }
} }
}
if (!tableName.schema && options.schema) {
tableName = self.QueryGenerator.addSchema({
tableName: tableName,
schema: options.schema
});
}
attributes = self.QueryGenerator.attributesToSQL(attributes, {
context: 'createTable'
});
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options);
return Promise.all(promises).then(function() {
return self.sequelize.query(sql, options);
});
});
} else {
if (!tableName.schema && options.schema) { if (!tableName.schema && options.schema) {
tableName = self.QueryGenerator.addSchema({ tableName = self.QueryGenerator.addSchema({
tableName: tableName, tableName: tableName,
...@@ -190,744 +172,760 @@ module.exports = (function() { ...@@ -190,744 +172,760 @@ module.exports = (function() {
}); });
sql = self.QueryGenerator.createTableQuery(tableName, attributes, options); sql = self.QueryGenerator.createTableQuery(tableName, attributes, options);
return self.sequelize.query(sql, options); return Promise.all(promises).then(function() {
return self.sequelize.query(sql, options);
});
});
} else {
if (!tableName.schema && options.schema) {
tableName = self.QueryGenerator.addSchema({
tableName: tableName,
schema: options.schema
});
} }
};
QueryInterface.prototype.dropTable = function(tableName, options) { attributes = self.QueryGenerator.attributesToSQL(attributes, {
// if we're forcing we should be cascading unless explicitly stated otherwise context: 'createTable'
options = options || {}; });
options.cascade = options.cascade || options.force || false; sql = self.QueryGenerator.createTableQuery(tableName, attributes, options);
return self.sequelize.query(sql, options);
}
};
var sql = this.QueryGenerator.dropTableQuery(tableName, options) QueryInterface.prototype.dropTable = function(tableName, options) {
, self = this; // if we're forcing we should be cascading unless explicitly stated otherwise
options = options || {};
options.cascade = options.cascade || options.force || false;
return this.sequelize.query(sql, options).then(function() { var sql = this.QueryGenerator.dropTableQuery(tableName, options)
var promises = []; , self = this;
// Since postgres has a special case for enums, we should drop the related return this.sequelize.query(sql, options).then(function() {
// enum type within the table and attribute var promises = [];
if (self.sequelize.options.dialect === 'postgres') {
var instanceTable = self.sequelize.modelManager.getModel(tableName, { attribute: 'tableName' }); // Since postgres has a special case for enums, we should drop the related
// enum type within the table and attribute
if (self.sequelize.options.dialect === 'postgres') {
var instanceTable = self.sequelize.modelManager.getModel(tableName, { attribute: 'tableName' });
if (!!instanceTable) { if (!!instanceTable) {
var getTableName = (!options || !options.schema || options.schema === 'public' ? '' : options.schema + '_') + tableName; var getTableName = (!options || !options.schema || options.schema === 'public' ? '' : options.schema + '_') + tableName;
var keys = Object.keys(instanceTable.rawAttributes) var keys = Object.keys(instanceTable.rawAttributes)
, keyLen = keys.length , keyLen = keys.length
, i = 0; , i = 0;
for (i = 0; i < keyLen; i++) { for (i = 0; i < keyLen; i++) {
if (instanceTable.rawAttributes[keys[i]].type instanceof DataTypes.ENUM) { if (instanceTable.rawAttributes[keys[i]].type instanceof DataTypes.ENUM) {
promises.push(self.sequelize.query(self.QueryGenerator.pgEnumDrop(getTableName, keys[i]), {logging: options.logging, raw: true})); promises.push(self.sequelize.query(self.QueryGenerator.pgEnumDrop(getTableName, keys[i]), {logging: options.logging, raw: true}));
}
} }
} }
} }
}
return Promise.all(promises).get(0);
});
};
QueryInterface.prototype.dropAllTables = function(options) {
var self = this;
return Promise.all(promises).get(0); options = options || {};
var dropAllTables = function(tableNames) {
return Promise.each(tableNames, function(tableName) {
// if tableName is not in the Array of tables names then dont drop it
if (skip.indexOf(tableName.tableName || tableName) === -1) {
return self.dropTable(tableName, { cascade: true, logging: options.logging });
}
}); });
}; };
QueryInterface.prototype.dropAllTables = function(options) { var skip = options.skip || [];
var self = this; return self.showAllTables().then(function(tableNames) {
if (self.sequelize.options.dialect === 'sqlite') {
return self.sequelize.query('PRAGMA foreign_keys;', options).then(function(result) {
var foreignKeysAreEnabled = result.foreign_keys === 1;
options = options || {}; if (foreignKeysAreEnabled) {
return self.sequelize.query('PRAGMA foreign_keys = OFF', options).then(function() {
var dropAllTables = function(tableNames) { return dropAllTables(tableNames).then(function() {
return Promise.each(tableNames, function(tableName) { return self.sequelize.query('PRAGMA foreign_keys = ON', options);
// if tableName is not in the Array of tables names then dont drop it });
if (skip.indexOf(tableName.tableName || tableName) === -1) { });
return self.dropTable(tableName, { cascade: true, logging: options.logging }); } else {
return dropAllTables(tableNames);
} }
}); });
}; } else {
return self.getForeignKeysForTables(tableNames).then(function(foreignKeys) {
var promises = [];
var skip = options.skip || []; tableNames.forEach(function(tableName) {
return self.showAllTables().then(function(tableNames) { var normalizedTableName = tableName;
if (self.sequelize.options.dialect === 'sqlite') { if (Utils._.isObject(tableName)) {
return self.sequelize.query('PRAGMA foreign_keys;', options).then(function(result) { normalizedTableName = tableName.schema + '.' + tableName.tableName;
var foreignKeysAreEnabled = result.foreign_keys === 1;
if (foreignKeysAreEnabled) {
return self.sequelize.query('PRAGMA foreign_keys = OFF', options).then(function() {
return dropAllTables(tableNames).then(function() {
return self.sequelize.query('PRAGMA foreign_keys = ON', options);
});
});
} else {
return dropAllTables(tableNames);
} }
});
} else {
return self.getForeignKeysForTables(tableNames).then(function(foreignKeys) {
var promises = [];
tableNames.forEach(function(tableName) {
var normalizedTableName = tableName;
if (Utils._.isObject(tableName)) {
normalizedTableName = tableName.schema + '.' + tableName.tableName;
}
foreignKeys[normalizedTableName].forEach(function(foreignKey) {
var sql = self.QueryGenerator.dropForeignKeyQuery(tableName, foreignKey);
promises.push(self.sequelize.query(sql, options));
});
});
return Promise.all(promises).then(function() { foreignKeys[normalizedTableName].forEach(function(foreignKey) {
return dropAllTables(tableNames); var sql = self.QueryGenerator.dropForeignKeyQuery(tableName, foreignKey);
promises.push(self.sequelize.query(sql, options));
}); });
}); });
}
});
};
QueryInterface.prototype.dropAllEnums = function(options) { return Promise.all(promises).then(function() {
if (this.sequelize.getDialect() !== 'postgres') { return dropAllTables(tableNames);
return Promise.resolve(); });
});
} }
});
};
options = options || {}; QueryInterface.prototype.dropAllEnums = function(options) {
if (this.sequelize.getDialect() !== 'postgres') {
return Promise.resolve();
}
var self = this options = options || {};
, sql = this.QueryGenerator.pgListEnums();
return this.sequelize.query(sql, { plain: false, raw: true, type: QueryTypes.SELECT, logging: options.logging }).map(function(result) { var self = this
return self.sequelize.query( , sql = this.QueryGenerator.pgListEnums();
self.QueryGenerator.pgEnumDrop(null, null, self.QueryGenerator.pgEscapeAndQuote(result.enum_name)),
{logging: options.logging, raw: true}
);
});
};
QueryInterface.prototype.renameTable = function(before, after, options) { return this.sequelize.query(sql, { plain: false, raw: true, type: QueryTypes.SELECT, logging: options.logging }).map(function(result) {
options = options || {}; return self.sequelize.query(
var sql = this.QueryGenerator.renameTableQuery(before, after); self.QueryGenerator.pgEnumDrop(null, null, self.QueryGenerator.pgEscapeAndQuote(result.enum_name)),
return this.sequelize.query(sql, { logging: options.logging }); {logging: options.logging, raw: true}
}; );
});
};
QueryInterface.prototype.renameTable = function(before, after, options) {
options = options || {};
var sql = this.QueryGenerator.renameTableQuery(before, after);
return this.sequelize.query(sql, { logging: options.logging });
};
QueryInterface.prototype.showAllTables = function(options) {
var self = this;
options = Utils._.extend({
raw: true,
type: QueryTypes.SHOWTABLES
}, options || {});
var showTablesSql = self.QueryGenerator.showTablesQuery();
return self.sequelize.query(showTablesSql, options).then(function(tableNames) {
return Utils._.flatten(tableNames);
});
};
QueryInterface.prototype.describeTable = function(tableName, options) {
var schema = null
, schemaDelimiter = null;
if (typeof options === 'string') {
schema = options;
} else if (typeof options === 'object' && options !== null) {
schema = options.schema || null;
schemaDelimiter = options.schemaDelimiter || null;
}
if (typeof tableName === 'object' && tableName !== null) {
schema = tableName.schema;
tableName = tableName.tableName;
}
var sql = this.QueryGenerator.describeTableQuery(tableName, schema, schemaDelimiter);
return this.sequelize.query(sql, { type: QueryTypes.DESCRIBE, logging: options && options.logging }).then(function(data) {
// If no data is returned from the query, then the table name may be wrong.
// Query generators that use information_schema for retrieving table info will just return an empty result set,
// it will not throw an error like built-ins do (e.g. DESCRIBE on MySql).
if (Utils._.isEmpty(data)) {
return Promise.reject('No description found for "' + tableName + '" table. Check the table name and schema; remember, they _are_ case sensitive.');
} else {
return Promise.resolve(data);
}
});
};
QueryInterface.prototype.addColumn = function(table, key, attribute, options) {
options = options || {};
attribute = this.sequelize.normalizeAttribute(attribute);
return this.sequelize.query(this.QueryGenerator.addColumnQuery(table, key, attribute), { logging: options.logging });
};
QueryInterface.prototype.removeColumn = function(tableName, attributeName, options) {
options = options || {};
if (this.sequelize.options.dialect === 'sqlite') {
// sqlite needs some special treatment as it cannot drop a column
return SQLiteQueryInterface.removeColumn.call(this, tableName, attributeName, {logging: options.logging});
} else {
var sql = this.QueryGenerator.removeColumnQuery(tableName, attributeName);
return this.sequelize.query(sql, {logging: options.logging});
}
};
QueryInterface.prototype.showAllTables = function(options) { QueryInterface.prototype.changeColumn = function(tableName, attributeName, dataTypeOrOptions, options) {
var self = this; var attributes = {};
options = Utils._.extend({ options = options || {};
raw: true,
type: QueryTypes.SHOWTABLES
}, options || {});
var showTablesSql = self.QueryGenerator.showTablesQuery(); if (Utils._.values(DataTypes).indexOf(dataTypeOrOptions) > -1) {
return self.sequelize.query(showTablesSql, options).then(function(tableNames) { attributes[attributeName] = { type: dataTypeOrOptions, allowNull: true };
return Utils._.flatten(tableNames); } else {
}); attributes[attributeName] = dataTypeOrOptions;
}; }
QueryInterface.prototype.describeTable = function(tableName, options) { attributes[attributeName].type = this.sequelize.normalizeDataType(attributes[attributeName].type);
var schema = null
, schemaDelimiter = null;
if (typeof options === 'string') { if (this.sequelize.options.dialect === 'sqlite') {
schema = options; // sqlite needs some special treatment as it cannot change a column
} else if (typeof options === 'object' && options !== null) { return SQLiteQueryInterface.changeColumn.call(this, tableName, attributes, {logging: options.logging});
schema = options.schema || null; } else {
schemaDelimiter = options.schemaDelimiter || null; var query = this.QueryGenerator.attributesToSQL(attributes)
} , sql = this.QueryGenerator.changeColumnQuery(tableName, query);
if (typeof tableName === 'object' && tableName !== null) { return this.sequelize.query(sql, {logging: options.logging});
schema = tableName.schema; }
tableName = tableName.tableName; };
}
var sql = this.QueryGenerator.describeTableQuery(tableName, schema, schemaDelimiter); QueryInterface.prototype.renameColumn = function(tableName, attrNameBefore, attrNameAfter, options) {
options = options || {};
return this.describeTable(tableName, options).then(function(data) {
data = data[attrNameBefore] || {};
return this.sequelize.query(sql, { type: QueryTypes.DESCRIBE, logging: options && options.logging }).then(function(data) { var _options = {};
// If no data is returned from the query, then the table name may be wrong.
// Query generators that use information_schema for retrieving table info will just return an empty result set,
// it will not throw an error like built-ins do (e.g. DESCRIBE on MySql).
if (Utils._.isEmpty(data)) {
return Promise.reject('No description found for "' + tableName + '" table. Check the table name and schema; remember, they _are_ case sensitive.');
} else {
return Promise.resolve(data);
}
});
};
QueryInterface.prototype.addColumn = function(table, key, attribute, options) {
options = options || {};
attribute = this.sequelize.normalizeAttribute(attribute);
return this.sequelize.query(this.QueryGenerator.addColumnQuery(table, key, attribute), { logging: options.logging });
};
QueryInterface.prototype.removeColumn = function(tableName, attributeName, options) { _options[attrNameAfter] = {
options = options || {}; attribute: attrNameAfter,
if (this.sequelize.options.dialect === 'sqlite') { type: data.type,
// sqlite needs some special treatment as it cannot drop a column allowNull: data.allowNull,
return SQLiteQueryInterface.removeColumn.call(this, tableName, attributeName, {logging: options.logging}); defaultValue: data.defaultValue
} else { };
var sql = this.QueryGenerator.removeColumnQuery(tableName, attributeName);
return this.sequelize.query(sql, {logging: options.logging});
}
};
QueryInterface.prototype.changeColumn = function(tableName, attributeName, dataTypeOrOptions, options) {
var attributes = {};
options = options || {};
if (Utils._.values(DataTypes).indexOf(dataTypeOrOptions) > -1) { // fix: a not-null column cannot have null as default value
attributes[attributeName] = { type: dataTypeOrOptions, allowNull: true }; if (data.defaultValue === null && !data.allowNull) {
} else { delete _options[attrNameAfter].defaultValue;
attributes[attributeName] = dataTypeOrOptions;
} }
attributes[attributeName].type = this.sequelize.normalizeDataType(attributes[attributeName].type);
if (this.sequelize.options.dialect === 'sqlite') { if (this.sequelize.options.dialect === 'sqlite') {
// sqlite needs some special treatment as it cannot change a column // sqlite needs some special treatment as it cannot rename a column
return SQLiteQueryInterface.changeColumn.call(this, tableName, attributes, {logging: options.logging}); return SQLiteQueryInterface.renameColumn.call(this, tableName, attrNameBefore, attrNameAfter, {logging: options.logging});
} else { } else {
var query = this.QueryGenerator.attributesToSQL(attributes) var sql = this.QueryGenerator.renameColumnQuery(
, sql = this.QueryGenerator.changeColumnQuery(tableName, query); tableName,
attrNameBefore,
this.QueryGenerator.attributesToSQL(_options)
);
return this.sequelize.query(sql, {logging: options.logging}); return this.sequelize.query(sql, {logging: options.logging});
} }
}; }.bind(this));
};
QueryInterface.prototype.renameColumn = function(tableName, attrNameBefore, attrNameAfter, options) {
options = options || {}; QueryInterface.prototype.addIndex = function(tableName, _attributes, options, _rawTablename) {
return this.describeTable(tableName, options).then(function(data) { var attributes, rawTablename;
data = data[attrNameBefore] || {};
// Support for passing tableName, attributes, options or tableName, options (with a fields param which is the attributes)
var _options = {}; if (Array.isArray(_attributes)) {
attributes = _attributes;
_options[attrNameAfter] = {
attribute: attrNameAfter, rawTablename = _rawTablename;
type: data.type, } else {
allowNull: data.allowNull, // Support for passing an options object with a fields attribute instead of attributes, options
defaultValue: data.defaultValue options = _attributes;
}; attributes = options.fields;
// fix: a not-null column cannot have null as default value rawTablename = options;
if (data.defaultValue === null && !data.allowNull) { }
delete _options[attrNameAfter].defaultValue;
} if (!rawTablename) {
// Map for backwards compat
if (this.sequelize.options.dialect === 'sqlite') { rawTablename = tableName;
// sqlite needs some special treatment as it cannot rename a column }
return SQLiteQueryInterface.renameColumn.call(this, tableName, attrNameBefore, attrNameAfter, {logging: options.logging});
} else { options = options || {};
var sql = this.QueryGenerator.renameColumnQuery( options.fields = attributes;
tableName, var sql = this.QueryGenerator.addIndexQuery(tableName, options, rawTablename);
attrNameBefore, return this.sequelize.query(sql, { logging: options.logging });
this.QueryGenerator.attributesToSQL(_options) };
);
return this.sequelize.query(sql, {logging: options.logging}); QueryInterface.prototype.showIndex = function(tableName, options) {
var sql = this.QueryGenerator.showIndexesQuery(tableName, options);
options = options || {};
return this.sequelize.query(sql, {
transaction: options.transaction,
logging: options.logging,
type: QueryTypes.SHOWINDEXES
});
};
QueryInterface.prototype.nameIndexes = function(indexes, rawTablename) {
return this.QueryGenerator.nameIndexes(indexes, rawTablename);
};
QueryInterface.prototype.getForeignKeysForTables = function(tableNames, options) {
var self = this;
options = options || {};
if (tableNames.length === 0) {
return Promise.resolve({});
}
return Promise.map(tableNames, function(tableName) {
return self.sequelize.query(self.QueryGenerator.getForeignKeysQuery(tableName, self.sequelize.config.database), {logging: options.logging}).get(0);
}).then(function(results) {
var result = {};
tableNames.forEach(function(tableName, i) {
if (Utils._.isObject(tableName)) {
tableName = tableName.schema + '.' + tableName.tableName;
} }
}.bind(this));
};
QueryInterface.prototype.addIndex = function(tableName, _attributes, options, _rawTablename) {
var attributes, rawTablename;
// Support for passing tableName, attributes, options or tableName, options (with a fields param which is the attributes)
if (Array.isArray(_attributes)) {
attributes = _attributes;
rawTablename = _rawTablename; result[tableName] = Utils._.compact(results[i]).map(function(r) {
} else { return r.constraint_name;
// Support for passing an options object with a fields attribute instead of attributes, options });
options = _attributes;
attributes = options.fields;
rawTablename = options;
}
if (!rawTablename) {
// Map for backwards compat
rawTablename = tableName;
}
options = options || {};
options.fields = attributes;
var sql = this.QueryGenerator.addIndexQuery(tableName, options, rawTablename);
return this.sequelize.query(sql, { logging: options.logging });
};
QueryInterface.prototype.showIndex = function(tableName, options) {
var sql = this.QueryGenerator.showIndexesQuery(tableName, options);
options = options || {};
return this.sequelize.query(sql, {
transaction: options.transaction,
logging: options.logging,
type: QueryTypes.SHOWINDEXES
}); });
};
QueryInterface.prototype.nameIndexes = function(indexes, rawTablename) {
return this.QueryGenerator.nameIndexes(indexes, rawTablename);
};
QueryInterface.prototype.getForeignKeysForTables = function(tableNames, options) {
var self = this;
options = options || {};
if (tableNames.length === 0) {
return Promise.resolve({});
}
return Promise.map(tableNames, function(tableName) {
return self.sequelize.query(self.QueryGenerator.getForeignKeysQuery(tableName, self.sequelize.config.database), {logging: options.logging}).get(0);
}).then(function(results) {
var result = {};
tableNames.forEach(function(tableName, i) { return result;
if (Utils._.isObject(tableName)) { });
tableName = tableName.schema + '.' + tableName.tableName; };
QueryInterface.prototype.removeIndex = function(tableName, indexNameOrAttributes, options) {
options = options || {};
var sql = this.QueryGenerator.removeIndexQuery(tableName, indexNameOrAttributes);
return this.sequelize.query(sql, {logging: options.logging});
};
QueryInterface.prototype.insert = function(instance, tableName, values, options) {
var sql = this.QueryGenerator.insertQuery(tableName, values, instance && instance.Model.rawAttributes, options);
options.type = QueryTypes.INSERT;
options.instance = instance;
return this.sequelize.query(sql, options).then(function(result) {
if (instance) result.isNewRecord = false;
return result;
});
};
QueryInterface.prototype.upsert = function(tableName, values, model, options) {
var wheres = []
, where
, indexFields
, indexes = []
, updateValues
, attributes = Object.keys(values);
where = {};
for (var i = 0; i < model.primaryKeyAttributes.length; i++) {
var key = model.primaryKeyAttributes[i];
if(key in values){
where[key] = values[key];
}
}
if (!Utils._.isEmpty(where)) {
wheres.push(where);
}
// Lets combine uniquekeys and indexes into one
indexes = Utils._.map(model.options.uniqueKeys, function (value) {
return value.fields;
});
Utils._.each(model.options.indexes, function (value) {
if (value.unique === true) {
// fields in the index may both the strings or objects with an attribute property - lets sanitize that
indexFields = Utils._.map(value.fields, function (field) {
if (Utils._.isPlainObject(field)) {
return field.attribute;
} }
return field;
result[tableName] = Utils._.compact(results[i]).map(function(r) {
return r.constraint_name;
});
}); });
indexes.push(indexFields);
return result;
});
};
QueryInterface.prototype.removeIndex = function(tableName, indexNameOrAttributes, options) {
options = options || {};
var sql = this.QueryGenerator.removeIndexQuery(tableName, indexNameOrAttributes);
return this.sequelize.query(sql, {logging: options.logging});
};
QueryInterface.prototype.insert = function(instance, tableName, values, options) {
var sql = this.QueryGenerator.insertQuery(tableName, values, instance && instance.Model.rawAttributes, options);
options.type = QueryTypes.INSERT;
options.instance = instance;
return this.sequelize.query(sql, options).then(function(result) {
if (instance) result.isNewRecord = false;
return result;
});
};
QueryInterface.prototype.upsert = function(tableName, values, model, options) {
var wheres = []
, where
, indexFields
, indexes = []
, updateValues
, attributes = Object.keys(values);
where = {};
for (var i = 0; i < model.primaryKeyAttributes.length; i++) {
var key = model.primaryKeyAttributes[i];
if(key in values){
where[key] = values[key];
}
} }
});
if (!Utils._.isEmpty(where)) { indexes.forEach(function (index) {
if (Utils._.intersection(attributes, index).length === index.length) {
where = {};
index.forEach(function (field) {
where[field] = values[field];
});
wheres.push(where); wheres.push(where);
} }
});
// Lets combine uniquekeys and indexes into one where = this.sequelize.or.apply(this.sequelize, wheres);
indexes = Utils._.map(model.options.uniqueKeys, function (value) {
return value.fields;
});
Utils._.each(model.options.indexes, function (value) { options.type = QueryTypes.UPSERT;
if (value.unique === true) { options.raw = true;
// fields in the index may both the strings or objects with an attribute property - lets sanitize that
indexFields = Utils._.map(value.fields, function (field) {
if (Utils._.isPlainObject(field)) {
return field.attribute;
}
return field;
});
indexes.push(indexFields);
}
});
indexes.forEach(function (index) {
if (Utils._.intersection(attributes, index).length === index.length) {
where = {};
index.forEach(function (field) {
where[field] = values[field];
});
wheres.push(where);
}
});
where = this.sequelize.or.apply(this.sequelize, wheres);
options.type = QueryTypes.UPSERT; if (model._timestampAttributes.createdAt) {
options.raw = true; // If we are updating an existing row, we shouldn't set createdAt
updateValues = Utils.cloneDeep(values);
delete updateValues[model._timestampAttributes.createdAt];
} else {
updateValues = values;
}
if (model._timestampAttributes.createdAt) { var sql = this.QueryGenerator.upsertQuery(tableName, values, updateValues, where, model.rawAttributes, options);
// If we are updating an existing row, we shouldn't set createdAt return this.sequelize.query(sql, options).then(function (rowCount) {
updateValues = Utils.cloneDeep(values); if (rowCount === undefined) {
return rowCount;
delete updateValues[model._timestampAttributes.createdAt];
} else {
updateValues = values;
} }
var sql = this.QueryGenerator.upsertQuery(tableName, values, updateValues, where, model.rawAttributes, options); // MySQL returns 1 for inserted, 2 for updated http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html. Postgres has been modded to do the same
return this.sequelize.query(sql, options).then(function (rowCount) {
if (rowCount === undefined) {
return rowCount;
}
// MySQL returns 1 for inserted, 2 for updated http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html. Postgres has been modded to do the same
return rowCount === 1; return rowCount === 1;
}); });
}; };
QueryInterface.prototype.bulkInsert = function(tableName, records, options, attributes) { QueryInterface.prototype.bulkInsert = function(tableName, records, options, attributes) {
options.type = QueryTypes.INSERT; options.type = QueryTypes.INSERT;
var sql = this.QueryGenerator.bulkInsertQuery(tableName, records, options, attributes); var sql = this.QueryGenerator.bulkInsertQuery(tableName, records, options, attributes);
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
}; };
QueryInterface.prototype.update = function(instance, tableName, values, identifier, options) { QueryInterface.prototype.update = function(instance, tableName, values, identifier, options) {
var self = this var self = this
, restrict = false , restrict = false
, sql = self.QueryGenerator.updateQuery(tableName, values, identifier, options, instance.Model.rawAttributes); , sql = self.QueryGenerator.updateQuery(tableName, values, identifier, options, instance.Model.rawAttributes);
options = options || {}; options = options || {};
options.type = QueryTypes.UPDATE; options.type = QueryTypes.UPDATE;
// Check for a restrict field // Check for a restrict field
if (!!instance.Model && !!instance.Model.associations) { if (!!instance.Model && !!instance.Model.associations) {
var keys = Object.keys(instance.Model.associations) var keys = Object.keys(instance.Model.associations)
, length = keys.length; , length = keys.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if (instance.Model.associations[keys[i]].options && instance.Model.associations[keys[i]].options.onUpdate && instance.Model.associations[keys[i]].options.onUpdate === 'restrict') { if (instance.Model.associations[keys[i]].options && instance.Model.associations[keys[i]].options.onUpdate && instance.Model.associations[keys[i]].options.onUpdate === 'restrict') {
restrict = true; restrict = true;
}
} }
} }
}
options.instance = instance; options.instance = instance;
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
}; };
QueryInterface.prototype.bulkUpdate = function(tableName, values, identifier, options, attributes) { QueryInterface.prototype.bulkUpdate = function(tableName, values, identifier, options, attributes) {
var sql = this.QueryGenerator.updateQuery(tableName, values, identifier, options, attributes) var sql = this.QueryGenerator.updateQuery(tableName, values, identifier, options, attributes)
, table = Utils._.isObject(tableName) ? tableName : { tableName: tableName } , table = Utils._.isObject(tableName) ? tableName : { tableName: tableName }
, model = Utils._.find(this.sequelize.modelManager.models, { tableName: table.tableName }); , model = Utils._.find(this.sequelize.modelManager.models, { tableName: table.tableName });
options = options || {}; options = options || {};
options.model = model; options.model = model;
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
}; };
QueryInterface.prototype.delete = function(instance, tableName, identifier, options) { QueryInterface.prototype.delete = function(instance, tableName, identifier, options) {
var self = this var self = this
, cascades = [] , cascades = []
, sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, instance.Model); , sql = self.QueryGenerator.deleteQuery(tableName, identifier, null, instance.Model);
options = options || {}; options = options || {};
// Check for a restrict field // Check for a restrict field
if (!!instance.Model && !!instance.Model.associations) { if (!!instance.Model && !!instance.Model.associations) {
var keys = Object.keys(instance.Model.associations) var keys = Object.keys(instance.Model.associations)
, length = keys.length; , length = keys.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if (instance.Model.associations[keys[i]].options && instance.Model.associations[keys[i]].options.onDelete) { if (instance.Model.associations[keys[i]].options && instance.Model.associations[keys[i]].options.onDelete) {
if (instance.Model.associations[keys[i]].options.onDelete === 'cascade' && instance.Model.associations[keys[i]].options.useHooks === true) { if (instance.Model.associations[keys[i]].options.onDelete === 'cascade' && instance.Model.associations[keys[i]].options.useHooks === true) {
cascades.push(instance.Model.associations[keys[i]].accessors.get); cascades.push(instance.Model.associations[keys[i]].accessors.get);
}
} }
} }
} }
}
return Promise.each(cascades, function (cascade) { return Promise.each(cascades, function (cascade) {
return instance[cascade]({ return instance[cascade]({
transaction: options.transaction, transaction: options.transaction,
logging: options.logging logging: options.logging
}).then(function (instances) { }).then(function (instances) {
if (!Array.isArray(instances)) instances = [instances]; if (!Array.isArray(instances)) instances = [instances];
return Promise.each(instances, function (instance) { return Promise.each(instances, function (instance) {
return instance.destroy({ return instance.destroy({
transaction: options.transaction, transaction: options.transaction,
logging: options.logging logging: options.logging
});
}); });
}); });
}).then(function () {
options.instance = instance;
return self.sequelize.query(sql, options);
}); });
}; }).then(function () {
QueryInterface.prototype.bulkDelete = function(tableName, identifier, options, model) {
var sql = this.QueryGenerator.deleteQuery(tableName, identifier, Utils._.defaults(options || {}, {limit: null}), model);
return this.sequelize.query(sql, options);
};
QueryInterface.prototype.select = function(model, tableName, options) {
options = options || {};
options.type = QueryTypes.SELECT;
options.model = model;
return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model),
options
);
};
QueryInterface.prototype.increment = function(instance, tableName, values, identifier, options) {
var sql = this.QueryGenerator.incrementQuery(tableName, values, identifier, options.attributes);
options = options || {};
options.type = QueryTypes.RAW;
options.instance = instance; options.instance = instance;
return this.sequelize.query(sql, options); return self.sequelize.query(sql, options);
}; });
};
QueryInterface.prototype.bulkDelete = function(tableName, identifier, options, model) {
var sql = this.QueryGenerator.deleteQuery(tableName, identifier, Utils._.defaults(options || {}, {limit: null}), model);
return this.sequelize.query(sql, options);
};
QueryInterface.prototype.select = function(model, tableName, options) {
options = options || {};
options.type = QueryTypes.SELECT;
options.model = model;
return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model),
options
);
};
QueryInterface.prototype.increment = function(instance, tableName, values, identifier, options) {
var sql = this.QueryGenerator.incrementQuery(tableName, values, identifier, options.attributes);
options = options || {};
options.type = QueryTypes.RAW;
options.instance = instance;
return this.sequelize.query(sql, options);
};
QueryInterface.prototype.rawSelect = function(tableName, options, attributeSelector, Model) {
if (options.schema) {
tableName = this.QueryGenerator.addSchema({
tableName: tableName,
schema: options.schema
});
}
QueryInterface.prototype.rawSelect = function(tableName, options, attributeSelector, Model) { options = _.defaults(options || {}, {
if (options.schema) { raw: true,
tableName = this.QueryGenerator.addSchema({ plain: true,
tableName: tableName, type: QueryTypes.SELECT
schema: options.schema });
});
}
options = _.defaults(options || {}, { var sql = this.QueryGenerator.selectQuery(tableName, options, Model);
raw: true,
plain: true,
type: QueryTypes.SELECT
});
var sql = this.QueryGenerator.selectQuery(tableName, options, Model); if (attributeSelector === undefined) {
throw new Error('Please pass an attribute selector!');
}
if (attributeSelector === undefined) { return this.sequelize.query(sql, options).then(function(data) {
throw new Error('Please pass an attribute selector!'); if (!options.plain) {
return data;
} }
return this.sequelize.query(sql, options).then(function(data) { var result = data ? data[attributeSelector] : null;
if (!options.plain) {
return data;
}
var result = data ? data[attributeSelector] : null;
if (options && options.dataType) { if (options && options.dataType) {
var dataType = options.dataType; var dataType = options.dataType;
if (dataType instanceof DataTypes.DECIMAL || dataType instanceof DataTypes.FLOAT) { if (dataType instanceof DataTypes.DECIMAL || dataType instanceof DataTypes.FLOAT) {
result = parseFloat(result); result = parseFloat(result);
} else if (dataType instanceof DataTypes.INTEGER || dataType instanceof DataTypes.BIGINT) { } else if (dataType instanceof DataTypes.INTEGER || dataType instanceof DataTypes.BIGINT) {
result = parseInt(result, 10); result = parseInt(result, 10);
} else if (dataType instanceof DataTypes.DATE) { } else if (dataType instanceof DataTypes.DATE) {
if (!Utils._.isNull(result) && !Utils._.isDate(result)) { if (!Utils._.isNull(result) && !Utils._.isDate(result)) {
result = new Date(result); result = new Date(result);
}
} else if (dataType instanceof DataTypes.STRING) {
// Nothing to do, result is already a string.
} }
} else if (dataType instanceof DataTypes.STRING) {
// Nothing to do, result is already a string.
} }
return result;
});
};
QueryInterface.prototype.createTrigger = function(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray, options) {
var sql = this.QueryGenerator.createTrigger(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray);
options = options || {};
if (sql) {
return this.sequelize.query(sql, {logging: options.logging});
} else {
return Promise.resolve();
} }
};
QueryInterface.prototype.dropTrigger = function(tableName, triggerName, options) {
var sql = this.QueryGenerator.dropTrigger(tableName, triggerName);
options = options || {};
if (sql) {
return this.sequelize.query(sql, {logging: options.logging});
} else {
return Promise.resolve();
}
};
QueryInterface.prototype.renameTrigger = function(tableName, oldTriggerName, newTriggerName, options) { return result;
var sql = this.QueryGenerator.renameTrigger(tableName, oldTriggerName, newTriggerName); });
options = options || {}; };
if (sql) { QueryInterface.prototype.createTrigger = function(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray, options) {
return this.sequelize.query(sql, {logging: options.logging}); var sql = this.QueryGenerator.createTrigger(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray);
} else { options = options || {};
return Promise.resolve(); if (sql) {
} return this.sequelize.query(sql, {logging: options.logging});
}; } else {
return Promise.resolve();
}
};
QueryInterface.prototype.createFunction = function(functionName, params, returnType, language, body, options) { QueryInterface.prototype.dropTrigger = function(tableName, triggerName, options) {
var sql = this.QueryGenerator.createFunction(functionName, params, returnType, language, body, options); var sql = this.QueryGenerator.dropTrigger(tableName, triggerName);
options = options || {}; options = options || {};
if (sql) { if (sql) {
return this.sequelize.query(sql, {logging: options.logging}); return this.sequelize.query(sql, {logging: options.logging});
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
}; };
QueryInterface.prototype.dropFunction = function(functionName, params, options) { QueryInterface.prototype.renameTrigger = function(tableName, oldTriggerName, newTriggerName, options) {
var sql = this.QueryGenerator.dropFunction(functionName, params); var sql = this.QueryGenerator.renameTrigger(tableName, oldTriggerName, newTriggerName);
options = options || {}; options = options || {};
if (sql) { if (sql) {
return this.sequelize.query(sql, {logging: options.logging}); return this.sequelize.query(sql, {logging: options.logging});
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
}; };
QueryInterface.prototype.renameFunction = function(oldFunctionName, params, newFunctionName, options) {
var sql = this.QueryGenerator.renameFunction(oldFunctionName, params, newFunctionName);
options = options || {};
if (sql) { QueryInterface.prototype.createFunction = function(functionName, params, returnType, language, body, options) {
return this.sequelize.query(sql, {logging: options.logging}); var sql = this.QueryGenerator.createFunction(functionName, params, returnType, language, body, options);
} else { options = options || {};
return Promise.resolve();
}
};
// Helper methods useful for querying if (sql) {
return this.sequelize.query(sql, {logging: options.logging});
} else {
return Promise.resolve();
}
};
/** QueryInterface.prototype.dropFunction = function(functionName, params, options) {
* Escape an identifier (e.g. a table or attribute name). If force is true, var sql = this.QueryGenerator.dropFunction(functionName, params);
* the identifier will be quoted even if the `quoteIdentifiers` option is options = options || {};
* false.
*/
QueryInterface.prototype.quoteIdentifier = function(identifier, force) {
return this.QueryGenerator.quoteIdentifier(identifier, force);
};
QueryInterface.prototype.quoteTable = function(identifier) { if (sql) {
return this.QueryGenerator.quoteTable(identifier); return this.sequelize.query(sql, {logging: options.logging});
}; } else {
return Promise.resolve();
}
};
/** QueryInterface.prototype.renameFunction = function(oldFunctionName, params, newFunctionName, options) {
* Split an identifier into .-separated tokens and quote each part. var sql = this.QueryGenerator.renameFunction(oldFunctionName, params, newFunctionName);
* If force is true, the identifier will be quoted even if the options = options || {};
* `quoteIdentifiers` option is false.
*/
QueryInterface.prototype.quoteIdentifiers = function(identifiers, force) {
return this.QueryGenerator.quoteIdentifiers(identifiers, force);
};
/** if (sql) {
* Escape a value (e.g. a string, number or date) return this.sequelize.query(sql, {logging: options.logging});
*/ } else {
QueryInterface.prototype.escape = function(value) { return Promise.resolve();
return this.QueryGenerator.escape(value); }
}; };
// Helper methods useful for querying
/**
* Escape an identifier (e.g. a table or attribute name). If force is true,
* the identifier will be quoted even if the `quoteIdentifiers` option is
* false.
*/
QueryInterface.prototype.quoteIdentifier = function(identifier, force) {
return this.QueryGenerator.quoteIdentifier(identifier, force);
};
QueryInterface.prototype.quoteTable = function(identifier) {
return this.QueryGenerator.quoteTable(identifier);
};
/**
* Split an identifier into .-separated tokens and quote each part.
* If force is true, the identifier will be quoted even if the
* `quoteIdentifiers` option is false.
*/
QueryInterface.prototype.quoteIdentifiers = function(identifiers, force) {
return this.QueryGenerator.quoteIdentifiers(identifiers, force);
};
/**
* Escape a value (e.g. a string, number or date)
*/
QueryInterface.prototype.escape = function(value) {
return this.QueryGenerator.escape(value);
};
QueryInterface.prototype.setAutocommit = function(transaction, value, options) {
if (!transaction || !(transaction instanceof Transaction)) {
throw new Error('Unable to set autocommit for a transaction without transaction object!');
}
options = Utils._.extend({
parent: options.transaction
}, options || {});
var sql = this.QueryGenerator.setAutocommitQuery(value, options);
if (sql) {
return this.sequelize.query(sql, { transaction: transaction, logging: options.logging });
} else {
return Promise.resolve();
}
};
QueryInterface.prototype.setAutocommit = function(transaction, value, options) { QueryInterface.prototype.setIsolationLevel = function(transaction, value, options) {
if (!transaction || !(transaction instanceof Transaction)) { if (!transaction || !(transaction instanceof Transaction)) {
throw new Error('Unable to set autocommit for a transaction without transaction object!'); throw new Error('Unable to set isolation level for a transaction without transaction object!');
} }
options = Utils._.extend({ options = Utils._.extend({
parent: options.transaction parent: options.transaction
}, options || {}); }, options || {});
var sql = this.QueryGenerator.setAutocommitQuery(value, options); var sql = this.QueryGenerator.setIsolationLevelQuery(value, options);
if (sql) {
return this.sequelize.query(sql, { transaction: transaction, logging: options.logging });
} else {
return Promise.resolve();
}
};
QueryInterface.prototype.setIsolationLevel = function(transaction, value, options) { if (sql) {
if (!transaction || !(transaction instanceof Transaction)) { return this.sequelize.query(sql, { transaction: transaction, logging: options.logging });
throw new Error('Unable to set isolation level for a transaction without transaction object!'); } else {
} return Promise.resolve();
}
};
options = Utils._.extend({ QueryInterface.prototype.startTransaction = function(transaction, options) {
parent: options.transaction if (!transaction || !(transaction instanceof Transaction)) {
}, options || {}); throw new Error('Unable to start a transaction without transaction object!');
}
var sql = this.QueryGenerator.setIsolationLevelQuery(value, options); options = Utils._.extend({
transaction: transaction,
parent: options.transaction
}, options || {});
if (sql) { var sql = this.QueryGenerator.startTransactionQuery(transaction, options);
return this.sequelize.query(sql, { transaction: transaction, logging: options.logging }); return this.sequelize.query(sql, options);
} else { };
return Promise.resolve();
}
};
QueryInterface.prototype.startTransaction = function(transaction, options) { QueryInterface.prototype.deferConstraints = function (transaction, options) {
if (!transaction || !(transaction instanceof Transaction)) { options = Utils._.extend({
throw new Error('Unable to start a transaction without transaction object!'); transaction: transaction,
} parent: options.transaction
}, options || {});
options = Utils._.extend({ var sql = this.QueryGenerator.deferConstraintsQuery(options);
transaction: transaction,
parent: options.transaction
}, options || {});
var sql = this.QueryGenerator.startTransactionQuery(transaction, options); if (sql) {
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
}; }
QueryInterface.prototype.deferConstraints = function (transaction, options) {
options = Utils._.extend({
transaction: transaction,
parent: options.transaction
}, options || {});
var sql = this.QueryGenerator.deferConstraintsQuery(options); return Promise.resolve();
};
if (sql) {
return this.sequelize.query(sql, options);
}
return Promise.resolve(); QueryInterface.prototype.commitTransaction = function(transaction, options) {
}; if (!transaction || !(transaction instanceof Transaction)) {
throw new Error('Unable to commit a transaction without transaction object!');
QueryInterface.prototype.commitTransaction = function(transaction, options) { }
if (!transaction || !(transaction instanceof Transaction)) {
throw new Error('Unable to commit a transaction without transaction object!');
}
options = Utils._.extend({ options = Utils._.extend({
transaction: transaction, transaction: transaction,
parent: options.transaction parent: options.transaction
}, options || {}); }, options || {});
var sql = this.QueryGenerator.commitTransactionQuery(options); var sql = this.QueryGenerator.commitTransactionQuery(options);
if (sql) { if (sql) {
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
}; };
QueryInterface.prototype.rollbackTransaction = function(transaction, options) { QueryInterface.prototype.rollbackTransaction = function(transaction, options) {
if (!transaction || !(transaction instanceof Transaction)) { if (!transaction || !(transaction instanceof Transaction)) {
throw new Error('Unable to rollback a transaction without transaction object!'); throw new Error('Unable to rollback a transaction without transaction object!');
} }
options = Utils._.extend({ options = Utils._.extend({
transaction: transaction, transaction: transaction,
parent: options.transaction parent: options.transaction
}, options || {}); }, options || {});
var sql = this.QueryGenerator.rollbackTransactionQuery(transaction, options); var sql = this.QueryGenerator.rollbackTransactionQuery(transaction, options);
return this.sequelize.query(sql, options); return this.sequelize.query(sql, options);
}; };
return QueryInterface; module.exports = QueryInterface;
})();
This diff could not be displayed because it is too large.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
var DataTypes = require('./data-types') var DataTypes = require('./data-types')
, SqlString = require('./sql-string') , SqlString = require('./sql-string')
, lodash = require('lodash') , lodash = require('lodash')
, ParameterValidator = require('./utils/parameter-validator') , parameterValidator = require('./utils/parameter-validator')
, inflection = require('inflection') , inflection = require('inflection')
, dottie = require('dottie') , dottie = require('dottie')
, uuid = require('node-uuid') , uuid = require('node-uuid')
...@@ -398,9 +398,7 @@ var Utils = module.exports = { ...@@ -398,9 +398,7 @@ var Utils = module.exports = {
this.logic = logic; this.logic = logic;
}, },
validateParameter: function(value, expectation, options) { validateParameter: parameterValidator,
return ParameterValidator.check(value, expectation, options);
},
formatReferences: function (obj) { formatReferences: function (obj) {
if (!lodash.isPlainObject(obj.references)) { if (!lodash.isPlainObject(obj.references)) {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
var _ = require('lodash'); var _ = require('lodash');
var util = require('util'); var util = require('util');
var validateDeprecation = function(value, expectation, options) { function validateDeprecation (value, expectation, options) {
if (!options.deprecated) { if (!options.deprecated) {
return; return;
} }
...@@ -17,9 +17,9 @@ var validateDeprecation = function(value, expectation, options) { ...@@ -17,9 +17,9 @@ var validateDeprecation = function(value, expectation, options) {
} }
return valid; return valid;
}; }
var validate = function(value, expectation) { function validate (value, expectation) {
// the second part of this check is a workaround to deal with an issue that occurs in node-webkit when // the second part of this check is a workaround to deal with an issue that occurs in node-webkit when
// using object literals. https://github.com/sequelize/sequelize/issues/2685 // using object literals. https://github.com/sequelize/sequelize/issues/2685
if (value instanceof expectation || Object.prototype.toString.call(value) === Object.prototype.toString.call(expectation.call())) { if (value instanceof expectation || Object.prototype.toString.call(value) === Object.prototype.toString.call(expectation.call())) {
...@@ -27,31 +27,31 @@ var validate = function(value, expectation) { ...@@ -27,31 +27,31 @@ var validate = function(value, expectation) {
} }
throw new Error(util.format('The parameter (value: %s) is no %s.', value, expectation.name)); throw new Error(util.format('The parameter (value: %s) is no %s.', value, expectation.name));
}; }
module.exports = { function check (value, expectation, options) {
check: function(value, expectation, options) { options = _.extend({
options = _.extend({ deprecated: false,
deprecated: false, index: null,
index: null, method: null,
method: null, optional: false
optional: false }, options || {});
}, options || {});
if (!value && options.optional) {
if (!value && options.optional) { return true;
return true; }
}
if (value === undefined) {
if (value === undefined) { throw new Error('No value has been passed.');
throw new Error('No value has been passed.');
}
if (expectation === undefined) {
throw new Error('No expectation has been passed.');
}
return false
|| validateDeprecation(value, expectation, options)
|| validate(value, expectation, options);
} }
};
if (expectation === undefined) {
throw new Error('No expectation has been passed.');
}
return false
|| validateDeprecation(value, expectation, options)
|| validate(value, expectation, options);
}
module.exports = check;
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!