Task: Generate real foreign keys into immutable entities
Table of Contents
This page documents a task in the Teach the code generator the trade shape story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Let components, links and participants reference the trade anchor with constraints the database enforces.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Teach the code generator the trade shape |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-03 |
3. Acceptance
- A foreign key marked to be enforced renders a REFERENCES constraint and no trigger check.
- The generator refuses an enforced foreign key whose target is temporal.
- A test shows the database rejecting a row whose key names no target row.
4. Plan
- Add
:enforce: trueto a node under* Foreign keys. The generator reads the target table's model and refuses the flag unless that model is immutable. - An enforced key renders a named constraint,
<table>_<column>_fk foreign key (column) references target (target_column), in both the temporal and the current-state table bodies. It setsskip_check, so the trigger check for that column is not generated. - A key that matches its tenant names
tenant_idon both sides, and a tenant-scoped immutable table keys on(tenant_id, id). Without this, a constraint onidalone would let a row reference another tenant's anchor, which the trigger check it replaces does not allow. - Keys over several columns belong to the composite pinned keys task.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
6. Notes
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2529 | [codegen] Generate real foreign keys into immutable entities |
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Constraint name could exceed 63 characters | core.py | Accept | The generator builds the name once and refuses one over 63 characters. |
| 2 | Constraint name is unquoted | template | Decline | Every generated function, trigger and index name is unquoted; the name is lower snake case by construction. |
| 3 | Unknown target table gives a misleading error | core.py | Accept | Separate refusal naming the table no model declares. |
| 4 | No test of nullable, multi-column or current-state keys | tests | Partly accept | Added a current-state child test. Postgres skips a null key itself, so a nullable key needs no generator code. Multi-column keys are the next task. |
| 5 | Drop order must remove children first | — | Record | Added to the acceptance of the anchor story, the first consumer. |
| 6 | (Own finding) A constraint on id alone ignores the tenant | core.py, template | Fix | The key matches the tenant, and a tenant-scoped immutable table keys on (tenant_id, id). |
10. Result
- A foreign key node with
:enforce: truerenders a namedREFERENCESconstraint and no trigger check. A node without it stays a trigger check. - The generator refuses
:enforce:when the target model is not immutable. - A key that matches its tenant names
tenant_idon both sides, and a tenant-scoped immutable table keys on(tenant_id, id). - The generator also refuses an unknown target table,
:use_system_tenant:, tenant scopes that differ, and a constraint name longer than 63 characters. - Twelve tests in
projects/ores.codegen/tests/test_enforced_foreign_key.pycover the tenant and untenanted constraints, a current-state child, the absent trigger check, the unenforced key and each refusal. - In Postgres, a rendered parent and child ran in a transaction that rolled
back. An insert that named an unknown anchor was refused with SQLSTATE
23503 by
ores_testcomp_child_records_anchor_id_fk. An insert that named a known anchor was accepted. With both tables tenant-scoped, a row in another tenant that named the anchor was refused by the same constraint, with the child's triggers off so that only the constraint could refuse it. - The SQL meta-model page and the variability index now document
:immutable:, which the previous task added. The meta-model page also documents:enforce:.