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

Commit 7e5b7c5a by Jan Aagaard Meier

Merge pull request #4156 from sequelize/feat-include-limit

Refactor join query generation
2 parents 1524d2d6 59e737c9
...@@ -18,4 +18,9 @@ Association.prototype.toInstanceArray = function (objs) { ...@@ -18,4 +18,9 @@ Association.prototype.toInstanceArray = function (objs) {
return obj; return obj;
}, this); }, this);
}; };
Association.prototype.inspect = function() {
return this.as;
};
module.exports = Association; module.exports = Association;
...@@ -4,6 +4,9 @@ var Utils = require('./../utils') ...@@ -4,6 +4,9 @@ var Utils = require('./../utils')
, Helpers = require('./helpers') , Helpers = require('./helpers')
, _ = require('lodash') , _ = require('lodash')
, Association = require('./base') , Association = require('./base')
, BelongsTo = require('./belongs-to')
, HasMany = require('./has-many')
, HasOne = require('./has-one')
, CounterCache = require('../plugins/counter-cache') , CounterCache = require('../plugins/counter-cache')
, util = require('util'); , util = require('util');
...@@ -323,18 +326,18 @@ BelongsToMany.prototype.injectAttributes = function() { ...@@ -323,18 +326,18 @@ BelongsToMany.prototype.injectAttributes = function() {
if (this.primaryKeyDeleted === true) { if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true; targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) { } else if (this.through.unique !== false) {
var uniqueKey = [this.through.model.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_'); var uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey; targetAttribute.unique = sourceAttribute.unique = uniqueKey;
} }
if (!this.through.model.rawAttributes[this.identifier]) { if (!this.through.model.rawAttributes[this.foreignKey]) {
this.through.model.rawAttributes[this.identifier] = { this.through.model.rawAttributes[this.foreignKey] = {
_autoGenerated: true _autoGenerated: true
}; };
} }
if (!this.through.model.rawAttributes[this.foreignIdentifier]) { if (!this.through.model.rawAttributes[this.otherKey]) {
this.through.model.rawAttributes[this.foreignIdentifier] = { this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true _autoGenerated: true
}; };
} }
...@@ -345,8 +348,8 @@ BelongsToMany.prototype.injectAttributes = function() { ...@@ -345,8 +348,8 @@ BelongsToMany.prototype.injectAttributes = function() {
key: sourceKeyField key: sourceKeyField
}; };
// For the source attribute the passed option is the priority // For the source attribute the passed option is the priority
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.identifier].onDelete; sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.identifier].onUpdate; sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE'; if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE'; if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
...@@ -356,25 +359,58 @@ BelongsToMany.prototype.injectAttributes = function() { ...@@ -356,25 +359,58 @@ BelongsToMany.prototype.injectAttributes = function() {
key: targetKeyField key: targetKeyField
}; };
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call) // 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.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.foreignIdentifier].onUpdate || this.options.onUpdate; targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE'; if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE'; if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
} }
this.through.model.rawAttributes[this.identifier] = _.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute); this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
this.through.model.rawAttributes[this.foreignIdentifier] = _.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute); this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute);
this.identifierField = this.through.model.rawAttributes[this.identifier].field || this.identifier; this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.foreignIdentifier].field || this.foreignIdentifier; this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
if (this.paired && !this.paired.foreignIdentifierField) { if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.foreignIdentifier].field || this.paired.foreignIdentifier; this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
} }
this.through.model.init(this.through.model.modelManager); this.through.model.init(this.through.model.modelManager);
this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey
});
this.manyFromSource = new HasMany(this.source, this.through.model, {
foreignKey: this.foreignKey
});
this.oneFromSource = new HasOne(this.source, this.through.model, {
foreignKey: this.foreignKey,
as: this.through.model.name
});
this.toTarget = new BelongsTo(this.through.model, this.target, {
foreignKey: this.otherKey
});
this.manyFromTarget = new HasMany(this.target, this.through.model, {
foreignKey: this.otherKey
});
this.oneFromTarget = new HasOne(this.target, this.through.model, {
foreignKey: this.otherKey,
as: this.through.model.name
});
if (this.paired && this.paired.otherKeyDefault) {
this.paired.toTarget = new BelongsTo(this.paired.through.model, this.paired.target, {
foreignKey: this.paired.otherKey
});
this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, {
foreignKey: this.paired.otherKey,
as: this.paired.through.model.name
});
}
Helpers.checkNamingCollision(this); Helpers.checkNamingCollision(this);
return this; return this;
...@@ -404,7 +440,7 @@ BelongsToMany.prototype.injectGetter = function(obj) { ...@@ -404,7 +440,7 @@ BelongsToMany.prototype.injectGetter = function(obj) {
if (Object(through.model) === through.model) { if (Object(through.model) === through.model) {
throughWhere = {}; throughWhere = {};
throughWhere[association.identifier] = instance.get(association.source.primaryKeyAttribute); throughWhere[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
if (through.scope) { if (through.scope) {
_.assign(throughWhere, through.scope); _.assign(throughWhere, through.scope);
...@@ -412,19 +448,10 @@ BelongsToMany.prototype.injectGetter = function(obj) { ...@@ -412,19 +448,10 @@ BelongsToMany.prototype.injectGetter = function(obj) {
options.include = options.include || []; options.include = options.include || [];
options.include.push({ options.include.push({
model: through.model, association: association.oneFromTarget,
as: through.model.name,
attributes: options.joinTableAttributes, attributes: options.joinTableAttributes,
association: {
isSingleAssociation: true,
source: association.target,
target: association.source,
identifier: association.foreignIdentifier,
identifierField: association.foreignIdentifierField
},
required: true, required: true,
where: throughWhere, where: throughWhere
_pseudo: true
}); });
} }
......
...@@ -25,14 +25,7 @@ var BelongsTo = function(source, target, options) { ...@@ -25,14 +25,7 @@ var BelongsTo = function(source, target, 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 (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
this.foreignKeyAttribute = {}; this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey;
}
if (this.as) { if (this.as) {
this.isAliased = true; this.isAliased = true;
...@@ -44,8 +37,15 @@ var BelongsTo = function(source, target, options) { ...@@ -44,8 +37,15 @@ var BelongsTo = function(source, target, options) {
this.options.name = this.target.options.name; this.options.name = this.target.options.name;
} }
if (!this.options.foreignKey) { if (_.isObject(this.options.foreignKey)) {
this.options.foreignKey = _.camelizeIf( this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = _.camelizeIf(
[ [
_.underscoredIf(this.as, this.source.options.underscored), _.underscoredIf(this.as, this.source.options.underscored),
this.target.primaryKeyAttribute this.target.primaryKeyAttribute
...@@ -54,15 +54,14 @@ var BelongsTo = function(source, target, options) { ...@@ -54,15 +54,14 @@ var BelongsTo = function(source, target, options) {
); );
} }
this.identifier = this.foreignKey || _.camelizeIf( this.identifier = this.foreignKey;
[
_.underscoredIf(this.options.name.singular, this.target.options.underscored), if (this.source.rawAttributes[this.identifier]) {
this.target.primaryKeyAttribute this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
].join('_'), }
!this.target.options.underscored
);
this.targetIdentifier = this.options.targetKey || this.target.primaryKeyAttribute; this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as; this.associationAccessor = this.as;
this.options.useHooks = options.useHooks; this.options.useHooks = options.useHooks;
...@@ -107,15 +106,19 @@ util.inherits(BelongsTo, Association); ...@@ -107,15 +106,19 @@ util.inherits(BelongsTo, Association);
BelongsTo.prototype.injectAttributes = function() { BelongsTo.prototype.injectAttributes = function() {
var newAttributes = {}; var newAttributes = {};
newAttributes[this.identifier] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.target.rawAttributes[this.targetIdentifier].type }); newAttributes[this.foreignKey] = _.defaults(this.foreignKeyAttribute, {
type: this.options.keyType || this.target.rawAttributes[this.targetKey].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.foreignKey], 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.foreignKey].field || this.foreignKey;
this.source.refreshAttributes(); this.source.refreshAttributes();
...@@ -130,9 +133,9 @@ BelongsTo.prototype.injectGetter = function(instancePrototype) { ...@@ -130,9 +133,9 @@ BelongsTo.prototype.injectGetter = function(instancePrototype) {
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.targetKey] = this.get(association.foreignKey);
options = association.target.__optClone(options) || {}; options = association.target.$optClone(options) || {};
options.where = { options.where = {
$and: [ $and: [
...@@ -167,16 +170,16 @@ BelongsTo.prototype.injectSetter = function(instancePrototype) { ...@@ -167,16 +170,16 @@ BelongsTo.prototype.injectSetter = function(instancePrototype) {
var value = associatedInstance; var value = associatedInstance;
if (associatedInstance instanceof association.target.Instance) { if (associatedInstance instanceof association.target.Instance) {
value = associatedInstance[association.targetIdentifier]; value = associatedInstance[association.targetKey];
} }
this.set(association.identifier, value); this.set(association.foreignKey, value);
if (options.save === false) return; if (options.save === false) return;
options = _.extend({ options = _.extend({
fields: [association.identifier], fields: [association.foreignKey],
allowNull: [association.identifier], allowNull: [association.foreignKey],
association: true association: true
}, options); }, options);
......
...@@ -28,18 +28,12 @@ var HasMany = function(source, target, options) { ...@@ -28,18 +28,12 @@ var HasMany = function(source, target, options) {
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;
this.foreignKeyAttribute = {};
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 (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
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 self association, this is the target association
...@@ -65,6 +59,31 @@ var HasMany = function(source, target, options) { ...@@ -65,6 +59,31 @@ var HasMany = function(source, target, options) {
this.options.name = this.target.options.name; this.options.name = this.target.options.name;
} }
/*
* Foreign key setup
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = _.camelizeIf(
[
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
this.associationAccessor = this.as; this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it // Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
...@@ -180,26 +199,19 @@ util.inherits(HasMany, Association); ...@@ -180,26 +199,19 @@ util.inherits(HasMany, Association);
// the id is in the target table // the id is in the target table
// or in an extra table which connects two tables // or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() { HasMany.prototype.injectAttributes = function() {
this.identifier = this.foreignKey || _.camelizeIf(
[
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
var newAttributes = {}; 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 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 }); newAttributes[this.foreignKey] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || this.source.rawAttributes[this.source.primaryKeyAttribute].type });
if (this.options.constraints !== false) { if (this.options.constraints !== false) {
constraintOptions.onDelete = constraintOptions.onDelete || 'SET NULL'; constraintOptions.onDelete = constraintOptions.onDelete || 'SET NULL';
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE'; constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, constraintOptions); Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions);
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.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.target.refreshAttributes(); this.target.refreshAttributes();
this.source.refreshAttributes(); this.source.refreshAttributes();
...@@ -225,7 +237,7 @@ HasMany.prototype.injectGetter = function(obj) { ...@@ -225,7 +237,7 @@ HasMany.prototype.injectGetter = function(obj) {
options.where = { options.where = {
$and: [ $and: [
new Utils.where( new Utils.where(
association.target.rawAttributes[association.identifier], association.target.rawAttributes[association.foreignKey],
this.get(association.source.primaryKeyAttribute, {raw: true}) this.get(association.source.primaryKeyAttribute, {raw: true})
), ),
scopeWhere, scopeWhere,
...@@ -335,7 +347,7 @@ HasMany.prototype.injectSetter = function(obj) { ...@@ -335,7 +347,7 @@ HasMany.prototype.injectSetter = function(obj) {
if (obsoleteAssociations.length > 0) { if (obsoleteAssociations.length > 0) {
update = {}; update = {};
update[association.identifier] = null; update[association.foreignKey] = null;
updateWhere = {}; updateWhere = {};
...@@ -355,7 +367,7 @@ HasMany.prototype.injectSetter = function(obj) { ...@@ -355,7 +367,7 @@ HasMany.prototype.injectSetter = function(obj) {
updateWhere = {}; updateWhere = {};
update = {}; update = {};
update[association.identifier] = instance.get(association.source.primaryKeyAttribute); update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope); _.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) { updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
...@@ -383,7 +395,7 @@ HasMany.prototype.injectSetter = function(obj) { ...@@ -383,7 +395,7 @@ HasMany.prototype.injectSetter = function(obj) {
newInstances = association.toInstanceArray(newInstances); newInstances = association.toInstanceArray(newInstances);
update[association.identifier] = instance.get(association.source.primaryKeyAttribute); update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope); _.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = newInstances.map(function (unassociatedObject) { where[association.target.primaryKeyAttribute] = newInstances.map(function (unassociatedObject) {
...@@ -403,10 +415,10 @@ HasMany.prototype.injectSetter = function(obj) { ...@@ -403,10 +415,10 @@ HasMany.prototype.injectSetter = function(obj) {
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects); oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
var update = {}; var update = {};
update[association.identifier] = null; update[association.foreignKey] = null;
var where = {}; var where = {};
where[association.identifier] = this.get(association.source.primaryKeyAttribute); where[association.foreignKey] = this.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = oldAssociatedObjects.map(function (oldAssociatedObject) { return oldAssociatedObject.get(association.target.primaryKeyAttribute); }); where[association.target.primaryKeyAttribute] = oldAssociatedObjects.map(function (oldAssociatedObject) { return oldAssociatedObject.get(association.target.primaryKeyAttribute); });
return association.target.unscoped().update( return association.target.unscoped().update(
...@@ -444,8 +456,8 @@ HasMany.prototype.injectCreator = function(obj) { ...@@ -444,8 +456,8 @@ HasMany.prototype.injectCreator = function(obj) {
}); });
} }
values[association.identifier] = instance.get(association.source.primaryKeyAttribute); values[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.identifier); if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options); return association.target.create(values, options);
}; };
......
...@@ -23,14 +23,7 @@ var HasOne = function(srcModel, targetModel, options) { ...@@ -23,14 +23,7 @@ var HasOne = function(srcModel, targetModel, 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 (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
this.foreignKeyAttribute = {}; this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey;
}
if (this.as) { if (this.as) {
this.isAliased = true; this.isAliased = true;
...@@ -42,28 +35,31 @@ var HasOne = function(srcModel, targetModel, options) { ...@@ -42,28 +35,31 @@ var HasOne = function(srcModel, targetModel, options) {
this.options.name = this.target.options.name; this.options.name = this.target.options.name;
} }
if (!this.options.foreignKey) { if (_.isObject(this.options.foreignKey)) {
this.options.foreignKey = _.camelizeIf( this.foreignKeyAttribute = this.options.foreignKey;
[ this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
_.underscoredIf(Utils.singularize(this.source.name), this.target.options.underscored), } else if (this.options.foreignKey) {
this.source.primaryKeyAttribute this.foreignKey = this.options.foreignKey;
].join('_'),
!this.source.options.underscored
);
} }
this.identifier = this.foreignKey || _.camelizeIf( if (!this.foreignKey) {
this.foreignKey = _.camelizeIf(
[ [
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored), _.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.sourceIdentifier = this.source.primaryKeyAttribute;
this.associationAccessor = this.as; this.associationAccessor = this.as;
this.options.useHooks = options.useHooks; this.options.useHooks = options.useHooks;
if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
// Get singular name, trying to uppercase the first letter, unless the model forbids it // Get singular name, trying to uppercase the first letter, unless the model forbids it
var singular = Utils.uppercaseFirst(this.options.name.singular); var singular = Utils.uppercaseFirst(this.options.name.singular);
...@@ -103,18 +99,18 @@ util.inherits(HasOne, Association); ...@@ -103,18 +99,18 @@ 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.source.primaryKeyAttribute].type;
newAttributes[this.identifier] = _.defaults(this.foreignKeyAttribute, { type: this.options.keyType || keyType }); newAttributes[this.foreignKey] = _.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.foreignKey].field || this.foreignKey;
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.foreignKey], 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();
...@@ -129,7 +125,7 @@ HasOne.prototype.injectGetter = function(instancePrototype) { ...@@ -129,7 +125,7 @@ HasOne.prototype.injectGetter = function(instancePrototype) {
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.foreignKey] = this.get(association.sourceIdentifier);
options = association.target.__optClone(options) || {}; options = association.target.__optClone(options) || {};
...@@ -167,10 +163,10 @@ HasOne.prototype.injectSetter = function(instancePrototype) { ...@@ -167,10 +163,10 @@ HasOne.prototype.injectSetter = function(instancePrototype) {
options.scope = false; options.scope = false;
return instance[association.accessors.get](options).then(function(oldInstance) { return instance[association.accessors.get](options).then(function(oldInstance) {
if (oldInstance) { if (oldInstance) {
oldInstance[association.identifier] = null; oldInstance[association.foreignKey] = null;
return oldInstance.save(_.extend({}, options, { return oldInstance.save(_.extend({}, options, {
fields: [association.identifier], fields: [association.foreignKey],
allowNull: [association.identifier], allowNull: [association.foreignKey],
association: true association: true
})); }));
} }
...@@ -183,7 +179,7 @@ HasOne.prototype.injectSetter = function(instancePrototype) { ...@@ -183,7 +179,7 @@ HasOne.prototype.injectSetter = function(instancePrototype) {
isNewRecord: false isNewRecord: false
}); });
} }
associatedInstance.set(association.identifier, instance.get(association.sourceIdentifier)); associatedInstance.set(association.foreignKey, instance.get(association.sourceIdentifier));
return associatedInstance.save(options); return associatedInstance.save(options);
} }
return null; return null;
...@@ -201,8 +197,8 @@ HasOne.prototype.injectCreator = function(instancePrototype) { ...@@ -201,8 +197,8 @@ HasOne.prototype.injectCreator = function(instancePrototype) {
values = values || {}; values = values || {};
options = options || {}; options = options || {};
values[association.identifier] = instance.get(association.sourceIdentifier); values[association.foreignKey] = instance.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.identifier); if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options); return association.target.create(values, options);
}; };
......
...@@ -7,6 +7,7 @@ var Utils = require('../../utils') ...@@ -7,6 +7,7 @@ var Utils = require('../../utils')
, _ = require('lodash') , _ = require('lodash')
, util = require('util') , util = require('util')
, Dottie = require('dottie') , Dottie = require('dottie')
, BelongsTo = require('../../associations/belongs-to')
, uuid = require('node-uuid'); , uuid = require('node-uuid');
/* istanbul ignore next */ /* istanbul ignore next */
...@@ -1041,6 +1042,7 @@ var QueryGenerator = { ...@@ -1041,6 +1042,7 @@ var QueryGenerator = {
, association = include.association , association = include.association
, through = include.through , through = include.through
, joinType = include.required ? ' INNER JOIN ' : ' LEFT OUTER JOIN ' , joinType = include.required ? ' INNER JOIN ' : ' LEFT OUTER JOIN '
, parentIsTop = !include.parent.association && include.parent.model.name === options.model.name
, whereOptions = Utils._.clone(options) , whereOptions = Utils._.clone(options)
, targetWhere; , targetWhere;
...@@ -1138,7 +1140,8 @@ var QueryGenerator = { ...@@ -1138,7 +1140,8 @@ var QueryGenerator = {
// Used by both join and subquery where // Used by both join and subquery where
// If parent include was in a subquery need to join on the aliased attribute // If parent include was in a subquery need to join on the aliased attribute
if (subQuery && !include.subQuery && include.parent.subQuery) {
if (subQuery && !include.subQuery && include.parent.subQuery && !parentIsTop) {
sourceJoinOn = self.quoteIdentifier(tableSource + '.' + attrSource) + ' = '; sourceJoinOn = self.quoteIdentifier(tableSource + '.' + attrSource) + ' = ';
} else { } else {
sourceJoinOn = self.quoteTable(tableSource) + '.' + self.quoteIdentifier(attrSource) + ' = '; sourceJoinOn = self.quoteTable(tableSource) + '.' + self.quoteIdentifier(attrSource) + ' = ';
...@@ -1211,22 +1214,14 @@ var QueryGenerator = { ...@@ -1211,22 +1214,14 @@ var QueryGenerator = {
if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) { if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
$query = self.selectQuery(topInclude.through.model.getTableName(), { $query = self.selectQuery(topInclude.through.model.getTableName(), {
attributes: [topInclude.through.model.primaryKeyAttributes[0]], attributes: [topInclude.through.model.primaryKeyAttributes[0]],
include: Model.$validateIncludedElements({
model: topInclude.through.model,
include: [{ include: [{
model: topInclude.model, association: topInclude.association.toTarget,
as: topInclude.model.name, required: true
attributes: [], }]
association: { }).include,
associationType: 'BelongsTo', model: topInclude.through.model,
isSingleAssociation: true,
source: topInclude.association.target,
target: topInclude.association.source,
identifier: topInclude.association.foreignIdentifier,
identifierField: topInclude.association.foreignIdentifierField
},
required: true,
include: topInclude.include,
_pseudo: true
}],
where: self.sequelize.and( where: self.sequelize.and(
self.sequelize.asIs([ self.sequelize.asIs([
self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(topParent.model.primaryKeyAttributes[0]), self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(topParent.model.primaryKeyAttributes[0]),
...@@ -1262,63 +1257,30 @@ var QueryGenerator = { ...@@ -1262,63 +1257,30 @@ var QueryGenerator = {
} }
} }
} else { } else {
var left = association.source if (subQuery && include.subQueryFilter) {
, right = association.target var associationWhere = {}
, primaryKeysLeft = left.primaryKeyAttributes , $query
, primaryKeysRight = right.primaryKeyAttributes , subQueryWhere;
, tableLeft = parentTable
, attrLeft = association.associationType === 'BelongsTo' ?
association.identifierField || association.identifier :
primaryKeysLeft[0]
, tableRight = as
, attrRight = association.associationType !== 'BelongsTo' ?
association.identifierField || association.identifier :
right.rawAttributes[association.targetIdentifier || primaryKeysRight[0]].field
, joinOn
, subQueryJoinOn;
// Filter statement
// Used by both join and where
if (subQuery && !include.subQuery && include.parent.subQuery && (include.hasParentRequired || include.hasParentWhere || include.parent.hasIncludeRequired || include.parent.hasIncludeWhere)) {
joinOn = self.quoteIdentifier(tableLeft + '.' + attrLeft);
} else {
if (association.associationType !== 'BelongsTo') {
// Alias the left attribute if the left attribute is not from a subqueried main table
// When doing a query like SELECT aliasedKey FROM (SELECT primaryKey FROM primaryTable) only aliasedKey is available to the join, this is not the case when doing a regular select where you can't used the aliased attribute
if (!subQuery || (subQuery && include.parent.model !== mainModel)) {
if (left.rawAttributes[attrLeft].field) {
attrLeft = left.rawAttributes[attrLeft].field;
}
}
}
joinOn = self.quoteTable(tableLeft) + '.' + self.quoteIdentifier(attrLeft);
}
subQueryJoinOn = self.quoteTable(tableLeft) + '.' + self.quoteIdentifier(attrLeft);
joinOn += ' = ' + self.quoteTable(tableRight) + '.' + self.quoteIdentifier(attrRight); associationWhere[association.identifierField] = {
subQueryJoinOn += ' = ' + self.quoteTable(tableRight) + '.' + self.quoteIdentifier(attrRight); $raw: self.quoteTable(parentTable) + '.' + self.quoteIdentifier(association.source.primaryKeyAttribute)
};
if (include.where) {
targetWhere = self.getWhereConditions(include.where, self.sequelize.literal(self.quoteIdentifier(as)), include.model, whereOptions);
if (targetWhere) {
joinOn += ' AND ' + targetWhere;
subQueryJoinOn += ' AND ' + targetWhere;
}
}
// If its a multi association and the main query is a subquery (because of limit) we need to filter based on this association in a subquery
if (subQuery && association.isMultiAssociation && include.required) {
if (!options.where) options.where = {}; if (!options.where) options.where = {};
// Creating the as-is where for the subQuery, checks that the required association exists // Creating the as-is where for the subQuery, checks that the required association exists
var $query = self.selectQuery(include.model.getTableName(), { $query = self.selectQuery(include.model.getTableName(), {
tableAs: as, attributes: [association.identifierField],
attributes: [attrRight], where: {
where: self.sequelize.asIs(subQueryJoinOn ? [subQueryJoinOn] : [joinOn]), $and: [
associationWhere,
include.where || {}
]
},
limit: 1 limit: 1
}, include.model); }, include.model);
var subQueryWhere = self.sequelize.asIs([ subQueryWhere = self.sequelize.asIs([
'(', '(',
$query.replace(/\;$/, ''), $query.replace(/\;$/, ''),
')', ')',
...@@ -1334,8 +1296,11 @@ var QueryGenerator = { ...@@ -1334,8 +1296,11 @@ var QueryGenerator = {
} }
} }
// Generate join SQL joinQueryItem = ' ' + self.joinIncludeQuery({
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ' + joinOn; model: mainModel,
subQuery: options.subQuery,
include: include
});
} }
if (include.subQuery && subQuery) { if (include.subQuery && subQuery) {
...@@ -1515,6 +1480,79 @@ var QueryGenerator = { ...@@ -1515,6 +1480,79 @@ var QueryGenerator = {
return query; return query;
}, },
joinIncludeQuery: function(options) {
var subQuery = options.subQuery
, include = options.include
, association = include.association
, parent = include.parent
, parentIsTop = !include.parent.association && include.parent.model.name === options.model.name
, $parent
, joinType = include.required ? 'INNER JOIN ' : 'LEFT OUTER JOIN '
, joinOn
, joinWhere
/* Attributes for the left side */
, left = association.source
, asLeft
, attrLeft = association instanceof BelongsTo ?
association.identifier :
left.primaryKeyAttribute
, fieldLeft = association instanceof BelongsTo ?
association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field
/* Attributes for the right side */
, right = association.target
, asRight = include.as
, tableRight = right.getTableName()
, fieldRight = association instanceof BelongsTo ?
right.rawAttributes[association.targetIdentifier || right.primaryKeyAttribute].field :
association.identifierField;
while (($parent = ($parent && $parent.parent || include.parent)) && $parent.association) {
if (asLeft) {
asLeft = [$parent.as, asLeft].join('.');
} else {
asLeft = $parent.as;
}
}
if (!asLeft) asLeft = parent.as || parent.model.name;
else asRight = [asLeft, asRight].join('.');
joinOn = [
this.quoteTable(asLeft),
this.quoteIdentifier(fieldLeft)
].join('.');
if (subQuery && include.parent.subQuery && !include.subQuery) {
if (parentIsTop) {
// The main model attributes is not aliased to a prefix
joinOn = [
this.quoteTable(parent.as || parent.model.name),
this.quoteIdentifier(attrLeft)
].join('.');
} else {
joinOn = this.quoteIdentifier(asLeft + '.' + attrLeft);
}
}
joinOn += ' = ' + this.quoteIdentifier(asRight) + '.' + this.quoteIdentifier(fieldRight);
if (include.where) {
joinWhere = this.whereItemsQuery(include.where, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});
if (joinWhere) {
joinOn += ' AND ' + joinWhere;
}
}
return joinType + this.quoteTable(tableRight, asRight) + ' ON ' + joinOn;
},
/** /**
* Returns a query that starts a transaction. * Returns a query that starts a transaction.
* *
...@@ -2033,6 +2071,8 @@ var QueryGenerator = { ...@@ -2033,6 +2071,8 @@ var QueryGenerator = {
value = (value.$between || value.$notBetween).map(function (item) { value = (value.$between || value.$notBetween).map(function (item) {
return self.escape(item); return self.escape(item);
}).join(' AND '); }).join(' AND ');
} else if (value && value.$raw) {
value = value.$raw;
} else { } else {
if (_.isPlainObject(value)) { if (_.isPlainObject(value)) {
_.forOwn(value, function (item, key) { _.forOwn(value, function (item, key) {
......
...@@ -702,7 +702,7 @@ Instance.prototype.save = function(options) { ...@@ -702,7 +702,7 @@ Instance.prototype.save = function(options) {
return include.association.throughModel.create(values, {transaction: options.transaction, logging: options.logging}); return include.association.throughModel.create(values, {transaction: options.transaction, logging: options.logging});
}); });
} else { } else {
instance.set(include.association.identifier, self.get(self.Model.primaryKeyAttribute, {raw: true})); instance.set(include.association.foreignKey, self.get(self.Model.primaryKeyAttribute, {raw: true}));
return instance.save({transaction: options.transaction, logging: options.logging}); return instance.save({transaction: options.transaction, logging: options.logging});
} }
}); });
......
...@@ -227,7 +227,7 @@ var findAutoIncrementField = function() { ...@@ -227,7 +227,7 @@ var findAutoIncrementField = function() {
}.bind(this)); }.bind(this));
}; };
var conformOptions = function(options, self) { function conformOptions(options, self) {
if (!options.include) { if (!options.include) {
return; return;
} }
...@@ -241,9 +241,23 @@ var conformOptions = function(options, self) { ...@@ -241,9 +241,23 @@ var conformOptions = function(options, self) {
// convert all included elements to { model: Model } form // convert all included elements to { model: Model } form
options.include = options.include.map(function(include) { options.include = options.include.map(function(include) {
include = conformInclude(include, self);
if (!include.all) {
_.defaults(include, include.model.$scope);
}
return include;
});
}
function conformInclude(include, self) {
var model; var model;
if (include._pseudo) return include;
if (include instanceof Association) { if (include instanceof Association) {
if (include.target.name === self.name) { if (self && include.target.name === self.name) {
model = include.source; model = include.source;
} else { } else {
model = include.target; model = include.target;
...@@ -256,7 +270,7 @@ var conformOptions = function(options, self) { ...@@ -256,7 +270,7 @@ var conformOptions = function(options, self) {
include = { model: include }; include = { model: include };
} else if (_.isPlainObject(include)) { } else if (_.isPlainObject(include)) {
if (include.association) { if (include.association) {
if (include.association.target.name === self.name) { if (self && include.association.target.name === self.name) {
model = include.association.source; model = include.association.source;
} else { } else {
model = include.association.target; model = include.association.target;
...@@ -277,13 +291,8 @@ var conformOptions = function(options, self) { ...@@ -277,13 +291,8 @@ var conformOptions = function(options, self) {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.'); throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
} }
if (!include.all) {
_.defaults(include, model.$scope);
}
return include; return include;
}); }
};
var optClone = Model.prototype.__optClone = Model.prototype.$optClone = function(options) { var optClone = Model.prototype.__optClone = Model.prototype.$optClone = function(options) {
options = options || {}; options = options || {};
...@@ -410,28 +419,79 @@ var expandIncludeAllElement = function(includes, include) { ...@@ -410,28 +419,79 @@ var expandIncludeAllElement = function(includes, include) {
var validateIncludedElement; var validateIncludedElement;
var validateIncludedElements = function(options, tableNames) { var validateIncludedElements = function(options, tableNames) {
if (!options.model) options.model = this;
tableNames = tableNames || {}; tableNames = tableNames || {};
options.includeNames = []; options.includeNames = [];
options.includeMap = {}; options.includeMap = {};
/* Legacy */
options.hasSingleAssociation = false; options.hasSingleAssociation = false;
options.hasMultiAssociation = false; options.hasMultiAssociation = false;
if (!options.model) options.model = this; if (!options.parent) {
options.topModel = options.model;
// validate all included elements options.topLimit = options.limit;
var includes = options.include; }
for (var index = 0; index < includes.length; index++) {
var include = includes[index] = validateIncludedElement.call(this, includes[index], tableNames, options);
options.include = options.include.map(function (include) {
include = conformInclude(include);
include.parent = options; include.parent = options;
// associations that are required or have a required child and is not a ?:M association are candidates for the subquery
include.subQuery = !include.association.isMultiAssociation && (include.hasIncludeRequired || include.required); validateIncludedElement.call(options.model, include, tableNames, options);
if (include.duplicating === undefined) {
include.duplicating = include.association.isMultiAssociation;
}
include.hasDuplicating = include.hasDuplicating || include.duplicating;
include.hasRequired = include.hasRequired || include.required;
options.hasDuplicating = options.hasDuplicating || include.hasDuplicating;
options.hasRequired = options.hasRequired || include.required;
options.hasWhere = options.hasWhere || include.hasWhere || !!include.where;
return include;
});
options.include.forEach(function (include) {
include.hasParentWhere = options.hasParentWhere || !!options.where; include.hasParentWhere = options.hasParentWhere || !!options.where;
include.hasParentRequired = options.hasParentRequired || !!options.required; include.hasParentRequired = options.hasParentRequired || !!options.required;
if (include.subQuery !== false && options.hasDuplicating && options.topLimit) {
if (include.duplicating) {
include.subQuery = false;
include.subQueryFilter = include.hasRequired;
} else {
include.subQuery = include.hasRequired;
include.subQueryFilter = false;
}
} else {
include.subQuery = include.subQuery || false;
if (include.duplicating) {
include.subQueryFilter = include.subQuery;
include.subQuery = false;
} else {
include.subQueryFilter = false;
}
}
options.includeMap[include.as] = include; options.includeMap[include.as] = include;
options.includeNames.push(include.as); options.includeNames.push(include.as);
// Set top level options
if (options.topModel === options.model && options.subQuery === undefined && options.topLimit) {
if (include.subQuery) {
options.subQuery = include.subQuery;
} else if (include.hasDuplicating) {
options.subQuery = true;
}
}
/* Legacy */
options.hasIncludeWhere = options.hasIncludeWhere || include.hasIncludeWhere || !!include.where;
options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required;
if (include.association.isMultiAssociation || include.hasMultiAssociation) { if (include.association.isMultiAssociation || include.hasMultiAssociation) {
options.hasMultiAssociation = true; options.hasMultiAssociation = true;
} }
...@@ -439,25 +499,17 @@ var validateIncludedElements = function(options, tableNames) { ...@@ -439,25 +499,17 @@ var validateIncludedElements = function(options, tableNames) {
options.hasSingleAssociation = true; options.hasSingleAssociation = true;
} }
options.hasIncludeWhere = options.hasIncludeWhere || include.hasIncludeWhere || !!include.where; return include;
options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required; });
if (options.topModel === options.model && options.subQuery === undefined) {
options.subQuery = false;
} }
return options;
}; };
Model.$validateIncludedElements = validateIncludedElements; Model.$validateIncludedElements = validateIncludedElements;
validateIncludedElement = function(include, tableNames, options) { validateIncludedElement = function(include, tableNames, options) {
if (!include.hasOwnProperty('model') && !include.hasOwnProperty('association')) {
throw new Error('Include malformed. Expected attributes: model or association');
}
if (include.association && !include._pseudo && !include.model) {
if (include.association.source.name === this.name) {
include.model = include.association.target;
} else {
include.model = include.association.source;
}
}
tableNames[include.model.getTableName()] = true; tableNames[include.model.getTableName()] = true;
if (include.attributes && !options.raw) { if (include.attributes && !options.raw) {
...@@ -483,7 +535,19 @@ validateIncludedElement = function(include, tableNames, options) { ...@@ -483,7 +535,19 @@ validateIncludedElement = function(include, tableNames, options) {
// check if the current Model is actually associated with the passed Model - or it's a pseudo include // check if the current Model is actually associated with the passed Model - or it's a pseudo include
var association = include.association || this.getAssociation(include.model, include.as); var association = include.association || this.getAssociation(include.model, include.as);
if (association) {
if (!association) {
var msg = include.model.name;
if (include.as) {
msg += ' (' + include.as + ')';
}
msg += ' is not associated to ' + this.name + '!';
throw new Error(msg);
}
include.association = association; include.association = association;
include.as = association.as; include.as = association.as;
...@@ -498,7 +562,8 @@ validateIncludedElement = function(include, tableNames, options) { ...@@ -498,7 +562,8 @@ validateIncludedElement = function(include, tableNames, options) {
association: { association: {
isSingleAssociation: true isSingleAssociation: true
}, },
_pseudo: true _pseudo: true,
parent: include
}); });
...@@ -524,17 +589,6 @@ validateIncludedElement = function(include, tableNames, options) { ...@@ -524,17 +589,6 @@ validateIncludedElement = function(include, tableNames, options) {
} }
return include; return include;
} else {
var msg = include.model.name;
if (include.as) {
msg += ' (' + include.as + ')';
}
msg += ' is not associated to ' + this.name + '!';
throw new Error(msg);
}
}; };
var expandIncludeAll = Model.$expandIncludeAll = function(options) { var expandIncludeAll = Model.$expandIncludeAll = function(options) {
...@@ -2327,6 +2381,10 @@ Model.$injectScope = function (scope, options) { ...@@ -2327,6 +2381,10 @@ Model.$injectScope = function (scope, options) {
} }
}; };
Model.prototype.inspect = function() {
return this.name;
};
Utils._.extend(Model.prototype, associationsMixin); Utils._.extend(Model.prototype, associationsMixin);
Hooks.applyTo(Model); Hooks.applyTo(Model);
......
...@@ -96,7 +96,7 @@ CounterCache.prototype.injectHooks = function() { ...@@ -96,7 +96,7 @@ CounterCache.prototype.injectHooks = function() {
_targetQuery: function (id) { _targetQuery: function (id) {
var query = {}; var query = {};
query[association.identifier] = id; query[association.foreignKey] = id;
return query; return query;
}, },
...@@ -110,7 +110,7 @@ CounterCache.prototype.injectHooks = function() { ...@@ -110,7 +110,7 @@ CounterCache.prototype.injectHooks = function() {
}; };
fullUpdateHook = function (target, options) { fullUpdateHook = function (target, options) {
var targetId = target.get(association.identifier) var targetId = target.get(association.foreignKey)
, promises = []; , promises = [];
if (targetId) { if (targetId) {
...@@ -126,14 +126,14 @@ CounterCache.prototype.injectHooks = function() { ...@@ -126,14 +126,14 @@ CounterCache.prototype.injectHooks = function() {
atomicHooks = { atomicHooks = {
create: function (target, options) { create: function (target, options) {
var targetId = target.get(association.identifier); var targetId = target.get(association.foreignKey);
if (targetId) { if (targetId) {
return CounterUtil.increment(targetId, options); return CounterUtil.increment(targetId, options);
} }
}, },
update: function (target, options) { update: function (target, options) {
var targetId = target.get(association.identifier) var targetId = target.get(association.foreignKey)
, promises = []; , promises = [];
if (targetId && !previousTargetId) { if (targetId && !previousTargetId) {
...@@ -150,7 +150,7 @@ CounterCache.prototype.injectHooks = function() { ...@@ -150,7 +150,7 @@ CounterCache.prototype.injectHooks = function() {
return Promise.all(promises); return Promise.all(promises);
}, },
destroy: function (target, options) { destroy: function (target, options) {
var targetId = target.get(association.identifier); var targetId = target.get(association.foreignKey);
if (targetId) { if (targetId) {
return CounterUtil.decrement(targetId, options); return CounterUtil.decrement(targetId, options);
...@@ -160,7 +160,7 @@ CounterCache.prototype.injectHooks = function() { ...@@ -160,7 +160,7 @@ CounterCache.prototype.injectHooks = function() {
// previousDataValues are cleared before afterUpdate, so we need to save this here // previousDataValues are cleared before afterUpdate, so we need to save this here
association.target.addHook('beforeUpdate', function (target) { association.target.addHook('beforeUpdate', function (target) {
previousTargetId = target.previous(association.identifier); previousTargetId = target.previous(association.foreignKey);
}); });
if (this.options.atomic === false) { if (this.options.atomic === false) {
......
...@@ -102,7 +102,11 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -102,7 +102,11 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
it('only get objects that fulfill the options', function() { it('only get objects that fulfill the options', function() {
return this.User.find({where: {username: 'John'}}).then(function(john) { return this.User.find({where: {username: 'John'}}).then(function(john) {
return john.getTasks({where: {active: true}}); return john.getTasks({
where: {
active: true
}
});
}).then(function(tasks) { }).then(function(tasks) {
expect(tasks).to.have.length(1); expect(tasks).to.have.length(1);
}); });
......
...@@ -7,6 +7,9 @@ var chai = require('chai') ...@@ -7,6 +7,9 @@ var chai = require('chai')
, stub = sinon.stub , stub = sinon.stub
, Support = require(__dirname + '/../support') , Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types') , DataTypes = require(__dirname + '/../../../lib/data-types')
, BelongsTo = require(__dirname + '/../../../lib/associations/belongs-to')
, HasMany = require(__dirname + '/../../../lib/associations/has-many')
, HasOne = require(__dirname + '/../../../lib/associations/has-one')
, current = Support.sequelize , current = Support.sequelize
, Promise = current.Promise; , Promise = current.Promise;
...@@ -110,6 +113,230 @@ describe(Support.getTestDialectTeaser('belongsToMany'), function() { ...@@ -110,6 +113,230 @@ describe(Support.getTestDialectTeaser('belongsToMany'), function() {
}); });
}); });
describe('pseudo associations', function () {
it('should setup belongsTo relations to source and target from join model with defined foreign/other keys', function () {
var Product = this.sequelize.define('Product', {
title: DataTypes.STRING
})
, Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
})
, ProductTag = this.sequelize.define('ProductTag', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
},
priority: DataTypes.INTEGER
}, {
timestamps: false
});
Product.Tags = Product.belongsToMany(Tag, {through: ProductTag, foreignKey: 'productId', otherKey: 'tagId'});
Tag.Products = Tag.belongsToMany(Product, {through: ProductTag, foreignKey: 'tagId', otherKey: 'productId'});
expect(Product.Tags.toSource).to.be.an.instanceOf(BelongsTo);
expect(Product.Tags.toTarget).to.be.an.instanceOf(BelongsTo);
expect(Tag.Products.toSource).to.be.an.instanceOf(BelongsTo);
expect(Tag.Products.toTarget).to.be.an.instanceOf(BelongsTo);
expect(Product.Tags.toSource.foreignKey).to.equal(Product.Tags.foreignKey);
expect(Product.Tags.toTarget.foreignKey).to.equal(Product.Tags.otherKey);
expect(Tag.Products.toSource.foreignKey).to.equal(Tag.Products.foreignKey);
expect(Tag.Products.toTarget.foreignKey).to.equal(Tag.Products.otherKey);
expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4);
expect(Object.keys(ProductTag.rawAttributes)).to.deep.equal(['id', 'priority', 'productId', 'tagId']);
});
it('should setup hasOne relations to source and target from join model with defined foreign/other keys', function () {
var Product = this.sequelize.define('Product', {
title: DataTypes.STRING
})
, Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
})
, ProductTag = this.sequelize.define('ProductTag', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
},
priority: DataTypes.INTEGER
}, {
timestamps: false
});
Product.Tags = Product.belongsToMany(Tag, {through: ProductTag, foreignKey: 'productId', otherKey: 'tagId'});
Tag.Products = Tag.belongsToMany(Product, {through: ProductTag, foreignKey: 'tagId', otherKey: 'productId'});
expect(Product.Tags.manyFromSource).to.be.an.instanceOf(HasMany);
expect(Product.Tags.manyFromTarget).to.be.an.instanceOf(HasMany);
expect(Tag.Products.manyFromSource).to.be.an.instanceOf(HasMany);
expect(Tag.Products.manyFromTarget).to.be.an.instanceOf(HasMany);
expect(Product.Tags.manyFromSource.foreignKey).to.equal(Product.Tags.foreignKey);
expect(Product.Tags.manyFromTarget.foreignKey).to.equal(Product.Tags.otherKey);
expect(Tag.Products.manyFromSource.foreignKey).to.equal(Tag.Products.foreignKey);
expect(Tag.Products.manyFromTarget.foreignKey).to.equal(Tag.Products.otherKey);
expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4);
expect(Object.keys(ProductTag.rawAttributes)).to.deep.equal(['id', 'priority', 'productId', 'tagId']);
});
it('should setup hasOne relations to source and target from join model with defined foreign/other keys', function () {
var Product = this.sequelize.define('Product', {
title: DataTypes.STRING
})
, Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
})
, ProductTag = this.sequelize.define('ProductTag', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
},
priority: DataTypes.INTEGER
}, {
timestamps: false
});
Product.Tags = Product.belongsToMany(Tag, {through: ProductTag, foreignKey: 'productId', otherKey: 'tagId'});
Tag.Products = Tag.belongsToMany(Product, {through: ProductTag, foreignKey: 'tagId', otherKey: 'productId'});
expect(Product.Tags.oneFromSource).to.be.an.instanceOf(HasOne);
expect(Product.Tags.oneFromTarget).to.be.an.instanceOf(HasOne);
expect(Tag.Products.oneFromSource).to.be.an.instanceOf(HasOne);
expect(Tag.Products.oneFromTarget).to.be.an.instanceOf(HasOne);
expect(Product.Tags.oneFromSource.foreignKey).to.equal(Product.Tags.foreignKey);
expect(Product.Tags.oneFromTarget.foreignKey).to.equal(Product.Tags.otherKey);
expect(Tag.Products.oneFromSource.foreignKey).to.equal(Tag.Products.foreignKey);
expect(Tag.Products.oneFromTarget.foreignKey).to.equal(Tag.Products.otherKey);
expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4);
expect(Object.keys(ProductTag.rawAttributes)).to.deep.equal(['id', 'priority', 'productId', 'tagId']);
});
it('should setup belongsTo relations to source and target from join model with only foreign keys defined', function () {
var Product = this.sequelize.define('Product', {
title: DataTypes.STRING
})
, Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
})
, ProductTag = this.sequelize.define('ProductTag', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
},
priority: DataTypes.INTEGER
}, {
timestamps: false
});
Product.Tags = Product.belongsToMany(Tag, {through: ProductTag, foreignKey: 'product_ID'});
Tag.Products = Tag.belongsToMany(Product, {through: ProductTag, foreignKey: 'tag_ID'});
expect(Product.Tags.toSource).to.be.ok;
expect(Product.Tags.toTarget).to.be.ok;
expect(Tag.Products.toSource).to.be.ok;
expect(Tag.Products.toTarget).to.be.ok;
expect(Product.Tags.toSource.foreignKey).to.equal(Product.Tags.foreignKey);
expect(Product.Tags.toTarget.foreignKey).to.equal(Product.Tags.otherKey);
expect(Tag.Products.toSource.foreignKey).to.equal(Tag.Products.foreignKey);
expect(Tag.Products.toTarget.foreignKey).to.equal(Tag.Products.otherKey);
expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4);
expect(Object.keys(ProductTag.rawAttributes)).to.deep.equal(['id', 'priority', 'product_ID', 'tag_ID']);
});
it('should setup hasOne relations to source and target from join model with only foreign keys defined', function () {
var Product = this.sequelize.define('Product', {
title: DataTypes.STRING
})
, Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
})
, ProductTag = this.sequelize.define('ProductTag', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
},
priority: DataTypes.INTEGER
}, {
timestamps: false
});
Product.Tags = Product.belongsToMany(Tag, {through: ProductTag, foreignKey: 'product_ID'});
Tag.Products = Tag.belongsToMany(Product, {through: ProductTag, foreignKey: 'tag_ID'});
expect(Product.Tags.oneFromSource).to.be.an.instanceOf(HasOne);
expect(Product.Tags.oneFromTarget).to.be.an.instanceOf(HasOne);
expect(Tag.Products.oneFromSource).to.be.an.instanceOf(HasOne);
expect(Tag.Products.oneFromTarget).to.be.an.instanceOf(HasOne);
expect(Product.Tags.oneFromSource.foreignKey).to.equal(Product.Tags.foreignKey);
expect(Product.Tags.oneFromTarget.foreignKey).to.equal(Product.Tags.otherKey);
expect(Tag.Products.oneFromSource.foreignKey).to.equal(Tag.Products.foreignKey);
expect(Tag.Products.oneFromTarget.foreignKey).to.equal(Tag.Products.otherKey);
expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4);
expect(Object.keys(ProductTag.rawAttributes)).to.deep.equal(['id', 'priority', 'product_ID', 'tag_ID']);
});
it('should setup belongsTo relations to source and target from join model with no foreign keys defined', function () {
var Product = this.sequelize.define('Product', {
title: DataTypes.STRING
})
, Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
})
, ProductTag = this.sequelize.define('ProductTag', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
},
priority: DataTypes.INTEGER
}, {
timestamps: false
});
Product.Tags = Product.belongsToMany(Tag, {through: ProductTag});
Tag.Products = Tag.belongsToMany(Product, {through: ProductTag});
expect(Product.Tags.toSource).to.be.ok;
expect(Product.Tags.toTarget).to.be.ok;
expect(Tag.Products.toSource).to.be.ok;
expect(Tag.Products.toTarget).to.be.ok;
expect(Product.Tags.toSource.foreignKey).to.equal(Product.Tags.foreignKey);
expect(Product.Tags.toTarget.foreignKey).to.equal(Product.Tags.otherKey);
expect(Tag.Products.toSource.foreignKey).to.equal(Tag.Products.foreignKey);
expect(Tag.Products.toTarget.foreignKey).to.equal(Tag.Products.otherKey);
expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4);
expect(Object.keys(ProductTag.rawAttributes)).to.deep.equal(['id', 'priority', 'ProductId', 'TagId']);
});
});
describe('self-associations', function () { describe('self-associations', function () {
it('does not pair multiple self associations with different through arguments', function () { it('does not pair multiple self associations with different through arguments', function () {
var User = current.define('user', {}) var User = current.define('user', {})
......
...@@ -4,9 +4,10 @@ ...@@ -4,9 +4,10 @@
var chai = require('chai') var chai = require('chai')
, expect = chai.expect , expect = chai.expect
, Support = require(__dirname + '/../support') , Support = require(__dirname + '/../support')
, Sequelize = require(__dirname + '/../../../index')
, current = Support.sequelize; , current = Support.sequelize;
describe(Support.getTestDialectTeaser('Include'), function() { describe(Support.getTestDialectTeaser('Model'), function() {
describe('all', function (){ describe('all', function (){
var Referral = current.define('referal'); var Referral = current.define('referal');
...@@ -23,4 +24,229 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -23,4 +24,229 @@ describe(Support.getTestDialectTeaser('Include'), function() {
]); ]);
}); });
}); });
describe('$validateIncludedElements', function () {
beforeEach(function () {
this.User = this.sequelize.define('User');
this.Task = this.sequelize.define('Task', {
title: Sequelize.STRING
});
this.Company = this.sequelize.define('Company', {
name: Sequelize.STRING
});
this.User.Tasks = this.User.hasMany(this.Task);
this.User.Company = this.User.belongsTo(this.Company);
this.Company.Employees = this.Company.hasMany(this.User);
this.Company.Owner = this.Company.belongsTo(this.User, {as: 'Owner', foreignKey: 'ownerId'});
});
describe('duplicating', function () {
it('should tag a hasMany association as duplicating: true if undefined', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
this.User.Tasks
]
});
expect(options.include[0].duplicating).to.equal(true);
});
it('should respect include.duplicating for a hasMany', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks, duplicating: false}
]
});
expect(options.include[0].duplicating).to.equal(false);
});
});
describe('subQuery', function () {
it('should be true if theres a duplicating association', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks}
],
limit: 3
});
expect(options.subQuery).to.equal(true);
});
it('should be false if theres a duplicating association but no limit', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks}
],
limit: null
});
expect(options.subQuery).to.equal(false);
});
it('should be true if theres a nested duplicating association', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, include: [
this.Company.Employees
]}
],
limit: 3
});
expect(options.subQuery).to.equal(true);
});
it('should be false if theres a nested duplicating association but no limit', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, include: [
this.Company.Employees
]}
],
limit: null
});
expect(options.subQuery).to.equal(false);
});
it('should tag a required hasMany association', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks, required: true}
],
limit: 3
});
expect(options.subQuery).to.equal(true);
expect(options.include[0].subQuery).to.equal(false);
expect(options.include[0].subQueryFilter).to.equal(true);
});
it('should not tag a required hasMany association with duplicating false', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks, required: true, duplicating: false}
],
limit: 3
});
expect(options.subQuery).to.equal(false);
expect(options.include[0].subQuery).to.equal(false);
expect(options.include[0].subQueryFilter).to.equal(false);
});
it('should tag a hasMany association with where', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks, where: {title: Math.random().toString()}}
],
limit: 3
});
expect(options.subQuery).to.equal(true);
expect(options.include[0].subQuery).to.equal(false);
expect(options.include[0].subQueryFilter).to.equal(true);
});
it('should not tag a hasMany association with where and duplicating false', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Tasks, where: {title: Math.random().toString()}, duplicating: false}
],
limit: 3
});
expect(options.subQuery).to.equal(false);
expect(options.include[0].subQuery).to.equal(false);
expect(options.include[0].subQueryFilter).to.equal(false);
});
it('should tag a required belongsTo alongside a duplicating association', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, required: true},
{association: this.User.Tasks}
],
limit: 3
});
expect(options.subQuery).to.equal(true);
expect(options.include[0].subQuery).to.equal(true);
});
it('should not tag a required belongsTo alongside a duplicating association with duplicating false', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, required: true},
{association: this.User.Tasks, duplicating: false}
],
limit: 3
});
expect(options.subQuery).to.equal(false);
expect(options.include[0].subQuery).to.equal(false);
});
it('should tag a belongsTo association with where alongside a duplicating association', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, where: {name: Math.random().toString()}},
{association: this.User.Tasks}
],
limit: 3
});
expect(options.subQuery).to.equal(true);
expect(options.include[0].subQuery).to.equal(true);
});
it('should tag a required belongsTo association alongside a duplicating association with a nested belongsTo', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, required: true, include: [
this.Company.Owner
]},
this.User.Tasks
],
limit: 3
});
expect(options.subQuery).to.equal(true);
expect(options.include[0].subQuery).to.equal(true);
expect(options.include[0].include[0].subQuery).to.equal(false);
expect(options.include[0].include[0].parent.subQuery).to.equal(true);
});
it('should tag a belongsTo association with where alongside a duplicating association with duplicating false', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{association: this.User.Company, where: {name: Math.random().toString()}},
{association: this.User.Tasks, duplicating: false}
],
limit: 3
});
expect(options.subQuery).to.equal(false);
expect(options.include[0].subQuery).to.equal(false);
});
});
});
}); });
\ No newline at end of file
'use strict';
/* jshint -W110 */
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, util = require('util')
, Sequelize = require(__dirname + '/../../../lib/sequelize')
, expectsql = Support.expectsql
, current = Support.sequelize
, sql = current.dialect.QueryGenerator;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
suite(Support.getTestDialectTeaser('SQL'), function() {
suite('joinIncludeQuery', function () {
var testsql = function (params, options, expectation) {
if (expectation === undefined) {
expectation = options;
options = undefined;
}
test(util.inspect(params, {depth: 10})+(options && ', '+util.inspect(options) || ''), function () {
return expectsql(sql.joinIncludeQuery(params, options), expectation);
});
};
var User = current.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id_user'
},
companyId: {
type: DataTypes.INTEGER,
field: 'company_id'
}
}, {
tableName: 'user'
});
var Task = current.define('Task', {
title: Sequelize.STRING,
userId: {
type: DataTypes.INTEGER,
field: 'user_id'
}
}, {
tableName: 'task'
});
var Company = current.define('Company', {
name: Sequelize.STRING,
ownerId: {
type: Sequelize.INTEGER,
field: 'owner_id'
}
}, {
tableName: 'company'
});
var Profession = current.define('Profession', {
name: Sequelize.STRING
}, {
tableName: 'profession'
});
User.Tasks = User.hasMany(Task, {as: 'Tasks', foreignKey: 'userId'});
User.Company = User.belongsTo(Company, {foreignKey: 'companyId'});
User.Profession = User.belongsTo(Profession, {foreignKey: 'professionId'});
Company.Employees = Company.hasMany(User, {as: 'Employees', foreignKey: 'companyId'});
Company.Owner = Company.belongsTo(User, {as: 'Owner', foreignKey: 'ownerId'});
/*
* BelongsTo
*/
testsql({
model: User,
subQuery: false,
include: Sequelize.Model.$validateIncludedElements({
model: User,
include: [
User.Company
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
User.Company
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, required: false, where: {
name: 'ABC'
}},
User.Tasks
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = 'ABC'"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, include: [
Company.Owner
]}
]
}).include[0].include[0]
}, {
default: "LEFT OUTER JOIN [user] AS [Company.Owner] ON [Company].[owner_id] = [Company.Owner].[id_user]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, include: [
{association: Company.Owner, include: [
User.Profession
]}
]}
]
}).include[0].include[0].include[0]
}, {
default: "LEFT OUTER JOIN [profession] AS [Company.Owner.Profession] ON [Company.Owner].[professionId] = [Company.Owner.Profession].[id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, required: true, include: [
Company.Owner
]},
User.Tasks
]
}).include[0].include[0]
}, {
default: "LEFT OUTER JOIN [user] AS [Company.Owner] ON [Company.ownerId] = [Company.Owner].[id_user]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, required: true}
]
}).include[0]
}, {
default: "INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
});
/*
* HasMany
*/
testsql({
model: User,
subQuery: false,
include: Sequelize.Model.$validateIncludedElements({
model: User,
include: [
User.Tasks
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
User.Tasks
]
}).include[0]
}, {
// The primary key of the main model will be aliased because it's coming from a subquery that the :M join is not a part of
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]"
});
});
});
\ No newline at end of file
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
/* jshint -W110 */ /* jshint -W110 */
var Support = require(__dirname + '/../support') var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types') , DataTypes = require(__dirname + '/../../../lib/data-types')
, Model = require(__dirname + '/../../../lib/model')
, expectsql = Support.expectsql , expectsql = Support.expectsql
, current = Support.sequelize , current = Support.sequelize
, sql = current.dialect.QueryGenerator; , sql = current.dialect.QueryGenerator;
...@@ -40,21 +41,20 @@ describe(Support.getTestDialectTeaser('SQL'), function() { ...@@ -40,21 +41,20 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
freezeTableName: true freezeTableName: true
}); });
User.Posts = User.hasMany(Post, {foreignKey: 'user_id'});
expectsql(sql.selectQuery('User', { expectsql(sql.selectQuery('User', {
attributes: ['name', 'age'], attributes: ['name', 'age'],
include: [ { include: Model.$validateIncludedElements({
model: Post, include: [{
attributes: ['title'], attributes: ['title'],
association: { association: User.Posts
source: User, }],
target: Post, model: User
identifier: 'user_id' }).include,
}, model: User
as: 'Post' }, User), {
} ], default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];'
tableAs: 'User'
}), {
default: 'SELECT [User].[name], [User].[age], [Post].[title] AS [Post.title] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Post] ON [User].[id] = [Post].[user_id];'
}); });
}); });
}); });
...@@ -98,22 +98,21 @@ describe(Support.getTestDialectTeaser('SQL'), function() { ...@@ -98,22 +98,21 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
freezeTableName: true freezeTableName: true
}); });
User.Posts = User.hasMany(Post, {foreignKey: 'user_id'});
expectsql(sql.selectQuery('User', { expectsql(sql.selectQuery('User', {
attributes: ['name', 'age'], attributes: ['name', 'age'],
include: [ { include: Model.$validateIncludedElements({
model: Post, include: [{
attributes: ['title'], attributes: ['title'],
association: { association: User.Posts
source: Post, }],
target: User, model: User
identifier: 'user_id' }).include,
}, model: User
as: 'Post' }, User), {
} ], default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];',
tableAs: 'User' postgres: 'SELECT User.name, User.age, Posts.id AS "Posts.id", Posts.title AS "Posts.title" FROM User AS User LEFT OUTER JOIN Post AS Posts ON User.id = Posts.user_id;'
}), {
default: 'SELECT [User].[name], [User].[age], [Post].[title] AS [Post.title] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Post] ON [User].[id] = [Post].[user_id];',
postgres: 'SELECT User.name, User.age, Post.title AS "Post.title" FROM User AS User LEFT OUTER JOIN Post AS Post ON User.id = Post.user_id;'
}); });
}); });
......
...@@ -314,6 +314,14 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -314,6 +314,14 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
}); });
}); });
suite('$raw', function () {
testsql('rank', {
$raw: 'AGHJZ'
}, {
default: '[rank] = AGHJZ'
});
});
suite('$like', function () { suite('$like', function () {
testsql('username', { testsql('username', {
$like: '%swagger' $like: '%swagger'
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!