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

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) {
return obj;
}, this);
};
Association.prototype.inspect = function() {
return this.as;
};
module.exports = Association;
......@@ -4,6 +4,9 @@ var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Association = require('./base')
, BelongsTo = require('./belongs-to')
, HasMany = require('./has-many')
, HasOne = require('./has-one')
, CounterCache = require('../plugins/counter-cache')
, util = require('util');
......@@ -323,18 +326,18 @@ BelongsToMany.prototype.injectAttributes = function() {
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('_');
var uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey;
}
if (!this.through.model.rawAttributes[this.identifier]) {
this.through.model.rawAttributes[this.identifier] = {
if (!this.through.model.rawAttributes[this.foreignKey]) {
this.through.model.rawAttributes[this.foreignKey] = {
_autoGenerated: true
};
}
if (!this.through.model.rawAttributes[this.foreignIdentifier]) {
this.through.model.rawAttributes[this.foreignIdentifier] = {
if (!this.through.model.rawAttributes[this.otherKey]) {
this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true
};
}
......@@ -345,8 +348,8 @@ BelongsToMany.prototype.injectAttributes = function() {
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;
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
......@@ -356,25 +359,58 @@ BelongsToMany.prototype.injectAttributes = function() {
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;
targetAttribute.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) targetAttribute.onDelete = '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.foreignIdentifier] = _.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
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.foreignIdentifierField = this.through.model.rawAttributes[this.foreignIdentifier].field || this.foreignIdentifier;
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
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.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);
return this;
......@@ -404,7 +440,7 @@ BelongsToMany.prototype.injectGetter = function(obj) {
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[association.identifier] = instance.get(association.source.primaryKeyAttribute);
throughWhere[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
if (through.scope) {
_.assign(throughWhere, through.scope);
......@@ -412,19 +448,10 @@ BelongsToMany.prototype.injectGetter = function(obj) {
options.include = options.include || [];
options.include.push({
model: through.model,
as: through.model.name,
association: association.oneFromTarget,
attributes: options.joinTableAttributes,
association: {
isSingleAssociation: true,
source: association.target,
target: association.source,
identifier: association.foreignIdentifier,
identifierField: association.foreignIdentifierField
},
required: true,
where: throughWhere,
_pseudo: true
where: throughWhere
});
}
......
......@@ -25,14 +25,7 @@ var BelongsTo = function(source, target, options) {
this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target);
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.foreignKey = this.options.foreignKey;
}
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
......@@ -44,8 +37,15 @@ var BelongsTo = function(source, target, options) {
this.options.name = this.target.options.name;
}
if (!this.options.foreignKey) {
this.options.foreignKey = _.camelizeIf(
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.as, this.source.options.underscored),
this.target.primaryKeyAttribute
......@@ -54,15 +54,14 @@ var BelongsTo = function(source, target, options) {
);
}
this.identifier = this.foreignKey || _.camelizeIf(
[
_.underscoredIf(this.options.name.singular, this.target.options.underscored),
this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
);
this.identifier = this.foreignKey;
this.targetIdentifier = this.options.targetKey || this.target.primaryKeyAttribute;
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
......@@ -107,15 +106,19 @@ util.inherits(BelongsTo, Association);
BelongsTo.prototype.injectAttributes = function() {
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) {
this.options.onDelete = this.options.onDelete || 'SET NULL';
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);
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
this.source.refreshAttributes();
......@@ -130,9 +133,9 @@ BelongsTo.prototype.injectGetter = function(instancePrototype) {
instancePrototype[this.accessors.get] = function(options) {
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 = {
$and: [
......@@ -167,16 +170,16 @@ BelongsTo.prototype.injectSetter = function(instancePrototype) {
var value = associatedInstance;
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;
options = _.extend({
fields: [association.identifier],
allowNull: [association.identifier],
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}, options);
......
......@@ -28,18 +28,12 @@ var HasMany = function(source, target, options) {
this.isMultiAssociation = true;
this.isSelfAssociation = this.source === this.target;
this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.options.through) {
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
......@@ -65,6 +59,31 @@ var HasMany = function(source, target, options) {
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;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
......@@ -180,26 +199,19 @@ util.inherits(HasMany, Association);
// the id is in the target table
// or in an extra table which connects two tables
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 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) {
constraintOptions.onDelete = constraintOptions.onDelete || 'SET NULL';
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);
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.source.refreshAttributes();
......@@ -225,7 +237,7 @@ HasMany.prototype.injectGetter = function(obj) {
options.where = {
$and: [
new Utils.where(
association.target.rawAttributes[association.identifier],
association.target.rawAttributes[association.foreignKey],
this.get(association.source.primaryKeyAttribute, {raw: true})
),
scopeWhere,
......@@ -335,7 +347,7 @@ HasMany.prototype.injectSetter = function(obj) {
if (obsoleteAssociations.length > 0) {
update = {};
update[association.identifier] = null;
update[association.foreignKey] = null;
updateWhere = {};
......@@ -355,7 +367,7 @@ HasMany.prototype.injectSetter = function(obj) {
updateWhere = {};
update = {};
update[association.identifier] = instance.get(association.source.primaryKeyAttribute);
update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
......@@ -383,7 +395,7 @@ HasMany.prototype.injectSetter = function(obj) {
newInstances = association.toInstanceArray(newInstances);
update[association.identifier] = instance.get(association.source.primaryKeyAttribute);
update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = newInstances.map(function (unassociatedObject) {
......@@ -403,10 +415,10 @@ HasMany.prototype.injectSetter = function(obj) {
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
var update = {};
update[association.identifier] = null;
update[association.foreignKey] = null;
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); });
return association.target.unscoped().update(
......@@ -444,8 +456,8 @@ HasMany.prototype.injectCreator = function(obj) {
});
}
values[association.identifier] = instance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.identifier);
values[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
......
......@@ -23,14 +23,7 @@ var HasOne = function(srcModel, targetModel, options) {
this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target);
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.foreignKey = this.options.foreignKey;
}
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
......@@ -42,8 +35,15 @@ var HasOne = function(srcModel, targetModel, options) {
this.options.name = this.target.options.name;
}
if (!this.options.foreignKey) {
this.options.foreignKey = _.camelizeIf(
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(Utils.singularize(this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute
......@@ -52,18 +52,14 @@ var HasOne = function(srcModel, targetModel, options) {
);
}
this.identifier = this.foreignKey || _.camelizeIf(
[
_.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
this.sourceIdentifier = this.source.primaryKeyAttribute;
this.associationAccessor = this.as;
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
var singular = Utils.uppercaseFirst(this.options.name.singular);
......@@ -103,18 +99,18 @@ util.inherits(HasOne, Association);
// the id is in the target table
HasOne.prototype.injectAttributes = function() {
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);
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) {
this.options.onDelete = this.options.onDelete || 'SET NULL';
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
this.target.refreshAttributes();
......@@ -129,7 +125,7 @@ HasOne.prototype.injectGetter = function(instancePrototype) {
instancePrototype[this.accessors.get] = function(options) {
var where = {};
where[association.identifier] = this.get(association.sourceIdentifier);
where[association.foreignKey] = this.get(association.sourceIdentifier);
options = association.target.__optClone(options) || {};
......@@ -167,10 +163,10 @@ HasOne.prototype.injectSetter = function(instancePrototype) {
options.scope = false;
return instance[association.accessors.get](options).then(function(oldInstance) {
if (oldInstance) {
oldInstance[association.identifier] = null;
oldInstance[association.foreignKey] = null;
return oldInstance.save(_.extend({}, options, {
fields: [association.identifier],
allowNull: [association.identifier],
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}));
}
......@@ -183,7 +179,7 @@ HasOne.prototype.injectSetter = function(instancePrototype) {
isNewRecord: false
});
}
associatedInstance.set(association.identifier, instance.get(association.sourceIdentifier));
associatedInstance.set(association.foreignKey, instance.get(association.sourceIdentifier));
return associatedInstance.save(options);
}
return null;
......@@ -201,8 +197,8 @@ HasOne.prototype.injectCreator = function(instancePrototype) {
values = values || {};
options = options || {};
values[association.identifier] = instance.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.identifier);
values[association.foreignKey] = instance.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
......
......@@ -7,6 +7,7 @@ var Utils = require('../../utils')
, _ = require('lodash')
, util = require('util')
, Dottie = require('dottie')
, BelongsTo = require('../../associations/belongs-to')
, uuid = require('node-uuid');
/* istanbul ignore next */
......@@ -1041,6 +1042,7 @@ var QueryGenerator = {
, association = include.association
, through = include.through
, joinType = include.required ? ' INNER JOIN ' : ' LEFT OUTER JOIN '
, parentIsTop = !include.parent.association && include.parent.model.name === options.model.name
, whereOptions = Utils._.clone(options)
, targetWhere;
......@@ -1138,7 +1140,8 @@ var QueryGenerator = {
// Used by both join and subquery where
// 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) + ' = ';
} else {
sourceJoinOn = self.quoteTable(tableSource) + '.' + self.quoteIdentifier(attrSource) + ' = ';
......@@ -1211,22 +1214,14 @@ var QueryGenerator = {
if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
$query = self.selectQuery(topInclude.through.model.getTableName(), {
attributes: [topInclude.through.model.primaryKeyAttributes[0]],
include: [{
model: topInclude.model,
as: topInclude.model.name,
attributes: [],
association: {
associationType: 'BelongsTo',
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
}],
include: Model.$validateIncludedElements({
model: topInclude.through.model,
include: [{
association: topInclude.association.toTarget,
required: true
}]
}).include,
model: topInclude.through.model,
where: self.sequelize.and(
self.sequelize.asIs([
self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(topParent.model.primaryKeyAttributes[0]),
......@@ -1262,63 +1257,30 @@ var QueryGenerator = {
}
}
} else {
var left = association.source
, right = association.target
, primaryKeysLeft = left.primaryKeyAttributes
, primaryKeysRight = right.primaryKeyAttributes
, 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);
if (subQuery && include.subQueryFilter) {
var associationWhere = {}
, $query
, subQueryWhere;
joinOn += ' = ' + self.quoteTable(tableRight) + '.' + self.quoteIdentifier(attrRight);
subQueryJoinOn += ' = ' + self.quoteTable(tableRight) + '.' + self.quoteIdentifier(attrRight);
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;
}
}
associationWhere[association.identifierField] = {
$raw: self.quoteTable(parentTable) + '.' + self.quoteIdentifier(association.source.primaryKeyAttribute)
};
// 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 = {};
// Creating the as-is where for the subQuery, checks that the required association exists
var $query = self.selectQuery(include.model.getTableName(), {
tableAs: as,
attributes: [attrRight],
where: self.sequelize.asIs(subQueryJoinOn ? [subQueryJoinOn] : [joinOn]),
$query = self.selectQuery(include.model.getTableName(), {
attributes: [association.identifierField],
where: {
$and: [
associationWhere,
include.where || {}
]
},
limit: 1
}, include.model);
var subQueryWhere = self.sequelize.asIs([
subQueryWhere = self.sequelize.asIs([
'(',
$query.replace(/\;$/, ''),
')',
......@@ -1334,8 +1296,11 @@ var QueryGenerator = {
}
}
// Generate join SQL
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ' + joinOn;
joinQueryItem = ' ' + self.joinIncludeQuery({
model: mainModel,
subQuery: options.subQuery,
include: include
});
}
if (include.subQuery && subQuery) {
......@@ -1515,6 +1480,79 @@ var QueryGenerator = {
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.
*
......@@ -2033,6 +2071,8 @@ var QueryGenerator = {
value = (value.$between || value.$notBetween).map(function (item) {
return self.escape(item);
}).join(' AND ');
} else if (value && value.$raw) {
value = value.$raw;
} else {
if (_.isPlainObject(value)) {
_.forOwn(value, function (item, key) {
......
......@@ -702,7 +702,7 @@ Instance.prototype.save = function(options) {
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}));
instance.set(include.association.foreignKey, self.get(self.Model.primaryKeyAttribute, {raw: true}));
return instance.save({transaction: options.transaction, logging: options.logging});
}
});
......
......@@ -227,7 +227,7 @@ var findAutoIncrementField = function() {
}.bind(this));
};
var conformOptions = function(options, self) {
function conformOptions(options, self) {
if (!options.include) {
return;
}
......@@ -241,49 +241,58 @@ var conformOptions = function(options, self) {
// convert all included elements to { model: Model } form
options.include = options.include.map(function(include) {
var model;
if (include instanceof Association) {
if (include.target.name === self.name) {
model = include.source;
} else {
model = include.target;
}
include = conformInclude(include, self);
include = { model: model, association: include, as: include.as };
} else if (include instanceof Model) {
model = include;
if (!include.all) {
_.defaults(include, include.model.$scope);
}
include = { model: include };
} else if (_.isPlainObject(include)) {
if (include.association) {
if (include.association.target.name === self.name) {
model = include.association.source;
} else {
model = include.association.target;
}
return include;
});
}
if (!include.model) {
include.model = model;
}
if (!include.as) {
include.as = include.association.as;
}
function conformInclude(include, self) {
var model;
if (include._pseudo) return include;
if (include instanceof Association) {
if (self && include.target.name === self.name) {
model = include.source;
} else {
model = include.target;
}
include = { model: model, association: include, as: include.as };
} else if (include instanceof Model) {
model = include;
include = { model: include };
} else if (_.isPlainObject(include)) {
if (include.association) {
if (self && include.association.target.name === self.name) {
model = include.association.source;
} else {
model = include.model;
model = include.association.target;
}
conformOptions(include, model);
if (!include.model) {
include.model = model;
}
if (!include.as) {
include.as = include.association.as;
}
} else {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
model = include.model;
}
if (!include.all) {
_.defaults(include, model.$scope);
}
conformOptions(include, model);
} else {
throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.');
}
return include;
});
};
return include;
}
var optClone = Model.prototype.__optClone = Model.prototype.$optClone = function(options) {
options = options || {};
......@@ -410,28 +419,79 @@ var expandIncludeAllElement = function(includes, include) {
var validateIncludedElement;
var validateIncludedElements = function(options, tableNames) {
if (!options.model) options.model = this;
tableNames = tableNames || {};
options.includeNames = [];
options.includeMap = {};
/* Legacy */
options.hasSingleAssociation = false;
options.hasMultiAssociation = false;
if (!options.parent) {
options.topModel = options.model;
options.topLimit = options.limit;
}
if (!options.model) options.model = this;
options.include = options.include.map(function (include) {
include = conformInclude(include);
include.parent = options;
validateIncludedElement.call(options.model, include, tableNames, options);
// validate all included elements
var includes = options.include;
for (var index = 0; index < includes.length; index++) {
var include = includes[index] = validateIncludedElement.call(this, includes[index], tableNames, options);
if (include.duplicating === undefined) {
include.duplicating = include.association.isMultiAssociation;
}
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);
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.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.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) {
options.hasMultiAssociation = true;
}
......@@ -439,25 +499,17 @@ var validateIncludedElements = function(options, tableNames) {
options.hasSingleAssociation = true;
}
options.hasIncludeWhere = options.hasIncludeWhere || include.hasIncludeWhere || !!include.where;
options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required;
return include;
});
if (options.topModel === options.model && options.subQuery === undefined) {
options.subQuery = false;
}
return options;
};
Model.$validateIncludedElements = validateIncludedElements;
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;
if (include.attributes && !options.raw) {
......@@ -483,58 +535,60 @@ validateIncludedElement = function(include, tableNames, options) {
// 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);
if (association) {
include.association = association;
include.as = association.as;
// If through, we create a pseudo child include, to ease our parsing later on
if (include.association.through && Object(include.association.through.model) === include.association.through.model) {
if (!include.include) include.include = [];
var through = include.association.through;
include.through = Utils._.defaults(include.through || {}, {
model: through.model,
as: through.model.name,
association: {
isSingleAssociation: true
},
_pseudo: true
});
if (through.scope) {
include.through.where = include.through.where ? { $and: [include.through.where, through.scope]} : through.scope;
}
if (!association) {
var msg = include.model.name;
include.include.push(include.through);
tableNames[through.tableName] = true;
if (include.as) {
msg += ' (' + include.as + ')';
}
if (include.required === undefined) {
include.required = !!include.where;
}
msg += ' is not associated to ' + this.name + '!';
if (include.association.scope) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
}
throw new Error(msg);
}
// Validate child includes
if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames, options);
}
include.association = association;
include.as = association.as;
// If through, we create a pseudo child include, to ease our parsing later on
if (include.association.through && Object(include.association.through.model) === include.association.through.model) {
if (!include.include) include.include = [];
var through = include.association.through;
include.through = Utils._.defaults(include.through || {}, {
model: through.model,
as: through.model.name,
association: {
isSingleAssociation: true
},
_pseudo: true,
parent: include
});
return include;
} else {
var msg = include.model.name;
if (include.as) {
msg += ' (' + include.as + ')';
if (through.scope) {
include.through.where = include.through.where ? { $and: [include.through.where, through.scope]} : through.scope;
}
msg += ' is not associated to ' + this.name + '!';
include.include.push(include.through);
tableNames[through.tableName] = true;
}
throw new Error(msg);
if (include.required === undefined) {
include.required = !!include.where;
}
if (include.association.scope) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
}
// Validate child includes
if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames, options);
}
return include;
};
var expandIncludeAll = Model.$expandIncludeAll = function(options) {
......@@ -2327,6 +2381,10 @@ Model.$injectScope = function (scope, options) {
}
};
Model.prototype.inspect = function() {
return this.name;
};
Utils._.extend(Model.prototype, associationsMixin);
Hooks.applyTo(Model);
......
......@@ -96,7 +96,7 @@ CounterCache.prototype.injectHooks = function() {
_targetQuery: function (id) {
var query = {};
query[association.identifier] = id;
query[association.foreignKey] = id;
return query;
},
......@@ -110,7 +110,7 @@ CounterCache.prototype.injectHooks = function() {
};
fullUpdateHook = function (target, options) {
var targetId = target.get(association.identifier)
var targetId = target.get(association.foreignKey)
, promises = [];
if (targetId) {
......@@ -126,14 +126,14 @@ CounterCache.prototype.injectHooks = function() {
atomicHooks = {
create: function (target, options) {
var targetId = target.get(association.identifier);
var targetId = target.get(association.foreignKey);
if (targetId) {
return CounterUtil.increment(targetId, options);
}
},
update: function (target, options) {
var targetId = target.get(association.identifier)
var targetId = target.get(association.foreignKey)
, promises = [];
if (targetId && !previousTargetId) {
......@@ -150,7 +150,7 @@ CounterCache.prototype.injectHooks = function() {
return Promise.all(promises);
},
destroy: function (target, options) {
var targetId = target.get(association.identifier);
var targetId = target.get(association.foreignKey);
if (targetId) {
return CounterUtil.decrement(targetId, options);
......@@ -160,7 +160,7 @@ CounterCache.prototype.injectHooks = function() {
// previousDataValues are cleared before afterUpdate, so we need to save this here
association.target.addHook('beforeUpdate', function (target) {
previousTargetId = target.previous(association.identifier);
previousTargetId = target.previous(association.foreignKey);
});
if (this.options.atomic === false) {
......
......@@ -102,7 +102,11 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
it('only get objects that fulfill the options', function() {
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) {
expect(tasks).to.have.length(1);
});
......
......@@ -7,6 +7,9 @@ var chai = require('chai')
, stub = sinon.stub
, Support = require(__dirname + '/../support')
, 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
, Promise = current.Promise;
......@@ -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 () {
it('does not pair multiple self associations with different through arguments', function () {
var User = current.define('user', {})
......
......@@ -4,9 +4,10 @@
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, Sequelize = require(__dirname + '/../../../index')
, current = Support.sequelize;
describe(Support.getTestDialectTeaser('Include'), function() {
describe(Support.getTestDialectTeaser('Model'), function() {
describe('all', function (){
var Referral = current.define('referal');
......@@ -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 @@
/* jshint -W110 */
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, Model = require(__dirname + '/../../../lib/model')
, expectsql = Support.expectsql
, current = Support.sequelize
, sql = current.dialect.QueryGenerator;
......@@ -40,21 +41,20 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
freezeTableName: true
});
User.Posts = User.hasMany(Post, {foreignKey: 'user_id'});
expectsql(sql.selectQuery('User', {
attributes: ['name', 'age'],
include: [ {
model: Post,
attributes: ['title'],
association: {
source: User,
target: Post,
identifier: 'user_id'
},
as: 'Post'
} ],
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];'
include: Model.$validateIncludedElements({
include: [{
attributes: ['title'],
association: User.Posts
}],
model: User
}).include,
model: User
}, 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];'
});
});
});
......@@ -98,22 +98,21 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
freezeTableName: true
});
User.Posts = User.hasMany(Post, {foreignKey: 'user_id'});
expectsql(sql.selectQuery('User', {
attributes: ['name', 'age'],
include: [ {
model: Post,
attributes: ['title'],
association: {
source: Post,
target: User,
identifier: 'user_id'
},
as: 'Post'
} ],
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];',
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;'
include: Model.$validateIncludedElements({
include: [{
attributes: ['title'],
association: User.Posts
}],
model: User
}).include,
model: User
}, 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];',
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;'
});
});
......
......@@ -314,6 +314,14 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
});
});
suite('$raw', function () {
testsql('rank', {
$raw: 'AGHJZ'
}, {
default: '[rank] = AGHJZ'
});
});
suite('$like', function () {
testsql('username', {
$like: '%swagger'
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!