Area: sdk · query — bug (correctness + perf) · found via WaveHouse-Stats dogfooding
Expected: Paginating e.g. from('gh_events').orderBy('event_ts','desc').limit(50) through Result.next()/hasMore returns every row exactly once, and each page's request stays a fixed size.
Actual — two defects in QueryBuilder._fetchNext (clients/ts/src/query-builder.ts:239-260):
- (a) drops boundary ties. The cursor keys only on
orderBy[0] (:245) with a strict </> (cursorOp = dir === "desc" ? "lt" : "gt", :254). When more rows share the boundary event_ts than fit in a page, the remainder are excluded from the next page and never returned — a silent gap on any non-unique sort column. There is no compound/tiebreak cursor (e.g. a unique event_id secondary key).
- (b) accumulates the cursor predicate. Each
next() does filters: [...this._state.filters, cursorFilter] (:257), keeping the prior page's bound and appending a new one, so page N sends N event_ts < filters (only the tightest binds). Correctness holds, but request body + server-side parse cost grow linearly with pagination depth.
Impact: Silent row loss at page seams on non-unique-ordered columns — fine for a recent-activity feed, wrong for exhaustive/audit reads (Stats skipped a same-second CI event on "Load more" over the live feed). (b) is request-size cruft at shallow depths. Neither defect causes duplication.
Note (not a spec): a fix picks a tiebreak/compound cursor key and replaces rather than accumulates the cursor bound. Client-side keyset cursor — distinct from the backend-owned server-side cursor follow-up.
Related: #274
From WaveHouse-Stats WAVEHOUSE-FEEDBACK.md dogfooding (logged 2026-07-08, Stats #57); validated by code-read against 774faec (origin/main) on 2026-07-09.
Area: sdk · query — bug (correctness + perf) · found via WaveHouse-Stats dogfooding
Expected: Paginating e.g.
from('gh_events').orderBy('event_ts','desc').limit(50)throughResult.next()/hasMorereturns every row exactly once, and each page's request stays a fixed size.Actual — two defects in
QueryBuilder._fetchNext(clients/ts/src/query-builder.ts:239-260):orderBy[0](:245) with a strict</>(cursorOp = dir === "desc" ? "lt" : "gt",:254). When more rows share the boundaryevent_tsthan fit in a page, the remainder are excluded from the next page and never returned — a silent gap on any non-unique sort column. There is no compound/tiebreak cursor (e.g. a uniqueevent_idsecondary key).next()doesfilters: [...this._state.filters, cursorFilter](:257), keeping the prior page's bound and appending a new one, so page N sends Nevent_ts <filters (only the tightest binds). Correctness holds, but request body + server-side parse cost grow linearly with pagination depth.Impact: Silent row loss at page seams on non-unique-ordered columns — fine for a recent-activity feed, wrong for exhaustive/audit reads (Stats skipped a same-second CI event on "Load more" over the live feed). (b) is request-size cruft at shallow depths. Neither defect causes duplication.
Note (not a spec): a fix picks a tiebreak/compound cursor key and replaces rather than accumulates the cursor bound. Client-side keyset cursor — distinct from the backend-owned server-side cursor follow-up.
Related: #274
From WaveHouse-Stats
WAVEHOUSE-FEEDBACK.mddogfooding (logged 2026-07-08, Stats #57); validated by code-read against774faec(origin/main) on 2026-07-09.