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

Commit 57c8a727 by Ruben Bridgewater Committed by Jan Aagaard Meier

Fix count queries with includes (#6190)

Add changelog entry
1 parent 5311e401
# Future # Future
- [FIXED] Fixed an issue where custom-named model fields break when offsetting, ordering, and including hasMany simultaneously. [#5985] (https://github.com/sequelize/sequelize/issues/5985) - [FIXED] Fixed an issue where custom-named model fields break when offsetting, ordering, and including hasMany simultaneously. [#5985](https://github.com/sequelize/sequelize/issues/5985)
- [FIXED] Don't remove includes from count queries and unify findAndCount and count queries. [#6123](https://github.com/sequelize/sequelize/issues/6123)
- [FIXED] `Model.count` don't include attributes [#5057](https://github.com/sequelize/sequelize/issues/5057)
# 3.23.3 # 3.23.3
- [FIXED] Pass ResourceLock instead of raw connection in MSSQL disconnect handling - [FIXED] Pass ResourceLock instead of raw connection in MSSQL disconnect handling
......
...@@ -1595,34 +1595,23 @@ Model.prototype.aggregate = function(attribute, aggregateFunction, options) { ...@@ -1595,34 +1595,23 @@ Model.prototype.aggregate = function(attribute, aggregateFunction, options) {
* @return {Promise<Integer>} * @return {Promise<Integer>}
*/ */
Model.prototype.count = function(options) { Model.prototype.count = function(options) {
options = Utils.cloneDeep(options);
_.defaults(options, { hooks: true });
var col = '*';
return Promise.bind(this).then(function() { return Promise.bind(this).then(function() {
conformOptions(options, this); options = _.defaults(Utils.cloneDeep(options), { hooks: true });
this.$injectScope(options);
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeCount', options); return this.runHooks('beforeCount', options);
} }
}).then(function() { }).then(function() {
if (options.include) { var col = options.include ? this.name + '.' + this.primaryKeyField : '*';
col = this.name + '.' + this.primaryKeyField;
expandIncludeAll.call(this, options);
validateIncludedElements.call(this, options);
}
Utils.mapOptionFieldNames(options, this); options.plain = !options.group;
options.plain = options.group ? false : true;
options.dataType = new DataTypes.INTEGER(); options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false; options.includeIgnoreAttributes = false;
// No limit, offset, order or attributes for the options max be given to count()
// Set them to null to prevent scopes setting those values
options.limit = null; options.limit = null;
options.offset = null; options.offset = null;
options.order = null; options.order = null;
options.attributes = [];
return this.aggregate(col, 'count', options); return this.aggregate(col, 'count', options);
}); });
...@@ -1650,6 +1639,7 @@ Model.prototype.count = function(options) { ...@@ -1650,6 +1639,7 @@ Model.prototype.count = function(options) {
* include: [ * include: [
* { model: Profile, required: true} * { model: Profile, required: true}
* ], * ],
* distinct: true,
* limit 3 * limit 3
* }); * });
* ``` * ```
...@@ -1666,38 +1656,11 @@ Model.prototype.findAndCount = function(options) { ...@@ -1666,38 +1656,11 @@ Model.prototype.findAndCount = function(options) {
throw new Error('The argument passed to findAndCount must be an options object, use findById if you wish to pass a single primary key value'); throw new Error('The argument passed to findAndCount must be an options object, use findById if you wish to pass a single primary key value');
} }
var self = this var self = this;
// no limit, offset, order, attributes for the options given to count() var countOptions = Utils.cloneDeep(options);
, countOptions = _.omit(_.clone(options), ['offset', 'limit', 'order', 'attributes']); if (countOptions.attributes) {
countOptions.attributes = undefined;
conformOptions(countOptions, this);
if (countOptions.include) {
countOptions.include = _.cloneDeepWith(countOptions.include, function (element) {
if (element instanceof Model) return element;
if (element instanceof Association) return element;
return undefined;
});
expandIncludeAll.call(this, countOptions);
validateIncludedElements.call(this, countOptions);
var keepNeeded = function(includes) {
return includes.filter(function (include) {
if (include.include) include.include = keepNeeded(include.include);
return include.required || include.hasIncludeRequired;
});
};
countOptions.include = keepNeeded(countOptions.include);
if (countOptions.include.length) {
// Use distinct to count the number of parent rows, instead of the number of matched includes
countOptions.distinct = true;
}
} }
return self.count(countOptions).then(function(count) { return self.count(countOptions).then(function(count) {
if (count === 0) { if (count === 0) {
return { return {
...@@ -1708,7 +1671,7 @@ Model.prototype.findAndCount = function(options) { ...@@ -1708,7 +1671,7 @@ Model.prototype.findAndCount = function(options) {
return self.findAll(options).then(function(results) { return self.findAll(options).then(function(results) {
return { return {
count: count || 0, count: count || 0,
rows: (results && Array.isArray(results) ? results : []) rows: results
}; };
}); });
}); });
......
...@@ -110,7 +110,7 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -110,7 +110,7 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}); });
}); });
it('should count on a where and not use an uneeded include', function() { it('should count on a where', function() {
var Project = this.sequelize.define('Project', { var Project = this.sequelize.define('Project', {
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true },
project_name: { type: DataTypes.STRING} project_name: { type: DataTypes.STRING}
...@@ -136,7 +136,8 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -136,7 +136,8 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}).then(function() { }).then(function() {
return User.findAndCountAll({ return User.findAndCountAll({
where: {id: userId}, where: {id: userId},
include: [Project] include: [Project],
distinct: true
}); });
}).then(function(result) { }).then(function(result) {
expect(result.rows.length).to.equal(1); expect(result.rows.length).to.equal(1);
...@@ -197,7 +198,8 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -197,7 +198,8 @@ describe(Support.getTestDialectTeaser('Include'), function() {
// Query for the first instance of Foo which have related Bars with m === 'yes' // Query for the first instance of Foo which have related Bars with m === 'yes'
return Foo.findAndCountAll({ return Foo.findAndCountAll({
include: [{ model: Bar, where: { m: 'yes' } }], include: [{ model: Bar, where: { m: 'yes' } }],
limit: 1 limit: 1,
distinct: true
}); });
}).then(function(result) { }).then(function(result) {
// There should be 2 instances matching the query (Instances 1 and 2), see the findAll statement // There should be 2 instances matching the query (Instances 1 and 2), see the findAll statement
......
...@@ -51,5 +51,23 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -51,5 +51,23 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}] }]
})).to.eventually.equal(1); })).to.eventually.equal(1);
}); });
it('should be able to use where clause on included models', function() {
var self = this;
var queryObject = {
col: 'username',
include: [self.Project],
where: {
'$Projects.name$': 'project1'
}
};
return self.User.count(queryObject).then(function(count) {
expect(parseInt(count)).to.be.eql(1);
queryObject.where['$Projects.name$'] = 'project2';
return self.User.count(queryObject);
}).then(function(count) {
expect(parseInt(count)).to.be.eql(0);
});
});
}); });
}); });
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, current = Support.sequelize
, sinon = require('sinon')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, Promise = require('bluebird');
describe(Support.getTestDialectTeaser('Model'), function () {
describe('method count', function () {
before(function () {
this.oldFindAll = current.Model.prototype.findAll;
this.oldAggregate = current.Model.prototype.aggregate;
current.Model.prototype.findAll = sinon.stub().returns(Promise.resolve());
this.User = current.define('User', {
username: DataTypes.STRING,
age: DataTypes.INTEGER
});
this.Project = current.define('Project', {
name: DataTypes.STRING
});
this.User.hasMany(this.Project);
this.Project.belongsTo(this.User);
});
beforeEach(function () {
this.stub = current.Model.prototype.aggregate = sinon.stub().returns(Promise.resolve());
});
after(function () {
current.Model.prototype.findAll = this.oldFindAll;
current.Model.prototype.aggregate = this.oldAggregate;
});
describe('should pass the same options to model.aggregate as findAndCount', function () {
it('with includes', function () {
var self = this;
var queryObject = {
include: [self.Project]
};
return self.User.count(queryObject).then(function () {
return self.User.findAndCount(queryObject);
}).then(function () {
var count = self.stub.getCall(0).args;
var findAndCount = self.stub.getCall(1).args;
expect(count).to.eql(findAndCount);
});
});
it('attributes should be stripped in case of findAndCount', function () {
var self = this;
var queryObject = {
attributes: ['username']
};
return self.User.count(queryObject).then(function () {
return self.User.findAndCount(queryObject);
}).then(function () {
var count = self.stub.getCall(0).args;
var findAndCount = self.stub.getCall(1).args;
expect(count[2].attributes).to.eql(['username']);
expect(count).not.to.eql(findAndCount);
count[2].attributes = undefined;
expect(count).to.eql(findAndCount);
});
});
});
});
});
\ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!