sequelize: limit + offset doesn't work together with where clause in include

The models I’ve created:

var Post = sequelize.define('post', {
        title: Sequelize.STRING,
        content: Sequelize.TEXT
    }),

    User = sequelize.define('user', {
        username: Sequelize.STRING
    }),

    Interest = sequelize.define('interest', {
        name: Sequelize.STRING
    })

User.hasMany(Post, {as: 'posts', foreignKey: 'userId'})
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'})

User.belongsToMany(Interest, {as: 'interests', through: 'users_interests'})
Interest.belongsToMany(User, {as: 'users', through: 'users_interests'})

inserted data:


sequelize.sync({force: true}).then(function(){
    var chainer = new Sequelize.Utils.QueryChainer()
    async.auto({
        users: function(callback) {
            User.bulkCreate([
                {username: "tyler"},
                {username: "skyler"},
                {username: "hector"}
            ]).done(function(){
                User.findAll().done(callback)
            })
        },
        posts: ['users', function(callback, result) {
            Post.bulkCreate([
                {title: "a", content: "aaa"},
                {title: "b", content: "bbb"}
            ]).done(function(){
                Post.findAll().then(function(posts) {
                    chainer.add(posts[0].setAuthor(result.users[1]))
                    chainer.add(posts[1].setAuthor(result.users[0]))
                    chainer.run().done(callback)
                })
            })
        }],
        interests: function(callback) {
            Interest.bulkCreate([
                {name: "walking"},
                {name: "sleeping"},
                {name: "hunting"},

            ]).done(function(){
                Interest.findAll().done(callback)
            })
        },

        users_interests: ['users', 'interests', function(callback, result) {

            chainer.add(result.users[0].setInterests([result.interests[1]]))
            chainer.add(result.users[2].setInterests([result.interests[0], result.interests[2]]))
            chainer.run().done(callback)
        }]

    })
}).catch(function(error) {
    console.error(error)
})

now I like to get all posts from users who have the interest sleeping, using limit and offset. I do it as follows:

    Post.findAll(
    {
        limit: 3, offset: 0,
        include: [{model: User, as: 'author', where: {}, include: [
            {model: Interest, as: 'interests', where: {name: 'sleeping'}}

        ]}]
    })

    .then(function(posts) {
        res.json(posts)
    })
    .catch(function(error) {
        next(error)
    })

error:

Unknown column 'user.userId' in 'where clause'

SequelizeDatabaseError: Unknown column 'user.userId' in 'where clause'
    at module.exports.Query.formatError (/home/ich/express/node-app/node_modules/sequelize/lib/dialects/mysql/query.js:158:16)
    at Query.results.on.on.on.alreadyEnded (/home/ich/express/node-app/node_modules/sequelize/lib/dialects/mariadb/query.js:99:30)
    at Query.emit (events.js:95:17)
    at Client.fnQueryErr (/home/ich/express/node-app/node_modules/mariasql/lib/Client.js:129:36)
    at Client.emit (events.js:95:17)
    at Client._processQueries (/home/ich/express/node-app/node_modules/mariasql/lib/Client.js:340:20)
    at /home/ich/express/node-app/node_modules/mariasql/lib/Client.js:239:38
    at process._tickCallback (node.js:442:13)

It works when I cut out limit and offset, OR the the where clause in the included. The same applies for findAndCountAll()

node v0.10.35 sequelize@2.0.0-rc8 mariasql@0.1.21

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 8
  • Comments: 55 (12 by maintainers)

Most upvoted comments

In my case, adding duplicating: false solves the issue.

Observations

It prevents it from generating subquery

Code Sample

let { admin, year, month, limit, offset } = args
let yearDatePart = [db.sequelize.fn('date_part', 'year', db.sequelize.col('Cards.createdAt')), "year"]
        let monthDatePart = [db.sequelize.fn('date_part', 'month', db.sequelize.col('Cards.createdAt')), "month"]
        let artistCount = [db.sequelize.fn('COUNT', db.sequelize.col('Cards.artistId')), "count"]
        
        let attributes = ["id", "firstName", "lastName", "displayName", "avatar", artistCount ]
        let group = ["User.id", "firstName", "lastName","displayName", "avatar"]

        let monthWhereCondition = db.sequelize.where(db.sequelize.fn('date_part', 'month', db.sequelize.col('Cards.createdAt')), { $eq: month })
        let yearWhereCondtion = db.sequelize.where(db.sequelize.fn('date_part', 'year', db.sequelize.col('Cards.createdAt')), { $eq: year })        
        
        let where = { $and: { yearWhereCondtion, monthWhereCondition } }
        
        let order = [[db.sequelize.literal('count'), 'DESC']]

        let include = [{
            model: db.Card,
            required: false,
            duplicating: false,
            attributes: [],            
            where
        }]
    return await db.User.findAll({ logging: console.log, attributes, limit, offset, include, group, order,  }, { raw: true})

I have the exact same issue and it is driving me nuts 😦 Pagination with some joins and a where clause is basically impossible while this bug is not fixed. Can this get a higher priority please?

@holographix then do a PR and solve it, it’s an open source project, you can do this any time. It’s a shame that in 2017 people think that anything is granted and they deserve stuff. Instead of this kind of behavior you can read all the comments above and learn something useful, like how to ask questions and say things nicely.

This is a rather big issue as the only “workaround” would be to load the entire table and splice the array. Try to do that on a table with millions of entries… I hope you have some ram and cpu time to spare… This happens on EVERY kind of pagination attempt, if the data is somehow associated and has a where in it.

I also don’t get what’s so hard about this to change? The limit/offset currently are applied to the include subquery, it just has to be applied to the main query (or the last inner join). There is NO need for another query or something. The offset/limit currently is simply applied to the wrong sub query. This is a clear bug in sequelize and not just a feature request.

Example:

Wrong:

SELECT * FROM table_through AS TableThrough INNER JOIN table2 AS Table2 ON TableThrough.Table2_id = Table2.id WHERE (Table1.id = TableThrough.Table1_id) LIMIT 1 ) IS NOT NULL LIMIT 1, 1) AS Table1 INNER JOIN (table_through AS table2.TableThrough INNER JOIN table2 AS table2 ON table2.id = table2.TableThrough.Table2_id) ON Table1.id = table2.TableThrough.Table1_id AND table2.id = 1;

correct:

SELECT * FROM TableThrough AS TableThrough INNER JOIN table2 AS Table2 ON TableThrough.Table2_id = Table2.id WHERE (Table1.id = TableThrough.Table1_id) LIMIT 1 ) IS NOT NULL) AS Table1 INNER JOIN (table_through AS table2.TableThrough INNER JOIN table2 AS table2 ON table2.id = table2.TableThrough.Table2_id) ON Table1.id = table2.TableThrough.Table1_id AND table2.id = 1 LIMIT 1, 1;

Generated by:

Table1.findAll({ include: [ {model: Table2, where: {id: 1}} ], offset: 1, limit: 1 })

Table1.belongsToMany(models.Table2, { through: ‘TableThrough’ }); Table2.belongsToMany(models.Table1, { through: ‘TableThrough’});

you can use duplicating: false on the included tables and probably solve the problem. LE: or you can use distinct: true where needed.

I also have same kind of issue when adding limit and offset after an include. It seems the JOINed tables are missing in sub-query in the generated SQL.

Here is my case (using sequelize 2.0.0-rc8):

return Invoice.findAll({
    // Consider all invoices which closed or have been refunded
    // during the period
    where: Sequelize.or(
      Sequelize.and(
        { 'closed_at': { gte: fromDate } },
        { 'closed_at': { lt : toDate } }
      ), Sequelize.and(
        { 'manually_refunded_at': { gte: fromDate } },
        { 'manually_refunded_at': { lt : toDate } }
      ), Sequelize.and(
        { 'transactions.created_at': { gte: fromDate } },
        { 'transactions.created_at': { lt : toDate } },
        { 'transactions.status': 'success' }
      )
    ),
    include: [
      // Needed for calculated invoice fields and payment method
      { model: Adjustment },
      // Needed for subscription start
      {
        model: Subscription,
        required: false
      },
      // Needed to create credit notes before 2015-01-01
      {
        model: Transaction,
        where: {
          status: 'success',
          action: 'refund'
        },
        // Create a left join
        required: false
      }
    ],
    order: [
      [ 'closed_at', 'ASC' ],
      [ 'invoice_number', 'ASC' ],
    ],
    limit: pagingLimit,
    offset: page * pagingLimit,
  });

The SQL

SELECT "invoice".*, "adjustments"."id" AS "adjustments.id", "adjustments"."recurly_id" AS "adjustments.recurly_id", "adjustments"."status" AS "adjustments.status", "adjustments"."description" AS "adjustments.description", "adjustments"."accounting_code" AS "adjustments.accounting_code", "adjustments"."product_code" AS "adjustments.product_code", "adjustments"."origin" AS "adjustments.origin", "adjustments"."unit_amount_in_cents" AS "adjustments.unit_amount_in_cents", "adjustments"."quantity" AS "adjustments.quantity", "adjustments"."discount_in_cents" AS "adjustments.discount_in_cents", "adjustments"."tax_in_cents" AS "adjustments.tax_in_cents", "adjustments"."total_in_cents" AS "adjustments.total_in_cents", "adjustments"."currency" AS "adjustments.currency", "adjustments"."taxable" AS "adjustments.taxable", "adjustments"."start_date" AS "adjustments.start_date", "adjustments"."end_date" AS "adjustments.end_date", "adjustments"."created_at" AS "adjustments.created_at", "adjustments"."inserted_at" AS "adjustments.inserted_at", "adjustments"."invoice_id" AS "adjustments.invoice_id", "subscription"."id" AS "subscription.id", "subscription"."recurly_id" AS "subscription.recurly_id", "subscription"."status" AS "subscription.status", "subscription"."unit_amount_in_cents" AS "subscription.unit_amount_in_cents", "subscription"."currency" AS "subscription.currency", "subscription"."quantity" AS "subscription.quantity", "subscription"."activated_at" AS "subscription.activated_at", "subscription"."canceled_at" AS "subscription.canceled_at", "subscription"."expires_at" AS "subscription.expires_at", "subscription"."total_billing_cycles" AS "subscription.total_billing_cycles", "subscription"."remaining_billing_cycles" AS "subscription.remaining_billing_cycles", "subscription"."current_period_started_at" AS "subscription.current_period_started_at", "subscription"."current_period_ends_at" AS "subscription.current_period_ends_at", "subscription"."trial_started_at" AS "subscription.trial_started_at", "subscription"."trial_ends_at" AS "subscription.trial_ends_at", "subscription"."tax_in_cents" AS "subscription.tax_in_cents", "subscription"."tax_type" AS "subscription.tax_type", "subscription"."tax_region" AS "subscription.tax_region", "subscription"."tax_rate" AS "subscription.tax_rate", "subscription"."inserted_at" AS "subscription.inserted_at", "subscription"."account_id" AS "subscription.account_id", "subscription"."plan_id" AS "subscription.plan_id", "transactions"."id" AS "transactions.id", "transactions"."recurly_id" AS "transactions.recurly_id", "transactions"."action" AS "transactions.action", "transactions"."amount_in_cents" AS "transactions.amount_in_cents", "transactions"."tax_in_cents" AS "transactions.tax_in_cents", "transactions"."currency" AS "transactions.currency", "transactions"."status" AS "transactions.status", "transactions"."payment_method" AS "transactions.payment_method", "transactions"."reference" AS "transactions.reference", "transactions"."gateway_type" AS "transactions.gateway_type", "transactions"."source" AS "transactions.source", "transactions"."recurring" AS "transactions.recurring", "transactions"."test" AS "transactions.test", "transactions"."voidable" AS "transactions.voidable", "transactions"."refundable" AS "transactions.refundable", "transactions"."cvv_result" AS "transactions.cvv_result", "transactions"."avs_result" AS "transactions.avs_result", "transactions"."avs_result_street" AS "transactions.avs_result_street", "transactions"."avs_result_postal" AS "transactions.avs_result_postal", "transactions"."created_at" AS "transactions.created_at", "transactions"."account_first_name" AS "transactions.account_first_name", "transactions"."account_last_name" AS "transactions.account_last_name", "transactions"."account_company" AS "transactions.account_company", "transactions"."account_email" AS "transactions.account_email", "transactions"."billing_info_first_name" AS "transactions.billing_info_first_name", "transactions"."billing_info_last_name" AS "transactions.billing_info_last_name", "transactions"."billing_info_address1" AS "transactions.billing_info_address1", "transactions"."billing_info_address2" AS "transactions.billing_info_address2", "transactions"."billing_info_city" AS "transactions.billing_info_city", "transactions"."billing_info_state" AS "transactions.billing_info_state", "transactions"."billing_info_zip" AS "transactions.billing_info_zip", "transactions"."billing_info_country" AS "transactions.billing_info_country", "transactions"."billing_info_phone" AS "transactions.billing_info_phone", "transactions"."billing_info_vat_number" AS "transactions.billing_info_vat_number", "transactions"."billing_info_card_type" AS "transactions.billing_info_card_type", "transactions"."billing_info_year" AS "transactions.billing_info_year", "transactions"."billing_info_month" AS "transactions.billing_info_month", "transactions"."billing_info_first_six" AS "transactions.billing_info_first_six", "transactions"."billing_info_last_four" AS "transactions.billing_info_last_four", "transactions"."inserted_at" AS "transactions.inserted_at", "transactions"."account_id" AS "transactions.account_id", "transactions"."invoice_id" AS "transactions.invoice_id"
FROM (
  SELECT "invoice"."id", "invoice"."recurly_id", "invoice"."invoice_number", "invoice"."status",      "invoice"."po_number", "invoice"."vat_number", "invoice"."subtotal_in_cents", "invoice"."tax_in_cents", "invoice"."total_in_cents", "invoice"."currency", "invoice"."created_at", "invoice"."closed_at", "invoice"."tax_type", "invoice"."tax_region", "invoice"."tax_rate", "invoice"."original_invoice_number", "invoice"."manually_refunded_at", "invoice"."account_first_name", "invoice"."account_last_name", "invoice"."account_company", "invoice"."billing_info_first_name", "invoice"."billing_info_last_name", "invoice"."billing_info_address1", "invoice"."billing_info_address2", "invoice"."billing_info_city", "invoice"."billing_info_state", "invoice"."billing_info_zip", "invoice"."billing_info_country", "invoice"."billing_info_phone", "invoice"."billing_info_vat_number", "invoice"."billing_info_card_type", "invoice"."billing_info_year", "invoice"."billing_info_month", "invoice"."billing_info_first_six", "invoice"."billing_info_last_four", "invoice"."inserted_at", "invoice"."account_id", "invoice"."coupon_redemption_id", "invoice"."subscription_id"
  FROM "invoices" AS "invoice"
  WHERE (
  ("invoice"."closed_at" >= '2015-02-05 12:23:45.000 +00:00' 
    AND "invoice"."closed_at" < '2015-02-05 13:00:00.000 +00:00')
  OR ("invoice"."manually_refunded_at" >= '2015-02-05 12:23:45.000 +00:00'
    AND "invoice"."manually_refunded_at" < '2015-02-05 13:00:00.000 +00:00'
  ) OR ("transactions"."created_at" >= '2015-02-05 12:23:45.000 +00:00' AND "transactions"."created_at" < '2015-02-05 13:00:00.000 +00:00' AND "transactions"."status"='success')) 
  ORDER BY "invoice"."closed_at" ASC, "invoice"."invoice_number" ASC LIMIT 1000
) AS "invoice"
LEFT OUTER JOIN "adjustments" AS "adjustments" ON "invoice"."id" = "adjustments"."invoice_id"
LEFT OUTER JOIN "subscriptions" AS "subscription" ON "invoice"."subscription_id" = "subscription"."id" 
LEFT OUTER JOIN "transactions" AS "transactions" ON "invoice"."id" = "transactions"."invoice_id" AND "transactions"."status"='success' AND "transactions"."action"='refund'
ORDER BY "invoice"."closed_at" ASC, "invoice"."invoice_number" ASC;

The error

SequelizeDatabaseError: missing FROM-clause entry for table "transactions"
    at module.exports.Query.formatError (/var/www/node_modules/sequelize/lib/dialects/postgres/query.js:309:16)
    at null.<anonymous> (/var/www/node_modules/sequelize/lib/dialects/postgres/query.js:64:21)
    at emit (events.js:95:17)
    at Query.handleError (/var/www/node_modules/pg/lib/query.js:101:8)
    at null.<anonymous> (/var/www/node_modules/pg/lib/client.js:176:26)
    at emit (events.js:95:17)
    at Socket.<anonymous> (/var/www/node_modules/pg/lib/connection.js:109:12)
    at Socket.emit (events.js:95:17)
    at Socket.<anonymous> (_stream_readable.js:764:14)
    at Socket.emit (events.js:92:17)
    at emitReadable_ (_stream_readable.js:426:10)
    at emitReadable (_stream_readable.js:422:5)
    at readableAddChunk (_stream_readable.js:165:9)
    at Socket.Readable.push (_stream_readable.js:127:10)
    at TCP.onread (net.js:528:21)

I’d like to add that you indeed can get around this problem by simply adding your limit as a string to the order

var where = *some_where;
var order = 'id LIMIT 1,1';

Table1.findAll({
            include: [
                {model: Table2, where: where}
            ],
            order: order
        }

Personally I’m happy with that and haven’t stumpled upon an issue.

@smithaitufe

duplicating: false

inside of the include models worked for me too

Thank you guys for the thread.

Still, not mention of it in the official Docs.

Okay, I created a small test that isolates the issue for me. It is actually not the limit,offset but the cause has its root in the fact that I use camel case for the fields in my javascript and underscores in my database. If I then offset/limit, there is no way I can order anymore. Run the following code and see for yourself. I am getting Unhandled rejection SequelizeDatabaseError: Unknown column 'post.title_string' in 'order clause' and it does not matter whether I use “title_string” or “titleString” in the order clause.

var Sequelize = require("sequelize");

var sequelize = new Sequelize("sequelize_test", "root", "", {
    "host": "databox",
    "logging": true,
    "dialect": "mysql",
    "pool": {
        "max": 5,
        "min": 0,
        "idle": 10000
    }
});

var Post = sequelize.define('post', {
    titleString: { type: Sequelize.STRING, field: 'title_string' },
    contentString: { type: Sequelize.TEXT, field: 'content_string' }
});

var User = sequelize.define('user', {
    username: Sequelize.STRING
});

var Interest = sequelize.define('interest', {
    name: Sequelize.STRING
});

User.hasMany(Post);
Post.belongsTo(User);
User.belongsToMany(Interest, {through: 'users_interests'});
Interest.belongsToMany(User, {through: 'users_interests'});

sequelize.sync({force: true}).then(function() {

    User.create({
        name: "tyler",
        interests: [
            {name: "walking"}
        ],
        posts: [
            {titleString: "post1", contentString: "content1"},
            {titleString: "post2", contentString: "content2"}
        ]
    }, {
        include: [ Interest, Post ]
    }).then(user => {

        return Post.findAll({
            include: [
                {
                    model: User,
                    include: [
                        {
                            model: Interest
                        }
                    ]
                }
            ],
            order: [
                ['titleString', 'ASC']
            ],
            where: {
                titleString: 'post1'
            },
            limit: 1,
            offset: 0
        }).then(function(posts) {
            console.log(posts.length);
        });

    });

}).catch(function(error) {
    console.log(error);
});

So for me the quick solution would be to stop using camel case field names in my code.

for my own case, when I remove the LEFT OUTER JOIN by adding required: true for every includes i need it worked.

Great, thanks @janmeier. I’ll give it a try. In other projects I’ve often ended up pulling in knex for building raw queries when I need to circumvent the ORM, maybe I don’t have to this time.

Has this bug been fixed? I’m having the same issue with version 3.23.4

Category
  .findAll({
    attributes: [
      'id',
      'name',
      'parentId'
    ],
    where: {
      parentId: req.query.parentId
    },
    include: [
      {
        model: Category,
        as: 'children',
        attributes: [
          'id',
          'name'
        ]
      },
      {
        model: User,
        as: 'createdBy',
        attributes: [
          'id',
          'username'
        ]
      },
      {
        model: User,
        as: 'updatedBy',
        attributes: [
          'id',
          'username'
        ]
      },
      {
        model: User,
        as: 'deletedBy',
        attributes: [
          'id',
          'username'
        ]
      }
    ],
    offset: 0,
    limit: 10
});

Generated SQL statement

SELECT `Category`.*, `children`.`id` AS `children.id`, `children`.`name` AS `children.name`, `createdBy`.`id` AS `createdBy.id`, `createdBy`.`username` AS `createdBy.username`, `updatedBy`.`id` AS `updatedBy.id`, `updatedBy`.`username` AS `updatedBy.username`, `deletedBy`.`id` AS `deletedBy.id`, `deletedBy`.`username` AS `deletedBy.username` FROM (SELECT `Category`.`id`, `Category`.`name`, `Category`.`parentId` FROM `Categories` AS `Category` WHERE (`Category`.`deletedAt` IS NULL AND `Category`.`parentId` = NULL) LIMIT 0, 10) AS `Category` LEFT OUTER JOIN `Categories` AS `children` ON `Category`.`id` = `children`.`parentId` AND `children`.`deletedAt` IS NULL LEFT OUTER JOIN `Users` AS `createdBy` ON `Category`.`createdById` = `createdBy`.`id` AND `createdBy`.`deletedAt` IS NULL LEFT OUTER JOIN `Users` AS `updatedBy` ON `Category`.`updatedById` = `updatedBy`.`id` AND `updatedBy`.`deletedAt` IS NULL LEFT OUTER JOIN `Users` AS `deletedBy` ON `Category`.`deletedById` = `deletedBy`.`id` AND `deletedBy`.`deletedAt` IS NULL;

and the error

SequelizeDatabaseError: SQLITE_ERROR: no such column: Category.createdById

Models:

User = sequelize.define('User', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV1,
      primaryKey: true
    },
    username: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false,
      validate: {
        notEmpty: true,
        is: regex.username
      }
    }
  }, {
    paranoid: true,
    classMethods: {
      associate: (models) => {
        User.belongsTo(models.User, {
          foreignKey: 'createdById',
          as: 'createdBy'
        });

        User.belongsTo(models.User, {
          foreignKey: 'updatedById',
          as: 'updatedBy'
        });

        User.belongsTo(models.User, {
          foreignKey: 'deletedById',
          as: 'deletedBy'
        });      }
    }
  });


Category = sequelize.define('Category', {
  id: {
    type: DataTypes.UUID,
    defaultValue: DataTypes.UUIDV1,
    primaryKey: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      notEmpty: true
    }
  }
}, {
  indexes: [{
    unique: true,
    fields: ['name', 'parentId']
  }],
  paranoid: true,
  classMethods: {
    associate: (models: any) => {
      Category.belongsTo(models.User, {
        foreignKey: 'createdById',
        as: 'createdBy'
      });

      Category.belongsTo(models.User, {
        foreignKey: 'updatedById',
        as: 'updatedBy'
      });

      Category.belongsTo(models.User, {
        foreignKey: 'deletedById',
        as: 'deletedBy'
      });

      Category.belongsTo(models.Category, {
        foreignKey: 'parentId',
        as: 'parent'
      });

      Category.hasMany(models.Category, {
        foreignKey: 'parentId',
        as: 'children'
      });
    }
  }
});

I have examined the pagination problem with associated where clause problems in this article. https://malicaliskan.medium.com/e50dbc9de01d You can find why the problem occurs, in what conditions and what can be an easy solution to the issue in the article.

This is fixed by https://github.com/sequelize/sequelize/pull/9188 , I request thread participants to verify this