Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v2plan/1f-121-repoint-saved-items #4010 +/- ##
=====================================================================
- Coverage 87.95% 87.95% -0.01%
=====================================================================
Files 259 259
Lines 16169 16174 +5
Branches 2636 2636
=====================================================================
+ Hits 14222 14226 +4
- Misses 1275 1276 +1
Partials 672 672 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…mands The policy and user-association qualification commands counted saved policies through the UserPolicy model. Both run before every v2 upgrade, so once the model takes a new table name they would query a table the database does not have yet. The policy qualification counts only while its revision is pending, when the table is still named user_policies, so it names that table. The user-association qualification counts on every run, so it counts the table under whichever name the database has: user_policy_associations once renamed, otherwise user_policies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The saved-policy table now follows the naming of the other user-association tables. Revision fa7fa1035444 was autogenerated from the renamed model. Its one documented post-generation correction replaces the generated create-and-drop pair, which would have deleted every saved policy, with an in-place rename; each constraint named after the table is dropped under its old name and created under the generated name, and the generated changes to the two convention-named indexes and the three foreign keys that refer to the table are kept around the rename. The composite listing indexes keep their ix_user_policies_* names, as the other association tables keep their short index names. The privilege declarations and the head revision constant use the new name. Dual-write's saved-policy copy goes through the model and follows it. Unchanged: the route /v2/user-policies, the user_policy_id columns, legacy_user_policy_mappings and the v1 Cloud SQL table user_policies. Not included: a temporary view named user_policies for the release still running during the deploy. A view is not autogenerated, so it needs a human migration decision; a strict expected-failure test records the gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The revision test finds fa7fa1035444 by its ID instead of through the head constant, so a later revision no longer makes it read the wrong file, and it checks the revision's parent through the script directory. - The lifecycle test checks each privilege of the probe role on its own (has_table_privilege with a list is true when any one is held), and checks grants and delete cascades after the downgrade as well as after the upgrade. - The test for the missing user_policies view now stores a saved policy and upgrades to head before reading it under the old name, and only a missing relation counts as the expected failure, so an empty database or a failure while setting up no longer satisfies it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge table and the rule for Stage 10 mirror rows name the table by its current name. The rollback and schema compatibility sections no longer say head lets earlier releases run: until a view under the old name exists, a release from before the rename fails every statement on saved policies, from the schema upgrade until the new release takes traffic, and after a rollback until the rename is downgraded. They also say that downgrading the rename alone keeps every row, and that the Stage 12A compatibility test now maps the renamed table and no longer proves the parent release runs against head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reconciliation read only ordinary and partitioned tables when it checked which declared relations exist, so a view declared in the privilege manifest counted as missing and blocked every grant. It now reads views too. GRANT ... ON TABLE and has_table_privilege already work on views, and the granted-privilege query already read them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revision fa7fa1035444 now creates a view named user_policies with every column of user_policy_associations after the rename, and drops it before the downgrade renames the table back. These two statements are the only hand-written SQL in the v2 chain: a maintainer approved them on 2026-10-22 as an exception to the generated-only rule, and a later revision drops the view. The revision comments and the Alembic skill say so, and the revision test allows exactly these two statements. A single-table view is automatically updatable, so the Public API release still serving during the deploy, or after a rollback to it, keeps reading and writing saved policies through it. The privilege manifest declares the view with its table's privileges, so the reconciliation grants the runtime role SELECT, INSERT, UPDATE and DELETE on it. Autogeneration does not reflect views, so alembic check and the head schema validation stay clean. Tests: the lifecycle test checks the view's columns after the upgrade and its absence after the downgrade, runs the parent revision's inserts, reads, updates and deletes through it, and reads a saved policy stored before the rename under the old name. A new integration test runs the saved-policy routes' services, the sign-in merge, report creation and the Stage 10 saved-policy copy as the runtime role with every statement naming user_policies. The Stage 12A document and the changelog describe the view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
459b031 to
035af26
Compare
…kflow The workflow runs an explicit list of integration modules, and test_v2_saved_policy_view.py was not in it. It now runs in the step for signed-in identity provisioning and user associations, and rule api.v2.runtime-privileges-tested-as-runtime-role names it, since it runs the saved-policy services as policyengine_v2_runtime. A unit test fails when an integration module that uses the managed v2 roles is missing from the workflow, or one that runs services as a managed role is missing from the rule's scope. No environment variable is added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reconciliation read tables and views as one set of names, so a view satisfied a table entry of the same name and a table satisfied a temporary-view entry: before the rename, the user_policies table satisfied the user_policies view's entry. It now reads tables and views separately. A declared table must exist as a table and a TEMPORARY_VIEW_TABLES entry as a view; anything else is reported under "Declared tables missing" or "Declared views missing", nothing is granted, and verify reports every privilege declared on it as missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review at Verdict: correct and safe for data; mergeable once the grant question in the description is settled. Stacked on #4005, so I reviewed only
Checked and fine: What I ran: fresh clone, 🤖 Generated with Claude Code |
Fixes #4008
Summary
user_policiesis renamed touser_policy_associations, matchinguser_household_associations,user_simulation_associationsanduser_report_associations. Its primary key, unique constraint, three check constraints, two foreign keys and two convention-named indexes take the new name; the foreign keys fromlegacy_user_policy_mappingsanduser_report_associationsfollow the table.ix_user_policies_country_user_created_id,ix_user_policies_country_policyandix_user_policies_country_legacy_policy. The sibling association tables keep short names for the same indexes (ix_user_households_*,ix_user_simulations_*,ix_user_reports_*), so the short names are the existing convention.user_policieswith every column of the renamed table lets the Public API release from before the rename keep reading and writing saved policies, during the deploy and after a rollback to it. It is the approved hand-written exception described below. Removing it is a separate, later change, made once no deployable release namesuser_policies.privileges.py) andV2_ALEMBIC_HEAD_REVISIONuse the new name. The privilege manifest declares the view with its table's privileges (TEMPORARY_VIEW_TABLES), and the privilege reconciliation now finds, grants and verifies privileges on views (its existing-relation query read only ordinary and partitioned tables before). It reads tables and views separately: a declared table must exist as a table and a declared temporary view as a view, otherwise it is reported as missing ("Declared tables missing" or "Declared views missing") and nothing is granted. The Stage 10 saved-policy dual-write copy goes through the model and follows it.user_policies, so it names that table. The user-association qualification counts on every run, so it countsuser_policy_associationswhen that table exists anduser_policiesotherwise (views are not listed as tables, so the view is never counted). Their JSON evidence keeps the keyuser_policies./v2/user-policies, theuser_policy_idcolumns,legacy_user_policy_mappings, and the v1 Cloud SQL tableuser_policies.Migration notes
policyuser-association routes keep their contracts; only the table behind them is renamed. The generated migration contracts do not change.user_policiestable is untouched.fa7fa1035444(parentfbb7488ebe8a).The migration correction, and why it is within the migration rules
The revision was generated with
uv run alembic -c alembic-v2.ini revision --autogenerate. Autogeneration cannot detect a rename: it created an emptyuser_policy_associationstable and droppeduser_policies, which would delete every saved policy, and its copies of the three composite indexes would have collided with the old table's indexes of the same names. The repository's Alembic rules allow a minimal, documented, tested post-generation correction of an objectively incorrect autogenerated operation. The revision has one such correction in each direction:op.rename_tablereplaces the create-and-drop pair.user_policy_associationsis dropped under its old name and created under the generated name with the generated definition, using Alembic operations only.Rebuilding the primary key, unique and check constraints takes a brief lock on the table during the upgrade.
The temporary
user_policiesview: an approved hand-written exceptionCreating a view is a hand-written schema operation, which the migration rules leave to a human decision. A maintainer approved this one on 2026-10-22. It is the only hand-written SQL in the v2 chain:
CREATE VIEW user_policies AS SELECT ... FROM user_policy_associations, listing every column in the table's order. The downgrade runsDROP VIEW user_policiesbefore renaming the table back. Both are marked in the revision as the approved exception, anddocs/engineering/skills/alembic-migrations.md("API v2 saved-policy table rename") documents them.test_alembic_v2.pyallows exactly these two statements and fails if any other v2 revision usesop.execute.SELECT,INSERT ... RETURNING,UPDATE,DELETEand row locks through it act on the table, with the table's defaults, constraints and cascades. The release before the rename uses noON CONFLICTon saved policies.SELECT, INSERT, UPDATE, DELETEon the view, as on the table, through the privilege reconciliation that runs after every upgrade. The view runs with its owner's (the migration role's) privileges on the table.alembic checkreports no operations and the head schema validation (which compares tables) passes with the view present; the lifecycle test checks both.user_policy_associationscolumn the view shows without dropping the view first.What the release before the rename can do:
/v2/user-policiesroutes (create, list, read, rename, repoint, delete), report creation that checks the saved policies a report names, the sign-in merge that moves an unauthenticated user's saved policies, account export, and the Stage 10 saved-policy dual-write copy (DB_WRITE_POLICYiscloud_sqltoday, so that copy is off).The Stage 12A document (
docs/migration/stage-12a-user-login.md) says this in its rollback and schema compatibility sections, and uses the new table name for current behavior.Open risk for a person to decide: the view's grants just after the upgrade
In the deploy's database job (
seed-v2-database.yml), the step "Upgrade and verify the v2 schema" runsalembic upgrade head(which commits the rename and the view), thenalembic current --check-headsandalembic check. Only the next step, "Grant declared v2 table privileges" (reconcile_v2_privileges.py apply), grants the runtime role its privileges on the view. In that interval, from the upgrade's commit until the grant step commits (normally the few seconds the threeuv runcommands take, longer if a step fails and is re-run), the release still serving uses saved policies through the view aspolicyengine_v2_runtime.public, the view has them from the moment it is created: PostgreSQL appliesON TABLESdefault privileges to views too (checked on a disposable database).docs/migration/stage-8-supabase-target.mdsays each project has such defaults ("tables the migration role creates give the runtime role read/write access"), but they are set outside the repository; the reconciliation only reports them in the job summary ("Default privileges for new tables"), and no test or deploy step checks them./v2/user-policiesroutes, report creation that names a saved policy, the sign-in merge and the Stage 10 saved-policy copy.Two options, for a person to choose:
publicgivepolicyengine_v2_runtimeSELECT, INSERT, UPDATE and DELETE on tables (the last deploy's job summary lists them).GRANT SELECT, INSERT, UPDATE, DELETE ON user_policies TO policyengine_v2_runtimeto the revision, right after the view is created. That is a second hand-written statement beyond the approved view, so it needs the same approval.This PR does neither.
Environment variables
The change adds no environment variable. It changes one workflow step: "Test signed-in identity provisioning and user associations" in
.github/workflows/v2-integration-check.yml(jobcatalog-runtime-integration, called frompr.ymlandpush.yml) now also runstests/integration/test_v2_saved_policy_view.py. That step receives the job-levelenv:block; every value is a literal in the workflow, pointing at the job's own service containers, and no secret reaches it:V2_MIGRATION_DATABASE_URLpostgres:17servicetests/integration/conftest.pyand both modules)V2_ALEMBIC_DISPOSABLE_TEST"1"DEPLOYMENT_ENVIRONMENTtest(names the Stage 12 rolepolicyengine_stage12_test)ALEMBIC_DATABASE_URLmysql:8.4serviceRUNTIME_CACHE_TEST_URLredis:7.2-alpineserviceCODECOV_TOKEN(GitHub secret, passed withsecrets:to the reusable workflow) is read only by the coverage upload step, which this PR does not change. The ruleapi.v2.runtime-privileges-tested-as-runtime-rolenow names the new module in its scope.What this PR waits for
core-developersreview for the workflow andrules/changes.GRANTin the revision).Testing
CI runs on this branch are cancelled at the user's request while the stack is prepared; the local runs below are the evidence. The branch is rebased onto #4005's branch, which is rebased onto
masteratb5cf71dc8(2026-10-26). Each commit was written test-first; the SHAs are after the rebase:3d0b10533(qualification commands):test_saved_policies_are_counted_under_their_name_after_the_renamefailed first (assert 0 == 3: the renamed table was never counted). Then both qualification unit files passed (16) and the qualification integration test passed against disposable PostgreSQL 17. The two tests for the pending case pin the old name; they passed before and after.320c5d20d(the rename): on a fresh disposable database, 11 unit tests failed first (KeyError onuser_policy_associations, the missing revision, the privilege manifest, the recorded saved-policy SQL) and 4 lifecycle tests failed (NoSuchTableError public.user_policy_associations,user_policiesstill present). Then they passed, withalembic checkclean, privilegeapply/verify, the v2 and v1 integration tests, and the fullmake testset.54bef66a1(review follow-up, tests): with a placeholder later revision added locally, the old revision test read the wrong file and failed, and the new one passed.has_table_privilege(role, table, 'SELECT, UPDATE')returned true for a role holding only SELECT, so each privilege is now checked on its own. WithON DELETE SET NULLremoved from the downgrade's report foreign key (locally, not committed), the new downgrade cascade check failed.c747f002e(review follow-up, document only): the Stage 12A document.d31f13df2(reconciliation and views): a new integration test that declares privileges on a view failed first (the view was reported as a missing table, so nothing was granted); then the reconciliation integration and unit tests passed (63).035af26e0(the view): before the view existed, the revision tests failed (no approved exception in the revision), the manifest test failed to importTEMPORARY_VIEW_TABLES, 3 lifecycle tests failed (relation "user_policies" does not exist, no view at head) and the 4 runtime-role view tests failed (relation "user_policies" does not exist). Then they passed: the lifecycle test checks the view's columns and absence after the downgrade and runs the parent revision's inserts, reads, updates and deletes through it;test_v2_saved_policy_view.pyruns the saved-policy services, the sign-in merge, report creation and the Stage 10 copy as the runtime role with every statement naminguser_policies, and checks the rows inuser_policy_associations.eb4c29c1d(review follow-up: the view tests in CI): a new unit test,test_every_managed_role_integration_module_runs_in_ci_and_is_in_its_rule, failed first (test_v2_saved_policy_view.pywas neither in the workflow nor in the rule's scope); then the workflow tests passed (20). The changed step's command,uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_user_identity_persistence.py tests/integration/test_v2_saved_policy_view.py, run locally after the job's earlier steps (v1 and v2upgrade head, privilegeapply --create-missing-rolesandverify) with the job's variables pointing at local disposable databases: 20 passed.c0794eb23(review follow-up: relation kinds): new unit and integration tests failed first (plan_reconciliation()andapply_declared_privileges()had no way to name views, so a view or table of either kind satisfied any entry); then the reconciliation unit tests (63 with the manifest tests) and integration tests (5) passed. With the database atfbb7488ebe8a,applynow reports "Declared views missing from the database (nothing was granted):user_policies" and exits 1 instead of counting the old table as the view.On the final branch, against disposable PostgreSQL 17 and MySQL 8.4:
alembic -c alembic-v2.ini upgrade headandcheck(no new operations);reconcile_v2_privileges.py apply --create-missing-roles(granted the runtime roleDELETE, INSERT, SELECT, UPDATEonuser_policies) andverify.make testset without coverage: 3,411 passed.ruff check .,ruff format --check .,uv run --frozen --extra dev mypy,python scripts/run_quality_guards.py,python -m scripts.rules validateandguardpassed;python scripts/export_migration_contracts.pyleft the generated files unchanged.No inline
# noqasuppressions are added. A rules fixture (rules/fixtures/api.v2.runtime-privileges-declared/reads_good.py) changes its docstring to the new table name, so this PR requests a review fromcore-developers.🤖 Generated with Claude Code