The first exemplar (`add 1:n relationship`) showed per-node keying is
too coarse for multi-form commands, so revise the mechanism to per-form.
- CommandNode `hint_id: Option<&str>` -> `hint_ids: &[&str]` (mirrors
usage_ids); hint_key_for_input_in_mode reuses a factored-out
pick_form_key (shared digit/m:n/suffix form disambiguation with
usage_key_for_input_in_mode)
- wire INSERT + ADD (all four forms) with hint_ids
- author the three approved exemplars: hint.cmd.insert,
hint.cmd.add_relationship, hint.err.foreign_key.child_side
(what/example/concept) + keys.rs registration
- revise ADR-0053 D3 to per-form; record clause-concept hints as a
deferred extension (issue #37); update README + plan
- +5 tests; 2488 pass / 1 ignored, clippy clean
Phased build plan: mechanism skeleton with tier-2 fallback first
(hint_id field, AppCommand::Hint, F1 read-only overlay, last_error_hint_key,
note_hint* renderer), then catalogue + the three approved exemplars,
then comprehensive content in batches, then polish. Reuses the existing
command_for_entry_word / usage_keys_for_input_in_mode lookups for
command identification. Test spine includes the comprehensiveness
coverage test that gates "comprehensive for v1".
Record the submission mode per history entry so advanced commands are
reusable in simple mode, and fix the bug where a ':'-one-shot command
lost its ':' across sessions (ADR-0052, closing #30).
Format: the history.log status token gains an optional ':adv' suffix
(ok / ok:adv / err / err:adv); 'source' stays last and canonical, so
replay is unaffected. The in-memory ring (still Vec<String>) stores
advanced entries ': '-prefixed; recall strips the ':' in advanced mode
and keeps it in simple; hydration reconstructs the prefix from the tag.
Journaling moved from the worker to the dispatch layer (spawn_dsl_-
dispatch / run_replay / app-command sites), where the mode is in scope
with no worker plumbing; finalize_persistence writes only yaml/csv
(commit-db-last still atomic for state). The journal write is now
best-effort (command already committed), consistent with the failure
path. App commands journal simple, so they recall bare. Journaling is
now uniform (every successful command, per ADR-0034) — closing a gap
where show tables/relationships/explain didn't journal.
Amends ADR-0034 (status tag + journaling location), ADR-0015 §6
(history.log out of the worker tx), ADR-0040 (journal-write best-effort).
15 worker-level journaling tests retired, re-covered at the new layer
(history.rs format, app.rs recall matrix, iteration6 cross-session
regression, replay). 2471 pass / 0 fail / 0 skip, clippy clean.
Each top-level tests/*.rs was its own crate → its own binary, each
statically linking the bundled engine + every dep. 26 of them, so an
edit to the lib relinked all 26. Moved the 25 standalone files into
tests/it/ under one tests/it/main.rs (the pattern typing_surface
already uses); cargo auto-detects it as the `it` target. End state: 2
integration-test binaries instead of 26.
Result: target/debug/deps 1.5 GB → 629 MB (-58%). Build time barely
moved (clean 22.9s→22.4s, lib-edit relink 13.3s→12.4s) — wall-clock is
dominated by compiling, not linking, so this is a disk win, not a speed
win (see docs/plans/20260602-test-consolidation.md). Tests unchanged at
2151/0/1; clippy clean; no fixups needed. typing_surface_matrix stays
its own already-consolidated binary.
Tradeoff: the 25 files now share one crate (a compile error fails the
whole `it` binary; module-scoped namespaces, no clashes) — negligible
for a solo project.
- F2-broad: replay failures now render with real schema context instead of
a contextless friendly_message(). Extract App::build_translate_context into
the shared App::translate_context_for(command, facts, verbosity); run_replay
enriches via enrich_dsl_failure + that builder. ctx_* fallbacks degrade to
neutral prose so the rare non-replay contextless callsites can't leak raw
{name} either. (SQL INSERT/UPDATE values aren't retained — ADR-0033 verbatim
— so those show real table/column + neutral "that value".)
- Gap C: SQL ALTER … ADD FOREIGN KEY on a missing child column refuses with an
SQL-appropriate "add it first", not the DSL-only --create-fk flag.
- Gap B: dropping a single-column-UNIQUE column refuses with a pointer to
`drop constraint unique from T.col` (was an opaque generic refusal).
- Gap D: 4e drop/rename CHECK-guard + 4f change-type FK-guard refusals reworded
to explain why; static_refusal reasons left as-is.
Tests: +4, 3 strengthened. 1926 pass / 0 fail / 0 skip; clippy clean.
F1/F2/F3 from the whole-Phase-4 /runda (handoff-42 §3):
- F3: drop an anonymous composite UNIQUE via a derived, engine-neutral
name `unique_<cols>` — recomputed live, nothing persisted, reusing the
existing `DROP CONSTRAINT <name>` grammar (no new syntax/metadata, the
§4g anonymity decision intact). A name matching more than one UNIQUE is
refused as ambiguous, never guessed. One undo step. `describe`
annotates each composite UNIQUE with its name.
- F1: dropping a column a composite UNIQUE covers is refused up-front
with the derived name + the actionable drop command (was an unhelpful
generic engine refusal).
- F2: contextless friendly_message() no longer leaks a literal `{table}`
in the generic hint (new `error.generic.hint_no_table`, selected when
no table is in context). The table-ful path is unchanged.
Docs: ADR-0035 Amendment 1 + Status + README index + plan
docs/plans/20260526-adr-0035-composite-unique-drop-f1f2f3.md.
Tests: +5 (drop-by-name, ambiguous-refused, one-undo-step, F1 guard,
F2 no-leak) + a describe-render assertion. 1922 pass / 0 fail / 0 skip;
clippy clean.
In advanced mode an entry word like `create`/`drop` has several candidate
nodes (the SQL forms + the DSL fallback), but the walker commits to one,
so completion offered only that node's continuations — `drop ` showed
just `table`, and `drop rel` dead-ended at an empty list even though the
DSL drops parse via fallback.
At the entry-word boundary (advanced mode), walk every candidate, keep the
viable (Incomplete) ones, and union their next-keyword continuations:
`drop ` → table·index·column·relationship·constraint; `drop rel` →
relationship; `create ` → table·unique·index. Deeper positions keep the
committed walk untouched (no change to insert/update/delete/select).
Each continuation is classified by producing category (Both/Advanced/
Simple) and block-ordered Both → Advanced → Simple, so they read as
contiguous groups (the foundation for the 4i(e) colour, landing next).
CompletionProbe carries a parallel expected_modes; the parse path is
unchanged (the merge is completion-only).
Tests: completion merge + partial + block-order cases; the two tests that
encoded the old single-node behaviour updated. Full suite 1911 passing /
0 failing / 1 ignored; clippy clean.
The one genuinely new low-level op in Phase 4: a native engine RENAME TO
plus one-transaction reconciliation (commit-db-last) of everything the
engine does not track —
- every metadata row naming the table: __rdbms_playground_columns, both
ends of __rdbms_playground_relationships (FK parent, child, and
self-referential), and __rdbms_playground_table_checks;
- the CSV file, via the existing persistence rewrite+delete path
(rewritten_tables=[new], deleted_tables=[old]) — no new method;
- CHECK text that qualifies a column with the old table name
(T.age → U.age, column- and table-level): the engine rewrites the live
CHECK but the stored text would drift and break a fresh rebuild (a
planning-/runda finding); rewrite_check_table_qualifier keeps them in
step. Bounded — a CHECK references only its own table.
Grammar: a fifth AlterTableAction (RenameTable { new }), added by
splitting the `rename` verb into one branch with an inner Choice on a
distinct second keyword (column vs to); the new-name slot mirrors the
CREATE TABLE name slot (NewName + reject_internal_table validator).
Refusals are engine-neutral and case-insensitive (the engine matches
names that way): same-name, case-only, existing-target, __rdbms_*, and
non-existent source. Auto-named indexes and relationships keep their
stale names (only table-name columns update — §6 scope). One undo step;
advanced-mode only; closes the rename half of C1.
Tests: 8 Tier-3 e2e + rewrite-helper unit tests + parse-dispatch tests.
Full suite 1903 passing / 0 failing / 1 ignored; clippy clean.
ALTER TABLE <T> ADD [CONSTRAINT <name>] (CHECK | UNIQUE | FOREIGN KEY)
and DROP CONSTRAINT <name>. ADD = table-CHECK + composite UNIQUE + FK
(ADD PRIMARY KEY and a named UNIQUE refused — composite UNIQUE is
anonymous in our model). Each ADD reuses a low-level path with a dry-run
guard (table-CHECK/UNIQUE rebuild; FK -> add_relationship, bare
REFERENCES -> parent single PK). DROP CONSTRAINT resolves the name to a
named table-CHECK then a child-side FK, else refuses. One undo step each.
Named table-CHECKs round-trip: a nullable `name` column on
__rdbms_playground_table_checks (rebuild-only arrival; a named add on a
pre-4g project is refused with a "rebuild first" hint) plus a project.yaml
check_constraints {expr, name} extension (bare-string form still reads).
The internal-__rdbms_* guard was folded into do_add_constraint /
do_add_relationship, completing that guard class.
Grammar: the action Choice keeps one branch per verb (add/drop/rename/
alter) with an inner Choice fanning out on the distinct second keyword,
since the walker's Choice does not backtrack between same-led branches.
Tests: 7 Tier-1 parse + 2 yaml round-trip + 1 internal-guard + 9 Tier-3
e2e. Help/usage refreshed; ADR-0035 §13 4g + README + requirements.md in
lockstep.
- docs/plans/20260525-adr-0035-sql-ddl-4f.md — ALTER TABLE ALTER COLUMN
TYPE plan (/runda'd; forks resolved). Advanced lossy = ForceConversion
(reuse do_change_column_type; existing transformed_lossy note); the
internal-__rdbms_* guard folds into do_change_column_type both surfaces
(user-confirmed); int->serial is ALLOWED (ADR-0018 §8), only non-int
->serial / blob / date<->datetime are static-refused.
- docs/handoff/20260525-handoff-39.md — 4d/4e shipped; 4f is next
(plan ready).
Advanced-only `alter` entry word; ALTER TABLE <T> ADD COLUMN <col> <type>
[constraints] | DROP COLUMN <col> | RENAME COLUMN <old> TO <new> ->
SqlAlterTable, runtime-decomposed to the existing column executors
(do_add_column / do_drop_column / do_rename_column) — one undo step each,
no new worker layer. The COLUMN keyword is required (reserves bare
RENAME TO for 4h, ADD CONSTRAINT for 4g).
- ADD COLUMN takes NOT NULL / UNIQUE / DEFAULT / CHECK (no PK / inline
REFERENCES). do_add_column extended to consume the SQL raw-text
default_sql / check_sql (sql_expr is validate-only, the 4a.2
mechanism), reaching parity with CREATE TABLE's column constraints.
- Drop/rename column refuse a column any CHECK references — table-level
AND column-level (incl. a column's own self-check on rename) — the
4a.3 deferral, detected up-front by tokenizing the raw CHECK text
(skipping string literals). In the shared executors, so it guards both
the simple and SQL surfaces and fixes a latent rename-drift bug that
desynced the stored CHECK text and broke rebuild.
- SQL DROP COLUMN refuses an index-covered column (no --cascade SQL
spelling — matches SQLite + the simple default).
- The column executors and do_add_index gained an internal-__rdbms_*
guard (refuse as "no such table"), closing a pre-existing exposure on
both surfaces. (do_change_column_type / do_add_constraint /
do_add_relationship are a tracked follow-up.)
- `alter` is advanced-only; AlterTableAction::AddColumn is boxed
(clippy::large_enum_variant).
Docs: ADR-0035 status + §13 4e; ADR README; requirements.md Q1. Plan:
docs/plans/20260525-adr-0035-sql-ddl-4e.md.
Tests: 1854 passing / 0 failing / 0 skipped / 1 ignored; clippy clean.
Advanced-mode SQL CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>] ON
<T> (cols) -> SqlCreateIndex and DROP INDEX [IF EXISTS] <name> ->
SqlDropIndex, both reusing the ADR-0025 executors (do_add_index /
do_drop_index), like 4c reused do_drop_table.
- CREATE UNIQUE INDEX admitted in advanced mode (ADR-0025 Amendment 1):
ADR-0025 deferred UNIQUE indexes for the simple-mode DSL, but advanced
mode trusts the user like SQL does. Adds an additive IndexSchema.unique
flag (project.yaml, serde-default, version stays 1); rebuild re-emits
CREATE UNIQUE INDEX; the redundant-set guard keys on (columns, unique).
Simple-mode `add unique index` stays deferred.
- IF [NOT] EXISTS on both forms reuses the 4c no-op-with-note skip
(journalled, not snapshotted) via CreateIndexOutcome / DropIndexOutcome.
- Unnamed CREATE INDEX auto-named (ADR-0025 convention); the [UNIQUE]
prefix is a concrete-keyword Choice and the optional name an on-led-first
selector (the drop-index selector precedent) — trap-safe.
- create/drop each gain a second advanced node; the existing all-candidates
dispatch handles it (locked by parse tests).
- Unique indexes marked [unique] in the structure view and items panel.
- do_add_index refuses internal __rdbms_* tables as "no such table",
closing a latent exposure on both the simple `add index` and the new
SQL CREATE INDEX surfaces (ADR-0025 Amendment 1).
Docs: ADR-0035 status + §13 4d + 4i; ADR-0025 Amendment 1; ADR README;
requirements.md Q1/C3. Plan: docs/plans/20260525-adr-0035-sql-ddl-4d.md.
Tests: 1834 passing / 0 failing / 0 skipped / 1 ignored; clippy clean.
Add advanced-mode SQL `DROP TABLE [IF EXISTS] <name>` -> SqlDropTable,
executing through the existing do_drop_table (cascade / inbound-
relationship refusal / metadata cleanup) — full parity with the simple
`drop table`. The only new behaviour is `IF EXISTS` as a
no-op-with-note: a new DropOutcome::Skipped mirroring
CreateOutcome::Skipped (journalled, no snapshot), rendered via a new
ddl.drop_skipped_absent note + DslDropSkipped event.
- Grammar: SQL_DROP_TABLE node (entry `drop`, shape `table [if exists]
<name> [;]`), registered Advanced. SQL-first dispatch: `drop table T`
-> SqlDropTable in advanced; `drop column`/`relationship`/`index`/
`constraint` fall back to the simple `drop` node (and still execute).
- Worker: Request::SqlDropTable + db.sql_drop_table; the if-exists-and-
absent arm journals + replies Skipped without a snapshot, else
snapshot_then(do_drop_table) -> Dropped.
- Completion: advanced `drop ` now surfaces the SQL `table` (the
shared-entry-word behaviour from `create`); test split into simple
(full DSL list) + advanced (SQL surface).
Known shared-entry-word completion unevenness (advanced `drop ` offers
only `table`; partial `drop rel` returns an empty list) deferred to 4i
(merge candidate sets for shared entry words) along with a flagged user
request to visually distinguish simple- vs advanced-mode completions in
the hint UI — tracked in ADR §13 4i (d)/(e), the 4c plan, and the
completion test. The DSL drops still parse + execute via fallback.
10 new tests (parse/builder + Tier-3: drop existing + one-undo-step +
restore, IF EXISTS skip + journal, plain-absent error, inbound refusal).
Docs: ADR-0035 Status/§13, README, requirements.md Q1.
Tests: 1805 passing, 0 failing, 1 ignored. Clippy clean.
Add foreign keys to advanced-mode SQL CREATE TABLE — the SQL spelling of
an ADR-0013 named relationship, created in the same transaction as the
table (one undo step).
- Grammar: inline `<col> … REFERENCES <parent>[(<col>)] [ON DELETE/UPDATE
…]` (a new column constraint) and table-level `[CONSTRAINT <name>]
FOREIGN KEY (<col>) REFERENCES …` (two new element branches — both
start on a concrete keyword, never a leading Optional, which would
abort the element Choice). Referential clauses reuse
shared::REFERENTIAL_CLAUSES.
- Builder: greedy FK-clause consumption (parens consumed internally so
they don't perturb the 4a.3 element-boundary depth tracker); inline FK
auto-named, table FK takes an optional CONSTRAINT name.
- Worker: do_create_table resolves + validates each FK before building
the DDL (self-ref validates against the in-statement columns/PK; bare
REFERENCES resolves to the parent's single-column PK, composite ->
error; PK-target + Type::fk_target_type compatibility), emits the
FOREIGN KEY clause identically to schema_to_ddl, and writes the
relationship metadata in the create transaction.
- Reuse: name/uniqueness/metadata-insert/type-compat factored into shared
helpers; do_add_relationship refactored to use them.
- FKs round-trip via the existing relationship plumbing (no new
persistence structures); describe surfaces the relationship.
Self-references and bare `REFERENCES <parent>` supported (user-confirmed).
Self-ref pre-submit indicator wrinkle deferred to 4i (tracked in ADR §13,
a code comment, and the plan).
DA/runda round added cross-cutting probes (FK survives the add-column
rebuild + a later rebuild_from_text; referential actions survive rebuild;
drop-child clears the relationship; drop-parent refused; bare self-ref
resolves to own PK) — all green, no fixes needed.
27 new tests (grammar/builder + Tier-3). Docs: ADR-0035 Status/§13,
README, requirements.md Q1.
Tests: 1795 passing, 0 failing, 1 ignored. Clippy clean.
Add table-level CHECK (e.g. `CREATE TABLE t (a int, b int, CHECK (a < b))`)
to advanced-mode SQL CREATE TABLE. Since SQLite exposes no PRAGMA for CHECK
constraints, a table-level CHECK cannot be read back from the engine and
becomes the source of truth in a new internal metadata table
`__rdbms_playground_table_checks (table_name, seq, check_expr)`.
- Grammar: new TABLE_CHECK element in ELEMENT_CHOICES.
- Builder: distinguishes a table-level CHECK from a column-level one by
element position (no column-def open in the element), using depth-aware
boundary tracking so a length-arg comma (`numeric(10,2)`) or a
table-PRIMARY KEY's inner comma is not mistaken for an element separator.
- Worker: do_create_table emits the CHECK clauses and writes the metadata
rows in its transaction; schema_to_ddl emits them identically on rebuild;
read_schema / read_schema_snapshot read them from the metadata table;
do_drop_table clears them.
- Persistence: TableSchema.check_constraints round-trips through project.yaml
(#[serde(default)], optional on read), mirroring unique_constraints.
- Composite UNIQUE deliberately stays PRAGMA-detected (engine-reportable,
unlike CHECK) — user-confirmed.
DA/runda round added cross-cutting tests and a forward-looking doc fix:
- table CHECK survives a rebuild triggered by `add column`, and a later
rebuild_from_text (the ADR-0013 rebuild primitive uses a raw DROP, so the
metadata rows keyed on the final name are preserved);
- dropping a column a table CHECK references fails cleanly (rollback, table
intact); detection is 4e, friendly wording is H1;
- dropping a table clears its CHECK metadata (no orphan rows on re-create);
- amended ADR §6 so 4h's RENAME also updates the new metadata table.
20 Tier-3 + 9 grammar/builder + 2 YAML tests. Docs: ADR-0035 Status/§13/§6,
README index, requirements.md Q1. Help/usage skeleton + describe display of
table-level constraints deferred to 4i (symmetric with 4a.2).
Tests: 1769 passing, 0 failing, 1 ignored. Clippy clean.
Advanced-mode SQL CREATE TABLE gains the constraints that need no new
internal table (the 4a.2 slice):
- Grammar (sql_create_table.rs): column-level DEFAULT/CHECK and
table-level UNIQUE(cols). DEFAULT is a literal or a *parenthesised*
expression (standard SQL) — a bare sql_expr greedily eats a following
NOT (NOT IN/LIKE/BETWEEN), breaking `DEFAULT 0 NOT NULL`; the parens
bound it. CHECK is paren-bounded already.
- Builder (ddl.rs): captures CHECK/DEFAULT raw SQL text by byte span
(sql_expr builds no AST) via capture_parenthesised_span /
capture_expr_span; routes single-column table UNIQUE into the
column's flag and composite UNIQUE into unique_constraints.
- Command/worker: ColumnSpec gains check_sql/default_sql (raw, preferred
over the typed Expr/Value); Command::SqlCreateTable + Request +
do_create_table gain unique_constraints; do_create_table emits raw
CHECK/DEFAULT and composite UNIQUE clauses.
- Round-trip (part D): ReadSchema/TableSchema gain unique_constraints;
read_schema detects composite UNIQUE via PRAGMA index_list origin 'u'
(single-column still folds to the column flag); schema_to_ddl emits
them; YAML RawTable/write_table round-trips (optional-on-read).
CHECK round-trips via __rdbms_playground_columns.check_expr, DEFAULT
via PRAGMA table_info — no new metadata table.
Table-level/multi-column CHECK remains 4a.3 (rejected "not yet
supported"); FK is 4b.
Tests: +7 builder (raw-text capture incl. the DEFAULT 0 NOT NULL
boundary the fix was found by; single/composite UNIQUE routing) and +4
Tier-3 (CHECK enforced, DEFAULT applied, composite UNIQUE enforced, and
all three survive a rebuild — the part-D round-trip). 1752 pass / 0 fail
/ 1 ignored; clippy clean. Plan + requirements.md updated.
Survey of the constraint persistence machinery revealed that
table-level/multi-column CHECK needs a NEW __rdbms_* metadata table
(SQLite exposes no PRAGMA for CHECK), unlike per-column CHECK/DEFAULT
(reuse __rdbms_playground_columns.check_expr + PRAGMA dflt_value) and
composite UNIQUE (PRAGMA index_list origin 'u' + a TableSchema field).
User-confirmed split: 4a.2 = per-column CHECK/DEFAULT (raw sql_expr
text) + composite UNIQUE(a,b), no new internal table; 4a.3 = table-level
CHECK + the new metadata table. ADR §13 and README updated in lockstep;
4a.2 plan doc added.
Command + builder + worker for advanced-mode SQL CREATE TABLE
(sub-phase 4a), executed structurally through do_create_table:
- Command::SqlCreateTable + build_sql_create_table (ddl.rs): aliases via
from_sql_name (incl. double precision), column- and table-level
PRIMARY KEY, redundant-flag de-dup off a sole PK, IF NOT EXISTS.
Advanced REGISTRY entry on the shared `create` word (SQL-first, DSL
fallback); no-PK tables allowed (user-confirmed).
- Worker (db.rs): Request::SqlCreateTable + CreateOutcome + snapshot_then
(one undo step); IF NOT EXISTS no-op (no snapshot, but journalled, like
read-only commands). do_create_table inline-PK rule aligned with the
rebuild generator schema_to_ddl — no round-trip DDL drift; serial
autoincrement is independent of inline-PK (verified by round-trip
tests).
- Runtime/App: dispatch + CommandOutcome::SchemaSkipped +
AppEvent::DslCreateSkipped (structure + "already exists — skipped"
note). Friendly catalog keys added (engine-neutral).
DEFAULT/CHECK/table-level UNIQUE are absent from the 4a grammar (parse
error with usage skeleton; friendly message + support land in the 4a.2
constraint slice) — user-confirmed.
Tests: type resolver, grammar shape, builder (incl. the PK
detection bug they caught), and tests/sql_create_table.rs (worker
round-trip, serial autoincrement first/non-first across rebuild, IF NOT
EXISTS no-op + journalling, no-PK table, one undo step) + a replay-as-
write test. 1739 pass / 0 fail / 1 ignored; clippy clean.
Exit gate: ADR-0035 Proposed -> Accepted (validated end-to-end by 4a);
README + requirements.md Q1 updated.
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".
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.
Settles the undo/snapshot half (U1/U2) before implementation:
- every-mutation single-step undo (supersedes destructive-only model)
- hybrid whole-project snapshot (db backup API + yaml/csv copy),
reconciling ADR-0006 with ADR-0015's derived-db model
- persisted N=50 ring; redo discarded on new work
- batch ops (replay + future) record one undo step; import excluded
- --no-undo disable switch
Adds the implementation plan and updates README index, requirements
U1/U2, and CLAUDE.md in lockstep.
Replay (§3): run_replay parses <ts>|<status>|<source> journal records — runs ok, skips non-ok — while still accepting bare .commands scripts (prefix-detected so a | inside a bare command isn't misread). Fixes replay history.log, which died on line 1.
Journal failures (§1/§2): failed commands are recorded err via a new Action::JournalFailure, emitted by the pure-sync App for both parse failures and worker-execution failures (runtime appends best-effort, never fatal). Hydration reads all records so typo'd/rejected commands are recallable across sessions.
Amendment 1 — replay filters app-lifecycle commands: a working replay history.log exposed that the journal also records save as/load/new/export/import/rebuild/mode (which would panic the worker dispatch or abort replay). Replay now re-applies only schema/data writes and skips every app-lifecycle command + nested replay, classified by entry word so modal/incomplete forms (save as, bare mode) and quit skip uniformly rather than aborting. All skips continue (reversing the nested-replay refusal); import and nested replay warn. replay.error_nested removed; replay.skipped_import/_replay added; ReplayCompleted carries warnings. requirements.md U3/U4 updated; app-command runtime-failure journalling tracked as a follow-up.
1659 passing / 0 failing / 0 skipped / 1 ignored. Clippy clean.
Two-sub-task test-first plan mapping 1:1 to ADR-0034's named sub-tasks: (1) journal failures + per-consumer filtering (status-tagged append, best-effort err writes, hydration reads all), (2) replay parses the journal format (ok-only filter, dual-shape input). Opens with a headline failing test that reproduces the live replay history.log bug.
Build-order plan (sub-phases 3a–3k) with per-sub-phase scope, exit
gates, and written DA gates, modelled on
docs/plans/20260520-adr-0032-phase-2.md. Centrepiece is the cross-cut
verification matrix scaffold (~75 rows) grouped by ADR-0033 section
(statement shapes, dispatch, RETURNING, shortid, cascade, diagnostics,
UPSERT, inherited Phase-2 diagnostics, ambient assistance,
engine-neutrality, persistence, OOS rejections), to be filled in
during 3k.
Carries the handoff-29 §4 process pins into the relevant sections: DA
critiques listed before verdict, no silent out-of-scope classification,
and matrix attribution requiring SQL-input tests for SQL claims. Records
four open questions to escalate before code starts (shortid SELECT
row-source path, R1 mechanism fallback, cascade pre-count construction,
UPSERT catalog wording).
Cross-cut verification matrix for ADR-0032 Phase 2 is now fully
populated with concrete test references — every row green. Filling
the matrix surfaced three real gaps that this commit closes.
1. Advanced-mode syntax highlighting (ADR-0030 §8 matrix row).
The `ui.rs` Advanced branch routed through `plain_input_spans`,
bypassing the highlight walker entirely. In production SQL
keywords past the entry word rendered as plain identifiers.
Fix: mode-aware variants of `highlight_runs`,
`render_input_runs`, `lex_to_runs`, and `input_diagnostics`;
the Advanced render path now uses the highlighted form with
`Mode::Advanced`. `plain_input_spans` removed (unused).
2. Engine.* key wiring (ADR-0032 §11.4 / §13 matrix rows + handoff
§3.3 follow-up). The four Phase-2 engine.* catalog entries
were authored in 2d but never reached: `translate_generic`
discarded the engine message and returned a vague catalog
entry. Fix: pattern-match the engine message text for the four
Phase-2 categories (aggregate misuse, group-by required,
compound arity mismatch fallback, scalar-subquery cardinality)
inside `translate_generic`, routing each to its engine-neutral
catalog entry.
3. Matrix-coverage tests. Thirteen new tests covering the rows
that had no explicit coverage:
- 3 SQL keyword/operator/CASE highlight tests
- 4 engine.* engine-message tests
- 3 sql_expr column-completion tests (WHERE, HAVING)
- 3 predicate-warning slot tests (CASE, ORDER BY, projection)
- 1 all-10-playground-types recovery test (tests/sql_select.rs)
Plan document (docs/plans/20260520-adr-0032-phase-2.md) updated:
every (TBD) row in the cross-cut matrix replaced with a concrete
test file::function reference and a green status marker.
Test totals: 1428 → 1441 passing (+13 new). Clippy clean.
The Phase-1 SQL `SELECT` grammar nodes that used to live in
`src/dsl/grammar/data.rs` retire — 22 statics / consts and the
`reject_internal_table` validator copy are removed, ~150 lines
of grammar machinery gone. `data::SELECT.shape` now references
the post-`SELECT` portion of the ADR-0032 fragment via a thin
`Node::Subgrammar(&sql_select::SQL_SELECT_TAIL)`.
`SQL_SELECT_TAIL` is a new export from `sql_select.rs`,
parallel to `SQL_SELECT_STATEMENT`. It represents what a
top-level `SELECT` statement looks like AFTER the registry's
entry-word dispatch has already consumed the leading `SELECT`
keyword: the DISTINCT/ALL prefix, projection list, optional
FROM / WHERE / GROUP BY / HAVING, the compound set-op chain
(each subsequent leg's `SELECT` is part of `SET_OP_TAIL`),
outer ORDER BY / LIMIT, and a tolerated trailing `;`.
WITH-prefixed statements (`WITH x AS (…) SELECT * FROM x`)
are NOT in 2c's scope — they need a separate `data::WITH`
`CommandNode` so the entry-word dispatch routes correctly.
For now, top-level WITH continues to fall through to the
chumsky parser route (the same as in Phase 1). The
`SQL_SELECT_STATEMENT` static (which includes the optional
WITH prefix) stays available for use by that future
CommandNode or by any other consumer that needs the full
statement shape.
All seven Phase-1 SQL `SELECT` integration tests
(`tests/sql_select.rs`) pass without modification, satisfying
the 2c exit gate's "behaviour preserved" requirement. The
70 fragment unit tests and the 26 driver-level scope tests
also pass — the migration is a refactor, no new tests
required.
Behaviour change explicitly sanctioned by ADR-0032 §8:
Phase-1's `LIMIT_VALIDATOR` (positive-int-only, parse-time)
is superseded by the full `sql_expr` admission. `LIMIT max(10,
x)` and similar now parse; the engine constrains the value at
execution time per the ADR's "grammar admits, engine
rejects" posture.
Plan §2b status note: the 2026-05-20 deferral of §10.3 stage 2
(CTE output-column harvest derivation) is recorded in
`docs/plans/20260520-adr-0032-phase-2.md` per the
user-approved deferral.
Test totals: 1366 passing (unchanged), 0 failed, 1 ignored.
Clippy clean. data.rs loses ~150 lines of dead grammar; the
single source of truth for the SQL `SELECT` shape is now
`sql_select.rs`.
ADR-0030 §3 commissioned a focused ADR for the full SELECT
grammar (the "SELECT — full" phase). ADR-0032 records the
decisions; docs/plans/20260520-adr-0032-phase-2.md is the
implementation plan walking the work.
Phase 2's grammar surface:
- Five JOIN flavours (INNER, LEFT, RIGHT, FULL OUTER, CROSS).
NATURAL/USING/comma-FROM explicitly OOS.
- All four set ops (UNION, UNION ALL, INTERSECT, EXCEPT).
- WITH and WITH RECURSIVE CTEs, with optional (col-list) renaming.
- Scalar subqueries, IN (SELECT …), [NOT] EXISTS as additive
primary branches in sql_expr (redeems ADR-0031 §7 OOS-1).
- Qualified column refs t.c / alias.c as a name_or_call tail
(redeems ADR-0031 §7 OOS-2).
- LIMIT n [OFFSET m]; legacy `LIMIT m, n` OOS.
- DISTINCT/ALL, t.* projection, bare-alias projection (lifts
Phase-1 §4.2's autonomous decision).
Walker-capability honesty (§10): ADR-0030 §8's "ambient
assistance comes for free" holds for grammar recursion (reuses
ADR-0026's Subgrammar + depth cap unchanged) but not for
completion scope. Phase 2 adds a new Node::ScopedSubgrammar
variant alongside the existing Node::Subgrammar (DSL Expr and
sql_expr recursion untouched), a from_scope_stack of
ScopeFrames holding from_scope / cte_bindings /
projection_aliases, qualified-prefix completion narrowing, and
a post-walk fixup pass that re-resolves projection-list
identifier highlighting/validity once FROM is parsed (the
projection-before-FROM problem).
CTE column resolution (§10.3): SELECT * and explicit-projection
CTE bodies both yield real column completion past cte_alias.|
via a body-projection derivation rule that runs at the body's
ScopedSubgrammar exit and writes derived columns back into the
binding.
Diagnostics (§11): every Phase-2 validation case classified
against ADR-0027's ERROR/WARNING guideline. Five new diagnostic.*
catalog keys for parse-time-detectable cases (unknown_qualifier,
ambiguous_column, projection_alias_misplaced, cte_arity_mismatch,
compound_arity_mismatch) plus eight engine.* translation keys.
A MatchedPath-walking predicate-warnings variant closes the
Phase-1 carry-over gap where SQL WHERE expressions emitted no
LIKE-on-numeric / = NULL / type-mismatch warnings — ADR-0027
Amendment 1 finally extends to the SQL surface.
Result-column type resolution (§12): rusqlite 0.39.0 exposes
column_table_name / column_origin_name / column_database_name
behind a `column_metadata` feature; verified. Bare column refs
recover their playground type — partially lifts Phase-1 §4.5's
bool→0/1 deferral.
The implementation plan breaks Phase 2 into seven sub-phases
(2a–2g) with explicit exit gates per sub-phase and a cross-cut
verification matrix that names every "X comes for free" claim
from ADR-0030/0031/0032. The Phase-1 SQL-expression
predicate-warning gap is a named row, preventing an analogous
silent gap from shipping. The plan encodes the user's standing
authorization for the implementer to walk uninterrupted between
gates and commit with standard messages — escalation
discipline preserved for design ambiguities and real blockers.
Pushes remain user-only.
New docs/plans/ directory sets a pattern for future phase plans.
Status: Accepted.