db-referential-integrity (M3)
Foreign keys are the database enforcing that a reference points at something real — the cheapest, most durable guarantee against orphan data. Missing them pushes integrity into application code, where it silently rots. This module is both-axis: integrity on design, and join/planning behaviour on performance. It applies to FK-supporting relational engines only; the document/KV profiles drop it entirely (no false penalty).
What it checks
- Missing FK: a column named/typed as a reference (
*_id matching another table's PK) with no FOREIGN KEY constraint. On financial or auth tables (orders, payments, sessions, memberships) an orphan-enabling missing FK is the severity-5 cap case.
- ON DELETE / ON UPDATE action: FK with no explicit referential action where the default (
NO ACTION/RESTRICT) is wrong for the relationship, or a dangerous CASCADE that can mass-delete (e.g. deleting a user cascades to invoices). Each action must be intentional.
- Reference cycles: FK cycles (A→B→C→A) that block ordered insert/delete and complicate migrations — severity 4.
- Composite FK mismatch: multi-column FK whose column set/order does not match the referenced unique key, or partial composite references.
- Untrusted/NOT VALID FK left unvalidated after a backfill (Postgres
NOT VALID).
Axis & severity
- Missing FK enabling orphan financial/auth rows: severity 5,
fail, axis both, confidence established (caps).
- FK cycle: severity 4,
warn, axis both.
- Missing/over-broad
ON DELETE (silent RESTRICT blocking, or unintended CASCADE): severity 3–4, warn, axis design.
- Composite-FK order mismatch: severity 3,
warn.
- Note: FK columns lacking a supporting index are owned by M11 (indexing), not here — cross-reference, do not double-count.
Tier-0 static check
Parse DDL via scripts/parse-schema.mjs: for each *_id-style column, check whether a matching FOREIGN KEY/REFERENCES clause exists; enumerate declared FKs and flag those without an explicit ON DELETE/ON UPDATE action; build the FK graph and detect cycles. Directional ORM-source parses never raise the sev-5 orphan cap.
Tier-1 verification query
List FK constraints and their actions (confirms missing/dangerous actions):
-- $DATABASE_URL, read-only
SELECT conrelid::regclass AS child, confrelid::regclass AS parent,
confdeltype AS on_delete, confupdtype AS on_update, convalidated AS validated
FROM pg_constraint WHERE contype = 'f';
Confirm real orphans exist for a suspected missing FK:
SELECT count(*) AS orphans FROM child c
LEFT JOIN parent p ON c.parent_id = p.id WHERE p.id IS NULL AND c.parent_id IS NOT NULL;
No live DB → needs_api for the orphan count, never a silent pass.
Findings
Emit per schema/finding.schema.json. Examples:
M3.payments.user_id_no_fk — user_id references no table; orphan payments possible (severity 5, fail, axis both, confidence established).
M3.invoices.fk_cascade_deletes_financial — ON DELETE CASCADE from users mass-deletes invoices (severity 4, warn, axis design, fixable: proposed).
M3.graph.fk_cycle_a_b_c — reference cycle blocks ordered load (severity 4, warn, axis both).
Each finding: evidence.observed quotes the FK/column DDL or catalog row verbatim; verification.reproduce is a runnable query above (method: ddl_parse / constraint_check / query_stat); expected_impact banded + confidence-tagged.
Honesty
- A missing FK only caps when the orphan risk is real (financial/auth integrity); a missing FK on a low-stakes lookup is at most a
warn.
- App-level integrity (validation, soft FKs) is a mitigation, not a substitute — note it, but the DB-level guarantee is absent. Do not claim app code "fixes" it.
- Never assert orphan rows exist without a Tier-1 count; static, it is
directional.
1---2name: db-referential-integrity3description: Audit foreign-key integrity — missing FKs that allow orphan rows (financial/auth = severity 5), absent or unsafe ON DELETE/ON UPDATE actions, reference cycles, and composite-FK column/order mismatches. Module M3. Feeds both the Design & Integrity (Integridad referencial) and Performance & Scale (Query) scores.4---56# db-referential-integrity (M3)78Foreign keys are the database enforcing that a reference points at something real — the cheapest, most durable guarantee against orphan data. Missing them pushes integrity into application code, where it silently rots. This module is **both**-axis: integrity on design, and join/planning behaviour on performance. It applies to FK-supporting relational engines only; the document/KV profiles drop it entirely (no false penalty).910## What it checks11- **Missing FK**: a column named/typed as a reference (`*_id` matching another table's PK) with no `FOREIGN KEY` constraint. On financial or auth tables (orders, payments, sessions, memberships) an orphan-enabling missing FK is the **severity-5 cap** case.12- **ON DELETE / ON UPDATE action**: FK with no explicit referential action where the default (`NO ACTION`/`RESTRICT`) is wrong for the relationship, or a dangerous `CASCADE` that can mass-delete (e.g. deleting a user cascades to invoices). Each action must be intentional.13- **Reference cycles**: FK cycles (A→B→C→A) that block ordered insert/delete and complicate migrations — severity 4.14- **Composite FK mismatch**: multi-column FK whose column set/order does not match the referenced unique key, or partial composite references.15- **Untrusted/NOT VALID FK** left unvalidated after a backfill (Postgres `NOT VALID`).1617## Axis & severity18- Missing FK enabling orphan financial/auth rows: severity 5, `fail`, axis `both`, confidence `established` (caps).19- FK cycle: severity 4, `warn`, axis `both`.20- Missing/over-broad `ON DELETE` (silent `RESTRICT` blocking, or unintended `CASCADE`): severity 3–4, `warn`, axis `design`.21- Composite-FK order mismatch: severity 3, `warn`.22- Note: FK columns lacking a supporting index are owned by **M11** (indexing), not here — cross-reference, do not double-count.2324## Tier-0 static check25Parse DDL via `scripts/parse-schema.mjs`: for each `*_id`-style column, check whether a matching `FOREIGN KEY`/`REFERENCES` clause exists; enumerate declared FKs and flag those without an explicit `ON DELETE`/`ON UPDATE` action; build the FK graph and detect cycles. Directional ORM-source parses never raise the sev-5 orphan cap.2627## Tier-1 verification query28List FK constraints and their actions (confirms missing/dangerous actions):29```sql30-- $DATABASE_URL, read-only31SELECT conrelid::regclass AS child, confrelid::regclass AS parent,32 confdeltype AS on_delete, confupdtype AS on_update, convalidated AS validated33FROM pg_constraint WHERE contype = 'f';34```35Confirm real orphans exist for a suspected missing FK:36```sql37SELECT count(*) AS orphans FROM child c38LEFT JOIN parent p ON c.parent_id = p.id WHERE p.id IS NULL AND c.parent_id IS NOT NULL;39```40No live DB → `needs_api` for the orphan count, never a silent pass.4142## Findings43Emit per `schema/finding.schema.json`. Examples:44- `M3.payments.user_id_no_fk` — `user_id` references no table; orphan payments possible (severity 5, `fail`, axis `both`, confidence `established`).45- `M3.invoices.fk_cascade_deletes_financial` — `ON DELETE CASCADE` from users mass-deletes invoices (severity 4, `warn`, axis `design`, `fixable: proposed`).46- `M3.graph.fk_cycle_a_b_c` — reference cycle blocks ordered load (severity 4, `warn`, axis `both`).47Each finding: `evidence.observed` quotes the FK/column DDL or catalog row verbatim; `verification.reproduce` is a runnable query above (`method: ddl_parse` / `constraint_check` / `query_stat`); `expected_impact` banded + confidence-tagged.4849## Honesty50- A missing FK only caps when the orphan risk is real (financial/auth integrity); a missing FK on a low-stakes lookup is at most a `warn`.51- App-level integrity (validation, soft FKs) is a mitigation, not a substitute — note it, but the DB-level guarantee is absent. Do not claim app code "fixes" it.52- Never assert orphan rows exist without a Tier-1 count; static, it is `directional`.