Repository navigation
Conversation
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean fix for #1838, no blocking or medium findings.
What I checked:
- Qualifies all 10 unqualified
to_regclass('name')existence checks withquote_ident(current_schema()), closing a multi-schema isolation gap where a ledger on another schema in thesearch_pathcould be read, skipped, orALTERed as the current schema's. - Verified the base package's 10 unqualified sites map exactly to the diff; each qualified guard pairs with a subsequent unqualified
FROM/ALTER/::regclassthat resolves tocurrent_schema()'s table (first on path), so guard and use stay consistent. applied_migrations()reads viasql.Identifier(current_schema, …)and returns{}when current_schema is null — no None passed to Identifier.- Migrations are recorded by version with no body checksum, so editing shipped migrations 0002/0006 is safe for already-migrated DBs; single-schema deployments unchanged.
- Test coverage: a static test refuses any new unqualified
to_regclass('; two real-Postgres tests prove a schema migrates in place beside a migrated one without touching it.
Gate checks: high_risk false; gated_paths false; no no-auto-approve team match; no prior decision. No critical/high/medium findings. Falls through to row 9.
…ema only
`to_regclass('name')` resolves through the whole search_path. With
`search_path=tenant,public` and a ledger already in `public`, every
unqualified existence check in the package could find `public`'s table
while working on `tenant`:
- `applied_migrations()` read `public`'s `adcp_schema_migrations`, so
`python -m adcp migrate` printed "up to date" and left `tenant`
unmigrated, and `postgres_storage`'s `check_schema` passed (#1838).
- Migration 0002 (`reporting_ledger_account_generations.sql`) and 0006
(`reporting_status_checkpoint_offerings.sql`) took another schema's
managed-delivery and status tables as present and ran their
`ALTER TABLE` on the unqualified name. `tenant`'s upgrade then altered
`public`'s tables, or failed with DuplicateColumn when `public` was
already migrated.
- The reconciliation DDL took another schema's feed as installed and
skipped this schema's feed backfill.
- `_period_close_generations_on` took another schema's materializer
candidates as present and read them.
Qualify each check with `quote_ident(current_schema())`, the pattern the
rest of the package already uses, and read the recorded migrations from
the current schema's table. A static test refuses any new unqualified
`to_regclass('...')` in the package. Real PostgreSQL tests run `migrate`
and `create_schema()` for a schema beside a migrated one on the same
search_path. They check that the schema is built in place and that the
other schema's catalog is unchanged.
Closes #1838
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
037ddb6 to
2391f8b
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean pass, no blocking or medium findings.
This PR qualifies every unqualified to_regclass('name') existence check across the reporting-storage package with quote_ident(current_schema()), closing a real multi-tenant boundary break where another schema's ledger on the search_path could skip a tenant's migration or run ALTER TABLE against the wrong schema. applied_migrations reads the existence flag and row from the same current_schema() (row[0] only used when row[1] is true), and the surviving ::regclass casts resolve to current_schema, confirmed by the preceding guard. Three tests back it — a static grep-guard against new unqualified checks plus two real-Postgres side-by-side-schema tests. fix(reporting-storage): is the correct semver signal (no public API change).
Decision table: no critical/high/medium findings (row 1 N/A). gated_paths false (row 2 N/A); review_decision is APPROVED regardless. high_risk false (rows 3/5 N/A). No medium findings (rows 4/8 N/A). Prior decision was approve (row 6 N/A). No no-auto-approve team match (row 7 N/A). Falls through to row 9 → approve. Prior pass was also clean and nothing re-surfaces.
Closes #1838. The fix is wider than the issue: the same unqualified
to_regclass('name')existence check appears in two shipped migrations as well as in the migrations ledger lookup.The bug
to_regclass('name')resolves through the wholesearch_path. Supposesearch_path=tenant,public, andpublicalready holds a ledger. Every unqualified check then findspublic's table while the code is working ontenant:migrations/__init__.pyapplied_migrations()public'sadcp_schema_migrations.python -m adcp migrateprints "up to date" and leavestenantunmigrated;postgres_storage().check_schema()passes (#1838).reporting_ledger_account_generations.sql(migration 0002), ×4public's reconciliation / materializer / status / production tables as present and runsALTER TABLE … ADD COLUMN offering_id … REFERENCES reporting_configurationson the unqualified name. Ifpublicis migrated,tenant's upgrade fails withDuplicateColumn(reproduced, see below). If not,tenant's upgrade alterspublic's tables.reporting_status_checkpoint_offerings.sql(migration 0006), ×3reporting_ledger_reconciliation.sqlledger/pg.py_period_close_generations_onreporting_materializer_candidatesas present, then reads it with an unqualifiedFROM.Each site now uses
to_regclass(quote_ident(current_schema()) || '.name'), which every other lookup in the package already uses (row_storage.py,period_retirement.py,pg.py:867, …).applied_migrations()reads fromsql.Identifier(current_schema, "adcp_schema_migrations"). Every other catalog query in the package already filters oncurrent_schema(); I checkedinformation_schemaandpg_classusage.Only setups with more than one schema on the
search_pathare affected. A single-schema deployment (ESA's included) behaves exactly as before. The edited SQL sits insideDOblocks, so a fresh or migrated catalog is unchanged. Migrations are recorded by version, so already-upgraded databases aren't re-run.Tests
tests/conformance/reporting/test_reporting_storage_migrations_pg.py:test_no_storage_existence_check_resolves_through_the_search_path: a static test that refuses any unqualifiedto_regclass('…')in the package's.py/.sqlfiles.test_migrate_upgrades_its_own_schema_beside_a_migrated_one(real PostgreSQL): fully migrates schema A, then runsmigratewithsearch_path=B,A. All six versions apply to B, B's catalog equals A's, and A's catalog (columns, constraints, recorded versions) is unchanged.test_create_schema_builds_its_own_schema_beside_a_migrated_one: the same setup throughPgReportingLedgerStore.create_schema(). B records 0001–0003, with 0004–0006 left pending, and A is unchanged.Without the fix:
pending_migrationsreturns[]for the unmigrated schema.applied_migrationsfix and the DDL reverted, both Postgres tests fail withpsycopg.errors.DuplicateColumn: column "offering_id" of relation "reporting_reconciliation_records" already exists. That is migration 0002 altering the other schema.Local runs (PostgreSQL 15):
test_reporting_storage_migrations_pg.py: 8 passed.tests/conformance/reporting/run:nextin a fresh database: they are the installed-distribution / rolling-upgrade tests (*_rolling,*_installed,notification_packaging), and CI excludes most of those files.make lint,make typecheck-all: pass.Found in the post-release review of the #1787 migration.
🤖 Generated with Claude Code