Files
rdbms-playground/docs/adr/0011-fk-column-type-compatibility.md
T
claude@clouddev1 c1e52920eb DSL parser, async DB worker, types, history, metadata, polish
Track 1 implementation plus polish round.

Parser (chumsky):
- Grammar-based DSL producing a typed Command AST.
- create table X with pk [name:type[,name:type...]] supports
  arbitrary names, any user type, compound PKs natively. Bare
  form errors with a friendly hint pointing at `with pk`.
- add column to table X: Name (type); drop table X.
- Required clauses use keyword grammar; -- reserved for opt-in
  flags (ADR-0009). Custom Rich reasons preferred when surfacing
  chumsky errors so unknown-type messages list valid alternatives.

Database (ADR-0010, ADR-0012):
- rusqlite + STRICT tables + foreign_keys=ON.
- Dedicated worker thread; mpsc Request inbox, oneshot replies.
- Typed DbError with friendly_message() hook for H1.
- Internal __rdbms_playground_columns metadata table preserves
  user-facing types across schema reads, atomically maintained
  alongside DDL via Connection transactions. list_tables hides
  it via the new __rdbms_ internal-table convention.

Types (ADR-0005, ADR-0011):
- All ten user-facing types: text, int, real, decimal, bool,
  date, datetime, blob, serial, shortid.
- Type::fk_target_type() for FK-side column-type rule
  (Serial->Int, ShortId->Text, others identity) -- foundation
  for the FK iteration.

App / Runtime / UI:
- update() stays pure-sync; runtime dispatches DSL via spawned
  tasks, results post back as AppEvent::Dsl*.
- Items panel renders live tables list; output panel shows the
  user-facing structure of the current table after each DDL.
- In-memory command history (Up/Down, draft preservation,
  consecutive-duplicate dedup) -- I2 partial.
- Mouse capture removed; terminal native text selection
  restored (toggle approach revisited when scroll/click
  features land).

Docs:
- ADRs 0009 (DSL syntax conventions), 0010 (DB worker),
  0011 (FK type compat), 0012 (internal metadata table).
- requirements.md progress notes; new V4 entry for the
  scrollable session-log + inline rich rendering + Markdown
  export direction.

Tests: 103 passing (91 lib + 12 integration), 0 skipped.
Clippy clean with nursery enabled.
2026-05-07 13:32:19 +00:00

3.0 KiB
Raw Blame History

ADR-0011: Foreign-key column type compatibility

Status

Accepted

Context

Two of our user-facing types (per ADR-0005) carry insert-time auto-generation semantics that only apply on the primary-key side of a relationship:

  • serial is INTEGER PRIMARY KEY with rowid-alias / auto-increment behaviour.
  • shortid is TEXT populated client-side with a 1012 char base58 random value when no value is supplied.

A foreign-key column referencing such a primary key does not auto-generate values — it stores the looked-up value of the primary key. Asking the user to declare the FK column with the same user-facing type as the PK would be wrong: serial on the FK side would imply the FK column has its own auto-increment counter (it doesn't), and similar for shortid.

Without this rule, the future FK declaration grammar (C3, C4) would either generate incorrect DDL or rely on the user to remember to use a different type on the FK side — an easy foot-gun.

Decision

Define Type::fk_target_type(self) -> Type returning the appropriate user-facing type for a foreign-key column that references a primary key of self:

User-facing type fk_target_type()
text text
int int
real real
decimal decimal
bool bool
date date
datetime datetime
blob blob
serial int
shortid text

All types other than serial and shortid are identity mappings. The two exceptions strip the auto-generation semantics by mapping to the underlying value type.

Consequences

  • The future FK declaration grammar uses fk_target_type() in one of two ways:
    • Auto-typing — when the FK column does not yet exist and --create-fk is given (or whatever the equivalent flag becomes), the column is created with pk_column.ty.fk_target_type().
    • Validation — when the FK column already exists, its type is compared against fk_target_type(). A mismatch yields a clear diagnostic ("Customers.id is serial; the FK column should be int, not text"). This is the teaching moment ADR-0009's design philosophy targets.
  • Adding a new user-facing type forces an explicit decision about its fk_target_type(). The type system is therefore closed under FK declaration.
  • The advisory feedback module (foreshadowed in V4 and the hint surface) can use the same mapping to surface recommendations during command typing — e.g. "you used serial for an FK; conventionally int is the right fit here." This is advice not gating, consistent with ADR-0009's separation of required grammar from optional guidance.
  • fk_target_type() is implemented and tested before the FK declaration grammar lands, so the FK iteration is grammar work only.