docs: ADR-0035 4a — refine scope (CHECK/DEFAULT to constraint slice; double-precision; serial-inline)
Three design questions settled during 4a implementation (plan + ADR §13 + README in lockstep): - CHECK/DEFAULT defer to the 4a.2 constraint slice: sql_expr is validate-only (no Expr AST), so they need raw-SQL-text storage on a separate path, not do_create_table's Expr->compile reuse. 4a.2 now also covers composite UNIQUE / multi-column table CHECK. - double precision (the lone two-word alias) handled via a keyword-pair branch; single-word aliases + discarded (len) cover the rest. - serial sole-PK in a multi-column table must inline PRIMARY KEY to keep autoincrement (worker-step do_create_table extension). 4a core narrows to columns + types + NOT NULL/UNIQUE/PRIMARY KEY + IF NOT EXISTS; everything else errors "not yet supported".
This commit is contained in:
@@ -305,20 +305,28 @@ with an explicit exit gate + a written Devil's-Advocate gate, mirroring
|
|||||||
ADR-0033's structure:
|
ADR-0033's structure:
|
||||||
|
|
||||||
- **4a — Dispatch + `CREATE TABLE` core.** Advanced `create`
|
- **4a — Dispatch + `CREATE TABLE` core.** Advanced `create`
|
||||||
dispatch; `SqlCreateTable` for columns + types (the §3 map) +
|
dispatch; `SqlCreateTable` for columns + types (the §3 map, incl. the
|
||||||
column constraints + single/compound `PRIMARY KEY`, plus
|
two-word `double precision` and discarded length args) + the
|
||||||
`IF NOT EXISTS` (no-op-with-note, §4). Single-column table-level
|
**clean-reuse column constraints only** — `NOT NULL` / `UNIQUE` /
|
||||||
`UNIQUE`/`CHECK` normalise into the column; **no FK** (4b). Reuses
|
column-level `PRIMARY KEY` — + single/compound table-level
|
||||||
`do_create_table`.
|
`PRIMARY KEY`, plus `IF NOT EXISTS` (no-op-with-note, §4). Reuses
|
||||||
- **4a.2 — Composite `UNIQUE(a,b)` / multi-column table `CHECK`.**
|
`do_create_table` (extended so a `serial` sole-PK inlines `PRIMARY
|
||||||
Split out (2026-05-24, user-confirmed) because these are the first
|
KEY` in a multi-column table, preserving autoincrement). **No FK**
|
||||||
structures the data model cannot already represent: `TableSchema`
|
(4b); **no `DEFAULT`/`CHECK`/table-level `UNIQUE`** (4a.2).
|
||||||
has no slot for them. A self-contained slice that extends
|
- **4a.2 — The constraint slice.** Split out (2026-05-24,
|
||||||
`TableSchema` + the YAML round-trip + `read_schema` detection +
|
user-confirmed) for the constraints that are *not* a clean reuse:
|
||||||
`do_create_table` DDL emission, with save/load/rebuild tests. (The
|
(1) **`CHECK`/`DEFAULT`** via the full `sql_expr` surface stored as
|
||||||
general rule: a DDL feature needs data-model work only when it
|
**raw SQL text** — needed because `sql_expr` is validate-only and
|
||||||
introduces a structure simple mode could never produce — cf. the
|
yields no `Expr` AST for `compile_check_sql`/`ColumnSpec`, so it is a
|
||||||
`UNIQUE`-index flag in 4d and the new rename op in 4h.)
|
separate execution path; (2) **composite `UNIQUE(a,b)` and
|
||||||
|
multi-column table `CHECK`** — the first structures `TableSchema`
|
||||||
|
cannot already represent, needing a model + YAML round-trip +
|
||||||
|
`read_schema` detection + `do_create_table` emission extension, with
|
||||||
|
save/load/rebuild tests. Until then 4a rejects all of these
|
||||||
|
"not yet supported". (The general rule: a DDL feature needs new
|
||||||
|
model/execution work only when it introduces a structure simple mode
|
||||||
|
could never produce, or an expression the structural helper cannot
|
||||||
|
consume — cf. the `UNIQUE`-index flag in 4d and the rename op in 4h.)
|
||||||
- **4b — Foreign keys in `CREATE TABLE`.** Inline `REFERENCES` +
|
- **4b — Foreign keys in `CREATE TABLE`.** Inline `REFERENCES` +
|
||||||
table-level `FOREIGN KEY` → relationship metadata, one undo step.
|
table-level `FOREIGN KEY` → relationship metadata, one undo step.
|
||||||
- **4c — `DROP TABLE [IF EXISTS]`** → `SqlDropTable` (cascade parity;
|
- **4c — `DROP TABLE [IF EXISTS]`** → `SqlDropTable` (cascade parity;
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -54,17 +54,16 @@ From the ADR-0035 design (handoff 36 §3) and the pre-implementation
|
|||||||
**advanced mode** and produces `Command::SqlCreateTable`.
|
**advanced mode** and produces `Command::SqlCreateTable`.
|
||||||
- [ ] Simple-mode `create table T with pk …` is **unchanged** and still
|
- [ ] Simple-mode `create table T with pk …` is **unchanged** and still
|
||||||
parses as `Command::CreateTable` (dispatch fallback verified).
|
parses as `Command::CreateTable` (dispatch fallback verified).
|
||||||
- [ ] Column element: `<name> <type> [constraints…]` where constraints
|
- [ ] Column element: `<name> <type> [NOT NULL] [UNIQUE]
|
||||||
are `NOT NULL`, `UNIQUE`, `PRIMARY KEY`, `DEFAULT <expr>`,
|
[PRIMARY KEY]` — the clean-reuse constraints only. `DEFAULT` /
|
||||||
`CHECK (<expr>)` — the per-column ADR-0029 set spelled in SQL.
|
`CHECK` are **not** in 4a (→ 4a.2, §6.1).
|
||||||
- [ ] Type slot: ten keywords + the §2.5 alias map; length/precision
|
- [ ] Type slot: ten keywords + the §2.5 alias map (incl. the two-word
|
||||||
arg accepted-and-ignored.
|
`double precision`, §6.3); length/precision arg accepted-and-ignored.
|
||||||
- [ ] `INTEGER PRIMARY KEY` → plain `int` PK (no auto-increment).
|
- [ ] `INTEGER PRIMARY KEY` → plain `int` PK (no auto-increment);
|
||||||
|
`serial` PK autoincrements even in a multi-column table (§6.4).
|
||||||
- [ ] Table-level `PRIMARY KEY (<col>, …)` — **single and compound**.
|
- [ ] Table-level `PRIMARY KEY (<col>, …)` — **single and compound**.
|
||||||
- [ ] Single-column table-level `UNIQUE(a)` / `CHECK(expr-over-a)`
|
- [ ] `DEFAULT`, `CHECK`, and any table-level `UNIQUE`/`CHECK` rejected
|
||||||
accepted by **normalizing into the column spec**; composite
|
"not yet supported" (→ 4a.2, §6.1).
|
||||||
`UNIQUE(a,b)` / multi-column table `CHECK` rejected "not yet
|
|
||||||
supported" (→ 4a.2, §6).
|
|
||||||
- [ ] `IF NOT EXISTS` → no-op-with-note on an existing table.
|
- [ ] `IF NOT EXISTS` → no-op-with-note on an existing table.
|
||||||
- [ ] Structural execution: reuses `do_create_table`; the new object is
|
- [ ] Structural execution: reuses `do_create_table`; the new object is
|
||||||
a first-class playground table — `__rdbms_playground_columns`
|
a first-class playground table — `__rdbms_playground_columns`
|
||||||
@@ -126,18 +125,17 @@ Mirror `src/dsl/grammar/sql_insert.rs`:
|
|||||||
- After `table` keyword: an optional `IF NOT EXISTS` keyword run that
|
- After `table` keyword: an optional `IF NOT EXISTS` keyword run that
|
||||||
sets a flag in the builder.
|
sets a flag in the builder.
|
||||||
- Column list: a `Repeated` of column-or-table elements inside `( … )`.
|
- Column list: a `Repeated` of column-or-table elements inside `( … )`.
|
||||||
- Column element: name slot (`IdentSource::NewName`) + type slot
|
- Column element: name slot (`IdentSource::NewName`) + type slot + a
|
||||||
(`IdentSource::Types`) + a constraint walk. The type slot consumes
|
constraint walk admitting only `NOT NULL` / `UNIQUE` /
|
||||||
an optional parenthesised number/number-pair and discards it
|
`PRIMARY KEY` (column-level). `DEFAULT` / `CHECK` are **not** in 4a
|
||||||
(length/precision ignored).
|
(→ 4a.2); typing them is "not yet supported".
|
||||||
- Table element (4a): `PRIMARY KEY ( col, … )`. **No** `FOREIGN KEY`,
|
- Type slot: `Choice[ Seq[Word("double"), Word("precision")],
|
||||||
no inline `REFERENCES` in 4a (those parse-error for now; 4b adds
|
Seq[ Ident{source: Types, validator: SQL_TYPE_VALIDATOR},
|
||||||
them).
|
Optional<( number [, number] )> ] ]` (§6.3). The validator uses
|
||||||
- `CHECK (<expr>)` / `DEFAULT <expr>` reuse the ADR-0031 fragment via
|
`Type::from_sql_name`; the length arg is discarded.
|
||||||
`Node::Subgrammar(&sql_expr::SQL_OR_EXPR)` (the same mechanism
|
- Table element (4a): `PRIMARY KEY ( col, … )` only. **No**
|
||||||
`sql_insert.rs` uses for value expressions). The matched expression
|
`FOREIGN KEY` / inline `REFERENCES` (4b), **no** table-level
|
||||||
is compiled to storable SQL via the existing `compile_check_sql`
|
`UNIQUE`/`CHECK` (4a.2) — those parse-error / "not yet supported".
|
||||||
path used by `do_create_table`.
|
|
||||||
|
|
||||||
Register in `REGISTRY` (`src/dsl/grammar/mod.rs`, Advanced group):
|
Register in `REGISTRY` (`src/dsl/grammar/mod.rs`, Advanced group):
|
||||||
`(&data::SQL_CREATE_TABLE, CommandCategory::Advanced)` alongside the
|
`(&data::SQL_CREATE_TABLE, CommandCategory::Advanced)` alongside the
|
||||||
@@ -233,53 +231,86 @@ enforces it.
|
|||||||
- `DROP TABLE [IF EXISTS]` → 4c. (`IF EXISTS` no-op-with-note reuses the
|
- `DROP TABLE [IF EXISTS]` → 4c. (`IF EXISTS` no-op-with-note reuses the
|
||||||
4.4 mechanism.)
|
4.4 mechanism.)
|
||||||
- Indexes, `ALTER TABLE`, `ALTER COLUMN TYPE`, table rename → 4d–4h.
|
- Indexes, `ALTER TABLE`, `ALTER COLUMN TYPE`, table rename → 4d–4h.
|
||||||
- **Composite `UNIQUE(a,b)` / multi-column table `CHECK` → 4a.2** (a
|
- **`CHECK`, `DEFAULT`, and all table-level `UNIQUE`/`CHECK` → 4a.2**
|
||||||
persistence-model extension; see §6). 4a accepts **single-column**
|
(the constraint slice; see §6.1). 4a rejects them with a
|
||||||
table-level `UNIQUE(a)` / `CHECK(expr-over-one-col)` by normalizing
|
"not yet supported" message. 4a keeps only the constraints that are a
|
||||||
them into the column spec (free — `ColumnSchema` already carries
|
clean `do_create_table` reuse: column-level `NOT NULL`, `UNIQUE`, and
|
||||||
per-column `unique`/`check`), and rejects the composite/multi-column
|
`PRIMARY KEY`, plus table-level `PRIMARY KEY (cols)`.
|
||||||
forms with a "not yet supported" message until 4a.2 lands.
|
|
||||||
- `CREATE TABLE … AS SELECT` (CTAS): **not in ADR-0035's surface** at
|
- `CREATE TABLE … AS SELECT` (CTAS): **not in ADR-0035's surface** at
|
||||||
all — an ordinary parse error, not a deferral.
|
all — an ordinary parse error, not a deferral.
|
||||||
|
|
||||||
## 6. Decisions settled this round (user-confirmed) + the 4a/4a.2 split
|
## 6. Decisions settled this round (all user-confirmed unless noted)
|
||||||
|
|
||||||
1. **4a/4a.2 split (user-confirmed).** Composite `UNIQUE(a,b)` and
|
**The general rule (the litmus test).** A DDL feature needs data-model
|
||||||
multi-column table `CHECK` are **deferred to a dedicated slice
|
or new-execution work exactly when it introduces a **structure simple
|
||||||
4a.2**. Rationale (the general rule): a DDL feature needs
|
mode could never produce, or an expression representation the structural
|
||||||
data-model work exactly when it introduces a **structure simple
|
helper can't consume** — not merely new syntax. Almost all of advanced
|
||||||
mode could never produce** — not merely new syntax. Almost all of
|
DDL (per-column NOT NULL/UNIQUE, compound PK, FK, indexes, drop,
|
||||||
advanced DDL (per-column constraints, compound PK, FK, indexes,
|
alter-column) maps onto structures the model already persists
|
||||||
drop, alter-column) maps onto structures the model already persists
|
(`ColumnSchema`, `TableSchema.primary_key`, `RelationshipSchema`,
|
||||||
(`ColumnSchema`, `TableSchema.primary_key`, `RelationshipSchema`,
|
`IndexSchema`) and feeds the existing helpers, so it is syntax-only +
|
||||||
`IndexSchema`), so it is syntax-only + reuse. Composite UNIQUE /
|
reuse. The exceptions earn their own slice. **Same class, already on the
|
||||||
table CHECK are the **first structures with no slot in
|
radar:** `CREATE UNIQUE INDEX` (4d — `IndexSchema` has no `unique`
|
||||||
`TableSchema`**, so they need a model + round-trip extension and
|
field; ADR-0025 deferred it) and table rename (4h — a new low-level op
|
||||||
earn their own slice. **Same class, already on the radar:**
|
the ADR flags).
|
||||||
`CREATE UNIQUE INDEX` (4d — `IndexSchema` has no `unique` field;
|
|
||||||
ADR-0025 deferred it) and table rename (4h — a new low-level op the
|
|
||||||
ADR already flags).
|
|
||||||
|
|
||||||
**4a.2 scope** (its own test-first slice, after 4a core):
|
1. **The constraint slice 4a.2 (user-confirmed).** A dedicated
|
||||||
- Extend `TableSchema` (`src/persistence/mod.rs`) with table-level
|
test-first slice, after 4a core, gathering every constraint that is
|
||||||
constraint slots — e.g. `unique_constraints: Vec<Vec<String>>` and
|
*not* a clean reuse:
|
||||||
`check_constraints: Vec<String>`.
|
- **`CHECK` / `DEFAULT`** — via the full ADR-0031 `sql_expr` surface,
|
||||||
- Extend the YAML writer/parser + `RawTable`
|
captured as **raw SQL text** and stored directly (`ColumnSchema.
|
||||||
(`src/persistence/yaml.rs`) — additive, backward-compatible,
|
check` / `.default` are already `String`/`Option<String>`). Needed
|
||||||
optional-on-read (the pattern used when `unique`/`not_null`/
|
because `sql_expr` is **validate-only** — it builds no `Expr` AST,
|
||||||
`check` were added; **no migration needed**).
|
so it cannot feed `compile_check_sql(expr: &Expr)` /
|
||||||
- Extend `read_schema_snapshot` (`src/db.rs` ~2225) to **detect**
|
`ColumnSpec.check: Option<Expr>`. This is a **separate execution
|
||||||
composite UNIQUE / table CHECK from the database.
|
path** (raw-text, not the `Expr`→compile path), threaded through
|
||||||
- Extend `do_create_table` to **emit** them in the DDL.
|
`SqlCreateTable` + a `do_create_table` variant — hence its own
|
||||||
- Extend the `SqlCreateTable` command shape + grammar to carry/parse
|
slice rather than 4a.
|
||||||
them (lifting the 4a "not yet supported" rejection).
|
- **Composite `UNIQUE(a,b)` and multi-column table `CHECK`** — the
|
||||||
- **Round-trip tests** (save → load → rebuild) proving they survive,
|
first structures with no slot in `TableSchema`; need the
|
||||||
on top of parse/execute/undo coverage.
|
model + round-trip extension: extend `TableSchema`
|
||||||
|
(`src/persistence/mod.rs`, e.g. `unique_constraints:
|
||||||
|
Vec<Vec<String>>`, `check_constraints: Vec<String>`); extend the
|
||||||
|
YAML writer/parser + `RawTable` (`src/persistence/yaml.rs`,
|
||||||
|
additive/backward-compatible/optional-on-read — the
|
||||||
|
`unique`/`not_null`/`check` pattern, **no migration**); extend
|
||||||
|
`read_schema_snapshot` (`src/db.rs` ~2225) to **detect** them;
|
||||||
|
extend `do_create_table` to **emit** them; **round-trip tests**
|
||||||
|
(save → load → rebuild).
|
||||||
|
- Until 4a.2: `CREATE TABLE` with `CHECK`, `DEFAULT`, or any
|
||||||
|
table-level `UNIQUE`/`CHECK` errors **"not yet supported"** (same
|
||||||
|
treatment as composite UNIQUE) — not a confusing parse error.
|
||||||
|
|
||||||
2. **No-op-with-note plumbing — mechanism (a)** (the `CreateOutcome`
|
2. **No-op-with-note plumbing — mechanism (a)** (the `CreateOutcome`
|
||||||
enum, §4.4). Explicit and reused by `DROP TABLE IF EXISTS` (4c);
|
enum, §4.4). Explicit and reused by `DROP TABLE IF EXISTS` (4c);
|
||||||
chosen now so the worker reply type is designed once.
|
chosen now so the worker reply type is designed once.
|
||||||
|
|
||||||
|
3. **`double precision` (implementer call).** The lone two-word alias is
|
||||||
|
handled by a dedicated keyword-pair branch in the type slot
|
||||||
|
(`Choice[ Seq[Word("double"), Word("precision")], <type-ident +
|
||||||
|
optional (len)> ]`); the builder maps the word-pair to `Type::Real`.
|
||||||
|
Single-word aliases + an optional discarded `(len[,len])` cover the
|
||||||
|
rest. Delivers the ADR §3 item without bending it.
|
||||||
|
|
||||||
|
4. **`serial` PK inline emission (implementer call, step 3/worker).**
|
||||||
|
`do_create_table` today inlines `PRIMARY KEY` (the rowid-alias that
|
||||||
|
makes `serial` autoincrement) **only when the table has one column**.
|
||||||
|
SQL mode allows `CREATE TABLE t (id serial primary key, name text)` —
|
||||||
|
a `serial` sole-PK in a multi-column table — which would otherwise
|
||||||
|
get a table-level PK and **lose autoincrement**. Step 3 extends the
|
||||||
|
inline condition to "sole-PK column whose type is `serial` → inline
|
||||||
|
`PRIMARY KEY` on that column," leaving the simple-mode paths
|
||||||
|
(single-column, or compound) unchanged. Covered by a worker test.
|
||||||
|
|
||||||
|
5. **Redundant PK constraints (implementer call).** SQL mode is
|
||||||
|
**lenient** like real engines: `id int primary key not null` /
|
||||||
|
`… unique` is accepted, and the builder silently de-dups the
|
||||||
|
redundant `not_null`/`unique` flag off a sole-PK column (so the
|
||||||
|
emitted DDL/metadata stay clean for `do_create_table`). Simple
|
||||||
|
mode's ADR-0029 §9 *rejection* is unchanged. (Diverges from simple
|
||||||
|
mode deliberately, matching the advanced-mode "trust the user like
|
||||||
|
SQL" posture, ADR-0035 §7.)
|
||||||
|
|
||||||
## 7. Devil's Advocate review of this plan
|
## 7. Devil's Advocate review of this plan
|
||||||
|
|
||||||
- **Does it reuse rather than fork execution?** Yes — `do_create_table`
|
- **Does it reuse rather than fork execution?** Yes — `do_create_table`
|
||||||
@@ -307,18 +338,22 @@ enforces it.
|
|||||||
(canonical + aliases + length-ignored + unknown→None) → red → add
|
(canonical + aliases + length-ignored + unknown→None) → red → add
|
||||||
`resolve_type_name` → green.
|
`resolve_type_name` → green.
|
||||||
2. **Command + parser** — write Tier-1 `ast_builder` tests (valid
|
2. **Command + parser** — write Tier-1 `ast_builder` tests (valid
|
||||||
columns/types/constraints, compound PK, single-column table-level
|
columns/types incl. aliases + `double precision` + discarded
|
||||||
`UNIQUE`/`CHECK` normalized, `INTEGER PRIMARY KEY`→int,
|
`(len)`, column-level `NOT NULL`/`UNIQUE`/`PRIMARY KEY`, compound
|
||||||
`IF NOT EXISTS` flag; FK-shape and composite `UNIQUE(a,b)` /
|
table-level PK, `INTEGER PRIMARY KEY`→int, `IF NOT EXISTS` flag;
|
||||||
multi-column `CHECK` rejected "not yet supported") → red → add
|
`DEFAULT`/`CHECK`/table-level `UNIQUE`/FK-shape rejected "not yet
|
||||||
`Command::SqlCreateTable`, `sql_create_table.rs`, REGISTRY entry →
|
supported") → red → add `Command::SqlCreateTable`,
|
||||||
|
`sql_create_table.rs`, REGISTRY entry, exhaustive-match stubs →
|
||||||
green. Include a fallback test: simple `create … with pk` still
|
green. Include a fallback test: simple `create … with pk` still
|
||||||
parses in advanced mode.
|
parses in advanced mode. *(Compiles only once the worker dispatch
|
||||||
|
handles the new `Command`, so steps 2–3 land together.)*
|
||||||
3. **Worker** — write Tier-3 `tests/sql_create_table.rs` (metadata, CSV,
|
3. **Worker** — write Tier-3 `tests/sql_create_table.rs` (metadata, CSV,
|
||||||
describe; alias end-to-end; `IF NOT EXISTS` no-op-with-note;
|
describe; alias end-to-end; `serial` PK autoincrements in a
|
||||||
duplicate-table plain error) → red → add `Request::SqlCreateTable` +
|
multi-column table, §6.4; `IF NOT EXISTS` no-op-with-note;
|
||||||
`snapshot_then` arm + the §4.4 outcome + reuse `do_create_table` →
|
duplicate-table plain error; redundant PK constraint de-duped, §6.5)
|
||||||
green.
|
→ red → add `Request::SqlCreateTable` + `snapshot_then` arm + the
|
||||||
|
§4.4 `CreateOutcome` + the §6.4 `do_create_table` serial-inline
|
||||||
|
extension + reuse `do_create_table` → green.
|
||||||
4. **Undo** — write the undo Tier-3 test (one step; undo/redo) → red →
|
4. **Undo** — write the undo Tier-3 test (one step; undo/redo) → red →
|
||||||
confirm it passes (the `snapshot_then` wrap should make it green with
|
confirm it passes (the `snapshot_then` wrap should make it green with
|
||||||
no extra code; if not, the wrap is wrong) → green.
|
no extra code; if not, the wrap is wrong) → green.
|
||||||
|
|||||||
Reference in New Issue
Block a user