Files
claude@clouddev1 102dff08c4 docs: handoff 22 — ADR-0029 through commit 4
ADR-0028 complete (per handoff-21); ADR-0029 (column
constraints) written, accepted, and implemented through
commit 4 of 6 — NOT NULL / UNIQUE / DEFAULT / CHECK at
`create table` and `add column`. Commits 5 (`add constraint`
/ `drop constraint` + the §5 dry-run) and 6 (friendly errors
+ typing-surface matrix) are planned in full in §4.
2026-05-19 16:44:42 +00:00

12 KiB
Raw Permalink Blame History

Session handoff — 2026-05-19 (22)

Twenty-second handover. Interim handoff: this session finished ADR-0028 (handed off in handoff-21), then started ADR-0029 — column constraints (NOT NULL / UNIQUE / CHECK / DEFAULT). ADR-0029 is written, accepted, and implemented through commit 4 of 6; commits 56 are planned in full detail in §4 and not yet built. A fresh session can implement them from this file + the ADR.

§1. State at handoff

Branch: main. Working tree clean. 8 commits since handoff-21 (02234e6), all local — push asynchronously, not blocking.

<this file>  docs: handoff 22 — ADR-0029 through commit 4
942222b constraints: CHECK — check (<expr>) at create/add (ADR-0029)
58d8958 add column: column constraints — NOT NULL/UNIQUE/DEFAULT (§6)
12395a9 create table: column constraints — NOT NULL/UNIQUE/DEFAULT grammar
a60e879 db: column-constraint infrastructure — NOT NULL/UNIQUE/DEFAULT
eff2ee8 refactor: ColumnSpec / AddColumn carry constraint fields (scaffolding)
7bfd213 docs: ADR-0029 — column constraints
59351e0 docs: move indexes/query-plans out of the deferred list
02234e6 docs: handoff 21 — ADR-0028 complete

Tests: 1201 passing, 0 failing, 1 ignored (cargo test). The ignored test is the long-standing ```ignore doc-test in src/friendly/mod.rs. Typing-surface matrix: 174 cells, unchanged this session.

Clippy: clean (cargo clippy --all-targets -- -D warnings, nursery group).

§2. Done before ADR-0029

  • ADR-0028 (query plans / explain) — finished and fully documented in handoff-21. Read that for context.
  • docs: move indexes/query-plans out of the deferred list (59351e0) — a CLAUDE.md tidy-up only.

§3. ADR-0029 — what it is, what's done

Read docs/adr/0029-column-constraints.md — it is the spec, and it has been kept current (the two deviations below are already folded in). The feature: the four column-level constraints, declared in a suffix after a column's (type) group and — for commit 5 — modifiable on existing columns.

Commits 14 (done):

  1. eff2ee8ColumnSpec / Command::AddColumn gained the four constraint fields (not_null / unique / default: Option<Value> / check: Option<Expr>), all defaulting off; Database::add_column now takes a ColumnSpec. ColumnSpec::new(name, ty) is the unconstrained constructor. 110 call sites updated.
  2. a60e879 — the db layer honours the ColumnSpec constraints: column_constraints_sql, do_create_table emits NOT NULL/UNIQUE/DEFAULT; ReadColumn gained default_sql; schema_to_ddl emits DEFAULT (so the rebuild primitive preserves it); ColumnDescription + do_describe_table (now sourced from read_schema) + constraints_display.
  3. 12395a9 — the create table constraint-suffix grammar (not null / unique / default <literal>); build_create_table collects per-column constraints; §9 redundancy check; project.yaml round-trip (ColumnSchema gained not_null / default).
  4. 58d8958add column with the same suffix; the §6 routing (unique / not_null-without-default route through the rebuild primitive via do_add_constrained_column_via_rebuild); §6 pre-flight refusals.
  5. 942222bCHECK. check ( <expr> ) reuses the ADR-0026 expression grammar via Subgrammar. The parsed Expr is compiled once to inline SQL (compile_check_sql) and stored in that form everywhere — a check_expr column in __rdbms_playground_columns, ColumnSchema.check, the column DDL.

Three decisions / discoveries this session (the ADR

already reflects all three)

  1. create table … with pk … makes every listed column a primary-key column — there is no simple-mode syntax for a non-PK column at create time (recorded in docs/simple-mode-limitations.md). Consequence: the create table constraint suffix is realistically only useful for default / check; not null / unique there are §9 redundancy errors. The real home for not null / unique is add column and (commit 5) add constraint.
  2. CHECK is stored as compiled SQL, not DSL text (user-ratified; ADR §7/§8 updated). Double-quoted identifiers, consistent with ADR-0028's explain display SQL. No Expr→text renderer, no re-parser.
  3. A CHECK on a serial/shortid column is create-table-onlyadd column … (serial) … check is refused with a friendly message (the auto-fill rebuild path does not thread it).

Key reusable pieces (commits 14 built these)

All in src/dsl/grammar/ddl.rs unless noted:

  • COLUMN_CONSTRAINT — the Choice over the four individual constraint nodes (NOT_NULL_CONSTRAINT, UNIQUE_CONSTRAINT, DEFAULT_CONSTRAINT, CHECK_CONSTRAINT). COLUMN_CONSTRAINT_SUFFIX is Repeated{min:0} of it. The add constraint command (commit 5) reuses the individual nodes.
  • collect_column_constraints(path)(bool, bool, Option<Value>, Option<Expr>) — scans a single-column path's constraint keywords. consume_check_expr — extracts a check ( … ) expression (paren-depth aware).
  • redundant_pk_constraint(column, constraint) — the ADR-0029 §9 friendly error.
  • src/db.rs: column_constraints_sql, default_sql_literal, compile_check_sql, insert_column_metadata, read_schema_for_specs, do_add_constrained_column_via_rebuild. ReadColumn carries notnull / unique / default_sql / check; schema_to_ddl emits all four — so the rebuild primitive already round-trips every constraint.

§4. ADR-0029 commits 56 — the plan

Commit 5 — add constraint / drop constraint (ADR §2.2, §5)

The surface (the user chose add constraint <kind> over a bare add <kind>, for grammar-hierarchy uniformity):

add constraint not null to <T>.<col>
add constraint unique to <T>.<col>
add constraint default <literal> to <T>.<col>
add constraint check ( <expr> ) to <T>.<col>
drop constraint not null from <T>.<col>
drop constraint unique from <T>.<col>
drop constraint default from <T>.<col>
drop constraint check from <T>.<col>

Grammar (src/dsl/grammar/ddl.rs): add a constraint form to the ADD and DROP command Choices. The word constraint after add / drop discriminates it (distinct from column / index / table / relationship / 1). After add constraint, reuse the existing COLUMN_CONSTRAINT Choice for the <constraint>; after drop constraint, a Choice of not null / unique / default / check keyword-only nodes (no payload). The dotted <T>.<col> — reuse the Ident '.' Ident shape from add 1:n relationship from <P>.<col> (same file). build_add / build_drop discriminate forms by the word after the entry verb — add a Some("constraint") arm to each.

AST (src/dsl/command.rs):

Command::AddConstraint  { table: String, column: String, constraint: Constraint }
Command::DropConstraint { table: String, column: String, kind: ConstraintKind }
pub enum Constraint     { NotNull, Unique, Default(Value), Check(Expr) }
pub enum ConstraintKind { NotNull, Unique, Default, Check }

GOTCHA — the two new Command variants break every exhaustive match Command (exactly ADR-0028's gotcha 3). Add arms to: Command::verb() and target_table() (command.rs); execute_command_typed (runtime.rs); build_translate_context (app.rs — map to an Operation, e.g. a new Operation::AddConstraint or reuse an existing one); command_kind_label (tests/typing_surface/mod.rs). Land grammar + AST + worker in one commit (a green intermediate is impossible otherwise).

DB worker (src/db.rs): Request::AddConstraint / DropConstraint; Database::add_constraint / drop_constraint; do_add_constraint / do_drop_constraint. Both return TableDescription (the post-rebuild structure) → route as CommandOutcome::Schema(Some(desc))AppEvent::DslSucceeded (the existing auto-show path; no new AppEvent needed).

do_add_constraint steps:

  1. read_schema(conn, table); find the target column.
  2. §9 redundancy (execution-time, since the parser has no schema): if the column is a PK column, reject not null (always) and unique (single-column PK only) — see ADR §9.
  3. §5 dry-run for not null / unique / check (skip for default): scan the existing rows; refuse with a learner-friendly pretty-table of offending rows if any. Mirror how do_change_column_type surfaces its ADR-0017 dry-run refusal table — that is the existing precedent for "scan data, refuse with a table." add not null → rows WHERE col IS NULL; add unique → non-NULL duplicate groups; add check → rows WHERE NOT (<sql>).
  4. Build new_schema = the read schema with the target ReadColumn's field set (notnull / unique / default_sql via default_sql_literal / check via compile_check_sql), then rebuild_table(...). The metadata_updates closure updates the check_expr metadata column when the constraint is a CHECK (the other three are pragma-recoverable and need no metadata).

do_drop_constraint: §9 check (cannot drop a PK-implied not null, or unique from a single-column PK — friendly error); build new_schema with the field cleared; rebuild. No dry-run (removing a constraint cannot violate data). drop check also NULLs the check_expr metadata.

Catalog: parse.usage.add_constraint / parse.usage.drop_constraint in en-US.yaml and keys.rs (the keys_validate_against_catalog test enforces the pair). The §5 refusal messages can be DbError::Unsupported detail strings for now (consistent with §6's commit-4 messages) or dedicated catalog keys.

Commit 6 — friendly errors + matrix + verification

  • CHECK-violation friendly error (ADR §10): an insert / update that violates a CHECK makes SQLite report CHECK constraint failed; add a friendly-layer catalog entry translating it (NOT NULL / UNIQUE are already enriched — see runtime.rs enrich_dsl_failure).
  • Typing-surface matrix cells in tests/typing_surface/ for the constraint grammar — create table … not null, add column … check (…), add constraint … to T.col, etc. Follow the matrix-snapshot discipline (handoff-17→21): a new cell's snapshot is created with INSTA_UPDATE=always cargo test --test typing_surface_matrix <filter> then reviewed before committing (cargo insta is not installed on this machine).
  • Full suite green vs. the 1201 baseline; clippy clean.
  • Final handoff (handoff-23) and tick C3 in docs/requirements.md.

§5. How to take over

  1. Read this file, then docs/adr/0029-column-constraints.md (the spec — current), then handoff-21 (ADR-0028).
  2. Read CLAUDE.md — working-style rules.
  3. Run cargo test — 1201 passing, 0 failing, 1 ignored.
  4. Run cargo clippy --all-targets -- -D warnings — clean.
  5. Implement ADR-0029 commit 5 per §4 — grammar + AST + worker in one commit (the match Command breakage forces it). Then commit 6. The §4 anchors and gotchas are this session's; the dry-run precedent to copy is do_change_column_type.

Note on mechanical churn

Adding a field to ColumnSpec / ColumnDescription / ReadColumn / ColumnSchema breaks 20110 construction sites, almost all in test code. This session delegated those purely-mechanical sweeps to a general-purpose sub-agent (mode: bypassPermissions) twice — it is the right tool for that drudgery. Commit 5 adds no such field, so it should not recur; but if commit 6 does, delegate it.