verbs were left materializing everything and slicing in R -- the pattern behind
the 2026-08-06 production incident. cog_gov_search() had no LIMIT at all, so an
unfiltered call returns the entire 40,336-row crosswalk.
Extracted the #39 machinery into R/pagination.R first (.validate_pagination(),
.paginate_sql(), .take_pagination_total()) rather than growing a third inline
copy: three definitions of what total_rows means is three places for it to
drift. Conflict refusals stay at the call sites because each verb's conflict
set differs. .verb_spendrev() now uses the shared helpers and is unchanged in
behaviour.
The empty-page fallback query is now passed as a thunk, so the unpaginated SQL
is only BUILT when an offset actually lands past the end instead of on every
paged call.
Two things #57 did not anticipate:
- cog_gov_search()'s ORDER BY was not a total order. population_acs DESC NULLS
LAST leaves ties -- and the whole NULL block -- in scan order, so two requests
can order them differently and a paged sweep duplicates one row while dropping
another. Added canonical_govid as tiebreaker. Unpaginated output changes only
in the relative order of already-tied rows.
- Basket mode returns one resolved row per requested name plus a sidecar
covering all of them, so a page of it is not a page of anything the caller
asked for. Refused with uscogdata_basket_pagination_conflict rather than
silently ignoring the arguments.
Both default to NULL, so cog-api adopts them behind its existing formals()
probe with no lockstep deploy.
Suite: 1067 passed, 0 failed, 0 warnings (2 pre-existing live-corpus skips).