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

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 @@ ...@@ -39,6 +39,9 @@
"afterEach", "afterEach",
"suite", "suite",
"setup", "setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"test" "test"
] ]
} }
\ No newline at end of file
...@@ -201,27 +201,26 @@ Mixin.belongsTo = singleLinked(BelongsTo); ...@@ -201,27 +201,26 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {string} [options.onUpdate='CASCADE'] * @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key. * @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 Mixin.hasMany = function(target, options) { // testhint options:none
if (!(targetModel instanceof this.sequelize.Model)) { if (!(target instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not an instance of 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) // Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option)
options = options || {}; options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks); options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = 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 // the id is in the foreign table or in a connecting table
var association = new HasMany(sourceModel, targetModel, options); var association = new HasMany(source, target, options);
sourceModel.associations[association.associationAccessor] = association.injectAttributes(); source.associations[association.associationAccessor] = association;
association.injectGetter(sourceModel.Instance.prototype); association.injectAttributes();
association.injectSetter(sourceModel.Instance.prototype); association.mixin(source.Instance.prototype);
association.injectCreator(sourceModel.Instance.prototype);
return association; return association;
}; };
......
...@@ -11,7 +11,8 @@ AbstractDialect.prototype.supports = { ...@@ -11,7 +11,8 @@ AbstractDialect.prototype.supports = {
'LIMIT ON UPDATE': false, 'LIMIT ON UPDATE': false,
'ON DUPLICATE KEY': true, 'ON DUPLICATE KEY': true,
'ORDER NULLS': false, 'ORDER NULLS': false,
'UNION': true,
'UNION ALL': true,
/* What is the dialect's keyword for INSERT IGNORE */ /* What is the dialect's keyword for INSERT IGNORE */
'IGNORE': '', 'IGNORE': '',
...@@ -46,6 +47,7 @@ AbstractDialect.prototype.supports = { ...@@ -46,6 +47,7 @@ AbstractDialect.prototype.supports = {
using: true, using: true,
}, },
joinTableDependent: true, joinTableDependent: true,
groupedLimit: true,
indexViaAlter: false, indexViaAlter: false,
JSON: false, JSON: false,
deferrableConstraints: false deferrableConstraints: false
......
'use strict'; 'use strict';
var Utils = require('../../utils') var Utils = require('../../utils')
, SqlString = require('../../sql-string') , SqlString = require('../../sql-string')
...@@ -767,6 +767,7 @@ var QueryGenerator = { ...@@ -767,6 +767,7 @@ var QueryGenerator = {
, model , model
, as , as
, association; , association;
for (var i = 0; i < len - 1; i++) { for (var i = 0; i < len - 1; i++) {
item = obj[i]; item = obj[i];
if (item._modelAttribute || Utils._.isString(item) || item._isSequelizeMethod || 'raw' in item) { if (item._modelAttribute || Utils._.isString(item) || item._isSequelizeMethod || 'raw' in item) {
...@@ -800,7 +801,7 @@ var QueryGenerator = { ...@@ -800,7 +801,7 @@ var QueryGenerator = {
} }
// add 1st string as quoted, 2nd as unquoted raw // 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 (i < len - 1) {
if (obj[i + 1]._isSequelizeMethod) { if (obj[i + 1]._isSequelizeMethod) {
sql += this.handleSequelizeMethod(obj[i + 1]); sql += this.handleSequelizeMethod(obj[i + 1]);
...@@ -954,7 +955,7 @@ var QueryGenerator = { ...@@ -954,7 +955,7 @@ var QueryGenerator = {
, limit = options.limit , limit = options.limit
, mainModel = model , mainModel = model
, mainQueryItems = [] , mainQueryItems = []
, mainAttributes = options.attributes && options.attributes.slice(0) , mainAttributes = options.attributes && options.attributes.slice()
, mainJoinQueries = [] , mainJoinQueries = []
// We'll use a subquery if we have a hasMany association and a limit // We'll use a subquery if we have a hasMany association and a limit
, subQuery = options.subQuery === undefined ? , subQuery = options.subQuery === undefined ?
...@@ -968,10 +969,10 @@ var QueryGenerator = { ...@@ -968,10 +969,10 @@ var QueryGenerator = {
if (options.tableAs) { if (options.tableAs) {
mainTableAs = this.quoteTable(options.tableAs); mainTableAs = this.quoteTable(options.tableAs);
} else if (!Array.isArray(tableName) && model) { } 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)) { if (Array.isArray(t)) {
return this.quoteTable(t[0], t[1]); return this.quoteTable(t[0], t[1]);
} }
...@@ -1002,6 +1003,7 @@ var QueryGenerator = { ...@@ -1002,6 +1003,7 @@ var QueryGenerator = {
} }
if (Array.isArray(attr) && attr.length === 2) { if (Array.isArray(attr) && attr.length === 2) {
attr = attr.slice();
if (attr[0]._isSequelizeMethod) { if (attr[0]._isSequelizeMethod) {
attr[0] = self.handleSequelizeMethod(attr[0]); attr[0] = self.handleSequelizeMethod(attr[0]);
...@@ -1024,10 +1026,10 @@ var QueryGenerator = { ...@@ -1024,10 +1026,10 @@ var QueryGenerator = {
mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']); mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery // 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 // We need primary keys
subQueryAttributes = mainAttributes; subQueryAttributes = mainAttributes;
mainAttributes = [mainTableAs + '.*']; mainAttributes = [(mainTableAs || table) + '.*'];
} }
if (options.include) { if (options.include) {
...@@ -1300,7 +1302,8 @@ var QueryGenerator = { ...@@ -1300,7 +1302,8 @@ var QueryGenerator = {
joinQueryItem = ' ' + self.joinIncludeQuery({ joinQueryItem = ' ' + self.joinIncludeQuery({
model: mainModel, model: mainModel,
subQuery: options.subQuery, subQuery: options.subQuery,
include: include include: include,
groupedLimit: options.groupedLimit
}); });
} }
...@@ -1311,7 +1314,9 @@ var QueryGenerator = { ...@@ -1311,7 +1314,9 @@ var QueryGenerator = {
} }
if (include.include) { if (include.include) {
include.include.forEach(function(childInclude) { include.include.filter(function (include) {
return !include.separate;
}).forEach(function(childInclude) {
if (childInclude._pseudo) return; if (childInclude._pseudo) return;
var childJoinQueries = generateJoinQueries(childInclude, as); var childJoinQueries = generateJoinQueries(childInclude, as);
...@@ -1329,8 +1334,10 @@ var QueryGenerator = { ...@@ -1329,8 +1334,10 @@ var QueryGenerator = {
}; };
// Loop through includes and generate subqueries // Loop through includes and generate subqueries
options.include.forEach(function(include) { options.include.filter(function (include) {
var joinQueries = generateJoinQueries(include, options.tableAs); return !include.separate;
}).forEach(function(include) {
var joinQueries = generateJoinQueries(include, mainTableAs);
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery); subJoinQueries = subJoinQueries.concat(joinQueries.subQuery);
mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery); mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery);
...@@ -1340,7 +1347,7 @@ var QueryGenerator = { ...@@ -1340,7 +1347,7 @@ var QueryGenerator = {
// If using subQuery select defined subQuery attributes and join subJoinQueries // If using subQuery select defined subQuery attributes and join subJoinQueries
if (subQuery) { if (subQuery) {
subQueryItems.push('SELECT ' + subQueryAttributes.join(', ') + ' FROM ' + options.table); subQueryItems.push('SELECT ' + subQueryAttributes.join(', ') + ' FROM ' + table);
if (mainTableAs) { if (mainTableAs) {
subQueryItems.push(' AS ' + mainTableAs); subQueryItems.push(' AS ' + mainTableAs);
} }
...@@ -1348,7 +1355,33 @@ var QueryGenerator = { ...@@ -1348,7 +1355,33 @@ var QueryGenerator = {
// Else do it the reguar way // Else do it the reguar way
} else { } 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) { if (mainTableAs) {
mainQueryItems.push(' AS ' + mainTableAs); mainQueryItems.push(' AS ' + mainTableAs);
} }
...@@ -1356,7 +1389,7 @@ var QueryGenerator = { ...@@ -1356,7 +1389,7 @@ var QueryGenerator = {
} }
// Add WHERE to sub or main query // 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); options.where = this.getWhereConditions(options.where, mainTableAs || tableName, model, options);
if (options.where) { if (options.where) {
if (subQuery) { if (subQuery) {
...@@ -1387,7 +1420,7 @@ var QueryGenerator = { ...@@ -1387,7 +1420,7 @@ var QueryGenerator = {
} }
} }
// Add ORDER to sub or main query // Add ORDER to sub or main query
if (options.order) { if (options.order && !options.groupedLimit) {
var mainQueryOrder = []; var mainQueryOrder = [];
var subQueryOrder = []; var subQueryOrder = [];
...@@ -1440,7 +1473,7 @@ var QueryGenerator = { ...@@ -1440,7 +1473,7 @@ var QueryGenerator = {
// Add LIMIT, OFFSET to sub or main query // Add LIMIT, OFFSET to sub or main query
var limitOrder = this.addLimitAndOffset(options, model); var limitOrder = this.addLimitAndOffset(options, model);
if (limitOrder) { if (limitOrder && !options.groupedLimit) {
if (subQuery) { if (subQuery) {
subQueryItems.push(limitOrder); subQueryItems.push(limitOrder);
} else { } else {
...@@ -1452,7 +1485,7 @@ var QueryGenerator = { ...@@ -1452,7 +1485,7 @@ var QueryGenerator = {
if (subQuery) { if (subQuery) {
query = 'SELECT ' + mainAttributes.join(', ') + ' FROM ('; query = 'SELECT ' + mainAttributes.join(', ') + ' FROM (';
query += subQueryItems.join(''); query += subQueryItems.join('');
query += ') AS ' + options.tableAs; query += ') AS ' + mainTableAs;
query += mainJoinQueries.join(''); query += mainJoinQueries.join('');
query += mainQueryItems.join(''); query += mainQueryItems.join('');
} else { } else {
...@@ -1526,7 +1559,7 @@ var QueryGenerator = { ...@@ -1526,7 +1559,7 @@ var QueryGenerator = {
this.quoteIdentifier(fieldLeft) this.quoteIdentifier(fieldLeft)
].join('.'); ].join('.');
if (subQuery && include.parent.subQuery && !include.subQuery) { if (options.groupedLimit || subQuery && include.parent.subQuery && !include.subQuery) {
if (parentIsTop) { if (parentIsTop) {
// The main model attributes is not aliased to a prefix // The main model attributes is not aliased to a prefix
joinOn = [ joinOn = [
......
...@@ -20,11 +20,13 @@ var SqliteDialect = function(sequelize) { ...@@ -20,11 +20,13 @@ var SqliteDialect = function(sequelize) {
SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), { SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), {
'DEFAULT': false, 'DEFAULT': false,
'DEFAULT VALUES': true, 'DEFAULT VALUES': true,
'UNION ALL': false,
'IGNORE': ' OR IGNORE', 'IGNORE': ' OR IGNORE',
index: { index: {
using: false using: false
}, },
joinTableDependent: false joinTableDependent: false,
groupedLimit: false
}); });
SqliteDialect.prototype.Query = Query; SqliteDialect.prototype.Query = Query;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
var Utils = require('./utils') var Utils = require('./utils')
, Instance = require('./instance') , Instance = require('./instance')
, Association = require('./associations/base') , Association = require('./associations/base')
, HasMany = require('./associations/has-many')
, DataTypes = require('./data-types') , DataTypes = require('./data-types')
, Util = require('util') , Util = require('util')
, Transaction = require('./transaction') , Transaction = require('./transaction')
...@@ -597,6 +598,18 @@ validateIncludedElement = function(include, tableNames, options) { ...@@ -597,6 +598,18 @@ validateIncludedElement = function(include, tableNames, options) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope; 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 // Validate child includes
if (include.hasOwnProperty('include')) { if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames, options); validateIncludedElements.call(include.model, include, tableNames, options);
...@@ -1227,6 +1240,8 @@ Model.prototype.all = function(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 {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 {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[].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 {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} [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 * @param {Array<Object|Model>} [options.include[].include] Load further nested related models
...@@ -1251,7 +1266,8 @@ Model.prototype.findAll = function(options) { ...@@ -1251,7 +1266,8 @@ Model.prototype.findAll = function(options) {
if (arguments.length > 1) { if (arguments.length > 1) {
throw new Error('Please note that find* was refactored and uses only one options object from now on.'); 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; tableNames[this.getTableName(options)] = true;
options = optClone(options); options = optClone(options);
...@@ -1302,15 +1318,57 @@ Model.prototype.findAll = function(options) { ...@@ -1302,15 +1318,57 @@ Model.prototype.findAll = function(options) {
return this.runHooks('beforeFindAfterOptions', options); return this.runHooks('beforeFindAfterOptions', options);
} }
}).then(function() { }).then(function() {
originalOptions = optClone(options);
options.tableNames = Object.keys(tableNames); options.tableNames = Object.keys(tableNames);
return this.QueryInterface.select(this, this.getTableName(options), options); return this.QueryInterface.select(this, this.getTableName(options), options);
}).tap(function(results) { }).tap(function(results) {
if (options.hooks) { if (options.hooks) {
return this.runHooks('afterFind', results, options); 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);
};
/** /**
* Search for a single instance by its primary key. This applies LIMIT 1, so the listener will always be called with a single instance. * Search for a single instance by its primary key. This applies LIMIT 1, so the listener will always be called with a single instance.
* *
......
...@@ -669,6 +669,7 @@ QueryInterface.prototype.select = function(model, tableName, options) { ...@@ -669,6 +669,7 @@ QueryInterface.prototype.select = function(model, tableName, options) {
options = options || {}; options = options || {};
options.type = QueryTypes.SELECT; options.type = QueryTypes.SELECT;
options.model = model; options.model = model;
return this.sequelize.query( return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model), this.QueryGenerator.selectQuery(tableName, options, model),
options options
......
...@@ -26,7 +26,160 @@ describe(Support.getTestDialectTeaser('HasMany'), function() { ...@@ -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('(1:N)', function() {
describe('hasSingle', function() { describe('hasSingle', function() {
beforeEach(function() { beforeEach(function() {
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING }); 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) { ...@@ -29,7 +29,7 @@ Sequelize.Promise.onPossiblyUnhandledRejection(function(e, promise) {
Sequelize.Promise.longStackTraces(); Sequelize.Promise.longStackTraces();
// shim all Sequelize methods for testing for correct `options.logging` passing // 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 = { var Support = {
Sequelize: Sequelize, Sequelize: Sequelize,
......
...@@ -7,11 +7,12 @@ var chai = require('chai') ...@@ -7,11 +7,12 @@ var chai = require('chai')
, stub = sinon.stub , stub = sinon.stub
, Support = require(__dirname + '/../support') , Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types') , DataTypes = require(__dirname + '/../../../lib/data-types')
, HasMany = require(__dirname + '/../../../lib/associations/has-many')
, current = Support.sequelize , current = Support.sequelize
, Promise = current.Promise; , Promise = current.Promise;
describe(Support.getTestDialectTeaser('hasMany'), function() { describe(Support.getTestDialectTeaser('hasMany'), function() {
describe('optimizations using bulk create, destroy and update', function() { describe('optimizations using bulk create, destroy and update', function() {
var User = current.define('User', { username: DataTypes.STRING }) var User = current.define('User', { username: DataTypes.STRING })
, Task = current.define('Task', { title: DataTypes.STRING }); , Task = current.define('Task', { title: DataTypes.STRING });
...@@ -61,4 +62,104 @@ describe(Support.getTestDialectTeaser('hasMany'), function() { ...@@ -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() { ...@@ -103,6 +103,21 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
testsql({ testsql({
model: User, model: User,
subQuery: true, 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({ include: Sequelize.Model.$validateIncludedElements({
limit: 3, limit: 3,
model: User, model: User,
...@@ -213,5 +228,20 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -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 // 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]" 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 @@ ...@@ -4,28 +4,143 @@
var Support = require(__dirname + '/../support') var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types') , DataTypes = require(__dirname + '/../../../lib/data-types')
, Model = require(__dirname + '/../../../lib/model') , Model = require(__dirname + '/../../../lib/model')
, util = require('util')
, expectsql = Support.expectsql , expectsql = Support.expectsql
, current = Support.sequelize , current = Support.sequelize
, sql = current.dialect.QueryGenerator; , 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 // 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() { suite(Support.getTestDialectTeaser('SQL'), function() {
describe('select', function () { suite('select', function () {
it('*', function () { var testsql = function (options, expectation) {
expectsql(sql.selectQuery('User'), { var model = options.model;
default: 'SELECT * FROM [User];'
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';"
}); });
it('with attributes', function () { testsql({
expectsql(sql.selectQuery('User', { table: 'User',
attributes: ['name', 'age'] attributes: [
}), { 'email',
default: 'SELECT [name], [age] FROM [User];' ['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];'
}); });
(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 () { it('include (left outer join)', function () {
var User = Support.sequelize.define('User', { var User = Support.sequelize.define('User', {
name: DataTypes.STRING, name: DataTypes.STRING,
...@@ -59,22 +174,22 @@ describe(Support.getTestDialectTeaser('SQL'), function() { ...@@ -59,22 +174,22 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
}); });
}); });
describe('queryIdentifiersFalse', function () { suite('queryIdentifiersFalse', function () {
before(function () { suiteSetup(function () {
sql.options.quoteIdentifiers = false; sql.options.quoteIdentifiers = false;
}); });
after(function () { suiteTeardown(function () {
sql.options.quoteIdentifiers = true; sql.options.quoteIdentifiers = true;
}); });
it('*', function () { test('*', function () {
expectsql(sql.selectQuery('User'), { expectsql(sql.selectQuery('User'), {
default: 'SELECT * FROM [User];', default: 'SELECT * FROM [User];',
postgres: 'SELECT * FROM User;' postgres: 'SELECT * FROM User;'
}); });
}); });
it('with attributes', function () { test('with attributes', function () {
expectsql(sql.selectQuery('User', { expectsql(sql.selectQuery('User', {
attributes: ['name', 'age'] attributes: ['name', 'age']
}), { }), {
...@@ -83,7 +198,7 @@ describe(Support.getTestDialectTeaser('SQL'), function() { ...@@ -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', { var User = Support.sequelize.define('User', {
name: DataTypes.STRING, name: DataTypes.STRING,
age: DataTypes.INTEGER age: DataTypes.INTEGER
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!