Skip to content

chunkById

Process matching rows in fixed-size pages, using keyset pagination instead of OFFSET. Safer than chunk when rows are inserted or deleted while you're iterating.

Signature

ts
FedacoBuilder<T>.chunkById(
  count: number,
  callback: (models: T[], page: number) => Promise<boolean | void> | boolean | void,
  column?: string,
  alias?: string,
): Promise<boolean>

Parameters

NameDescription
countPage size.
callbackReceives the page's models. Return false to stop iteration.
columnColumn to paginate by. Defaults to the model's primary key.
aliasAlias for the column if it's been aliased in the SELECT (joins, subqueries).

Returns

Promise<boolean>true if iteration completed, false if a callback short-circuited.

Real-World Use Cases

1. Backfill safe under concurrent writes

ts
await User.createQuery()
  .where('legacy_password_hash', 'is not', null)
  .chunkById(500, async (users) => {
    for (const u of users) {
      await migrateHash(u);
    }
  });

Each chunk is WHERE id > <last_id> ORDER BY id LIMIT 500. Even if other code inserts users mid-scan, you keep moving forward — no rows are skipped or revisited.

2. Custom column

Useful when the table has a sequential created_at and you want time-ordered iteration:

ts
await Event.createQuery()
  .where('processed', false)
  .chunkById(100, async (batch) => {
    for (const e of batch) await process(e);
  }, 'created_at');

The column should be unique and monotonic to avoid skipping ties.

3. Stop early

ts
let processed = 0;
await Order.createQuery().chunkById(1000, (orders) => {
  for (const o of orders) processOrder(o);
  processed += orders.length;
  if (processed >= 100_000) return false;
});

4. Aliased column

When joining and the primary key column has been aliased:

ts
await Order.createQuery()
  .leftJoin('users', 'users.id', '=', 'orders.user_id')
  .select('orders.id as order_id', 'users.email')
  .chunkById(500, async (rows) => {...}, 'orders.id', 'order_id');

chunkById vs chunk vs each / eachById

MethodPagination strategyStable under writes?Per-row callback?
chunkLIMIT/OFFSET✗ shifts on insert/delete✗ — chunk
chunkByIdWHERE id > X✗ — chunk
eachwraps chunk
eachByIdwraps chunkById

Default to chunkById / eachById unless you've measured that OFFSET is faster on a static table.

Common Pitfalls

  • Column must be unique and monotonic. chunkById on a non-unique column may skip rows that share the same value at a page boundary.
  • Order is impliedchunkById adds its own ORDER BY column ASC. Don't add a conflicting one.
  • Cursor lives in memory. If your iteration takes hours, the last id is still in JS — fine for processes, but a long-running cursor could be lost on crash. Add idempotent restart logic if needed.

See Also

Released under the MIT License.