Skip to content

Add the simulation report tables - #849

Closed
anth-volk wants to merge 4 commits into
apiv2/1a-job-storefrom
apiv2/1a-report-tables
Closed

anth-volk wants to merge 4 commits into
apiv2/1a-job-storefrom
apiv2/1a-report-tables

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #846

What this changes

Step 1 of the API v2 migration, item 1A-5. Third of three stacked PRs: #847, then #848, then #849 (this one). It needs both.

Tables (revision 0003).

  • simulation.reports: kind (economy or household), country (two-letter code), a nullable, unique legacy_report_id for reports migrated from the Public API, an optional job_id, and timestamps. A migrated report starts not_started with no job. Once a job is attached the report is submitted, and its progress is that job's status, so the report and its job cannot disagree. A check constraint ties the status to the presence of job_id.
  • simulation.report_results: one aggregate per finished report job, keyed by the job, so reports with identical inputs (and therefore one shared job) share one stored result. It holds the aggregate as JSONB, aggregate_schema_version, aggregate_sha256, summary columns (the budget impacts, households, baseline_net_income, model_version, data_version) and artifacts, a JSON array of validated artifact references (role, gs://, hf:// or https:// URI, SHA-256, media type).

Validation. REPORT_AGGREGATE_MODELS maps a report kind and schema version to its Pydantic model. Economy version 1 is the contract's SingleYearMacroOutput. JobStore.complete_report validates the aggregate before writing anything, then stores the result in the same transaction as the conditional running -> succeeded update, like every job write in #848.

Collector. A job kind with report_kind set stores its result in report_results instead of job_results, and a status read returns that aggregate. No report job kind is registered and nothing creates report rows, so nothing uses these tables yet.

ReportStore adds report creation, an idempotent create_migrated_report keyed by legacy_report_id (for the one-time migration), and reads.

Before merging

No new operator steps. The operator steps of #847 (database roles and secrets) and #848 (Cloud Tasks queue, invoker service account, Cloud Scheduler job and GitHub variables) must be done in both environments first. The runtime role receives its privileges on the new tables from the default privileges set by revision 0001; this was confirmed locally with \dp after migrating to 0003.

No environment variables or deployment files change in this PR.

Checks run

  • libs/policyengine-simulation-persistence: against a local PostgreSQL 17 with SIMULATION_REQUIRE_DATABASE_TESTS=1, 61 passed, including the upgrade, alembic check, downgrade and upgrade cycle through 0003 and a race of eight concurrent report collectors.
  • projects/policyengine-simulation-entry: uv run pytest tests/, 657 passed.
  • ruff check ., ruff format --check, the rules guard and the quality guards.
  • Built the entrypoint image; it imports the report module and reports head 0003.

Decisions to confirm in review

  • Report status is not_started or submitted, and a submitted report's progress is read from its job, rather than a second copy of the job status on the report.
  • report_results is keyed by job, so identical reports share one result.
  • Only the economy single-year aggregate has a model. Household aggregates get one when the household contract (group 1C) exists, and budget-window aggregates with the native budget window (Step 3).
  • The persistence library now depends on policyengine-simulation-contract for the aggregate model.

🤖 Generated with Claude Code

anth-volk and others added 4 commits October 20, 2026 17:14
Add simulation.reports and simulation.report_results (revision 0003).

A report row records an economy or household report. A row migrated from
the Public API keeps the v1 report ID in a unique, nullable
legacy_report_id and starts not_started with no job; once a job is attached
the report is submitted and its progress is that job's status. A check
constraint ties the status to the presence of the job.

report_results holds one aggregate per finished report job, so reports with
identical inputs share one stored result. JobStore.complete_report validates
the aggregate with the Pydantic model registered for its report kind and
schema version (economy version 1: SingleYearMacroOutput) before writing,
then stores the JSONB aggregate, its schema version and SHA-256, budget
summary columns, the model and data versions and validated artifact
references, finishing the job in the same transaction and only while it is
running. A job kind with report_kind set now stores its result there, and a
status read returns that aggregate.

No report job kind is registered and nothing creates report rows yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation-only naming change in the persistence README, the reports
module docstring and the docstring of revision 0003: reports migrated from
the Public API keep its v1 report ID. No behaviour changes, so no test can
fail first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The persistence README now requires every module that defines tables to be
imported by migrations/env.py. The report tables already reached the schema
comparison through the job store's own import of the reports module, so
no test can fail first; the standalone "alembic check" test covers the
report tables and passes before and after this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReportArtifact is the first Pydantic model in policyengine-simulation-persistence,
but rule sim.quality.runtime-evaluated-models-resolve checked only the
executor's packages and policyengine_simulation_entry, so nothing verified
that ruff treats the library's models as runtime-evaluated or that they
resolve their annotations. Only the entrypoint installs the library, so its
copy of the guard now imports and checks it too.

A new test asserts that the guard checks at least one model from the library;
it failed before the package was added to the checked list. It is registered
under the rule, and the typing-only imports skill document names the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add the simulation report tables (reports and report_results)

1 participant