feat(schema): foreign keys for every table reference, with orphan quarantine #11

Merged
KofTwentyTwo merged 4 commits from feature/foreign-keys into develop 2026-10-14 23:00:28 +00:00
KofTwentyTwo commented 2026-10-14 21:16:55 +00:00 (Migrated from github.com)

Summary

Adds database foreign keys for every reference between the QBit's tables (there were none). Changesets 12-26 in web_performance.changelog.sql, constraint names fk_<child table>_<column> (the same names kof22 uses, so this release is a no-op there).

Child column Parent ON DELETE Why
lab_finding.run_id (NOT NULL) lab_run CASCADE owned detail of the run
lab_report.run_id (NOT NULL) lab_run CASCADE owned detail of the run
comparison.change_event_id change_event CASCADE computed for that change
regression.cause_change_event_id change_event SET NULL optional pointer
lab_run.change_event_id change_event SET NULL optional pointer
regression.first_run_id, checkpoint_run_id lab_run SET NULL evidence pointers
lab_run / lab_finding / regression / comparison .target_id target RESTRICT history is kept (D8); SyncPagesSteps and WebPerfService never delete pages, they mark them not-in-sitemap; disable a page instead
regression.budget_id budget RESTRICT a breach keeps its budget; disable a budget instead

Behaviour change: deleting a Tested Page or Budget that has history now fails with a foreign key error. The QBit itself never deletes from any table.

Orphan-safe upgrade

Each FK changeset first copies parent-less rows to qqq_fk_quarantine (created IF NOT EXISTS, shared DDL across qbits) and then nulls the column, or, for the two NOT NULL run_id columns, stores the full row (row_to_json) and deletes it (dbms:postgresql changesets 14 and 17). Each is guarded by a precondition on the constraint name, so a host that already added the constraint marks it ran. Changeset 12 indexes the FK columns that had no leading index.

Tests

  • New WebPerfForeignKeyTest (Testcontainers PostgreSQL 17 + Liquibase 4.33): applies changesets 1-11, inserts orphans, applies the rest, asserts the quarantine rows and repairs, asserts every table possible-value reference in the meta-data has an FK (exemption list with reasons, empty today), then re-runs the FK changesets against existing constraints (no-op). Red without the new changesets: 12 references reported missing.
  • The H2 harness skips dbms: changesets that are not for H2 (as Liquibase does); the FKs themselves apply on H2 too, and all existing tests pass.
  • ./mvnw verify green locally.

🤖 Generated with Claude Code

## Summary Adds database foreign keys for every reference between the QBit's tables (there were none). Changesets 12-26 in `web_performance.changelog.sql`, constraint names `fk_<child table>_<column>` (the same names kof22 uses, so this release is a no-op there). | Child column | Parent | ON DELETE | Why | |---|---|---|---| | lab_finding.run_id (NOT NULL) | lab_run | CASCADE | owned detail of the run | | lab_report.run_id (NOT NULL) | lab_run | CASCADE | owned detail of the run | | comparison.change_event_id | change_event | CASCADE | computed for that change | | regression.cause_change_event_id | change_event | SET NULL | optional pointer | | lab_run.change_event_id | change_event | SET NULL | optional pointer | | regression.first_run_id, checkpoint_run_id | lab_run | SET NULL | evidence pointers | | lab_run / lab_finding / regression / comparison .target_id | target | RESTRICT | history is kept (D8); `SyncPagesSteps` and `WebPerfService` never delete pages, they mark them not-in-sitemap; disable a page instead | | regression.budget_id | budget | RESTRICT | a breach keeps its budget; disable a budget instead | Behaviour change: deleting a Tested Page or Budget that has history now fails with a foreign key error. The QBit itself never deletes from any table. ## Orphan-safe upgrade Each FK changeset first copies parent-less rows to `qqq_fk_quarantine` (created IF NOT EXISTS, shared DDL across qbits) and then nulls the column, or, for the two NOT NULL `run_id` columns, stores the full row (`row_to_json`) and deletes it (`dbms:postgresql` changesets 14 and 17). Each is guarded by a precondition on the constraint name, so a host that already added the constraint marks it ran. Changeset 12 indexes the FK columns that had no leading index. ## Tests - New `WebPerfForeignKeyTest` (Testcontainers PostgreSQL 17 + Liquibase 4.33): applies changesets 1-11, inserts orphans, applies the rest, asserts the quarantine rows and repairs, asserts every table possible-value reference in the meta-data has an FK (exemption list with reasons, empty today), then re-runs the FK changesets against existing constraints (no-op). Red without the new changesets: 12 references reported missing. - The H2 harness skips `dbms:` changesets that are not for H2 (as Liquibase does); the FKs themselves apply on H2 too, and all existing tests pass. - `./mvnw verify` green locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.