Context
Follow-up from ADR-0002 (docs/adr/0002-unified-column-fact-derivation.md) and PR #256 (ColumnSpec refactor, #255).
Ferro does not validate that a model declares at most one primary key. A model can legally declare primary_key=True on more than one column — including one via FerroField/Field() and another via raw json_schema_extra={"primary_key": True} — with no error at class-definition time.
Before #256, the runtime PK-resolution helpers (save()'s id assignment, _primary_key_field_name, relationship descriptors, shadow-FK typing) scanned ferro_fields first and therefore always preferred the ferro-declared PK for such a malformed model. After #256 (which unified column-fact derivation to be declaration-path-agnostic — the whole point of the refactor), PK resolution is by column declaration order instead. Neither behaviour is defensibly correct, because the model is already malformed: it has two primary keys, which no SQL backend supports (the Rust layer resolves by first-flagged-in-IR-order regardless).
Ask
Add validation at class-definition time (the metaclass, where the other column-shape validations live) that raises a clear TypeError when more than one column is flagged primary_key=True, naming the offending columns and both declaration paths. This turns a silent declaration-order ambiguity into a loud, actionable error.
Notes
- Composite primary keys are not currently a Ferro feature, so "at most one PK column" is the correct invariant today. If composite PKs are ever added, this validation would need to move to "a coherent PK definition" rather than "exactly one column".
- Low-risk: no valid existing model declares two PKs (it would already produce broken DDL / a Postgres CREATE panic, per ADR-0002).
Context
Follow-up from ADR-0002 (
docs/adr/0002-unified-column-fact-derivation.md) and PR #256 (ColumnSpec refactor, #255).Ferro does not validate that a model declares at most one primary key. A model can legally declare
primary_key=Trueon more than one column — including one viaFerroField/Field()and another via rawjson_schema_extra={"primary_key": True}— with no error at class-definition time.Before #256, the runtime PK-resolution helpers (
save()'s id assignment,_primary_key_field_name, relationship descriptors, shadow-FK typing) scannedferro_fieldsfirst and therefore always preferred the ferro-declared PK for such a malformed model. After #256 (which unified column-fact derivation to be declaration-path-agnostic — the whole point of the refactor), PK resolution is by column declaration order instead. Neither behaviour is defensibly correct, because the model is already malformed: it has two primary keys, which no SQL backend supports (the Rust layer resolves by first-flagged-in-IR-order regardless).Ask
Add validation at class-definition time (the metaclass, where the other column-shape validations live) that raises a clear
TypeErrorwhen more than one column is flaggedprimary_key=True, naming the offending columns and both declaration paths. This turns a silent declaration-order ambiguity into a loud, actionable error.Notes