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

Commit 4f39a8a4 by Ruben Bridgewater Committed by Jan Aagaard Meier

Fix includes being removed from count queries (#6192)

Add unit test

Don't set distinct to true as a default
1 parent 43359bed
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
- [FIXED] Index names not quoted properly in `removeIndex` [#5888](https://github.com/sequelize/sequelize/issues/5888) - [FIXED] Index names not quoted properly in `removeIndex` [#5888](https://github.com/sequelize/sequelize/issues/5888)
- [FIXED] `Int4` range not properly parsed [#5747](https://github.com/sequelize/sequelize/issues/5747) - [FIXED] `Int4` range not properly parsed [#5747](https://github.com/sequelize/sequelize/issues/5747)
- [FIXED] `upsert` does not fail anymore on not null validations [#5711](https://github.com/sequelize/sequelize/issues/5711) - [FIXED] `upsert` does not fail anymore on not null validations [#5711](https://github.com/sequelize/sequelize/issues/5711)
- [FIXED] Don't remove includes from count queries and unify findAndCount and count queries. [#6123](https://github.com/sequelize/sequelize/issues/6123)
## BC breaks: ## BC breaks:
- Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive), previously was `()` (exclusive, exclusive) - Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive), previously was `()` (exclusive, exclusive)
......
...@@ -1521,7 +1521,7 @@ class Model { ...@@ -1521,7 +1521,7 @@ class Model {
* @param {Object} [options.where] A hash of search attributes. * @param {Object} [options.where] A hash of search attributes.
* @param {Object} [options.include] Include options. See `find` for details * @param {Object} [options.include] Include options. See `find` for details
* @param {Boolean} [options.paranoid=true] Set `true` to count only non-deleted records. Can be used on models with `paranoid` enabled * @param {Boolean} [options.paranoid=true] Set `true` to count only non-deleted records. Can be used on models with `paranoid` enabled
* @param {Boolean} [options.distinct] Apply COUNT(DISTINCT(col)) on primary key, `Model.aggregate` should be used for other columns * @param {Boolean} [options.distinct] Apply COUNT(DISTINCT(col)) on primary key or on options.col.
* @param {String} [options.col] Column on which COUNT() should be applied * @param {String} [options.col] Column on which COUNT() should be applied
* @param {Object} [options.attributes] Used in conjunction with `group` * @param {Object} [options.attributes] Used in conjunction with `group`
* @param {Object} [options.group] For creating complex counts. Will return multiple rows as needed. * @param {Object} [options.group] For creating complex counts. Will return multiple rows as needed.
...@@ -1533,35 +1533,28 @@ class Model { ...@@ -1533,35 +1533,28 @@ class Model {
* @return {Promise<Integer>} * @return {Promise<Integer>}
*/ */
static count(options) { static count(options) {
options = Utils.cloneDeep(options);
_.defaults(options, { hooks: true });
let col = options.col || '*';
let attributes = options.attributes;
return Promise.try(() => { return Promise.try(() => {
this._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(() => { }).then(() => {
let col = options.col || '*';
if (options.include) { if (options.include) {
col = this.name + '.' + this.primaryKeyField; col = this.name + '.' + this.primaryKeyField;
this._expandIncludeAll(options);
this._validateIncludedElements(options);
} }
Utils.mapOptionFieldNames(options, this); Utils.mapOptionFieldNames(options, this);
options.plain = options.group ? false : true; options.plain = !options.group;
options.dataType = new DataTypes.INTEGER(); options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false; options.includeIgnoreAttributes = false;
// No limit, offset or order 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 = attributes || [];
return this.aggregate(col, 'count', options); return this.aggregate(col, 'count', options);
}); });
...@@ -1605,37 +1598,10 @@ class Model { ...@@ -1605,37 +1598,10 @@ class Model {
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');
} }
// no limit, offset, order, attributes for the options given to count() const countOptions = Utils.cloneDeep(options);
const countOptions = _.omit(_.clone(options), ['offset', 'limit', 'order', 'attributes']); if (countOptions.attributes) {
countOptions.attributes = undefined;
this._conformOptions(countOptions, this);
if (countOptions.include) {
countOptions.include = _.cloneDeepWith(countOptions.include, element => {
if (element instanceof Model) return element;
if (element instanceof Association) return element;
return undefined;
});
this._expandIncludeAll(countOptions);
this._validateIncludedElements(countOptions);
const keepNeeded = includes => {
return includes.filter(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 this.count(countOptions).then(count => { return this.count(countOptions).then(count => {
if (count === 0) { if (count === 0) {
return { return {
...@@ -1645,7 +1611,7 @@ class Model { ...@@ -1645,7 +1611,7 @@ class Model {
} }
return this.findAll(options).then(results => ({ return this.findAll(options).then(results => ({
count: count || 0, count: count || 0,
rows: (results && Array.isArray(results) ? results : []) rows: results
})); }));
}); });
} }
......
...@@ -148,7 +148,8 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -148,7 +148,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);
...@@ -209,7 +210,8 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -209,7 +210,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
......
...@@ -1772,24 +1772,22 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1772,24 +1772,22 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}); });
}); });
it('supports distinct option', function() { it('supports distinct option', function () {
var Post = this.sequelize.define('Post', {}); const Post = this.sequelize.define('Post', {});
var PostComment = this.sequelize.define('PostComment', {}); const PostComment = this.sequelize.define('PostComment', {});
Post.hasMany(PostComment); Post.hasMany(PostComment);
return Post.sync({ force: true }).then(function() { return Post.sync({ force: true })
return PostComment.sync({ force: true }).then(function() { .then(() => PostComment.sync({ force: true }))
return Post.create({}).then(function(post) { .then(() => Post.create({}))
return PostComment.bulkCreate([{ PostId: post.id },{ PostId: post.id }]).then(function() { .then((post) => PostComment.bulkCreate([{ PostId: post.id },{ PostId: post.id }]))
return Post.count({ include: [{ model: PostComment, required: false }] }).then(function(count1) { .then(() => Promise.join(
return Post.count({ distinct: true, include: [{ model: PostComment, required: false }] }).then(function(count2) { Post.count({ distinct: false, include: [{ model: PostComment, required: false }] }),
expect(count1).to.equal(2); Post.count({ distinct: true, include: [{ model: PostComment, required: false }] }),
expect(count2).to.equal(1); (count1, count2) => {
}); expect(count1).to.equal(2);
}); expect(count2).to.equal(1);
}); })
}); );
});
});
}); });
}); });
......
...@@ -133,5 +133,22 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -133,5 +133,22 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}); });
}); });
it('should be able to use where clause on included models', function() {
const queryObject = {
col: 'username',
include: [this.Project],
where: {
'$Projects.name$': 'project1'
}
};
return this.User.count(queryObject).then((count) => {
expect(parseInt(count)).to.be.eql(1);
queryObject.where['$Projects.name$'] = 'project2';
return this.User.count(queryObject);
}).then((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'), () => {
describe('method count', () => {
before(() => {
this.oldFindAll = current.Model.findAll;
this.oldAggregate = current.Model.aggregate;
current.Model.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);
});
after(() => {
current.Model.findAll = this.oldFindAll;
current.Model.aggregate = this.oldAggregate;
});
beforeEach(() => {
this.stub = current.Model.aggregate = sinon.stub().returns(Promise.resolve());
});
describe('should pass the same options to model.aggregate as findAndCount', () => {
it('with includes', () => {
const queryObject = {
include: [this.Project]
};
return this.User.count(queryObject)
.then(() => this.User.findAndCount(queryObject))
.then(() => {
const count = this.stub.getCall(0).args;
const findAndCount = this.stub.getCall(1).args;
expect(count).to.eql(findAndCount);
});
});
it('attributes should be stripped in case of findAndCount', () => {
const queryObject = {
attributes: ['username']
};
return this.User.count(queryObject)
.then(() => this.User.findAndCount(queryObject))
.then(() => {
const count = this.stub.getCall(0).args;
const findAndCount = this.stub.getCall(1).args;
expect(count).not.to.eql(findAndCount);
count[2].attributes = undefined;
expect(count).to.eql(findAndCount);
});
});
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!