fix: push limit/offset into the query instead of materializing then slicing
R-CMD-check / check (push) Successful in 4m6s
R-CMD-check / check (pull_request) Successful in 3m33s

cog-api's paginate() sliced an already-fully-materialized result: every
page of a deep sweep re-ran the whole cog_spending()/cog_revenue() query
and re-listified every row, just to keep up to 1000 and discard the
rest. A 193,105-row/194-page fleet-wide sweep (cog_explorer's Southern
guide, corpus summary build) repeated that full cost 194 times and
wedged the production server for hours on 2026-08-06 -- single request,
CPU-bound, single-threaded plumber process, no other request could get
through, not even /health.

cog_spending()/cog_revenue() gain optional limit/offset, pushed into
.build_verb_sql() as SQL LIMIT/OFFSET behind the existing (already
deterministic) ORDER BY. The full unpaginated row count rides along via
COUNT(*) OVER() in the same scan -- exposed as a total_rows attribute --
so a caller walking pages never needs a second round trip to ask how
many there are. A page now costs O(limit), not O(full result).

Mutually exclusive with complete = TRUE (which fills a grid over the
FULL requested (year, category) space -- pagination over a partial slice
of already-grouped rows has no defined meaning for the cells it would
fill) and with recipe (whose result comes from a separate, not-yet-wired
query path). Both abort with a clear classed condition rather than
silently ignoring the parameter.

limit/offset default to NULL; every existing call site is unaffected.
This commit is contained in:
2026-08-06 13:56:27 -04:00
parent e3ab26c3e6
commit 6a06302036
6 changed files with 256 additions and 12 deletions
+29
View File
@@ -0,0 +1,29 @@
# Mirror of test-spending-pagination.R for cog_revenue(), which shares the
# same .verb_spendrev()/.build_verb_sql() pushdown -- see that file for the
# incident this fixes.
test_that("cog_revenue limit/offset page correctly and report total_rows", {
skip_if_no_corpus()
full <- cog_revenue("121011212191", years = 2019:2020, category = NULL)
page <- cog_revenue("121011212191", years = 2019:2020, category = NULL,
limit = 5L, offset = 3L)
expect_equal(nrow(page), 5L)
expect_equal(page[c("year", "canonical_govid", "revenue_subtype", "category")],
full[4:8, c("year", "canonical_govid", "revenue_subtype", "category")],
ignore_attr = TRUE)
expect_equal(attr(page, "total_rows"), nrow(full))
})
test_that("cog_revenue limit unset by default leaves total_rows absent", {
skip_if_no_corpus()
r <- cog_revenue("121011212191", 2020L, "Property Tax")
expect_null(attr(r, "total_rows"))
})
test_that("cog_revenue complete + limit conflict aborts the same way as cog_spending", {
skip_if_no_corpus()
expect_error(
cog_revenue("121011212191", 2020L, "Property Tax", complete = TRUE, limit = 5L),
class = "uscogdata_complete_pagination_conflict"
)
})