A cursor encodes the sort field it was minted under (field|value|id, cursor.ts:15). On decode, buildWhereClause uses the cursor's field for the keyset column but buildOrderClause uses the query's sort field (query.ts:139-147 vs 155-159). If a cursor minted under sort: createdAt is replayed against sort: version, the keyset predicate (on created_at) and the ORDER BY (on version) disagree — producing skipped or duplicated rows across the page boundary rather than an error.
This is a structurally-incoherent request, exactly the class decodeCursor already rejects for malformed/unknown-field cursors.
Fix
Cross-check the cursor's embedded field against the query's effective sort field (getSortField(query)); on mismatch throw StackQueryError (bad_request/400), consistent with the rest of the cursor codec. One comparison, at the point the cursor is decoded for the query.
Tests
Refs
#50 (cursor pagination), #53 (malformed-cursor → 400 bad_request). From docs/design-assessment-2026-07.md §F6.
A cursor encodes the sort field it was minted under (
field|value|id,cursor.ts:15). On decode,buildWhereClauseuses the cursor's field for the keyset column butbuildOrderClauseuses the query's sort field (query.ts:139-147vs155-159). If a cursor minted undersort: createdAtis replayed againstsort: version, the keyset predicate (oncreated_at) and theORDER BY(onversion) disagree — producing skipped or duplicated rows across the page boundary rather than an error.This is a structurally-incoherent request, exactly the class
decodeCursoralready rejects for malformed/unknown-field cursors.Fix
Cross-check the cursor's embedded field against the query's effective sort field (
getSortField(query)); on mismatch throwStackQueryError(bad_request/400), consistent with the rest of the cursor codec. One comparison, at the point the cursor is decoded for the query.Tests
sort.field, throwsStackQueryErrorrather than returning an incoherent pageRefs
#50 (cursor pagination), #53 (malformed-cursor → 400
bad_request). Fromdocs/design-assessment-2026-07.md§F6.