The foreign key on lab_report.run_id rejects develop's placeholder run 999999,
so the 40-day-old report now belongs to an inserted FAILED run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A join's child column is on the side whose field is not the primary key,
so a many-to-one join (child -> parent) is checked on the child too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changesets 12-26 add a foreign key for each possible-value reference between
the QBit's tables, named fk_<child table>_<column>:
- lab_finding.run_id, lab_report.run_id -> lab_run: CASCADE (owned detail)
- comparison.change_event_id -> change_event: CASCADE (computed for the change)
- regression.cause_change_event_id, lab_run.change_event_id -> change_event,
regression.first_run_id / checkpoint_run_id -> lab_run: SET NULL (pointers)
- lab_run / lab_finding / regression / comparison .target_id -> target and
regression.budget_id -> budget: RESTRICT (history is kept, D8; pages and
budgets are disabled, not deleted; the QBit never deletes either)
Each changeset first records parent-less rows in qqq_fk_quarantine and nulls
them (or, for the NOT NULL run_id columns, copies the row as JSON and deletes
it, PostgreSQL only), and is marked ran when the constraint already exists.
Changeset 12 indexes the foreign key columns that had no leading index.
WebPerfForeignKeyTest runs the changelog with Liquibase on PostgreSQL
(Testcontainers), checks the orphan path and re-run idempotency, and fails
when a table reference in the meta-data has no constraint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>