sequelize: Sequelize count returns wrong count when association models included

Sequelize count returns wrong count when association models included, in my case, via the model’s defaultScope.

What are you doing?

I have a Post model. The Post model has attachments (oneToMany)

I’m trying to count posts [with attachments] using Sequelize’s count

// Post model
module.exports = (sequelize, DataTypes) => {
  const Post = sequelize.define(
    'Post',
    {
      id: {
        primaryKey: true,
        type: DataTypes.UUID
      },
      title: DataTypes.STRING
    }
  )

  Post.associate = models => {
    Post.hasMany(models.PostAttachment, { as: 'attachments' })

    Post.addScope('defaultScope', {
      include: [
        { model: models.PostAttachment, as: 'attachments' }
      ],
      order: [
        ['id', 'ASC']
      ]
    }, {
     override: true
    })
  }

  return Post
}
// PostAttachment model
module.exports = (sequelize, DataTypes) => {
  const PostAttachment = sequelize.define(
    'PostAttachment',
    {
      id: {
        primaryKey: true,
        type: DataTypes.UUID
      },
      key: DataTypes.STRING
    }
  )

  PostAttachment.associate = models => {
    PostAttachment.belongsTo(models.Post)
  }

  return PostAttachment
}

Sequelize query

const posts = await Post.findAll()
const total = await Post.count()

return { posts, total }

Scenario 1: Single Post, multiple PostAttachments

Posts table

id title createdAt updatedAt
f8d68fc0-48b1-4e90-af73-c9a4dc577461 Title1 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00

PostAttachments table

id key PostId createdAt updatedAt
410819f6-d1c4-44f4-9a52-edbac765e714 Key1 f8d68fc0-48b1-4e90-af73-c9a4dc577461 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
754ca095-4967-4472-a0f4-1a8e2d761a24 Key2 f8d68fc0-48b1-4e90-af73-c9a4dc577461 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
754ca095-4967-4472-a0f4-1a8e2d761a25 Key3 f8d68fc0-48b1-4e90-af73-c9a4dc577461 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00

What do you expect to happen?

I’m expecting to get 1 post and total sum 1:

{
  "posts": [{...}],
  "total": 1
}

What is actually happening?

I’m actually getting 1 post and total sum 3:

{
  "posts": [{...}],
  "total": 3
}

Scenario 2: Multiple Posts, multiple PostAttachments

Posts table

id title createdAt updatedAt
f8d68fc0-48b1-4e90-af73-c9a4dc577461 Title1 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
01a528d2-8696-465f-8739-2b32bc52ceca Title2 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
01a528d2-8696-465f-8739-2b32bc52cecb Title3 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00

PostAttachments table

id key PostId createdAt updatedAt
410819f6-d1c4-44f4-9a52-edbac765e714 Key1 f8d68fc0-48b1-4e90-af73-c9a4dc577461 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
754ca095-4967-4472-a0f4-1a8e2d761a24 Key2 f8d68fc0-48b1-4e90-af73-c9a4dc577461 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
754ca095-4967-4472-a0f4-1a8e2d761a25 Key3 f8d68fc0-48b1-4e90-af73-c9a4dc577461 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
754ca095-4967-4472-a0f4-1a8e2d761a26 Key4 01a528d2-8696-465f-8739-2b32bc52ceca 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00
754ca095-4967-4472-a0f4-1a8e2d761a27 Key5 01a528d2-8696-465f-8739-2b32bc52ceca 2018-05-23 16:47:09.228000 +00:00 2018-05-23 16:47:09.228000 +00:00

What do you expect to happen?

I’m expecting to get 3 posts and total sum 3:

{
  "posts": [{...}, {...}, {...}],
  "total": 3
}

What is actually happening?

I’m actually getting 3 posts and total sum 6:

{
  "posts": [{...}, {...}, {...}],
  "total": 6
}

I ran a few configurations through. It appears the count calculations are based on each post multiplied by the maximum between 1 and the number of attachments per post, so:

  • PostA * Math.max(1, 3 attachments) = 3
  • PostB * Math.max(1, 2 attachments) = 2
  • PostC * Math.max(1, 0 attachments) = 1

Dialect: postgres Dialect version: XXX Database version: 11.1 Sequelize version: 4.43.0 Tested with latest release: No (If yes, specify that version)

Related: #1517, #9481, #9669, #10239

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 33
  • Comments: 16 (2 by maintainers)

Most upvoted comments

My solution, which may break other queries but works for my application, is to add a distinct condition. The distinct param is only applied to the count query so it ensures that the correct document count is returned, whilst allowing the internal Sequelize aggregation of associations to continue as intended.

Adding distinct: 'Article.id' to my Article.findAndCountAll query gives me the correct article count.

Here’s an enhanced workaround that may not cover every case but hasn’t failed me so far:

// Add a permanent global hook to prevent unknowingly hitting this Sequelize bug:
//   https://github.com/sequelize/sequelize/issues/10557
sequelize.addHook('beforeCount', function (options) {
  if (this._scope.include && this._scope.include.length > 0) {
    options.distinct = true
    options.col = this._scope.col || options.col || `"${this.options.name.singular}".id`
  }

  if (options.include && options.include.length > 0) {
    options.include = null
  }
})

HTH others. 💝

Although I agree this appears to be odd but count and findAll both are returning correct result.

Let’s first talk about findAll, it joins two tables, find 6 rows, sequelize arrange them under attachments so finally you get 3 array items. You can test this by passing raw: true which will return 6 rows.

Similarly count executes same query and see 6 rows, but it don’t need to arrange result into attachments (it can’t as db is returning row count directly) thus returns plain result of 6.

So in your situation I would try these alternatives

  1. Won’t use count, I already got findAll result I can use that to show count.
  2. Get rid of default scope and use a named scope, only apply that to findAll so count can return result correctly regardless of scope used in findAll

@sushantdhiman I understand why it’s happening but I don’t think this would be the expected behavior for anyone unless they were including { raw: true }.

This is even more troublesome for the folks using findAndCountAll (as in the other issues I’ve linked to). Per the Sequelize docs:

…this is useful when dealing with queries related to pagination where you want to retrieve data with a limit and offset but also need to know the total number of records that match the query.

So, if I used findAndCountAll as the DB in-between for my paginated API query, I could be told there were 6 total records but only have 3 returned. Unfortunately, that does not make any sense from the API consumer standpoint [unless I eventually had 6 finalized records and I was limiting that page to just 3 😉 ].

…although now I’m concerned how the limit and offset options would interact with association-expanded rows like this, too. 😬

I had the same issue and my solution is used distinct: true and col: ${DB.InventoryItem.name}.id` in options of findAndCountAll()

let items = await DB.InventoryItem.findAndCountAll({
        offset: offset,
        limit: limit,
        where: where,
        col: `${DB.InventoryItem.name}.id`,
        distinct: true
})

The root cause must be resolved by remove table left join and left outer join to get match condition based from options when using subfunction this.count(countOptions) in sequelize

adding distinct: true worked for me

@JamesMGreene I 100% agree that the expected behaviour of the count value from findAndCountAll should be the actual number of documents that exist, and not how it is currently implemented.

If I have a document with 2 associations, the count is returning 2 but only 1 document is returned. If I delete one of the associations, then count and returned documents are 1.

I would expect that the count matches the total number of documents, not the total number of documents + total number of associations.

Here’s an enhanced workaround that may not cover every case but hasn’t failed me so far:

// Add a permanent global hook to prevent unknowingly hitting this Sequelize bug:
//   https://github.com/sequelize/sequelize/issues/10557
sequelize.addHook('beforeCount', function (options) {
  if (this._scope.include && this._scope.include.length > 0) {
    options.distinct = true
    options.col = this._scope.col || options.col || `"${this.options.name.singular}".id`
  }

  if (options.include && options.include.length > 0) {
    options.include = null
  }
})

HTH others. 💝

It’s work

I developed this workaround that addresses the issue for inclusions from options.include:

// Add a permanent global hook to prevent unknowingly hitting this Sequelize bug:
//   https://github.com/sequelize/sequelize/issues/10557
sequelize.addHook('beforeCount', function (options) {
  if (this._scope.include && this._scope.include.length > 0) {
    throw new Error('A "count" operation executed with scope-based inclusions will return an incorrect result')
  }

  if (options.include && options.include.length > 0) {
    options.include = null
  }
})

However, as you can see in the code, it does NOT fix inclusions from the model’s active scope. It could but it would have to actually alter the scope itself, which is not really acceptable as it would negatively impact subsequent use of the scope (or the scoped model, anyway). 😭

The availability of a beforeCountAfterOptions hook (equivalent in concept to the beforeFindAfterOptions hook) would make this achievable but I can see that’s a bit more challenging to implement since this filters down into the aggregate function.