constraints: CHECK — check (<expr>) at create table & add column (ADR-0029)

The fourth constraint. `check ( <expr> )` reuses the ADR-0026
WHERE-expression grammar via `Subgrammar`, so a check is
written in the same language as a `where` filter.

- Grammar: a `CHECK_CONSTRAINT` arm joins the shared
  constraint-suffix Choice; `consume_check_expr` extracts the
  parenthesised expression (paren-depth aware) into
  `ColumnSpec.check` / `Command::AddColumn.check`.
- Storage: the parsed `Expr` is compiled once to inline SQL
  (`compile_check_sql` — `compile_expr` + ADR-0028's
  param-inliner) and stored in that form everywhere — a new
  `check_expr` column in `__rdbms_playground_columns`,
  `project.yaml`'s `ColumnSchema.check`, and the column DDL
  emitted by `do_create_table` / `schema_to_ddl`.
- `add column … check` routes through the rebuild primitive
  (SQLite's `ALTER … ADD COLUMN` cannot carry it); a CHECK on
  a serial/shortid column is create-table-only and refused at
  add-column with a friendly message.
- `describe` surfaces the CHECK. ADR-0029 §7/§8 updated to the
  SQL-form decision — double-quoted identifiers, consistent
  with ADR-0028's `explain` display SQL.

1201 tests pass (+8); clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-19 16:42:18 +00:00
parent 58d8958822
commit 942222bfc9
11 changed files with 421 additions and 73 deletions
+31 -31
View File
@@ -80,7 +80,7 @@ A column spec gains an optional, repeatable constraint suffix
**after** the `(type)` group:
```
create table Books with pk isbn(text) check (length(isbn) = 13)
create table Books with pk isbn(text) check (isbn like '978%')
add column to Books: title (text) not null
add column to Books: stock (int) default 0 check (stock >= 0)
@@ -283,28 +283,31 @@ the data.
constraints must round-trip through `project.yaml` or they
vanish on `rebuild` / `export` / `import`.
The `CHECK` expression is stored — everywhere — as the
**compiled SQL** form: the parsed `Expr` run through
`compile_expr` with literals inlined (§4). Identifiers are
double-quoted, exactly as ADR-0028's `explain` display SQL
already renders them, so the form is consistent with what the
learner already meets. (The original plan stored canonical
*DSL text*; that needs both an `Expr`→text renderer and a
text→`Expr` re-parser for the round-trip, while the rebuild
path needs the SQL form regardless — storing SQL once removes
both.)
- **`project.yaml`** — the `ColumnSchema` record gains
`not_null: bool`, `default: Option<Value>`, and
`not_null: bool`, `default: Option<String>`, and
`check: Option<String>`. (`unique: bool` already exists,
from ADR-0018's `serial` / `shortid` contract.) The `check`
expression is stored as its **canonical DSL text** — see the
`Expr` text renderer below.
from ADR-0018's `serial` / `shortid` contract.) `check`
holds the compiled SQL.
- **Metadata table** — `NOT NULL`, `UNIQUE`, and `DEFAULT` are
all recoverable from SQLite itself (`pragma_table_info`'s
`notnull` and `dflt_value`; `pragma_index_list` origin `u`),
so they need no metadata row. `CHECK` is *not* exposed by
any pragma — only by the raw `sqlite_master` SQL, which is
in engine syntax. So `__rdbms_playground_columns` carries a
nullable `check_expr TEXT` column holding the canonical DSL
text, keeping `describe` independent of engine-syntax
parsing. It is part of the internal table's `CREATE TABLE`
definition — there are no existing databases to migrate.
- **`Expr` → DSL text renderer** — a new `render_expr` (small;
the `Expr` tree from ADR-0026 is shallow) produces canonical
DSL text for an expression. One renderer, three consumers:
`project.yaml` serialization, the `check_expr` metadata
column, and the structure view (§8). The check round-trips
text → `Expr` (re-parsed on load) → text.
any pragma. So `__rdbms_playground_columns` carries a
nullable `check_expr TEXT` column holding the compiled SQL,
which `schema_to_ddl` and `describe` echo verbatim. It is
part of the internal table's `CREATE TABLE` definition —
there are no existing databases to migrate.
### 8. Structure rendering
@@ -315,17 +318,17 @@ Option<String>`, and `check: Option<String>`.
every constraint a column carries:
```
┌───────┬─────────────────────────────────────────┐
│ Name │ Type │ Constraints
├───────┼─────────────────────────────────────────┤
┌───────┬─────────────────────────────────────────┐
│ Name │ Type │ Constraints │
├───────┼─────────────────────────────────────────┤
│ id │ serial │ PK │
│ email │ text │ NOT NULL, UNIQUE
│ age │ int │ DEFAULT 18, CHECK age >= 0
└───────┴─────────────────────────────────────────┘
│ email │ text │ NOT NULL, UNIQUE │
│ age │ int │ DEFAULT 18, CHECK ("age" >= 0)
└───────┴─────────────────────────────────────────┘
```
The `CHECK` expression renders in DSL form (`render_expr`),
not engine SQL.
The `CHECK` renders in its compiled-SQL form (§7) — the same
double-quoted-identifier style as ADR-0028's `explain` SQL.
### 9. PK columns — redundant and impossible constraints
@@ -409,10 +412,7 @@ additions:
tables go through the ADR-0016 pretty-table renderer.
- The internal metadata table `__rdbms_playground_columns`
carries a new `check_expr` column — its first change since
ADR-0012.
- A new `Expr` → DSL-text renderer (`render_expr`) is added;
it is also reusable by any future feature that needs to
show an expression back to the user.
ADR-0012 — holding the compiled-SQL form of the `CHECK`.
- `project.yaml`'s `ColumnSchema` grows three fields; the
format stays backward-compatible (the new keys default to
"absent" — `not_null: false`, no `default`, no `check`).
@@ -433,8 +433,8 @@ A sensible build order, each step test-guarded:
honour it, including the §6 `add column` rules. The
`Expr` → SQL compile for `CHECK`.
3. **Storage round-trip.** `ColumnSchema` fields, the
`check_expr` metadata column, `render_expr`, and the
`project.yaml` read/write paths.
`check_expr` metadata column, and the `project.yaml`
read/write paths.
4. **`add constraint …` / `drop constraint …`.** The two
commands, the rebuild-table path, and the §5 dry-run with
its pretty-table refusals.