ReviewOS

stacks/bun-query-builder

cursorPaginate/chunkById/eachById emit a second WHERE, so they throw on any filtered query

#1090
Closed glennmichael123 opened this 22 days ago · 0 comments
22 days ago

Found on main @ 68fd61c. Reproduces on every dialect — this is not the SQLite-wrapper class of bug.

cursorPaginate appends its cursor predicate with an unconditional WHERE keyword rather than continuing an existing one with AND. So the moment the builder already has a filter, the SQL has two top-level WHEREs and fails to parse.

chunkById and eachById both delegate to cursorPaginate, so they inherit it.

Repro

const q = () => db.selectFrom('t').selectAll()

// works — no preceding where, so no cursor predicate collision
await q().cursorPaginate(3, 5, 'id')                     // { data: [6,7,8] }
await q().chunkById(3, 'id', rows => { /* ... */ })      // all 20 rows

// throws — the moment a filter exists
await q().where('b', '=', 1).cursorPaginate(3, 5, 'id')
await q().where('b', '=', 1).chunkById(3, 'id', rows => {})
await q().where('b', '=', 1).eachById(3, 'id', row => {})

All three raise:

SQLiteError: near "WHERE": syntax error

Note the cursor has to be non-null to see it: with cursor === undefined the predicate is never emitted, which is why chunkById's first page succeeds and the failure only lands on the second iteration.

Impact

.where(...).chunkById(...) is the natural way to walk a filtered table in batches, and it has never worked. Because the first page succeeds, a short test fixture that fits in one chunk passes and the failure only shows up against real data.

Source

cursorPaginate in packages/bun-query-builder/src/client.ts builds the predicate as sql`${q} WHERE ${...} > ${cursor}` . It needs the same AND-vs-WHERE decision every other where-method makes.

Verified by execution, not by reading.

Sign in to comment on this issue.