Repository navigation
feat: type provenance (#195), prod DB naming (#172), tenant T&E migrations (#184) - #213
Merged
Merged
Conversation
This was referenced Nov 9, 2026
) Product-owner decision (4 Nov): dynamic-mode tables are created from types a person confirmed, and the registry says which. - Migration 0009 adds csv_files.types_provenance (CHECK: inferred_unconfirmed | user_confirmed). Existing dynamic rows are backfilled inferred_unconfirmed, since nothing recorded a confirmation; T&E rows stay NULL. - UploadRequest gains typesConfirmed. Only types sent together with typesConfirmed count as user_confirmed; a bare `types` list or no types is inferred_unconfirmed. typesConfirmed without types is a 422, and is rejected in T&E mode rather than ignored. - The upload response returns typesProvenance and GET /api/csv/files returns types_provenance. - The uploader sends typesConfirmed for the types from the user's confirmed mapping, and the table preview notes when types were never confirmed. - Tests: preview creates no table or registry row; confirming creates the table with the confirmed types (including types that differ from the inferred ones); provenance is stored and returned; a T&E import rejects a dynamic table as its target; migration backfill, CHECK and downgrade. - test_migrations/test_tenant_isolation now read the Alembic head from the script directory instead of pinning it, so the next migration doesn't break them. Not enforced: the API still accepts a dynamic upload without confirmed types (recorded as inferred_unconfirmed) so existing API clients keep working. Rejecting those is a one-line change if the owner wants it strict. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
…rypoint (#172) Terraform created te_prod owned by pgadmin; the deployer created te_mgmt_prod owned by postgres; the entrypoint expected an env_prod.sql that nothing in the image produced. A fresh image failed at the first step. Decided: the Azure production database is te_prod owned by pgadmin; the on-prem installer keeps te_mgmt_prod (DECISIONS/2026-11-09-prod-db-naming.md). - deploy_all.sh reads db_name and db_owner from the environment's launcher instead of hard-coding te_mgmt_<env> / postgres, refuses unsafe identifiers, and gains --dry-run. A launcher that sets neither keeps the old names. - entrypoint.sh generates env_<env>.sql at run time from PGDATABASE, PGUSER and APP_DB_PASSWORD (read by psql with \getenv, never written), so nothing credential-bearing is baked into the image (#174). A mounted launcher wins. New `render` action; MIGRATION_HOME makes it testable outside a container. - te_core_schema.sql no longer echoes the application password: the set_config SELECT printed it into deployment output, which a container job logs. Both copies use \gset. - Terraform allow-lists dblink, pg_trgm and uuid-ossp (azure.extensions, which Azure requires before CREATE EXTENSION) and passes the application role's password to the job from Key Vault. terraform validate passes. - tests/test_prod_db_naming.py (17 tests): deployer, entrypoint, Terraform consistency and the password echo. 13 fail against main's versions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
amar-python
force-pushed
the
claude/csv-loading-postgres-migration-qvkl4l
branch
from
November 9, 2026 09:37
7771d79 to
468c2ae
Compare
provision_te_tables cloned only the T&E tables a tenant was missing, so a change to the template (#177's program_id and composite keys) never reached a tenant that already had them. - alembic_te/: a second Alembic chain, version table alembic_version_te inside each tenant schema. te0001 is the baseline (the shape before #177); te0002 ports phase 4B (program_id backfill, composite FKs, fill triggers): idempotent, one transaction, aborts leaving the tenant unchanged and lists cross-program rows if any exist. - api/tenancy.py migrate_te_schema: a new tenant is cloned from the template, stamped at the template's version and brought forward; a tenant from before versioning is stamped at the baseline and upgraded; others are upgraded. Serialised per tenant by an advisory lock (and in-process, as Alembic's op and context are global). Runs from migrate_tenant, so at API start for every tenant. - The template stamps its version (COMMENT ON SCHEMA 'te_schema_version=te0002') in both copies of te_core_schema.sql. A template with no stamp is refused with the way out; one at a lower version is cloned then brought forward; an unknown version is refused. - provisioning renames cloned indexes and unique constraints to the template's names (LIKE ... INCLUDING ALL uses default names), found by the parity test; te0002 adopts an equivalent unique constraint rather than adding a duplicate. - alembic_version_te is read-only for the runtime role. - tests/test_tenant_te_migrations.py (16): a tenant reached by any route equals the template (columns, constraints, indexes, triggers, functions); the old-shape fixture is asserted, not assumed; data and updated_at survive; cross-program rows stop the upgrade and change nothing; the next schema change reaches existing and new tenants; concurrent starts leave one correct tenant. - docs/TENANT_SCHEMA_MIGRATIONS.md and a DECISIONS note. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
python-validator-tests runs on a Windows runner, where `bash` on PATH is the WSL
launcher ("no installed distributions"), so the 10 deployer/entrypoint tests added
for #172 failed there.
- Find Git Bash explicitly on Windows, as test_csv_utilise.py does, and fail rather
than skip if there is no working bash (the job runs with --strict).
- Inherit the full environment (Git Bash needs SYSTEMROOT, TEMP, ...) instead of
PATH and HOME only; a None value removes a variable, so APP_DB_PASSWORD from the
parent cannot leak into the missing-password test.
- Decode output as UTF-8 (the scripts print symbols the Windows locale codec cannot).
- Pass POSIX-style paths to bash and write launcher files with LF newlines.
Not run on Windows locally; the Windows job is the check.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
This was referenced Nov 9, 2026
Resolves three frontend conflicts with #187 (uploadId on every outcome) and #188 (stop is not offered once the request is on the server): both sides are kept. csv.functions.ts declares typesProvenanceSchema and uploadIdField; index.tsx and job-outcome.test.ts import canStop and typesProvenanceNote. tsc --noEmit clean; vitest 168 passed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
The only conflict was the generated test catalogue: taken from main and regenerated with scripts/test_catalogue.py so it lists both sides' tests (831). Full pytest suite passes against PostgreSQL 16 with the four environments deployed from the merged schema. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
This was referenced Nov 9, 2026
…ace the new tests The reorganisation of tests/ (#221) is on main. Git followed the renames of test_migrations.py and test_tenant_isolation.py; the three test files this branch adds are moved to the homes #221 suggested: test_prod_db_naming.py to unit/, test_tenant_te_migrations.py and test_type_provenance.py to system/. Paths a text merge cannot see are fixed: the repo root is parents[2] in the two moved files that compute it, and in test_tenant_isolation.py, whose _ALEMBIC_INI line (added here for #195) still pointed at tests/alembic.ini. Two references to the old path (te0002's docstring, docs/TENANT_SCHEMA_MIGRATIONS.md) are updated. The generated test catalogue was the only conflict: regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
Only the generated test catalogue conflicted: regenerated with scripts/test_catalogue.py. Evals in CI order on a fresh PostgreSQL 16: 27 passed, including the Tier E control. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
amar-python
marked this pull request as ready for review
November 9, 2026 14:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three independent changes on one branch, one commit each: #195, #172, #184. They touch no common files except
build/te_core_schema.sql(both copies), where #172 changes the password-echo line and #184 adds the version stamp, in different places. Each can be reviewed or reverted on its own.This branch was restarted from
mainafter #202 merged.mainhas since been merged in twice (see Merged with main below). All three were chosen or confirmed by the owner on 9 Nov.#195 — record whether a dynamic table's column types were confirmed
Implements the 4 Nov product-owner decision: dynamic-mode tables are created from types a person confirmed, and the registry says which.
alembic/versions/0009_type_provenance.py(new):csv_files.types_provenancewith a CHECK (inferred_unconfirmed|user_confirmed). Existing dynamic rows are backfilledinferred_unconfirmed, since nothing recorded a confirmation. T&E rows stay NULL. Runs per tenant.UploadRequest.typesConfirmed: only types sent together with it count asuser_confirmed. It is a 422 withouttypes, and is rejected in T&E mode rather than ignored. The upload response returnstypesProvenance;GET /api/csv/filesreturnstypes_provenance.typesConfirmedfor the user's confirmed mapping; the preview notes when types were never confirmed.test_migrations.py/test_tenant_isolation.pyread the Alembic head from the script directory instead of pinning it.tests/test_type_provenance.py): preview creates no table; confirming creates it with the confirmed types; provenance stored and returned; a T&E import rejects a dynamic table as its target; migration backfill, CHECK and downgrade.Decision for the owner: the API still accepts a dynamic upload without confirmed types (recorded
inferred_unconfirmed) so existing clients keep working. Making that a hard 422 is small, but breaks every caller that omitstypes.#172 — Terraform, deployer and entrypoint agree on the production database
Decision (9 Nov): the Azure production database is
te_prodowned bypgadmin; scope is Azure only. The on-prem installer, prod tests, CI and docs keepte_mgmt_prod. Recorded inDECISIONS/2026-11-09-prod-db-naming.md.build/deploy_all.shreadsdb_name/db_ownerfrom the launcher instead of hard-codingte_mgmt_<env>/postgres; refuses unsafe identifiers; new--dry-run.infra/entrypoint.shgeneratesenv_<env>.sqlat run time fromPGDATABASE,PGUSERandAPP_DB_PASSWORD, so the image carries templates only (RV-S4: Docker image can bake build/config.local.env and build/environments/env_*.sql from configured checkout #174). The password goes to psql via\getenvand is never written. A mounted launcher wins. Newrenderaction.build/te_core_schema.sql(both copies): theset_configSELECT printed the application password into deployment output, which a container job logs. It now uses\gset. Found while verifying; not in the issue.infra/terraform-prod/main.tf: allow-listsdblink,pg_trgm,uuid-ossp(azure.extensions; Azure refusesCREATE EXTENSIONotherwise, so the first deployment would have failed partway), and passes the application role's password to the job from Key Vault.tests/test_prod_db_naming.py(17 tests); 13 fail againstmain's versions of the five files. They run under Git Bash on Windows (the Windows job'sbashis the WSL launcher, which has no distribution), and fail rather than skip without a working bash.Fresh-server verification (PostgreSQL 16 whose only role is
pgadmin,te_prodpre-created as Terraform does, an "image" of tracked files only, thenPGUSER=pgadmin PGDATABASE=te_prod TARGET_ENV=prod APP_DB_PASSWORD=… ./entrypoint.sh deploy):Not proven: no Docker daemon here, so no image was built (the entrypoint and deployer were run directly with the image's layout). The local
pgadminis a superuser; Azure's admin login is not, so a privilege difference could show on the first real run.terraform init+validatepass (v1.9.8); noplanorapply(no Azure access).#184 — tenant T&E tables have an upgrade path
Decision (9 Nov): an Alembic chain with a parity test. Recorded in
DECISIONS/2026-11-09-tenant-te-migrations.md; how to make the next change is indocs/TENANT_SCHEMA_MIGRATIONS.md.Before,
provision_te_tablescloned only missing tables, so a template change never reached an existing tenant. The issue's reproduction, run on a tenant that predates #177:alembic_te/(new): a second chain with its version tablealembic_version_teinside each tenant schema.te0001is the baseline (the shape before RV-F2 (BR-07) + RV-F9 (BR-08) + RV-F8 (BR-05): composite FKs for VCRM, results, defects, coverage #177).te0002ports phase 4B:program_idbackfill (withupdated_attriggers paused so it doesn't look like an edit), composite FKs, fill triggers. Idempotent, one transaction; if cross-program rows exist it aborts, leaves the tenant unchanged, and lists them.api/tenancy.pymigrate_te_schema: new tenant → cloned from the template, stamped at the template's version, brought forward; tenant from before versioning → stamped at the baseline, upgraded; otherwise upgraded. Serialised per tenant by an advisory lock (and in-process, since Alembic'sop/contextare process-global). Runs frommigrate_tenant, so at API start for every tenant.COMMENT ON SCHEMA ... 'te_schema_version=te0002'in both copies ofte_core_schema.sql. A template with no stamp is refused with the way out; one at a lower version is cloned then brought forward; an unknown version is refused.CREATE TABLE ... LIKE ... INCLUDING ALLnames copied indexes and unique constraints by PostgreSQL's defaults, so every tenant already differed from its template in index names, andte0002'suq_*constraints would have been added a second time. Provisioning now renames the copies to the template's names;te0002adopts an equivalent unique constraint instead of duplicating it.alembic_version_teis read-only for the runtime role.tests/test_tenant_te_migrations.py(16): a tenant reached by any route equals the template on columns, constraints, indexes, triggers and functions; the old-shape fixture is asserted rather than assumed; data andupdated_atsurvive; cross-program rows stop the upgrade and change nothing; the next schema change (a throwawayte9001) reaches an existing tenant and a new one cloned from the older template; concurrent starts leave one correct tenant; the stamp is checked against the head in both schema files.Trade-offs, stated in the decision note
downgrade, not an independent dump of the pre-RV-F2 (BR-07) + RV-F9 (BR-08) + RV-F8 (BR-05): composite FKs for VCRM, results, defects, coverage #177 schema; the test asserts what that shape is.Merged with main
mainmoved twice while this was open, and each time the PR stopped merging cleanly (which also stops GitHub from starting CI for it). Both times it was merged in, not rebased.csv.functions.ts,index.tsxandjob-outcome.test.ts. Both sides were independent additions to the same lines (typesProvenanceSchemanext touploadIdField;typesProvenanceNotenext tocanStopin the import lists), so both are kept.tsc --noEmitclean; vitest 168 passed.docs/TEST_CASES.md: taken frommainand regenerated withscripts/test_catalogue.py(831 tests).On the merged tree the full pytest suite passes against a freshly deployed PostgreSQL 16.
CI-equivalent run (local, PostgreSQL 16, all four environments deployed from the changed schema)
Full pytest suite: pass (
test_oidc.pyexcluded locally only: unrelatedcffiimport problem in this sandbox)Coverage floors: csv_parse 82.1% (>=79), dynamic_loader 85.6% (>=84), te_loader 97.3% (>=96).
dynamic_loadervaries by run: its lost-race branch is covered only when the concurrency tests actually collide (87.4% in an earlier run of the same code).On a clean database, in CI order:
evals/runner.py --tiers p,i,s,e26/26;tests/run_tests.sh dev test159/159 eachmake lintclean,make health89 passed / 0 failed, bandit clean,test_catalogue.py --checkcurrentFrontend: vitest 168 passed,
tsc --noEmitcleanCI green on the current head (b6e59a5): free tier, integration (postgres service) and the Windows
python-validator-testsjob.Generated by Claude Code