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

Commit ec6f4628 by Mick Hansen

Merge pull request #4177 from sequelize/feat-include-seperate

include.seperate and include.limit support for hasMany
2 parents 9d0850a9 6ba2b674
......@@ -39,6 +39,9 @@
"afterEach",
"suite",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"test"
]
}
\ No newline at end of file
......@@ -221,30 +221,85 @@ HasMany.prototype.injectAttributes = function() {
return this;
};
HasMany.prototype.injectGetter = function(obj) {
HasMany.prototype.mixin = function(obj) {
var association = this;
obj[this.accessors.get] = function(options) {
var scopeWhere = association.scope ? {} : null
, Model = association.target;
return association.get(this, options);
};
options = association.target.__optClone(options) || {};
if (this.accessors.count) {
obj[this.accessors.count] = function(options) {
return association.count(this, options);
};
}
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
return association.has(this, instances, options);
};
obj[this.accessors.set] = function(instances, options) {
return association.set(this, instances, options);
};
obj[this.accessors.add] = obj[this.accessors.addMultiple] = function(instances, options) {
return association.add(this, instances, options);
};
obj[this.accessors.remove] = obj[this.accessors.removeMultiple] = function(instances, options) {
return association.remove(this, instances, options);
};
obj[this.accessors.create] = function(values, options) {
return association.create(this, values, options);
};
};
HasMany.prototype.get = function(instances, options) {
var association = this
, where = {}
, Model = association.target
, instance
, values;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
options = association.target.$optClone(options) || {};
if (association.scope) {
_.assign(scopeWhere, association.scope);
_.assign(where, association.scope);
}
options.where = {
$and: [
new Utils.where(
association.target.rawAttributes[association.foreignKey],
this.get(association.source.primaryKeyAttribute, {raw: true})
),
scopeWhere,
options.where
]
if (instances) {
values = instances.map(function (instance) {
return instance.get(association.source.primaryKeyAttribute, {raw: true});
});
if (options.limit) {
options.groupedLimit = {
limit: options.limit,
on: association.foreignKey,
values: values
};
delete options.limit;
} else {
where[association.foreignKey] = {
$in: values
};
}
} else {
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
}
options.where = options.where ?
{$and: [where, options.where]} :
where;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Model = Model.unscoped();
......@@ -253,12 +308,25 @@ HasMany.prototype.injectGetter = function(obj) {
}
}
return Model.all(options);
};
return Model.findAll(options).then(function (results) {
if (instance) return results;
if (this.accessors.count) {
obj[this.accessors.count] = function(options) {
var model = association.target
var result = {};
instances.forEach(function (instance) {
result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = [];
});
results.forEach(function (instance) {
result[instance.get(association.foreignKey, {raw: true})].push(instance);
});
return result;
});
};
HasMany.prototype.count = function(instance, options) {
var association = this
, model = association.target
, sequelize = model.sequelize;
options = association.target.__optClone(options) || {};
......@@ -268,23 +336,24 @@ HasMany.prototype.injectGetter = function(obj) {
options.raw = true;
options.plain = true;
return obj[association.accessors.get].call(this, options).then(function (result) {
return this.get(instance, options).then(function (result) {
return parseInt(result.count, 10);
});
};
}
};
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
var where = {};
HasMany.prototype.has = function(sourceInstance, targetInstances, options) {
var association = this
, where = {};
if (!Array.isArray(instances)) {
instances = [instances];
if (!Array.isArray(targetInstances)) {
targetInstances = [targetInstances];
}
options = options || {};
options.scope = false;
options.raw = true;
where.$or = instances.map(function (instance) {
where.$or = targetInstances.map(function (instance) {
if (instance instanceof association.target.Instance) {
return instance.where();
} else {
......@@ -301,43 +370,34 @@ HasMany.prototype.injectGetter = function(obj) {
]
};
return this[association.accessors.get](
options,
{ raw: true }
return this.get(
sourceInstance,
options
).then(function(associatedObjects) {
return associatedObjects.length === instances.length;
return associatedObjects.length === targetInstances.length;
});
};
return this;
};
HasMany.prototype.injectSetter = function(obj) {
HasMany.prototype.set = function(sourceInstance, targetInstances, options) {
var association = this;
obj[this.accessors.set] = function(newAssociatedObjects, additionalAttributes) {
var options = additionalAttributes || {};
additionalAttributes = additionalAttributes || {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
if (targetInstances === null) {
targetInstances = [];
} else {
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
targetInstances = association.toInstanceArray(targetInstances);
}
var instance = this;
return instance[association.accessors.get](_.defaults({
return association.get(sourceInstance, _.defaults({
scope: false,
raw: true
}, options)).then(function(oldAssociations) {
var promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !_.find(newAssociatedObjects, function(obj) {
return !_.find(targetInstances, function(obj) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, unassociatedObjects = newAssociatedObjects.filter(function(obj) {
, unassociatedObjects = targetInstances.filter(function(obj) {
return !_.find(oldAssociations, function(old) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
......@@ -367,7 +427,7 @@ HasMany.prototype.injectSetter = function(obj) {
updateWhere = {};
update = {};
update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
......@@ -382,23 +442,25 @@ HasMany.prototype.injectSetter = function(obj) {
));
}
return Utils.Promise.all(promises).return(instance);
return Utils.Promise.all(promises).return(sourceInstance);
});
};
};
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, options) {
// If newInstance is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
options = options || {};
HasMany.prototype.add = function(sourceInstance, targetInstances, options) {
if (!targetInstances) return Utils.Promise.resolve();
var instance = this, update = {}, where = {};
var association = this
, update = {}
, where = {};
newInstances = association.toInstanceArray(newInstances);
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = newInstances.map(function (unassociatedObject) {
where[association.target.primaryKeyAttribute] = targetInstances.map(function (unassociatedObject) {
return unassociatedObject.get(association.target.primaryKeyAttribute);
});
......@@ -407,19 +469,23 @@ HasMany.prototype.injectSetter = function(obj) {
_.defaults({
where: where
}, options)
).return(instance);
};
).return(sourceInstance);
};
HasMany.prototype.remove = function(sourceInstance, targetInstances, options) {
var association = this
, update = {}
, where = {};
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
options = options || {};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
targetInstances = association.toInstanceArray(targetInstances);
var update = {};
update[association.foreignKey] = null;
var where = {};
where[association.foreignKey] = this.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = oldAssociatedObjects.map(function (oldAssociatedObject) { return oldAssociatedObject.get(association.target.primaryKeyAttribute); });
where[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (targetInstance) {
return targetInstance.get(association.target.primaryKeyAttribute);
});
return association.target.unscoped().update(
update,
......@@ -427,16 +493,11 @@ HasMany.prototype.injectSetter = function(obj) {
where: where
}, options)
).return(this);
};
return this;
};
HasMany.prototype.injectCreator = function(obj) {
HasMany.prototype.create = function(sourceInstance, values, options) {
var association = this;
obj[this.accessors.create] = function(values, options) {
var instance = this;
options = options || {};
if (Array.isArray(options)) {
......@@ -456,12 +517,9 @@ HasMany.prototype.injectCreator = function(obj) {
});
}
values[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
return this;
};
module.exports = HasMany;
......@@ -201,27 +201,26 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/
Mixin.hasMany = function(targetModel, options) { // testhint options:none
if (!(targetModel instanceof this.sequelize.Model)) {
Mixin.hasMany = function(target, options) { // testhint options:none
if (!(target instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not an instance of Sequelize.Model');
}
var sourceModel = this;
var source = this;
// Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = _.extend(options, _.omit(sourceModel.options, ['hooks']));
options = _.extend(options, _.omit(source.options, ['hooks']));
// the id is in the foreign table or in a connecting table
var association = new HasMany(sourceModel, targetModel, options);
sourceModel.associations[association.associationAccessor] = association.injectAttributes();
var association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectGetter(sourceModel.Instance.prototype);
association.injectSetter(sourceModel.Instance.prototype);
association.injectCreator(sourceModel.Instance.prototype);
association.injectAttributes();
association.mixin(source.Instance.prototype);
return association;
};
......
......@@ -11,7 +11,8 @@ AbstractDialect.prototype.supports = {
'LIMIT ON UPDATE': false,
'ON DUPLICATE KEY': true,
'ORDER NULLS': false,
'UNION': true,
'UNION ALL': true,
/* What is the dialect's keyword for INSERT IGNORE */
'IGNORE': '',
......@@ -46,6 +47,7 @@ AbstractDialect.prototype.supports = {
using: true,
},
joinTableDependent: true,
groupedLimit: true,
indexViaAlter: false,
JSON: false,
deferrableConstraints: false
......
'use strict';
'use strict';
var Utils = require('../../utils')
, SqlString = require('../../sql-string')
......@@ -767,6 +767,7 @@ var QueryGenerator = {
, model
, as
, association;
for (var i = 0; i < len - 1; i++) {
item = obj[i];
if (item._modelAttribute || Utils._.isString(item) || item._isSequelizeMethod || 'raw' in item) {
......@@ -800,7 +801,7 @@ var QueryGenerator = {
}
// add 1st string as quoted, 2nd as unquoted raw
var sql = (i > 0 ? this.quoteIdentifier(tableNames.join('.')) + '.' : (Utils._.isString(obj[0]) ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force);
var sql = (i > 0 ? this.quoteIdentifier(tableNames.join('.')) + '.' : (Utils._.isString(obj[0]) && parent ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force);
if (i < len - 1) {
if (obj[i + 1]._isSequelizeMethod) {
sql += this.handleSequelizeMethod(obj[i + 1]);
......@@ -954,7 +955,7 @@ var QueryGenerator = {
, limit = options.limit
, mainModel = model
, mainQueryItems = []
, mainAttributes = options.attributes && options.attributes.slice(0)
, mainAttributes = options.attributes && options.attributes.slice()
, mainJoinQueries = []
// We'll use a subquery if we have a hasMany association and a limit
, subQuery = options.subQuery === undefined ?
......@@ -968,10 +969,10 @@ var QueryGenerator = {
if (options.tableAs) {
mainTableAs = this.quoteTable(options.tableAs);
} else if (!Array.isArray(tableName) && model) {
options.tableAs = mainTableAs = this.quoteTable(model.name);
mainTableAs = this.quoteTable(model.name);
}
options.table = table = !Array.isArray(tableName) ? this.quoteTable(tableName) : tableName.map(function(t) {
table = !Array.isArray(tableName) ? this.quoteTable(tableName) : tableName.map(function(t) {
if (Array.isArray(t)) {
return this.quoteTable(t[0], t[1]);
}
......@@ -1002,6 +1003,7 @@ var QueryGenerator = {
}
if (Array.isArray(attr) && attr.length === 2) {
attr = attr.slice();
if (attr[0]._isSequelizeMethod) {
attr[0] = self.handleSequelizeMethod(attr[0]);
......@@ -1024,10 +1026,10 @@ var QueryGenerator = {
mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery
if (subQuery) {
if (subQuery || options.groupedLimit) {
// We need primary keys
subQueryAttributes = mainAttributes;
mainAttributes = [mainTableAs + '.*'];
mainAttributes = [(mainTableAs || table) + '.*'];
}
if (options.include) {
......@@ -1300,7 +1302,8 @@ var QueryGenerator = {
joinQueryItem = ' ' + self.joinIncludeQuery({
model: mainModel,
subQuery: options.subQuery,
include: include
include: include,
groupedLimit: options.groupedLimit
});
}
......@@ -1311,7 +1314,9 @@ var QueryGenerator = {
}
if (include.include) {
include.include.forEach(function(childInclude) {
include.include.filter(function (include) {
return !include.separate;
}).forEach(function(childInclude) {
if (childInclude._pseudo) return;
var childJoinQueries = generateJoinQueries(childInclude, as);
......@@ -1329,8 +1334,10 @@ var QueryGenerator = {
};
// Loop through includes and generate subqueries
options.include.forEach(function(include) {
var joinQueries = generateJoinQueries(include, options.tableAs);
options.include.filter(function (include) {
return !include.separate;
}).forEach(function(include) {
var joinQueries = generateJoinQueries(include, mainTableAs);
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery);
mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery);
......@@ -1340,7 +1347,7 @@ var QueryGenerator = {
// If using subQuery select defined subQuery attributes and join subJoinQueries
if (subQuery) {
subQueryItems.push('SELECT ' + subQueryAttributes.join(', ') + ' FROM ' + options.table);
subQueryItems.push('SELECT ' + subQueryAttributes.join(', ') + ' FROM ' + table);
if (mainTableAs) {
subQueryItems.push(' AS ' + mainTableAs);
}
......@@ -1348,7 +1355,33 @@ var QueryGenerator = {
// Else do it the reguar way
} else {
mainQueryItems.push('SELECT ' + mainAttributes.join(', ') + ' FROM ' + options.table);
if (options.groupedLimit) {
if (!mainTableAs) {
mainTableAs = table;
}
mainQueryItems.push('SELECT '+mainAttributes.join(', ')+' FROM ('+
options.groupedLimit.values.map(function (value) {
var where = _.assign({}, options.where);
where[options.groupedLimit.on] = value;
return '('+self.selectQuery(
table,
{
attributes: options.attributes,
limit: options.groupedLimit.limit,
order: options.order,
where: where
},
model
).replace(/;$/, '')+')';
}).join(
self._dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION '
)
+')');
} else {
mainQueryItems.push('SELECT ' + mainAttributes.join(', ') + ' FROM ' + table);
}
if (mainTableAs) {
mainQueryItems.push(' AS ' + mainTableAs);
}
......@@ -1356,7 +1389,7 @@ var QueryGenerator = {
}
// Add WHERE to sub or main query
if (options.hasOwnProperty('where')) {
if (options.hasOwnProperty('where') && !options.groupedLimit) {
options.where = this.getWhereConditions(options.where, mainTableAs || tableName, model, options);
if (options.where) {
if (subQuery) {
......@@ -1387,7 +1420,7 @@ var QueryGenerator = {
}
}
// Add ORDER to sub or main query
if (options.order) {
if (options.order && !options.groupedLimit) {
var mainQueryOrder = [];
var subQueryOrder = [];
......@@ -1440,7 +1473,7 @@ var QueryGenerator = {
// Add LIMIT, OFFSET to sub or main query
var limitOrder = this.addLimitAndOffset(options, model);
if (limitOrder) {
if (limitOrder && !options.groupedLimit) {
if (subQuery) {
subQueryItems.push(limitOrder);
} else {
......@@ -1452,7 +1485,7 @@ var QueryGenerator = {
if (subQuery) {
query = 'SELECT ' + mainAttributes.join(', ') + ' FROM (';
query += subQueryItems.join('');
query += ') AS ' + options.tableAs;
query += ') AS ' + mainTableAs;
query += mainJoinQueries.join('');
query += mainQueryItems.join('');
} else {
......@@ -1526,7 +1559,7 @@ var QueryGenerator = {
this.quoteIdentifier(fieldLeft)
].join('.');
if (subQuery && include.parent.subQuery && !include.subQuery) {
if (options.groupedLimit || subQuery && include.parent.subQuery && !include.subQuery) {
if (parentIsTop) {
// The main model attributes is not aliased to a prefix
joinOn = [
......
......@@ -20,11 +20,13 @@ var SqliteDialect = function(sequelize) {
SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), {
'DEFAULT': false,
'DEFAULT VALUES': true,
'UNION ALL': false,
'IGNORE': ' OR IGNORE',
index: {
using: false
},
joinTableDependent: false
joinTableDependent: false,
groupedLimit: false
});
SqliteDialect.prototype.Query = Query;
......
......@@ -3,6 +3,7 @@
var Utils = require('./utils')
, Instance = require('./instance')
, Association = require('./associations/base')
, HasMany = require('./associations/has-many')
, DataTypes = require('./data-types')
, Util = require('util')
, Transaction = require('./transaction')
......@@ -597,6 +598,18 @@ validateIncludedElement = function(include, tableNames, options) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
}
if (include.limit && include.separate === undefined) {
include.separate = true;
}
if (include.separate === true && !(include.association instanceof HasMany)) {
throw new Error('Only HasMany associations support include.separate');
}
if (include.separate === true) {
include.duplicating = false;
}
// Validate child includes
if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames, options);
......@@ -1227,6 +1240,8 @@ Model.prototype.all = function(options) {
* @param {Object} [options.include[].where] Where clauses to apply to the child models. Note that this converts the eager load to an inner join, unless you explicitly set `required: false`
* @param {Array<String>} [options.include[].attributes] A list of attributes to select from the child model
* @param {Boolean} [options.include[].required] If true, converts to an inner join, which means that the parent model will only be loaded if it has any matching children. True if `include.where` is set, false otherwise.
* @param {Boolean} [options.include[].separate] If true, runs a separate query to fetch the associated instances, only supported for hasMany associations
* @param {Number} [options.include[].limit] Limit the joined rows, only supported with include.separate=true
* @param {Object} [options.include[].through.where] Filter on the join model for belongsToMany relations
* @param {Array} [options.include[].through.attributes] A list of attributes to select from the join model for belongsToMany relations
* @param {Array<Object|Model>} [options.include[].include] Load further nested related models
......@@ -1251,7 +1266,8 @@ Model.prototype.findAll = function(options) {
if (arguments.length > 1) {
throw new Error('Please note that find* was refactored and uses only one options object from now on.');
}
var tableNames = {};
var tableNames = {}
, originalOptions;
tableNames[this.getTableName(options)] = true;
options = optClone(options);
......@@ -1302,13 +1318,55 @@ Model.prototype.findAll = function(options) {
return this.runHooks('beforeFindAfterOptions', options);
}
}).then(function() {
originalOptions = optClone(options);
options.tableNames = Object.keys(tableNames);
return this.QueryInterface.select(this, this.getTableName(options), options);
}).tap(function(results) {
if (options.hooks) {
return this.runHooks('afterFind', results, options);
}
}).then(function (results) {
return Model.$findSeperate(results, originalOptions);
});
};
Model.$findSeperate = function(results, options) {
if (!options.include || options.raw || !results) return Promise.resolve(results);
var original = results;
if (options.plain) results = [results];
return Promise.map(options.include, function (include) {
if (!include.separate) {
return Model.$findSeperate(
results.reduce(function (memo, result) {
var associations = result.get(include.association.as);
if (!Array.isArray(associations)) associations = [associations];
return memo.concat(associations);
}, []),
_.assign(
{},
_.omit(options, 'include', 'attributes', 'order', 'where', 'limit', 'plain'),
{include: include.include || []}
)
);
}
return include.association.get(results, _.assign(
{},
_.omit(options, 'include', 'attributes', 'order', 'where', 'limit', 'plain'),
include
)).then(function (map) {
results.forEach(function (result) {
result.set(
include.association.as,
map[result.get(include.association.source.primaryKeyAttribute)]
);
});
});
}).return(original);
};
/**
......
......@@ -669,6 +669,7 @@ QueryInterface.prototype.select = function(model, tableName, options) {
options = options || {};
options.type = QueryTypes.SELECT;
options.model = model;
return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model),
options
......
......@@ -26,7 +26,160 @@ describe(Support.getTestDialectTeaser('HasMany'), function() {
});
});
describe('get', function () {
if (current.dialect.supports.groupedLimit) {
describe('multiple', function () {
it('should fetch associations for multiple instances', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {});
User.Tasks = User.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
tasks: [
{},
{},
{}
]
}, {
include: [User.Tasks]
}),
User.create({
id: 2,
tasks: [
{}
]
}, {
include: [User.Tasks]
}),
User.create({
id: 3
})
);
}).then(function (users) {
return User.Tasks.get(users).then(function (result) {
expect(result[users[0].id].length).to.equal(3);
expect(result[users[1].id].length).to.equal(1);
expect(result[users[2].id].length).to.equal(0);
});
});
});
it('should fetch associations for multiple instances with limit and order', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {
title: DataTypes.STRING
});
User.Tasks = User.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
tasks: [
{title: 'b'},
{title: 'd'},
{title: 'c'},
{title: 'a'}
]
}, {
include: [User.Tasks]
}),
User.create({
tasks: [
{title: 'a'},
{title: 'c'},
{title: 'b'}
]
}, {
include: [User.Tasks]
})
);
}).then(function (users) {
return User.Tasks.get(users, {
limit: 2,
order: [
['title', 'ASC']
]
}).then(function (result) {
expect(result[users[0].id].length).to.equal(2);
expect(result[users[0].id][0].title).to.equal('a');
expect(result[users[0].id][1].title).to.equal('b');
expect(result[users[1].id].length).to.equal(2);
expect(result[users[1].id][0].title).to.equal('a');
expect(result[users[1].id][1].title).to.equal('b');
});
});
});
it('should fetch associations for multiple instances with limit and order and a belongsTo relation', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {
title: DataTypes.STRING,
categoryId: {
type: DataTypes.INTEGER,
field: 'category_id'
}
})
, Category = this.sequelize.define('Category', {});
User.Tasks = User.hasMany(Task, {as: 'tasks'});
Task.Category = Task.belongsTo(Category, {as: 'category', foreignKey: 'categoryId'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
tasks: [
{title: 'b', category: {}},
{title: 'd', category: {}},
{title: 'c', category: {}},
{title: 'a', category: {}}
]
}, {
include: [{association: User.Tasks, include: [Task.Category]}]
}),
User.create({
tasks: [
{title: 'a', category: {}},
{title: 'c', category: {}},
{title: 'b', category: {}}
]
}, {
include: [{association: User.Tasks, include: [Task.Category]}]
})
);
}).then(function (users) {
return User.Tasks.get(users, {
limit: 2,
order: [
['title', 'ASC']
],
include: [Task.Category],
}).then(function (result) {
expect(result[users[0].id].length).to.equal(2);
expect(result[users[0].id][0].title).to.equal('a');
expect(result[users[0].id][0].category).to.be.ok;
expect(result[users[0].id][1].title).to.equal('b');
expect(result[users[0].id][1].category).to.be.ok;
expect(result[users[1].id].length).to.equal(2);
expect(result[users[1].id][0].title).to.equal('a');
expect(result[users[1].id][0].category).to.be.ok;
expect(result[users[1].id][1].title).to.equal('b');
expect(result[users[1].id][1].category).to.be.ok;
});
});
});
});
}
});
describe('(1:N)', function() {
describe('hasSingle', function() {
beforeEach(function() {
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING });
......
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, sinon = require('sinon')
, Support = require(__dirname + '/../support')
, Sequelize = require(__dirname + '/../../../index')
, current = Support.sequelize
, Promise = Sequelize.Promise;
if (current.dialect.supports.groupedLimit) {
describe(Support.getTestDialectTeaser('Include'), function() {
describe('separate', function () {
it('should run a hasMany association in a separate query', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Tasks = User.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
tasks: [
{},
{},
{}
]
}, {
include: [User.Tasks]
}),
User.create({
id: 2,
tasks: [
{}
]
}, {
include: [User.Tasks]
})
).then(function () {
return User.findAll({
include: [
{association: User.Tasks, separate: true}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('tasks')).to.be.ok;
expect(users[0].get('tasks').length).to.equal(3);
expect(users[1].get('tasks')).to.be.ok;
expect(users[1].get('tasks').length).to.equal(1);
expect(sqlSpy).to.have.been.calledTwice;
});
});
});
it('should run a hasMany association with limit in a separate query', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Tasks = User.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
tasks: [
{},
{},
{}
]
}, {
include: [User.Tasks]
}),
User.create({
id: 2,
tasks: [
{},
{},
{},
{}
]
}, {
include: [User.Tasks]
})
).then(function () {
return User.findAll({
include: [
{association: User.Tasks, limit: 2}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('tasks')).to.be.ok;
expect(users[0].get('tasks').length).to.equal(2);
expect(users[1].get('tasks')).to.be.ok;
expect(users[1].get('tasks').length).to.equal(2);
expect(sqlSpy).to.have.been.calledTwice;
});
});
});
it('should run a nested (from a non-separate include) hasMany association in a separate query', function () {
var User = this.sequelize.define('User', {})
, Company = this.sequelize.define('Company')
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Company = User.belongsTo(Company, {as: 'company'});
Company.Tasks = Company.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
company: {
tasks: [
{},
{},
{}
]
}
}, {
include: [
{association: User.Company, include: [Company.Tasks]}
]
}),
User.create({
id: 2,
company: {
tasks: [
{}
]
}
}, {
include: [
{association: User.Company, include: [Company.Tasks]}
]
})
).then(function () {
return User.findAll({
include: [
{association: User.Company, include: [
{association: Company.Tasks, separate: true}
]}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('company').get('tasks')).to.be.ok;
expect(users[0].get('company').get('tasks').length).to.equal(3);
expect(users[1].get('company').get('tasks')).to.be.ok;
expect(users[1].get('company').get('tasks').length).to.equal(1);
expect(sqlSpy).to.have.been.calledTwice;
});
});
});
it('should run two nested hasMany association in a separate queries', function () {
var User = this.sequelize.define('User', {})
, Project = this.sequelize.define('Project', {})
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Projects = User.hasMany(Project, {as: 'projects'});
Project.Tasks = Project.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
projects: [
{
id: 1,
tasks: [
{},
{},
{}
]
},
{
id: 2,
tasks: [
{}
]
}
]
}, {
include: [
{association: User.Projects, include: [Project.Tasks]}
]
}),
User.create({
id: 2,
projects: [
{
id: 3,
tasks: [
{},
{}
]
}
]
}, {
include: [
{association: User.Projects, include: [Project.Tasks]}
]
})
).then(function () {
return User.findAll({
include: [
{association: User.Projects, separate: true, include: [
{association: Project.Tasks, separate: true}
]}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('projects')).to.be.ok;
expect(users[0].get('projects')[0].get('tasks')).to.be.ok;
expect(users[0].get('projects')[1].get('tasks')).to.be.ok;
expect(users[0].get('projects').length).to.equal(2);
expect(users[0].get('projects')[0].get('tasks').length).to.equal(3);
expect(users[0].get('projects')[1].get('tasks').length).to.equal(1);
expect(users[1].get('projects')).to.be.ok;
expect(users[1].get('projects')[0].get('tasks')).to.be.ok;
expect(users[1].get('projects').length).to.equal(1);
expect(users[1].get('projects')[0].get('tasks').length).to.equal(2);
expect(sqlSpy).to.have.been.calledThrice;
});
});
});
});
});
}
\ No newline at end of file
......@@ -29,7 +29,7 @@ Sequelize.Promise.onPossiblyUnhandledRejection(function(e, promise) {
Sequelize.Promise.longStackTraces();
// shim all Sequelize methods for testing for correct `options.logging` passing
if (!process.env.COVERAGE) supportShim(Sequelize);
if (!process.env.COVERAGE && false) supportShim(Sequelize);
var Support = {
Sequelize: Sequelize,
......
......@@ -7,6 +7,7 @@ var chai = require('chai')
, stub = sinon.stub
, Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, HasMany = require(__dirname + '/../../../lib/associations/has-many')
, current = Support.sequelize
, Promise = current.Promise;
......@@ -61,4 +62,104 @@ describe(Support.getTestDialectTeaser('hasMany'), function() {
});
});
});
describe('mixin', function () {
var User = current.define('User')
, Task = current.define('Task');
it('should mixin association methods', function () {
var as = Math.random().toString()
, association = new HasMany(User, Task, {as: as})
, obj = {};
association.mixin(obj);
expect(obj[association.accessors.get]).to.be.an('function');
expect(obj[association.accessors.set]).to.be.an('function');
expect(obj[association.accessors.addMultiple]).to.be.an('function');
expect(obj[association.accessors.add]).to.be.an('function');
expect(obj[association.accessors.remove]).to.be.an('function');
expect(obj[association.accessors.removeMultiple]).to.be.an('function');
expect(obj[association.accessors.hasSingle]).to.be.an('function');
expect(obj[association.accessors.hasAll]).to.be.an('function');
expect(obj[association.accessors.count]).to.be.an('function');
});
});
describe('get', function () {
var User = current.define('User', {})
, Task = current.define('Task', {})
, idA = Math.random().toString()
, idB = Math.random().toString()
, idC = Math.random().toString()
, foreignKey = 'user_id';
it('should fetch associations for a single instance', function () {
var findAll = stub(Task, 'findAll').returns(Promise.resolve([
Task.build({}),
Task.build({})
]))
, where = {}
, actual;
User.Tasks = User.hasMany(Task, {foreignKey: foreignKey});
actual = User.Tasks.get(User.build({id: idA}));
where[foreignKey] = idA;
expect(findAll).to.have.been.calledOnce;
expect(findAll.firstCall.args[0].where).to.deep.equal(where);
return actual.then(function (results) {
expect(results).to.be.an('array');
expect(results.length).to.equal(2);
}).finally(function () {
findAll.restore();
});
});
it('should fetch associations for multiple source instances', function () {
var findAll = stub(Task, 'findAll').returns(Promise.resolve([
Task.build({
'user_id': idA
}),
Task.build({
'user_id': idA
}),
Task.build({
'user_id': idA
}),
Task.build({
'user_id': idB
})
]))
, where = {}
, actual;
User.Tasks = User.hasMany(Task, {foreignKey: foreignKey});
actual = User.Tasks.get([
User.build({id: idA}),
User.build({id: idB}),
User.build({id: idC})
]);
where[foreignKey] = {
$in: [idA, idB, idC]
};
expect(findAll).to.have.been.calledOnce;
expect(findAll.firstCall.args[0].where).to.deep.equal(where);
return actual.then(function (result) {
expect(result).to.be.an('object');
expect(Object.keys(result)).to.deep.equal([idA, idB, idC]);
expect(result[idA].length).to.equal(3);
expect(result[idB].length).to.equal(1);
expect(result[idC].length).to.equal(0);
}).finally(function () {
findAll.restore();
});
});
});
});
......@@ -103,6 +103,21 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
testsql({
model: User,
subQuery: true,
groupedLimit: {},
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
include: [
User.Company
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model.$validateIncludedElements({
limit: 3,
model: User,
......@@ -213,5 +228,20 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
// 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]"
});
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
......@@ -4,28 +4,143 @@
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, Model = require(__dirname + '/../../../lib/model')
, util = require('util')
, 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
describe(Support.getTestDialectTeaser('SQL'), function() {
describe('select', function () {
it('*', function () {
expectsql(sql.selectQuery('User'), {
default: 'SELECT * FROM [User];'
suite(Support.getTestDialectTeaser('SQL'), function() {
suite('select', function () {
var testsql = function (options, expectation) {
var model = options.model;
test(util.inspect(options, {depth: 2}), function () {
return expectsql(
sql.selectQuery(
options.table || model && model.getTableName(),
options,
options.model
),
expectation
);
});
};
testsql({
table: 'User',
attributes: [
'email',
['first_name', 'firstName']
],
where: {
email: 'jon.snow@gmail.com'
}
}, {
default: "SELECT [email], [first_name] AS [firstName] FROM [User] WHERE [User].[email] = 'jon.snow@gmail.com';"
});
testsql({
table: 'User',
attributes: [
'email',
['first_name', 'firstName'],
['last_name', 'lastName']
],
order: [
['last_name', 'ASC']
],
groupedLimit: {
limit: 3,
on: 'companyId',
values: [
1,
5
]
}
}, {
default: 'SELECT [User].* FROM ('+
[
'(SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 1 ORDER BY [last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')',
'(SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 5 ORDER BY [last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')'
].join(current.dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ')
+') AS [User];'
});
it('with attributes', function () {
expectsql(sql.selectQuery('User', {
attributes: ['name', 'age']
}), {
default: 'SELECT [name], [age] FROM [User];'
(function () {
var User = Support.sequelize.define('user', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id_user'
},
email: DataTypes.STRING,
firstName: {
type: DataTypes.STRING,
field: 'first_name'
},
lastName: {
type: DataTypes.STRING,
field: 'last_name'
}
},
{
tableName: 'users'
});
var Post = Support.sequelize.define('Post', {
title: DataTypes.STRING,
userId: {
type: DataTypes.INTEGER,
field: 'user_id'
}
},
{
tableName: 'post'
});
User.Posts = User.hasMany(Post, {foreignKey: 'userId', as: 'POSTS'});
var include = Model.$validateIncludedElements({
include: [{
attributes: ['title'],
association: User.Posts
}],
model: User
}).include;
testsql({
table: User.getTableName(),
model: User,
include: include,
attributes: [
['id_user', 'id'],
'email',
['first_name', 'firstName'],
['last_name', 'lastName']
],
order: [
['last_name', 'ASC']
],
groupedLimit: {
limit: 3,
on: 'companyId',
values: [
1,
5
]
}
}, {
default: 'SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title] FROM ('+
[
'(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')',
'(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 5 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')'
].join(current.dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ')
+') AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id] = [POSTS].[user_id];'
});
})();
it('include (left outer join)', function () {
var User = Support.sequelize.define('User', {
name: DataTypes.STRING,
......@@ -59,22 +174,22 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
});
});
describe('queryIdentifiersFalse', function () {
before(function () {
suite('queryIdentifiersFalse', function () {
suiteSetup(function () {
sql.options.quoteIdentifiers = false;
});
after(function () {
suiteTeardown(function () {
sql.options.quoteIdentifiers = true;
});
it('*', function () {
test('*', function () {
expectsql(sql.selectQuery('User'), {
default: 'SELECT * FROM [User];',
postgres: 'SELECT * FROM User;'
});
});
it('with attributes', function () {
test('with attributes', function () {
expectsql(sql.selectQuery('User', {
attributes: ['name', 'age']
}), {
......@@ -83,7 +198,7 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
});
});
it('include (left outer join)', function () {
test('include (left outer join)', function () {
var User = Support.sequelize.define('User', {
name: DataTypes.STRING,
age: DataTypes.INTEGER
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!