feat: complete = TRUE fills absent cells with their meaning (#18)
R-CMD-check / check (push) Successful in 3m7s
R-CMD-check / check (pull_request) Successful in 3m9s

Sparsification (cog_pipeline#64, SB194) stopped the corpus storing the wide
era's explicit zeros, which made absence ambiguous:

  <= FY2011  dense_source   absent => Census published $0
  >= FY2012  sparse_source  absent => not reported, unknown

A wide-era query whose cells were all $0 had begun returning nothing at all,
with no way to get them back -- strictly less than the reader exposed before,
which is why #64 filed this follow-on.

complete = TRUE fills the requested grid from `code_set` and stamps every row
with value_source: "reported", "census_zero" (amt 0), or "not_reported"
(amt NA). The NA is the point. Filling a modern absence with 0 would invent
data, which is exactly the error the representation contract exists to
prevent -- and it makes this strictly MORE informative than the
pre-sparsification corpus, which could not tell a published zero from an
unreported cell either.

Measured on the fixture, Broward County: FY2011 returns 28 reported + 16
census_zero; FY2019 returns 30 reported + 14 not_reported. The five
categories that walkthrough finding F-006 read as "retired at FY2012" now
report themselves correctly as census_zero before and not_reported after.

Scoping decisions, each of which would invent rows if taken loosely:

  - The grid is per government TYPE (code_set.type). Filling against the
    union of all types would give a county cells like "state IG transfer to
    school districts", indistinguishable from real census zeros.
  - NOT is_aggregate, mirroring spending_long/revenue_long. Without it the
    grid offers cells those views never return, so each would fill as a
    phantom $0.
  - Filling happens BEFORE per_capita and inflation, so a census_zero stays
    0 through both and a not_reported stays NA rather than becoming 0.

Two new views (36-representation, 37-code_set) are gated on the manifest
LISTING those tables, not on schema_version. Sparsification did not bump the
version -- the fixture this package shipped against until 2026-07-30 was
already v6 and carried neither table -- so a version gate would register a
view over a missing file and fail at CREATE VIEW time on exactly the corpora
the check exists to tolerate. with_corpus_missing_representation() models
that corpus and asserts the abort.

Refused where the fill would be guesswork, both classed
uscogdata_complete_unsupported: a recipe defines its own component codes and
never touches summary_categories; the intergovernmental leg deliberately
keeps aggregate rows (inst/sql/24-ig_long.sql) so its cells are not the ones
code_set describes.

Expected cell sets in the tests are computed from the corpus parquet
directly, never through the verb -- verifying what a filter does through
that same filter proves nothing.

Closes DoD 2, 3 and 4 of #18. DoD 5 (the cog-api follow-on) is filed
separately.

Suite: 658 pass / 0 fail / 3 skip (was 629/0/3). rcmdcheck clean.
This commit is contained in:
2026-07-30 11:47:51 -04:00
parent 8db944e4a0
commit af85a23ea7
14 changed files with 624 additions and 42 deletions
+36
View File
@@ -85,6 +85,42 @@ with_doctored_schema_version <- function(version, code) {
force(code)
}
# Copy the bundled fixture to a temp dir with representation.parquet and
# code_set.parquet removed (and dropped from the manifest's metadata list),
# then run `code` against it. Models a corpus published BEFORE sparsification:
# schema_version is left alone deliberately, because it was never bumped for
# that change -- the pre-sparsification fixture this package shipped until
# 2026-07-30 was schema v6 and carried neither table. Presence in the manifest
# is therefore the only honest signal, and this helper is what proves the
# package keys off it rather than off the version number.
with_corpus_missing_representation <- function(code) {
src <- fixture_corpus_path()
tmp <- withr::local_tempdir(.local_envir = parent.frame())
file.copy(list.files(src, full.names = TRUE), tmp, recursive = TRUE)
dropped <- c("representation.parquet", "code_set.parquet")
file.remove(file.path(tmp, "data", dropped))
manifest_path <- file.path(tmp, "manifest.json")
m <- jsonlite::fromJSON(manifest_path, simplifyVector = FALSE)
m$files$metadata <- Filter(
function(f) !basename(f$path) %in% dropped, m$files$metadata
)
writeLines(
jsonlite::toJSON(m, auto_unbox = TRUE, pretty = TRUE, null = "null"),
manifest_path
)
old_url <- Sys.getenv("USCOGDATA_URL", unset = NA)
uscogdata:::cog_close()
Sys.setenv(USCOGDATA_URL = paste0(tmp, "/"))
on.exit({
uscogdata:::cog_close()
if (is.na(old_url)) Sys.unsetenv("USCOGDATA_URL") else Sys.setenv(USCOGDATA_URL = old_url)
}, add = TRUE)
force(code)
}
# Copy the bundled fixture to a temp dir with summary_categories.parquet
# rewritten to drop every M/L (intergovernmental) row, then run `code`
# against it with a clean session (mirrors with_fixture_corpus()/
+188
View File
@@ -0,0 +1,188 @@
# tests/testthat/test-complete.R
#
# uscogdata#18. The published corpus no longer stores the wide era's explicit
# zeros (cog_pipeline#64, series break SB194), so absence means two different
# things:
#
# <= FY2011 (dense_source) : cell absent => Census published $0
# >= FY2012 (sparse_source): cell absent => not reported, unknown
#
# `complete = TRUE` fills the requested grid from `code_set` and stamps every
# row's `value_source` so the two are distinguishable. Expected row sets here
# are built from the corpus parquet directly, never from the verb under test --
# verifying what a filter does through that same filter proves nothing.
# The (subtype, category) cells that SHOULD exist for one government-year:
# every code in force for that government's type, mapped through
# summary_categories, matching the verb's flow prefixes and excluding
# aggregate-flagged codes (which spending_long/revenue_long drop).
raw_expected_cells <- function(govid, year, prefixes, subtype_col) {
fx <- sub("/$", "", Sys.getenv("USCOGDATA_URL"))
q <- function(f) sprintf("read_parquet('%s/data/%s')", fx, f)
wt_raw_query(sprintf(
"SELECT DISTINCT c.%s AS subtype, c.category
FROM %s cs
JOIN %s x ON x.govs_type = cs.type
JOIN %s c ON c.item_code = cs.item_code
WHERE x.canonical_govid = '%s'
AND cs.year = %d
AND NOT cs.is_aggregate
AND LEFT(cs.item_code, 1) IN (%s)
AND c.category IS NOT NULL
AND c.%s IS NOT NULL",
subtype_col, q("code_set.parquet"), q("canonical_fips_xwalk.parquet"),
q("summary_categories.parquet"), govid, year,
paste0("'", prefixes, "'", collapse = ","), subtype_col
))
}
test_that("complete = FALSE is the default and changes nothing", {
skip_if_no_corpus()
with_fixture_corpus({
plain <- cog_spending("121011212191", 2011L)
explicit <- cog_spending("121011212191", 2011L, complete = FALSE)
expect_equal(nrow(plain), nrow(explicit))
expect_false("value_source" %in% names(plain))
})
})
test_that("complete = TRUE round-trips a dense-source year to the pre-sparsification cells", {
skip_if_no_corpus()
with_fixture_corpus({
# FY2011 is dense_source: before sparsification this government carried a
# row for every code in force, most of them $0. complete = TRUE must
# reproduce that cell set exactly.
r <- cog_spending("121011212191", 2011L, complete = TRUE)
expected <- raw_expected_cells("121011212191", 2011L,
c("E", "F", "G"), "spend_subtype")
key <- function(sub, cat) paste(sub, cat, sep = "|")
expect_setequal(key(r$spend_subtype, r$category),
key(expected$subtype, expected$category))
expect_gt(nrow(expected), 0L)
# Every filled cell in a dense-source year is a Census-published $0 --
# never "unknown", which is what the modern era's absences mean.
expect_setequal(unique(r$value_source), c("reported", "census_zero"))
expect_true(all(r$amt_nominal[r$value_source == "census_zero"] == 0))
expect_true(all(r$amt_nominal[r$value_source == "reported"] != 0))
})
})
test_that("complete = TRUE preserves the reported rows and their amounts exactly", {
skip_if_no_corpus()
with_fixture_corpus({
plain <- cog_spending("121011212191", 2011L)
full <- cog_spending("121011212191", 2011L, complete = TRUE)
# Filling adds rows; it must never alter or drop one.
expect_gt(nrow(full), nrow(plain))
reported <- full[full$value_source == "reported", ]
expect_equal(nrow(reported), nrow(plain))
expect_equal(sum(reported$amt_nominal), sum(plain$amt_nominal))
# ... and the total is unchanged, because every added cell is $0.
expect_equal(sum(full$amt_nominal, na.rm = TRUE), sum(plain$amt_nominal))
})
})
test_that("a sparse-source year's absences are unknown, not zero", {
skip_if_no_corpus()
with_fixture_corpus({
# FY2019 is sparse_source: an absent cell means the government did not
# report, which is NOT a zero. Filling those with 0 would invent data --
# the exact error the representation contract exists to prevent.
r <- cog_spending("121011212191", 2019L, complete = TRUE)
filled <- r[r$value_source != "reported", ]
expect_gt(nrow(filled), 0L)
expect_true(all(filled$value_source == "not_reported"))
expect_true(all(is.na(filled$amt_nominal)))
expect_false(any(r$value_source == "census_zero"))
})
})
test_that("the fill is scoped to each government's own type", {
skip_if_no_corpus()
with_fixture_corpus({
# Filling against the union of all types would invent cells for codes a
# county can never report. Every filled category must be one that
# code_set puts in force for type 1 (county) specifically.
r <- cog_spending("121011212191", 2011L, complete = TRUE)
county_cells <- raw_expected_cells("121011212191", 2011L,
c("E", "F", "G"), "spend_subtype")
expect_true(all(r$category %in% county_cells$category))
})
})
test_that("complete = TRUE respects the category filter", {
skip_if_no_corpus()
with_fixture_corpus({
r <- cog_spending("121011212191", 2011L, category = "Police",
complete = TRUE)
expect_true(all(r$category == "Police"))
expect_true("value_source" %in% names(r))
})
})
test_that("cog_revenue() completes on its own flow", {
skip_if_no_corpus()
with_fixture_corpus({
r <- cog_revenue("121011212191", 2011L, complete = TRUE)
expected <- raw_expected_cells("121011212191", 2011L,
c("T", "A", "U", "B", "C", "D"),
"revenue_subtype")
key <- function(sub, cat) paste(sub, cat, sep = "|")
expect_setequal(key(r$revenue_subtype, r$category),
key(expected$subtype, expected$category))
expect_setequal(unique(r$value_source), c("reported", "census_zero"))
})
})
test_that("provenance records the completion and its absence rule", {
skip_if_no_corpus()
with_fixture_corpus({
prov <- attr(cog_spending("121011212191", 2011L, complete = TRUE),
"provenance")
expect_true(prov$completion$applied)
expect_equal(prov$completion$absence_means$`2011`, "census_zero")
expect_gt(prov$completion$rows_filled, 0L)
off <- attr(cog_spending("121011212191", 2011L), "provenance")
expect_false(off$completion$applied)
expect_equal(off$completion$rows_filled, 0L)
})
})
test_that("complete = TRUE is refused where the fill would be guesswork", {
skip_if_no_corpus()
with_fixture_corpus({
# A recipe defines its own component codes and does not go through
# summary_categories at all, so there is no grid to fill from.
expect_error(
cog_spending("121011212191", 2011L, recipe = "corrections_combined",
complete = TRUE),
class = "uscogdata_complete_unsupported"
)
# The intergovernmental leg keeps aggregate rows by design
# (inst/sql/24-ig_long.sql), so its grid is not code_set's grid.
expect_error(
cog_spending("121011212191", 2011L, expenditure_concept = "total",
complete = TRUE),
class = "uscogdata_complete_unsupported"
)
})
})
test_that("complete = TRUE aborts on a corpus with no representation contract", {
skip_if_no_corpus()
# A corpus published before sparsification carries neither table, so there
# is nothing to fill from and no rule saying what an absence means. That
# must abort rather than guess.
with_corpus_missing_representation({
expect_error(
cog_spending("121011212191", 2011L, complete = TRUE),
class = "uscogdata_representation_unavailable"
)
# ... while an ordinary query on the same corpus still works.
expect_gt(nrow(cog_spending("121011212191", 2011L)), 0L)
})
})