docs: ADR-0035 4a plan + 4a.2 split for composite UNIQUE / table CHECK
Add the sub-phase 4a implementation plan (docs/plans/), test-first, mirroring the ADR-0033 DML sub-phase model: SqlCreateTable as its own command executed structurally through the existing do_create_table helper; shared-entry-word dispatch (SQL-first, simple fallback); the type-alias resolver; IF NOT EXISTS no-op-with-note (CreateOutcome enum); INTEGER PRIMARY KEY -> plain int; one-undo-step wiring. Records the user-confirmed 4a/4a.2 split: composite UNIQUE(a,b) and multi-column table CHECK move to a dedicated slice because they are the first structures TableSchema cannot already represent, so they need a persistence-model + round-trip extension rather than parse+execute reuse. ADR-0035 §13 gains 4a.2; README sub-phase line updated in lockstep.
This commit is contained in:
@@ -307,7 +307,18 @@ 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) +
|
||||||
column constraints + single/compound `PRIMARY KEY`, plus
|
column constraints + single/compound `PRIMARY KEY`, plus
|
||||||
`IF NOT EXISTS` (no-op-with-note, §4). No FK yet.
|
`IF NOT EXISTS` (no-op-with-note, §4). Single-column table-level
|
||||||
|
`UNIQUE`/`CHECK` normalise into the column; **no FK** (4b). Reuses
|
||||||
|
`do_create_table`.
|
||||||
|
- **4a.2 — Composite `UNIQUE(a,b)` / multi-column table `CHECK`.**
|
||||||
|
Split out (2026-05-24, user-confirmed) because these are the first
|
||||||
|
structures the data model cannot already represent: `TableSchema`
|
||||||
|
has no slot for them. A self-contained slice that extends
|
||||||
|
`TableSchema` + the YAML round-trip + `read_schema` detection +
|
||||||
|
`do_create_table` DDL emission, with save/load/rebuild tests. (The
|
||||||
|
general rule: a DDL feature needs data-model work only when it
|
||||||
|
introduces a structure simple mode could never produce — cf. the
|
||||||
|
`UNIQUE`-index flag in 4d and the new 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
@@ -0,0 +1,353 @@
|
|||||||
|
# Plan: ADR-0035 Phase 4, sub-phase 4a — dispatch + `CREATE TABLE` core
|
||||||
|
|
||||||
|
This is the first slice of ADR-0035 (advanced-mode SQL DDL). It lands
|
||||||
|
advanced-mode `create` dispatch and a structurally-executed
|
||||||
|
`SqlCreateTable` command for the column/constraint/PK core — **no
|
||||||
|
foreign keys** (those are 4b). It mirrors the ADR-0033 DML sub-phase
|
||||||
|
model (`tests/sql_insert.rs` et al.) and reuses the existing
|
||||||
|
low-level create-table machinery per ADR-0035 §1.
|
||||||
|
|
||||||
|
## 1. Baseline
|
||||||
|
|
||||||
|
- Tests: **1698 passing, 0 failing, 0 skipped, 1 ignored** (the
|
||||||
|
long-standing `friendly/mod.rs` ` ```ignore ` doctest). Clippy clean
|
||||||
|
(`cargo clippy --all-targets -- -D warnings`).
|
||||||
|
- Branch `main`; last commit `19d3cd3` (the ADR-0035 `/runda`
|
||||||
|
refinements). Re-confirmed green at the start of this work.
|
||||||
|
|
||||||
|
## 2. Decisions locked with the user (do not re-litigate)
|
||||||
|
|
||||||
|
From the ADR-0035 design (handoff 36 §3) and the pre-implementation
|
||||||
|
`/runda` round (2026-05-24), all user-confirmed:
|
||||||
|
|
||||||
|
1. **Own command, structural execution.** `SqlCreateTable` is its own
|
||||||
|
`Command` / `Request` variant (not a reuse of `Command::CreateTable`).
|
||||||
|
It executes **structurally** by reusing the low-level helper
|
||||||
|
`do_create_table` — *not* verbatim SQL. Simple mode is untouched.
|
||||||
|
2. **`IF NOT EXISTS` is admitted** as a **no-op that succeeds with a
|
||||||
|
note** ("table already exists — skipped"), not a parse error and not
|
||||||
|
the plain-form "already exists" error. (ADR §3/§4/§12/§13.)
|
||||||
|
3. **`INTEGER PRIMARY KEY` → plain `int` PK.** The type map is purely
|
||||||
|
lexical; auto-increment is reachable only via the explicit `serial`
|
||||||
|
type. (ADR §3.)
|
||||||
|
4. **Dispatch:** `create` is a shared entry word — it stays
|
||||||
|
`CommandCategory::Simple` for `ddl::CREATE` *and* gains an
|
||||||
|
`Advanced` entry for the new SQL node, exactly as `insert`/`update`/
|
||||||
|
`delete` do (ADR-0033 Amendment 1: SQL-first in advanced, simple
|
||||||
|
fallback).
|
||||||
|
5. **Type vocabulary:** the ten playground keywords **and** the
|
||||||
|
standard-SQL aliases (`integer`/`smallint`/`bigint`→`int`,
|
||||||
|
`varchar`/`char`→`text`, `boolean`→`bool`, `timestamp`→`datetime`,
|
||||||
|
`numeric`→`decimal`, `float`/`double precision`→`real`,
|
||||||
|
`binary`/`varbinary`→`blob`); a length/precision argument
|
||||||
|
(`varchar(255)`) is **accepted and ignored**.
|
||||||
|
6. **Undo:** `SqlCreateTable` is a user mutation carrying a `source`,
|
||||||
|
wrapped in `snapshot_then` like the existing 19 mutating arms → one
|
||||||
|
undo step. `create` needs **no** `is_app_lifecycle_entry_word`
|
||||||
|
change (it's a write).
|
||||||
|
|
||||||
|
## 3. Phase 1 — Requirements checklist (4a scope)
|
||||||
|
|
||||||
|
### Functional
|
||||||
|
|
||||||
|
- [ ] `CREATE TABLE <name> ( <col-elements>, [table-PK] )` parses in
|
||||||
|
**advanced mode** and produces `Command::SqlCreateTable`.
|
||||||
|
- [ ] Simple-mode `create table T with pk …` is **unchanged** and still
|
||||||
|
parses as `Command::CreateTable` (dispatch fallback verified).
|
||||||
|
- [ ] Column element: `<name> <type> [constraints…]` where constraints
|
||||||
|
are `NOT NULL`, `UNIQUE`, `PRIMARY KEY`, `DEFAULT <expr>`,
|
||||||
|
`CHECK (<expr>)` — the per-column ADR-0029 set spelled in SQL.
|
||||||
|
- [ ] Type slot: ten keywords + the §2.5 alias map; length/precision
|
||||||
|
arg accepted-and-ignored.
|
||||||
|
- [ ] `INTEGER PRIMARY KEY` → plain `int` PK (no auto-increment).
|
||||||
|
- [ ] Table-level `PRIMARY KEY (<col>, …)` — **single and compound**.
|
||||||
|
- [ ] Single-column table-level `UNIQUE(a)` / `CHECK(expr-over-a)`
|
||||||
|
accepted by **normalizing into the column spec**; composite
|
||||||
|
`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.
|
||||||
|
- [ ] Structural execution: reuses `do_create_table`; the new object is
|
||||||
|
a first-class playground table — `__rdbms_playground_columns`
|
||||||
|
populated with the playground `user_type`, `STRICT` applied, CSV layer
|
||||||
|
+ `project.yaml` correct (ADR-0015 §6 ordering preserved).
|
||||||
|
- [ ] One undo step per `CREATE TABLE`; `undo` removes the table +
|
||||||
|
metadata; `redo` recreates it.
|
||||||
|
- [ ] Errors route through the friendly layer, engine-neutral wording
|
||||||
|
(unknown type; duplicate column name; the `IF NOT EXISTS` note).
|
||||||
|
|
||||||
|
### Cross-cutting / integration
|
||||||
|
|
||||||
|
- [ ] `history.log` records the literal submitted SQL line; replay
|
||||||
|
re-runs it as a write (no replay-filter change — ADR §10).
|
||||||
|
- [ ] Ambient assistance comes free from the walker: highlighting,
|
||||||
|
`[ERR]`/`[WRN]`, usage skeleton, completion. The node **authors**
|
||||||
|
`IdentSource::NewName` on the table-name slot, `IdentSource::Types`
|
||||||
|
on the type slot, its `help_id`/`usage_ids`, and the 4a DDL
|
||||||
|
diagnostics.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [ ] On 4a end-to-end validation, flip ADR-0035 status
|
||||||
|
**Proposed → Accepted** (the ADR-0033 lifecycle) and update
|
||||||
|
`docs/adr/README.md` in the same edit.
|
||||||
|
- [ ] `requirements.md`: `Q4` progresses (DDL create landing); do **not**
|
||||||
|
mark `C1` `[x]` (rename is 4h, advanced-only).
|
||||||
|
|
||||||
|
### Testing (ADR-0008 four tiers)
|
||||||
|
|
||||||
|
- [ ] **Tier 1** (unit, in-crate): type-alias map; the `ast_builder`
|
||||||
|
for valid/invalid `CREATE TABLE` shapes; column-constraint parsing;
|
||||||
|
compound PK; `INTEGER PRIMARY KEY`→int; `IF NOT EXISTS` flag captured.
|
||||||
|
- [ ] **Tier 2** (insta): structure-view snapshot after a SQL create,
|
||||||
|
if it adds signal over existing create-table snapshots (mirror the
|
||||||
|
existing simple-mode create snapshot; skip if redundant).
|
||||||
|
- [ ] **Tier 3** (`tests/sql_create_table.rs`, mirror
|
||||||
|
`tests/sql_insert.rs`): full-stack parse→dispatch→worker; assert
|
||||||
|
metadata rows, CSV/yaml, `describe` output; alias mapping;
|
||||||
|
`IF NOT EXISTS` no-op-with-note; plain-form duplicate error;
|
||||||
|
simple-mode create still works in advanced mode (fallback).
|
||||||
|
- [ ] **Undo Tier 3** (in `tests/undo_snapshots.rs` or a sibling):
|
||||||
|
`CREATE TABLE` is one undo step; undo drops the table + clears its
|
||||||
|
metadata; redo recreates it identically.
|
||||||
|
|
||||||
|
## 4. Architecture & design
|
||||||
|
|
||||||
|
### 4.1 New grammar node — `src/dsl/grammar/sql_create_table.rs`
|
||||||
|
|
||||||
|
Mirror `src/dsl/grammar/sql_insert.rs`:
|
||||||
|
|
||||||
|
- `CommandNode` `SQL_CREATE_TABLE` with `entry: Word::keyword("create")`,
|
||||||
|
a `CREATE_TABLE` shape, `ast_builder: build_sql_create_table`,
|
||||||
|
`help_id: Some("ddl.sql_create_table")`,
|
||||||
|
`usage_ids: &["parse.usage.sql_create_table"]`.
|
||||||
|
- Table-name slot: `IdentSource::NewName`, role `"table_name"`,
|
||||||
|
`writes_table: false` (the table doesn't exist yet), plus the
|
||||||
|
internal-`__rdbms_*` / `validate_user_name` rejection used elsewhere.
|
||||||
|
- After `table` keyword: an optional `IF NOT EXISTS` keyword run that
|
||||||
|
sets a flag in the builder.
|
||||||
|
- Column list: a `Repeated` of column-or-table elements inside `( … )`.
|
||||||
|
- Column element: name slot (`IdentSource::NewName`) + type slot
|
||||||
|
(`IdentSource::Types`) + a constraint walk. The type slot consumes
|
||||||
|
an optional parenthesised number/number-pair and discards it
|
||||||
|
(length/precision ignored).
|
||||||
|
- Table element (4a): `PRIMARY KEY ( col, … )`. **No** `FOREIGN KEY`,
|
||||||
|
no inline `REFERENCES` in 4a (those parse-error for now; 4b adds
|
||||||
|
them).
|
||||||
|
- `CHECK (<expr>)` / `DEFAULT <expr>` reuse the ADR-0031 fragment via
|
||||||
|
`Node::Subgrammar(&sql_expr::SQL_OR_EXPR)` (the same mechanism
|
||||||
|
`sql_insert.rs` uses for value expressions). The matched expression
|
||||||
|
is compiled to storable SQL via the existing `compile_check_sql`
|
||||||
|
path used by `do_create_table`.
|
||||||
|
|
||||||
|
Register in `REGISTRY` (`src/dsl/grammar/mod.rs`, Advanced group):
|
||||||
|
`(&data::SQL_CREATE_TABLE, CommandCategory::Advanced)` alongside the
|
||||||
|
existing `(&ddl::CREATE, CommandCategory::Simple)`. The category-grouped
|
||||||
|
dispatcher then tries SQL-first in advanced mode and falls back to the
|
||||||
|
simple `create table … with pk …` node when the SQL shape doesn't match
|
||||||
|
— identical to the `insert` precedent. (Confirm the exact re-export
|
||||||
|
path: simple `CREATE` is `ddl::CREATE`; place the new node so it's
|
||||||
|
reachable as the registry expects, mirroring how `SQL_INSERT` is wired.)
|
||||||
|
|
||||||
|
### 4.2 Command AST — `src/dsl/command.rs`
|
||||||
|
|
||||||
|
New variant (4a shape; 4b will extend it with FK/relationship specs):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
SqlCreateTable {
|
||||||
|
name: String,
|
||||||
|
columns: Vec<ColumnSpec>, // reuse the existing ColumnSpec
|
||||||
|
primary_key: Vec<String>, // single or compound, table- or column-level
|
||||||
|
if_not_exists: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ColumnSpec` (`name`, `ty: Type`, `not_null`, `unique`, `default:
|
||||||
|
Option<Value>`, `check: Option<Expr>`) is reused unchanged — the SQL and
|
||||||
|
simple paths build the *same* spec. Column-level `PRIMARY KEY` and
|
||||||
|
table-level `PRIMARY KEY (…)` both normalise into `primary_key`.
|
||||||
|
|
||||||
|
### 4.3 Worker — `src/db.rs`
|
||||||
|
|
||||||
|
- New `Request::SqlCreateTable { name, columns, primary_key,
|
||||||
|
if_not_exists, source: Option<String>, reply }`.
|
||||||
|
- Dispatch arm wraps in `snapshot_then(snap, batch, conn,
|
||||||
|
source.as_deref(), reply, || …)` exactly like the `CreateTable` arm.
|
||||||
|
- The closure:
|
||||||
|
- If `if_not_exists` **and** the table already exists → **no-op**:
|
||||||
|
return a success outcome that the runtime renders as a note rather
|
||||||
|
than a structure refresh. (See 4.4.)
|
||||||
|
- Else → call the existing `do_create_table(conn, persistence,
|
||||||
|
source, &name, &columns, &primary_key)`; structural execution,
|
||||||
|
metadata, CSV, `STRICT` all come from the shared helper.
|
||||||
|
- `handle_request`'s exhaustive match forces handling the new variant —
|
||||||
|
it **must** be wrapped, never `reply.send`-ed raw (the handoff
|
||||||
|
must-not-forget).
|
||||||
|
- **No** `is_app_lifecycle_entry_word` change: `create` is a write.
|
||||||
|
|
||||||
|
### 4.4 The `IF NOT EXISTS` no-op-with-note outcome
|
||||||
|
|
||||||
|
`do_create_table` returns `TableDescription`. For the skip path we need
|
||||||
|
the runtime to show a note instead of (or alongside) a structure. Two
|
||||||
|
candidate mechanisms — **pick one, escalate if non-obvious**:
|
||||||
|
|
||||||
|
- **(a)** Worker reply is an enum `CreateOutcome { Created(TableDescription),
|
||||||
|
Skipped(TableDescription) }`; the runtime emits the engine-neutral note
|
||||||
|
on `Skipped` and the structure on `Created`.
|
||||||
|
- **(b)** Reply stays `TableDescription` and carries an optional
|
||||||
|
`note: Option<NoteKey>` the runtime renders.
|
||||||
|
|
||||||
|
**Decided: mechanism (a)** (§6.2) — explicit and reused by
|
||||||
|
`DROP TABLE IF EXISTS` (4c). This touches the worker reply type and the
|
||||||
|
runtime/event rendering; the one piece of 4a that adds plumbing beyond
|
||||||
|
"mirror `SqlInsert`".
|
||||||
|
|
||||||
|
### 4.5 Type-alias map — `src/dsl/types.rs`
|
||||||
|
|
||||||
|
No alias map exists today (`Type: FromStr` covers only the ten canonical
|
||||||
|
names). Add a single resolver, e.g.
|
||||||
|
`fn resolve_type_name(raw: &str) -> Option<Type>` that lowercases, maps
|
||||||
|
the §2.5 aliases onto the canonical `Type`, and falls through to the
|
||||||
|
canonical `FromStr`. The grammar's type slot calls this; an
|
||||||
|
unrecognised name yields the engine-neutral `diagnostic`/error
|
||||||
|
"unknown type". Length/precision is stripped by the grammar before the
|
||||||
|
name reaches the resolver.
|
||||||
|
|
||||||
|
### 4.6 Friendly catalog + keys — lockstep
|
||||||
|
|
||||||
|
For every new key, add a body to `src/friendly/strings/en-US.yaml`
|
||||||
|
**and** an entry to `KEYS_AND_PLACEHOLDERS` in `src/friendly/keys.rs`
|
||||||
|
(the `keys_validate_against_catalog` test enforces both). 4a keys:
|
||||||
|
|
||||||
|
- `ddl.sql_create_table` — the per-command `help_id` body.
|
||||||
|
- `parse.usage.sql_create_table` — the usage skeleton.
|
||||||
|
- `diagnostic.*` DDL peers of the ADR-0033 DML diagnostics: unknown
|
||||||
|
type, duplicate column name (and any 4a-relevant pre-submit checks).
|
||||||
|
- The `IF NOT EXISTS` skipped **note** key.
|
||||||
|
|
||||||
|
All wording engine-neutral (no SQLite/STRICT/PRAGMA) — the vocab audit
|
||||||
|
enforces it.
|
||||||
|
|
||||||
|
## 5. Out of 4a scope (no pre-emptive cuts — just later sub-phases)
|
||||||
|
|
||||||
|
- Foreign keys: inline `REFERENCES` + table-level `FOREIGN KEY` → 4b.
|
||||||
|
- `DROP TABLE [IF EXISTS]` → 4c. (`IF EXISTS` no-op-with-note reuses the
|
||||||
|
4.4 mechanism.)
|
||||||
|
- Indexes, `ALTER TABLE`, `ALTER COLUMN TYPE`, table rename → 4d–4h.
|
||||||
|
- **Composite `UNIQUE(a,b)` / multi-column table `CHECK` → 4a.2** (a
|
||||||
|
persistence-model extension; see §6). 4a accepts **single-column**
|
||||||
|
table-level `UNIQUE(a)` / `CHECK(expr-over-one-col)` by normalizing
|
||||||
|
them into the column spec (free — `ColumnSchema` already carries
|
||||||
|
per-column `unique`/`check`), and rejects the composite/multi-column
|
||||||
|
forms with a "not yet supported" message until 4a.2 lands.
|
||||||
|
- `CREATE TABLE … AS SELECT` (CTAS): **not in ADR-0035's surface** at
|
||||||
|
all — an ordinary parse error, not a deferral.
|
||||||
|
|
||||||
|
## 6. Decisions settled this round (user-confirmed) + the 4a/4a.2 split
|
||||||
|
|
||||||
|
1. **4a/4a.2 split (user-confirmed).** Composite `UNIQUE(a,b)` and
|
||||||
|
multi-column table `CHECK` are **deferred to a dedicated slice
|
||||||
|
4a.2**. Rationale (the general rule): a DDL feature needs
|
||||||
|
data-model work exactly when it introduces a **structure simple
|
||||||
|
mode could never produce** — not merely new syntax. Almost all of
|
||||||
|
advanced DDL (per-column constraints, compound PK, FK, indexes,
|
||||||
|
drop, alter-column) maps onto structures the model already persists
|
||||||
|
(`ColumnSchema`, `TableSchema.primary_key`, `RelationshipSchema`,
|
||||||
|
`IndexSchema`), so it is syntax-only + reuse. Composite UNIQUE /
|
||||||
|
table CHECK are the **first structures with no slot in
|
||||||
|
`TableSchema`**, so they need a model + round-trip extension and
|
||||||
|
earn their own slice. **Same class, already on the radar:**
|
||||||
|
`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):
|
||||||
|
- Extend `TableSchema` (`src/persistence/mod.rs`) with table-level
|
||||||
|
constraint slots — e.g. `unique_constraints: Vec<Vec<String>>` and
|
||||||
|
`check_constraints: Vec<String>`.
|
||||||
|
- Extend the YAML writer/parser + `RawTable`
|
||||||
|
(`src/persistence/yaml.rs`) — additive, backward-compatible,
|
||||||
|
optional-on-read (the pattern used when `unique`/`not_null`/
|
||||||
|
`check` were added; **no migration needed**).
|
||||||
|
- Extend `read_schema_snapshot` (`src/db.rs` ~2225) to **detect**
|
||||||
|
composite UNIQUE / table CHECK from the database.
|
||||||
|
- Extend `do_create_table` to **emit** them in the DDL.
|
||||||
|
- Extend the `SqlCreateTable` command shape + grammar to carry/parse
|
||||||
|
them (lifting the 4a "not yet supported" rejection).
|
||||||
|
- **Round-trip tests** (save → load → rebuild) proving they survive,
|
||||||
|
on top of parse/execute/undo coverage.
|
||||||
|
|
||||||
|
2. **No-op-with-note plumbing — mechanism (a)** (the `CreateOutcome`
|
||||||
|
enum, §4.4). Explicit and reused by `DROP TABLE IF EXISTS` (4c);
|
||||||
|
chosen now so the worker reply type is designed once.
|
||||||
|
|
||||||
|
## 7. Devil's Advocate review of this plan
|
||||||
|
|
||||||
|
- **Does it reuse rather than fork execution?** Yes — `do_create_table`
|
||||||
|
is the single executor; `SqlCreateTable` only differs at the
|
||||||
|
parse/AST layer and the `if_not_exists` branch. No drift risk. ✓
|
||||||
|
- **Is the dispatch genuinely the proven precedent?** Yes — same
|
||||||
|
category-grouped SQL-first/simple-fallback as `insert`. The plan
|
||||||
|
verifies the simple-mode form still parses in advanced mode (a real
|
||||||
|
fallback test, not just the SQL happy path). ✓
|
||||||
|
- **Undo actually one step?** The plan wires `snapshot_then` exactly as
|
||||||
|
the existing arms and adds a dedicated undo test (create = one step,
|
||||||
|
undo drops + clears metadata, redo recreates). ✓
|
||||||
|
- **Silent scope drop?** The table-level UNIQUE/CHECK gap was the one
|
||||||
|
place 4a could quietly under-deliver against §4's surface — it was
|
||||||
|
escalated and **resolved** as the 4a.2 split (§6), with composite
|
||||||
|
forms rejected by an explicit "not yet supported" message (not a
|
||||||
|
confusing parse error), not silently dropped. ✓
|
||||||
|
- **Engine neutrality?** All new strings are catalog keys subject to the
|
||||||
|
vocab audit; the plan names no engine in user-facing text. ✓
|
||||||
|
- **Tests first?** §8 orders failing tests before code at every step. ✓
|
||||||
|
|
||||||
|
## 8. Implementation sequence (test-first throughout)
|
||||||
|
|
||||||
|
1. **Type-alias resolver** — write Tier-1 tests for the §2.5 map
|
||||||
|
(canonical + aliases + length-ignored + unknown→None) → red → add
|
||||||
|
`resolve_type_name` → green.
|
||||||
|
2. **Command + parser** — write Tier-1 `ast_builder` tests (valid
|
||||||
|
columns/types/constraints, compound PK, single-column table-level
|
||||||
|
`UNIQUE`/`CHECK` normalized, `INTEGER PRIMARY KEY`→int,
|
||||||
|
`IF NOT EXISTS` flag; FK-shape and composite `UNIQUE(a,b)` /
|
||||||
|
multi-column `CHECK` rejected "not yet supported") → red → add
|
||||||
|
`Command::SqlCreateTable`, `sql_create_table.rs`, REGISTRY entry →
|
||||||
|
green. Include a fallback test: simple `create … with pk` still
|
||||||
|
parses in advanced mode.
|
||||||
|
3. **Worker** — write Tier-3 `tests/sql_create_table.rs` (metadata, CSV,
|
||||||
|
describe; alias end-to-end; `IF NOT EXISTS` no-op-with-note;
|
||||||
|
duplicate-table plain error) → red → add `Request::SqlCreateTable` +
|
||||||
|
`snapshot_then` arm + the §4.4 outcome + reuse `do_create_table` →
|
||||||
|
green.
|
||||||
|
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
|
||||||
|
no extra code; if not, the wrap is wrong) → green.
|
||||||
|
5. **Friendly catalog/keys** — add the 4a keys + bodies; run
|
||||||
|
`keys_validate_against_catalog` and the vocab audit → green.
|
||||||
|
6. **Full sweep** — `cargo test` (expect 1698 + new, 0 fail, 0 skip) and
|
||||||
|
`cargo clippy --all-targets -- -D warnings` clean. Compare against the
|
||||||
|
§1 baseline; no regressions.
|
||||||
|
7. **Docs** — flip ADR-0035 to **Accepted** (README in lockstep);
|
||||||
|
update `requirements.md` `Q4`. Propose the commit message; wait for
|
||||||
|
approval.
|
||||||
|
|
||||||
|
## 9. Engine-neutral string notes (ADR-0002)
|
||||||
|
|
||||||
|
Every user-facing string added in 4a — help body, usage skeleton,
|
||||||
|
diagnostics, the `IF NOT EXISTS` skipped note, the unknown-type and
|
||||||
|
duplicate-column messages — refers to "the table" / "the database" /
|
||||||
|
"the type", never SQLite / STRICT / PRAGMA / rusqlite. The vocab audit
|
||||||
|
test is the gate.
|
||||||
|
|
||||||
|
## 10. Exit gate for 4a (mirrors ADR-0035 §13 / ADR-0033)
|
||||||
|
|
||||||
|
- All §3 checklist items satisfied or explicitly escalated (§6).
|
||||||
|
- All four tiers green, zero skips; baseline not regressed.
|
||||||
|
- A written DA pass on the delivered slice (not just this plan).
|
||||||
|
- ADR-0035 flipped Proposed → Accepted; `requirements.md` updated.
|
||||||
|
|
||||||
|
## 11. Next action after approval
|
||||||
|
|
||||||
|
All design questions are settled (§2, §6). Start at §8 step 1: the
|
||||||
|
type-alias resolver, test-first. 4a.2 (composite UNIQUE / table CHECK,
|
||||||
|
§6.1) follows 4a as its own test-first slice.
|
||||||
Reference in New Issue
Block a user