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: true to 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 sets skip_check, so the trigger check for that column is not generated.
  • A key that matches its tenant names tenant_id on both sides, and a tenant-scoped immutable table keys on (tenant_id, id). Without this, a constraint on id alone 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: true renders a named REFERENCES constraint 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_id on 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.py cover 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:.

Emacs 29.3 (Org mode 9.6.15)