Commit Graph

245 Commits

Author SHA1 Message Date
claude@clouddev1 2aab457c44 feat: DSL→SQL teaching echo — §4 styled-runs polish (ADR-0038)
Lands the last open item on ADR-0038: the de-emphasised styled-runs
rendering treatment for the echo + every category-3 prose line. The
echoed SQL now reads as code — the dimmed `Executing SQL:` label
plus the SQL portion lexed and coloured the same way the input echo
treats user-typed input (ADR-0028 §5 styled-runs over
input_render::lex_to_runs in advanced mode). Category-3 prose lines
(the DontConvert caveat and the existing illuminating
`client_side.*` notes — shortid auto-fill, type-conversion
transforms) all render dimmed too, per §6's "de-emphasised prose
line" wording, so every cat-3 line is visually consistent.

* New `OutputKind::TeachingEcho` variant + a custom branch in
  `ui::render_output_line` mirroring the OutputKind::Echo input-echo
  path: strip the canonical `Executing SQL:` prefix, render it with
  `theme.muted`, then lex the rest in `Mode::Advanced` and emit one
  span per token. Tag stays `[system]` for visual consistency with
  other system output.
* New `OutputStyleClass::Hint` styled-runs class, resolved to
  `theme.muted` in `output_span_style`. Used for the cat-3 prose
  lines (dont_convert caveat + the existing client_side notes).
* New const `crate::echo::TEACHING_ECHO_LABEL = "Executing SQL: "` —
  the byte boundary the ui.rs branch needs is fixed (an i18n template
  can't provide that), so the label moves from i18n to a constant.
  The `echo.executing_sql` i18n key is retired (en-US.yaml + keys.rs);
  a comment in en-US.yaml points future locales at re-introducing it
  if needed.
* App-side helpers: `push_teaching_echo(sql)` builds the
  TeachingEcho line; `push_category_three_prose(text)` builds a
  System line with a whole-text Hint span. `note_ok_summary` and
  `handle_dsl_change_column_success` / `handle_dsl_add_column_success`
  use these instead of plain `note_system` for the echo, the caveat,
  and the illuminating notes.

Existing tests pass unchanged — text content is the same; only
styling changes. New tests pin the polish:

* `ui::tests::teaching_echo_line_renders_dim_prefix_and_lexed_sql`
  asserts the TeachingEcho rendering produces a dim prefix span +
  keyword-coloured SQL spans (confirming the lexer ran in advanced
  mode).
* `ui::tests::category_three_prose_line_renders_all_dim` pins the
  whole-text Hint coverage.
* `ui::tests::hint_class_resolves_to_muted_foreground` pins the
  theme resolution across both light and dark.
* `app::tests::polished_echo_carries_teaching_echo_kind_and_caveat_a_hint_span`
  pins the App-side wiring (kind + styled_runs shape).

Tests: 2019 passed / 0 failed / 1 ignored (pre-existing); clippy
clean (`--all-targets -D warnings`, nursery).

ADR-0038 is now feature-complete — every catalogue row implemented,
round-tripped, AND polished per §4.
2026-05-28 12:16:28 +00:00
claude@clouddev1 5cb105b74b docs(adr): /runda DA cleanup — reflect Phases 1-3 done, pin Bucket C
Surfaces from a Devil's-Advocate audit of the DSL → SQL teaching echo
(ADR-0038) after Phases 1-3 landed: three doc-drift bugs introduced
by the earlier handoff-47 / ADR-promotion commits — requirements.md
M4 and both ADR-0038 README index entry + Status block still said
"Phase 2 / Phase 3 remain," but `275c726` and `e6ad1ae` shipped them.
Updated to reflect actual state: Buckets A + B complete plus the
category-3 prose; only the §4 styled-runs polish remains. ADR-0037's
README entry also touched to note all four shipping commits of its
consumer.

Plus a missing test slice the DA flagged: explicit no-echo coverage
for the Bucket C cases that flow through command_to_sql's catch-all
(show table, explain, replay, every Command::App variant). The
contract — ADR-0030 §10 / ADR-0038 §7 Bucket C — forbids echoes for
these; a future renderer arm added at the wrong place could silently
leak one. The new bucket_c_no_echo_commands_all_return_none pins
that.

Tests: 2015 passed / 0 failed / 1 ignored (pre-existing); clippy
clean. Nothing to escalate.
2026-05-28 10:06:16 +00:00
claude@clouddev1 e6ad1aec3d feat: DSL→SQL teaching echo — Phase 3 cat-3 caveat (ADR-0038)
Lands the only piece of category-3 prose not already covered by the
existing `client_side.*` notes infrastructure: the `change column …
--dont-convert` *caveat* (ADR-0038 §6, the only Bucket A caveat —
every other category-3 line is illuminating).

`--dont-convert` skips the client-side layer entirely, so the headline
SQL echo (`ALTER TABLE … SET DATA TYPE …`) is the nearest SQL but
*not* equivalent: running the line in advanced mode would convert the
stored values, but the playground left them as-is. The new caveat
states that divergence explicitly.

* New i18n key `client_side.dont_convert_caveat` (no placeholders) —
  registered in keys::KEYS_AND_PLACEHOLDERS.
* New `dont_convert_caveat: bool` field on DslChangeColumnSucceeded,
  set in the runtime when submission_mode is advanced *and* the
  command is ChangeColumnType { mode: DontConvert, .. }. Gated on
  advanced mode because the caveat references "the line above" — the
  echo, which only fires in advanced mode.
* App's handle_dsl_change_column_success emits the caveat line
  between the existing client-side notes and the structure render,
  so it reads alongside the echo, not after the table view.

The other two category-3 lines from §6 (shortid generation,
type-conversion transforms) were already in place via
`client_side.auto_fill_*` / `client_side.transformed*` — those notes
already render after the echo via handle_dsl_add_column_success /
handle_dsl_change_column_success, in the right position per the ADR.
This commit just adds the missing caveat.

Tests: 2014 passed / 0 failed / 1 ignored (pre-existing); clippy
clean. An App-level test pins the rendering order (caveat sits
after the echo, before the structure) and the simple-mode gate
(no caveat without an echo to refer to).

The §4 de-emphasised styled-runs rendering polish remains —
the echo + caveat lines are still plain `[system]` lines.
2026-05-28 08:50:08 +00:00
claude@clouddev1 275c726ad4 feat: DSL→SQL teaching echo — Bucket B renderer (ADR-0038 Phase 2)
Expands the renderer to Bucket B — resolved-name single-statement
echoes plus the two category-2 multi-statement forms. Every catalogue
row round-trips per line through the advanced-mode walker (the §1
copy-paste contract; §6 category 2 holds the contract per line):

  add index [as N] on T (cols)           → CREATE INDEX <name> ON T (cols)
  drop index on T (cols) (positional)    → DROP INDEX <name>
  add 1:n relationship [as N] …          → ALTER TABLE C ADD CONSTRAINT
                                            <name> FOREIGN KEY (cc)
                                            REFERENCES P (pc) [ON …]
  drop relationship (endpoints or named) → ALTER TABLE C DROP CONSTRAINT
                                            <name>
  drop column T.c --cascade              → DROP INDEX <ix1> ⏎ … ⏎
                                            ALTER TABLE T DROP COLUMN c
  add relationship … --create-fk         → ALTER TABLE C ADD COLUMN cc <ty>
   (child column newly created)            ⏎ ALTER TABLE … ADD CONSTRAINT
   (already existed) collapses to a single-line FK echo

Refactors the echo payload from Option<String> to Option<Vec<String>>
across the 7 success events + arms + render path — one entry per
statement; the Bucket A single-line echoes wrap as Some(vec![s]). Plain
rendering repeats `Executing SQL:` per line; the de-emphasised
styled-runs polish (ADR-0038 §4) will refine it later.

Adds the two echo build paths the handoff §5 ⚠️ gotcha foreshadowed:

* collect_echo_lookups (pre-execution, runtime): resolves names the
  dropped thing or not-yet-created column would erase post-execution —
  drop index (positional), drop relationship (both endpoints and named
  selectors, the latter via a list_tables scan acceptable for teaching-
  playground schemas), and the --create-fk pre-state (whether the child
  column existed + the parent PK type to derive the new column type via
  Type::fk_target_type).
* build_schema_echo (post-execution, runtime): subsumes the Bucket A
  pure-Command schema cases and renders Bucket B from the description +
  the lookups.

The DropColumn arm gains build_drop_column_cascade_echo, which reads
DropColumnResult.dropped_indexes to emit the multi-line cascade echo;
non-cascade falls through to the pre-execution Bucket A echo unchanged.

Tests: 2013 passed / 0 failed / 1 ignored (pre-existing); clippy clean
(`--all-targets -D warnings`, nursery). Two end-to-end runtime tests
exercise the resolved-name and multi-statement flows against a real
worker (auto-named index, both drop-relationship selector forms, both
--create-fk branches). One app-level test pins the multi-line rendering
(one Executing SQL: per statement, in order, beneath [ok]).

Phase 3 (category-3 prose — shortid generation, type-conversion
transforms, `change column --dont-convert` caveat) and the §4
de-emphasised styled-runs rendering polish remain per ADR-0038 §8
phasing.
2026-05-28 07:54:05 +00:00
claude@clouddev1 90479cb879 feat: DSL→SQL teaching echo — Bucket A renderer (ADR-0038 Phase 1)
Expands the renderer skeleton from ADR-0038's first slice to the full
single-statement catalogue. Every Bucket A row round-trips through the
advanced-mode walker (the §1 copy-paste contract):

  add column / drop column (non-cascade) / rename column / change column
  (SET DATA TYPE) / add constraint (not null, default, unique, check) /
  drop constraint (not null, default) / show data [where] [limit] /
  delete --all-rows / update --all-rows

Adds the Expr→SQL and Value→SQL-literal renderers (ADR-0038 §5) — bare
identifiers, inlined literals, NULL uppercase, standard <> for inequality
— and threads `echo: Option<String>` onto the six remaining success
events (DslAddColumn/DropColumn/ChangeColumn/Data/Update/Delete
Succeeded) with matching runtime construction and App stash arms.

`show data` is the one Bucket A row whose echo needs schema info beyond
the Command (the `ORDER BY <pk>` for a limited query): the pure
renderer takes the primary key as a parameter, and the runtime sources
it post-execution via describe_table — gated on advanced mode + limit
present, mirroring the enrich_dsl_failure describe pattern. An
end-to-end test pins the describe→PK→ORDER BY glue against a real
worker; the simple-mode gate and unlimited-no-lookup paths are covered
too.

Also fixes a contract gap surfaced while completing the catalogue: the
existing create-table echo silently dropped per-column DEFAULT / CHECK,
which simple-mode `create table … with pk c(ty) check (…)` does parse
(ADR-0029) — so the echo was non-equivalent. The render now emits the
full ADR-0029 column-constraint suffix, sharing one append_constraints
helper with `add column`.

Phase 2 (Bucket B — resolved-name + multi-line echoes, including
`add index`), Phase 3 (category-3 prose), and the de-emphasised
styled-runs polish remain deferred per ADR-0038 §8 phasing.

Tests: 2000 passed / 0 failed / 1 ignored (pre-existing); clippy clean
(`--all-targets -D warnings`, nursery).
2026-05-28 06:53:43 +00:00
claude@clouddev1 04c8e4295f feat: DSL→SQL teaching echo — channel + create-table slice (ADR-0037 + ADR-0038)
Walking skeleton validating the whole echo architecture end to end; the
Command→SQL renderer currently covers `create table`, with the rest of
Bucket A / B / category-3 to follow (ADR-0038 §8).

- Channel (ADR-0037): the three-way EffectiveMode (reusing the existing
  enum, not a new SubmissionMode — recorded in the ADR) rides on
  Action::ExecuteDsl to the runtime. `replay` bypasses the interactive
  spawn, so it never echoes (silent, for free).
- Echo (ADR-0038): built at the runtime's ExecuteDsl dispatch — the worker
  gets decomposed calls, not the Command, so ADR §4's "worker builds it"
  was corrected to the dispatch layer. Gated by echo_for (advanced
  effective mode + DSL-form). Carried on DslSucceeded; rendered by
  note_ok_summary as `Executing SQL: …` immediately beneath `[ok]`. New
  src/echo.rs renderer; echo.executing_sql i18n key.
- command_to_sql: `create table` → `CREATE TABLE T (id serial PRIMARY KEY)`
  (single inline / compound table-level PK), playground type vocabulary,
  round-trip-verified against the advanced walker (the §1 contract).

Tests: echo.rs (render, round-trip contract, mode gate, Sql*-not-echoed);
app.rs (submit carries the 3-way mode; echo renders beneath [ok]).
Suite 1970/0/1; clippy clean.
2026-05-27 22:09:54 +00:00
claude@clouddev1 9a23e28f30 fix: update … --all-rows falls back to the DSL instead of misparsing (ADR-0033 Am4)
Advanced-mode `update T set x = 42 --all-rows` parsed the `--all-rows`
DSL flag as the arithmetic `42 - -all - rows` over phantom columns
`all`/`rows` (Amendment 3's counter-example), masked only by the engine's
`--` comment leniency. The playground supports no `--` line comment, so
this was a misparse (ADR-0027: flag input known to fail at runtime).

Fix: walk_punct refuses a `-` that begins an adjacent `--`. Only the SQL
expression uses Node::Punct('-'), so this is scoped to it. The SET
expression then stops, the SQL UPDATE shape fails, and dispatch falls
back to the DSL Update { AllRows } — symmetry with delete … --all-rows.

Behaviour: `42 --all-rows` → DSL Update{AllRows}; spaced `42 - -3` stays
SqlUpdate (= 45, preserved); adjacent `42--3` → parse error (contrived;
no `--` comment support).

Tests: inverted parse test (+ arithmetic-preserved + adjacent-error
assertions); new full-pipeline update_all_rows_flag_in_advanced_updates_every_row.
Suite 1963/0/1; clippy clean.
2026-05-27 21:25:02 +00:00
claude@clouddev1 338dc8a4cf feat: advanced ALTER COLUMN SET/DROP NOT NULL & DEFAULT, SET DATA TYPE (ADR-0035 Am2)
The standard-first ALTER COLUMN constraint gap-fill advanced mode lacked:

- ALTER COLUMN <c> SET DATA TYPE <ty> — ISO canonical synonym for the
  PostgreSQL TYPE shorthand (same AlterColumnType action + executor).
- SET NOT NULL / DROP NOT NULL — reuse the ADR-0029 do_add_constraint /
  do_drop_constraint executors (dry-run + internal-table guards free).
- SET DEFAULT <expr> / DROP DEFAULT — SET DEFAULT uses a dedicated
  raw-SQL executor (do_set_column_default); sql_expr yields no typed
  Value, so it can't go through do_add_constraint. DROP DEFAULT reuses
  do_drop_constraint.

Grammar: AT_ALTER_COLUMN gains a tail Choice (type / set / drop), reusing
SQL_TYPE and the CREATE TABLE DEFAULT_NODES; builder dispatch routes the
new column-attribute forms; runtime decomposes to the executors.

ADR-0035 Am2 corrected in-place: SET DEFAULT decomposes to
do_set_column_default, not do_add_constraint (Value-based) — found during
build.

Tests (test-first): 6 parse + 7 Tier-3 execution via run_replay. Suite
1962/0/1; clippy clean.
2026-05-27 21:03:14 +00:00
claude@clouddev1 42306d33e3 fix: X4 — advanced-mode SQL INSERT auto-fills omitted non-PK serial (MAX+1)
A Form-A advanced-mode INSERT that omitted a non-PK serial column left it
silently NULL (the column is INTEGER UNIQUE, not NOT NULL, so SQLite
permits it), while simple-mode do_insert auto-fills it with MAX+1. That
violated ADR-0018 §1's "auto-generated on every path" contract and was the
unprincipled serial-vs-shortid asymmetry the ADR set out to remove
(advanced mode already auto-fills shortid).

Fix (decision: advanced mode matches simple mode): the advanced-mode
auto-fill reconstruction — renamed plan_shortid_autofill →
plan_autogen_autofill — now also fills an omitted non-PK serial with
MAX(col)+1 … MAX+n per row (single- and multi-row), reading MAX once under
the worker's single-writer serialisation. PK serial stays on the rowid
alias; Form B (no column list) still supplies every column. Honours
ADR-0018 §1/§5; no ADR amendment needed (the contract already said "every
path"). requirements.md X4 marked resolved.

Tests: 1949 passing (+1), 0 failed, 0 skipped, 1 ignored; clippy clean.
2026-05-27 11:18:57 +00:00
claude@clouddev1 d98717156e fix: ADR-0036 — name the offending value for natural-order SQL INSERTs
A no-column-list (natural-order / Form B) SQL INSERT that hit a UNIQUE/
CHECK violation degraded to the neutral "that value" because
user_value_for_column only resolved the offending value for the explicit-
column-list form. Extend user_value_for_column_with_schema to map each
VALUES position to the schema's columns in declaration order (ALL columns
— advanced-mode Form B auto-fills nothing, so every column has a value),
single-row only (multi-row stays ambiguous). Closes the Phase 1 carryover
gap so the error names the real value either way.

Tests: 1948 passing (+1), 0 failed, 0 skipped, 1 ignored; clippy clean.
2026-05-27 10:54:29 +00:00
claude@clouddev1 8906661f69 feat: ADR-0036 Phase 3b — live typed-slot hints + highlighting for INSERT VALUES
Give each positional INSERT VALUES position its column identity so a lone
literal gets the column-typed slot (live per-column hint + mismatch
highlight) and any expression falls through to sql_expr — completing the
typed-DML-values feature for the INSERT surface (single/multi-row, Form A
and Form B).

New zero-width Node::SetColumn(&TableColumn) primitive establishes the
active column for the value position that follows (sets current_column +
pending_value_column, like an Ident{writes_column} but without consuming
input); a DynamicSubgrammar emits SetColumn(col) + the shared SET_VALUE
per position. Column mapping mirrors do_sql_insert: Form A → listed
columns; Form B → all columns in declaration order (advanced-mode Form B
auto-fills nothing; an omitted shortid in Form A is auto-filled and has no
VALUES position).

Reconcile with the per-tuple arity diagnostic (ADR-0033 §8.1): a
fixed-length typed Seq would reject wrong-arity tuples and suppress that
post-walk diagnostic, so the tuple value list is an arity-gating lookahead
— a correct-arity tuple uses the typed Seq; a wrong-arity tuple keeps the
type-blind sql_expr repeat so §8.1 fires unchanged. Correct-arity tuples
get full live feedback, including a wrong-kind literal like 'text' into an
int column.

Records ADR-0036 Amendment 1 (Phase 3b detail + the arity reconciliation);
ADR-0036 is now fully implemented.

Tests: 1947 passing (+8), 0 failed, 0 skipped, 1 ignored; clippy clean.
2026-05-27 07:22:44 +00:00
claude@clouddev1 49ea03b0d5 feat: ADR-0036 Phase 3a — live typed-slot hints + highlighting for SQL SET values
Wire the DSL's column-typed value slots into the advanced-mode SQL
UPDATE/UPSERT `SET col = <rhs>` value position so a learner gets the same
per-column hint ("for `Email`: type a quoted string") and live numeric-
shape mismatch highlight the simple-mode DSL gives.

Discriminate literal-vs-expression with a boundary-aware lookahead
(shared::SET_VALUE), NOT the naive `Choice(typed-slot, sql_expr)` the ADR
originally sketched: the walker's Choice is first-match-wins with no
backtrack, so a typed slot would greedily match the leading `1` of `1 + 2`
and commit, regressing valid SQL (e.g. the existing `values (1, 1 + 2)`
test). The lookahead peeks the whole value position: a literal routes to
the typed slot only when it fills the position up to the next
`,`/`)`/`;`/`where`/`returning`/end; everything else falls through to the
full sql_expr grammar unchanged. The SET column ident gets
`writes_column: true` so `current_column` drives the slot + hint.

Scope: Phase 3a covers UPDATE's assignment list and INSERT's ON CONFLICT
DO UPDATE SET. Phase 3b (INSERT VALUES — needs a per-position grammar
restructure + multi-row) is deferred. Records ADR-0036 Amendment 1 with
the mechanism correction + the 3a/3b split.

Tests: 1939 passing (+5), 0 failed, 0 skipped, 1 ignored; clippy clean.
2026-05-26 22:48:46 +00:00
claude@clouddev1 8c3b13b313 feat: ADR-0036 Phase 2 — validate advanced-mode UPDATE SET literals + retain the value
Mirror Phase 1's capture-at-parse technique on the UPDATE SET assignment
list. build_sql_update calls the new capture_set_literals (data.rs), which
walks the matched tokens (no reparse, no grammar change) and classifies
each top-level `SET col = <rhs>` as a literal (Some, incl. signed numbers)
or an expression (None), using paren depth so a comma inside a function
call or a `where` inside a scalar subquery is not mistaken for a boundary,
and the trailing top-level WHERE is excluded.

Command::SqlUpdate gains set_literals; do_sql_update validates the literals
against their column types via the shared impl_value_for before the still
verbatim update; user_value_for_column reads them so a constraint error
names the offending value. WHERE stays unvalidated; execution and command
identity are unchanged.

Also corrects the stale data.rs header comment (DSL typed slots are wired,
not "deferred") and flips ADR-0036 + README to Phases 1–2 implemented.

Tests: 1934 passing (+4), 0 failed, 0 skipped, 1 ignored; clippy clean.
2026-05-26 22:20:12 +00:00
claude@clouddev1 1d5534b2bd feat: ADR-0036 Phase 1 — validate advanced-mode INSERT literals + show the value
Capture literal VALUES at parse onto Command::SqlInsert (no grammar change,
no reparse); validate them against column types before the still-verbatim
insert (reusing impl_value_for for DSL-parity wording); read them in the
error enricher so a constraint error names the real value. Execution,
auto-fill, and command identity unchanged. Adds run_sql_insert_with_literals
(runtime path); run_sql_insert stays the no-capture raw entry.

Proven: malformed date 2025/01/15 now refused in advanced-mode SQL; replayed
UNIQUE shows the real value. Tests +3 (expression runs, multi-row, natural
order) + 2 flipped/strengthened. 1930 pass / 0 fail / 0 skip; clippy clean.
2026-05-26 21:58:25 +00:00
claude@clouddev1 f8a91f41c9 feat: ADR-0035 Amendment 1 follow-up — enrich replay errors + close message gaps
- 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.
2026-05-26 18:30:31 +00:00
claude@clouddev1 cb8ff8a7c2 feat: ADR-0035 Amendment 1 — drop composite UNIQUE; friendlier drop-column + generic-error wording
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.
2026-05-26 16:20:08 +00:00
claude@clouddev1 22e5bf5d6a feat: ADR-0035 4i(a,b) — CREATE TABLE help/usage + describe table constraints; Phase 4 complete
(b) describe shows table-level constraints: TableDescription gains
unique_constraints + check_constraints (populated by do_describe_table
from read_schema), rendered in a new "Table constraints:" section —
composite UNIQUE and table-level CHECK (named + unnamed). The per-column
Constraints column already covered single-column NOT NULL/UNIQUE/PK/CHECK.

(a) CREATE TABLE help/usage skeleton refreshed for the column DEFAULT/
CHECK/REFERENCES, table-level composite UNIQUE, table CHECK, and
table-level FOREIGN KEY forms (4a.2/4a.3/4b) — engine-neutral,
vocab-audit clean.

With 4i's (c)/(d)/(e) already shipped, this completes sub-phase 4i — the
verification sweep — and therefore ADR-0035 Phase 4 (4a–4i). ADR-0035
Status, §13 4i, the ADR index, and requirements.md Q1 updated to
"Phase 4 complete".

Tests: render_structure table-level-constraints unit test +
e2e_describe_shows_table_level_constraints. Full suite 1917 passing /
0 failing / 1 ignored; clippy clean.
2026-05-26 14:38:28 +00:00
claude@clouddev1 c2eb8cb982 fix: ADR-0035 4i(c) — don't pre-flag a self-referencing FK parent
A `CREATE TABLE` whose foreign key references the table being created
(`create table T (id int primary key, parent_id int references T(id))`)
parses and executes correctly, but the pre-submit schema-existence
diagnostic flagged the not-yet-created table as "no such table" — the FK
parent slot is `IdentSource::Tables`, and the target isn't in the schema
yet.

schema_existence_diagnostics now collects the CREATE TABLE target(s)
(`IdentSource::NewName`, role `table_name`) and exempts a `Tables`
reference matching one (case-insensitively) from the unknown-table flag.
A FK to a genuinely-unknown *other* table is still flagged.

Tests: self-ref FK not flagged; FK to an unknown other table still
flagged. Full suite 1915 passing / 0 failing / 1 ignored; clippy clean.
2026-05-26 12:20:30 +00:00
claude@clouddev1 f85261032d feat: ADR-0035 4i(e) — colour DSL vs SQL completions when mixed
Building on the 4i(d) merge: tag each completion Candidate with a
ModeClass (Both/Advanced/Simple) and, in the hint UI, colour the
continuations by mode ONLY when a candidate list actually mixes modes
(a shared entry word offering both SQL and DSL forms) — Advanced →
theme.mode_advanced, Simple → theme.mode_simple, Both → the token-kind
colour. A single-mode list (the common case, e.g. deep inside a SQL
statement) keeps the token-kind colours, so the tint appears only where
it distinguishes DSL from SQL. With (d)'s Both → Advanced → Simple
block-ordering, each colour reads as one contiguous block.

Candidate gains a `mode` field (typing_surface snapshots regenerated —
uniformly `mode: Both`, no semantic change). Tests: render_candidate_line
mixed-mode colours + the single-mode-keeps-kind-colour rule. Full suite
1913 passing / 0 failing / 1 ignored; clippy clean.
2026-05-26 12:11:12 +00:00
claude@clouddev1 1afcf4ed29 feat: ADR-0035 4i(d) — merge shared-entry-word completions
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.
2026-05-26 11:36:18 +00:00
claude@clouddev1 a95c8074f3 fix: resolve table names case-insensitively across all executors
SQL identifiers are case-insensitive, so the engine resolves a table
named in any capitalization — but our metadata tables (keyed by
table_name / parent_table / child_table) and data/<table>.csv files use
case-sensitive TEXT '=', so an operation naming a table in a different
case than stored drifted: schema ops orphaned metadata rows, and a
wrong-case insert/update/delete silently skipped the CSV write, losing
the change on the next reload/rebuild. This contradicted ADR-0009's
stated rule (case-insensitive resolution, case-preserving display).

Add a canonical_table_name helper (resolve to the stored case via
COLLATE NOCASE, excluding sqlite_* and __rdbms_* tables) and apply it at
the entry of every table-naming executor — drop table, add/drop/rename
column, change column type, add/drop constraint, add relationship, add
index, rename table, insert/update/delete, and the advanced SQL DML —
so the live schema, the metadata, and the CSV stay in step regardless of
how the user capitalized the name. This also folds the internal-table
guard into the same lookup (executors that previously lacked it now
refuse __rdbms_*/sqlite_* as "no such table"). do_rename_table now
accepts a case-variant source too.

Column names remain matched case-sensitively (a wrong case is refused as
"no such column" — strict, but never drifting), per the scope agreed
with the user.

Tests: tests/case_insensitive_names.rs — wrong-case rename-column,
insert (survives a fresh rebuild — no data loss), add-column, drop-table,
rename-table, and add-relationship, all with fresh-rebuild round-trips.
Full suite 1909 passing / 0 failing / 1 ignored; clippy clean.
2026-05-26 10:04:27 +00:00
claude@clouddev1 f7e77a86f8 feat: ADR-0035 4h — ALTER TABLE … RENAME TO
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.
2026-05-26 08:38:39 +00:00
claude@clouddev1 50a889e599 fix: ADR-0035 4g — reconstruct table-CHECK metadata on rebuild
do_rebuild_from_text re-emitted table-level CHECKs into the recreated
DDL (so they stayed enforced) but never repopulated __rdbms_playground_
table_checks. A fresh rebuild (missing .db, reconstructed from
project.yaml) therefore left the CHECK metadata empty: DROP CONSTRAINT,
describe, and a later save would lose it — including a named CHECK's
name. In-place rebuilds only worked because the wipe never touched the
table. (Latent since 4a.3 for unnamed checks; exposed by 4g's named
round-trip claim.)

Rebuild now wipes and repopulates CHECK_TABLE from the yaml snapshot
(name + seq + expr), like META/REL, and adds the 4g `name` column if a
pre-4g table predates it (the rebuild-only migration). Regression test:
a named CHECK's metadata survives a fresh rebuild (DROP CONSTRAINT by
name resolves).
2026-05-25 22:16:26 +00:00
claude@clouddev1 6ff97f6e20 feat: ADR-0035 4g — ALTER TABLE add/drop constraint + add FK
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.
2026-05-25 22:07:50 +00:00
claude@clouddev1 5b76315d1e feat: ADR-0035 4f — ALTER TABLE … ALTER COLUMN TYPE
Fourth AlterTableAction (AlterColumnType), runtime-decomposed to the
existing change_column_type executor with ForceConversion — which IS the
§7 advanced policy: lossy converts with a note (no force flag),
incompatible + the ADR-0017 static refusals (↔blob, same-type,
date↔datetime, non-int→serial) still refuse, while int→serial is allowed
(auto-fills nulls + UNIQUE, ADR-0018 §8). No new mode/note/persistence;
undo is the advanced safety net.

Grammar adds a fourth action branch leading on `alter`, discriminated in
the builder by the `type` keyword (unique — ADD COLUMN's type is an
ident); the type slot reuses SQL_TYPE. The internal-__rdbms_* guard was
folded into do_change_column_type (user-confirmed), closing the simple
`change column` exposure.

Tests: 7 Tier-3 e2e via run_replay + 4 Tier-1 parse (incl. a column-named-
`type` discriminator probe) + the simple-surface guard. Help/usage
refreshed; ADR-0035 §13 4f + README + requirements.md in lockstep.
2026-05-25 21:16:37 +00:00
claude@clouddev1 bbc2e34b33 feat: ADR-0035 4e — ALTER TABLE add/drop/rename column
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.
2026-05-25 19:49:13 +00:00
claude@clouddev1 701217d29f feat: ADR-0035 4d — CREATE [UNIQUE] INDEX / DROP INDEX
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.
2026-05-25 18:54:32 +00:00
claude@clouddev1 e52e90c45b feat: ADR-0035 4c — DROP TABLE [IF EXISTS]
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.
2026-05-25 16:31:41 +00:00
claude@clouddev1 76d60591bf feat: ADR-0035 4b — foreign keys in CREATE TABLE
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.
2026-05-25 15:35:48 +00:00
claude@clouddev1 60111f69d5 feat: ADR-0035 4a.3 — table-level / multi-column CHECK
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.
2026-05-25 14:06:52 +00:00
claude@clouddev1 c0f5626787 feat: ADR-0035 4a.2 — per-column CHECK/DEFAULT + composite UNIQUE
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.
2026-05-25 11:04:59 +00:00
claude@clouddev1 631074ff9c feat: ADR-0035 4a — SQL CREATE TABLE command, worker, and exit gate
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.
2026-05-25 10:04:28 +00:00
claude@clouddev1 80310929d7 feat: ADR-0035 4a — SQL CREATE TABLE grammar shape
The post-CREATE shape (src/dsl/grammar/sql_create_table.rs):
TABLE [IF NOT EXISTS] <name> ( <col-def | table-PK> , … ) [;]
- col-def: <name> <type> [NOT NULL] [UNIQUE] [PRIMARY KEY]
- type: ten keywords + standard-SQL aliases (via from_sql_name) +
  the two-word `double precision` branch + discarded (len[,len]) arg
- table-level PRIMARY KEY (cols) — single and compound
- __rdbms_* target rejected at walk time

DEFAULT/CHECK/table-level UNIQUE shapes are deliberately absent (the
4a.2 constraint slice); FK is absent (4b). 13 accept/reject tests
mirror sql_insert's walk_node harness. Shape only — the CommandNode +
builder + worker wiring follow.
2026-05-25 08:11:39 +00:00
claude@clouddev1 58386d77e9 feat: ADR-0035 4a — SQL type-alias resolver (Type::from_sql_name)
Advanced-mode SQL type slot accepts the ten playground keywords plus the
standard-SQL aliases (integer/varchar/timestamp/numeric/float/double
precision/binary/..., case-insensitive). Simple-mode FromStr is unchanged
(rejects aliases). Unknown names -> None for the friendly diagnostic.
2026-05-25 07:55:26 +00:00
claude@clouddev1 df6aa69155 fix: ADR-0006 — clear redo when new work commits without a snapshot
/runda found silent data loss: with the non-fatal snapshot-failure
policy, a committed mutation whose snapshot couldn't be staged left
the redo stack stale (redo-clear was only a side effect of finalize),
so a later redo silently discarded the new work. Same gap in batches.

- SnapshotStore::clear_redo() drops the redo stack + payloads
- snapshot_then / end_batch call it when committed user work has no
  staged snapshot; for disk-full it succeeds where a full backup
  couldn't (tiny index write + payload deletes)
- unit test + integration regression (forced staging failure)
- ADR-0006 implementation note records the fix + residual edge

1698 passed / 0 failed / 1 ignored; clippy clean.
2026-05-24 21:10:44 +00:00
claude@clouddev1 d6c5674bf5 feat: ADR-0006 §8 step 6 — .snapshots/ gitignore + export + cleanup
The undo ring is local working state, handled at all three
project-file seams (R13):
- .gitignore template ignores /.snapshots/
- export excludes .snapshots/ (like playground.db / history.log)
- safely_delete_temp_project allowlists .snapshots/ so a temp that
  was modified then undone back to empty stays auto-deletable
- undo::SNAPSHOTS_DIR is now a pub const referenced by all three
- tests: gitignore content, export exclusion, cleanup allowlist

1693 passed / 0 failed / 1 ignored; clippy clean.
2026-05-24 20:53:00 +00:00
claude@clouddev1 25800e3eb5 feat: ADR-0006 §8 steps 4-5 — undo/redo commands + confirm-modal flow
Commands & grammar (step 4):
- AppCommand::Undo/Redo, grammar nodes + REGISTRY entries, catalog
  help/usage + keys; parse tests
- replay skips undo/redo (is_app_lifecycle_entry_word) + completion
  entry-keyword lockstep; replay-skip test extended

Wiring (step 5):
- Action::{PrepareUndo,PrepareRedo,Undo,Redo} + AppEvent::{UndoPrepared,
  UndoUnavailable,UndoSucceeded,UndoFailed}
- App: undo_enabled flag, Modal::UndoConfirm, dispatch + event handling
  + confirm-key handler (Y confirms / N/Esc cancels); "turned off" when
  --no-undo; "nothing to undo/redo" when empty
- ui::render_undo_confirm names the command + snapshot time
- runtime: opens with undo enabled (!--no-undo), threads it through the
  project-switch path, spawn_prepare_undo/spawn_undo (peek->modal,
  restore->refresh tables + schema cache)
- 9 Tier-1 app tests + 3 parse tests

1692 passed / 0 failed / 1 ignored; clippy clean.
2026-05-24 20:48:30 +00:00
claude@clouddev1 a97069c02e feat: ADR-0006 §8 step 3 — wire the snapshot ring into the db worker
- snapshot_then() brackets all 19 mutating dispatch arms: stage a
  pre-op snapshot, finalise on success / discard on rollback; gated
  on a user command source (internal ops like open-time rebuild are
  not snapshotted) and on undo being enabled
- BatchState + BeginBatch/EndBatch requests: a batch takes one
  boundary snapshot, suppresses per-command snapshots, and finalises
  iff a mutation committed (one undo step per replay/batch)
- Undo/Redo/PeekUndo/PeekRedo requests handled in worker_loop with
  &mut conn for the restore; cleanup() sweeps crash leftovers on open
- Database::{undo,redo,peek_undo,peek_redo,begin_batch,end_batch} +
  open_with_persistence_and_undo(); snapshot failures are non-fatal
  (logged), restore failures surface
- 6 Tier-3 integration tests through the real worker

1680 passed / 0 failed / 1 ignored; clippy clean.
2026-05-24 20:31:05 +00:00
claude@clouddev1 64eee3ed6d feat: ADR-0006 §8 steps 1-2 — --no-undo flag + snapshot ring module
Step 1 (Cargo + CLI):
- add the `backup` feature to rusqlite (online backup API)
- `--no-undo` flag (test-first) + help-banner entry

Step 2 (snapshot store, src/undo.rs):
- SnapshotStore: a persisted undo ring + redo stack under
  <project>/.snapshots/ (index.yaml + per-snapshot payload dirs)
- hybrid whole-project snapshot: db via backup API + project.yaml /
  data/*.csv copied as files; restore is text-first, db-last
  (ADR-0015 §6 commit-db-last)
- stage/finalize/discard, undo/redo (each snapshots current to keep
  the inverse possible), N=50 eviction, redo-cleared-on-new-work,
  orphan/staging cleanup, monotonic ids
- 12 Tier-1 tests; adds a crate-visible persistence::utc_iso8601_now

No worker wiring yet (step 3). 1674 passed / 0 failed / 1 ignored; clippy clean.
2026-05-24 20:17:03 +00:00
claude@clouddev1 e4f2f5fa15 feat: ADR-0034 — history journal records err + replay parses/filters the journal
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.
2026-05-24 18:59:06 +00:00
claude@clouddev1 380c4238ef test+docs: 3k Phase-3 verification sweep — e2e DML + filled cross-cut matrix
Sub-phase 3k of ADR-0033. Adds the Tier-3 end-to-end DML suite (tests/sql_dml_e2e.rs) and the cross-cut gap-fill tests, fills the verification matrix (every row a verified file::function), and produces the phase-exit report.

- tests/sql_dml_e2e.rs: INSERT…SELECT cross-table, all-ten-type multi-row INSERT + RETURNING type recovery, UPDATE-with-subquery-in-SET, cascade DELETE, UPSERT round-trip, RETURNING x3, history.log replay, OOS rejections (full §13 table), validity-indicator-from-SQL-DML.
- walker/mod.rs, highlight.rs, completion.rs, input_render.rs: inherited-diagnostic, DML-keyword highlight, INSERT INTO completion, and advanced-mode DML hint-panel cross-cuts.
- Matrix correction (user-confirmed): predicate warnings fire on row-scoped DML slots; INSERT VALUES has no row scope (ADR-0033 §8.4).
- Auto-snapshot row marked N/A (user-confirmed): ADR-0006 unimplemented for both paths; deferred.

/runda round: added an advanced-mode DML hint-panel test (A6 was attributed to simple-mode prose under the §8 advanced heading); extended OOS coverage to the full ADR-0033 §13 table (OOS-5 INDEXED BY / OOS-6 multi-statement) + a trailing-semicolon guard.

1645 passing / 0 failing / 0 skipped / 1 ignored. Clippy clean.
2026-05-23 22:26:04 +00:00
claude@clouddev1 d5c7f63513 grammar+walker: 3j — shared insert/update/delete entry words (ADR-0033 §2 / Amendments 1 & 3)
Wire `insert`/`update`/`delete` as shared DSL/SQL entry words through the
category-grouped dispatcher (ADR-0033 Amendment 1): the Advanced SQL nodes
move off the dev words (`sqlinsert`/`sql_update`/`sql_delete`) to the real
keywords, registered alongside the Simple DSL nodes. Remove the dev-word
scaffold; collapse build_sql_{insert,update,delete} to source.trim();
de-duplicate the two REGISTRY entry-word listing sites.

Dispatch model (ADR-0033 Amendment 3, written this round):
- A command is the mode-rooted grammar-path outcome; identity is intrinsic.
  Advanced mode tries SQL first, falling back to the Simple DSL command when
  no SQL branch matches a token (`delete … --all-rows` falls back;
  `update … --all-rows` does not — the SET expression absorbs it, harmless
  since the engine treats `--all-rows` as a comment).
- Simple mode commits the DSL candidate for a shared word, surfacing the real
  DSL error; bare "this is SQL" is reserved for SQL-only entry words
  (`select`/`with`). A content rejection on the SQL candidate (internal
  table) is committed, never masked by the DSL fallback.

Combined DSL-error + advanced-SQL pointer (ADR-0033 Amendment 3): a Simple-mode
definite DSL error that would run as SQL in advanced mode gains the
`advanced_mode.also_valid_sql` suffix — in the live hint (ambient_hint_in_mode)
and on submit (dispatch_dsl), via the shared advanced_alternative_note — so the
actionable DSL fix and the mode pointer coexist (submit covers constructs that
surface only on submit, e.g. `delete … returning`).

Internal-table rejection symmetrised (/runda finding B, ADR-0030 §6): the DSL
data-command target slots (insert/update/delete/show data/show table) gained
reject_internal_table, so `__rdbms_*` tables are refused in Simple mode too —
previously only the advanced SQL grammar rejected them.

Mode-awareness: classify_input_with_schema_in_mode and
invalid_ident_at_cursor_in_mode stop leaking the advanced SQL view into
simple-mode hints for shared words.

Tests: dev-word inputs migrated to the real words (advanced); DSL grammar /
completion / phase-D / db tests parse in Simple mode (the DSL surface); replay
keeps its advanced-mode model (one stale assertion fixed); dispatcher routing,
combined-pointer, and internal-table tests added. Suite 1626 pass / 0 fail /
1 ignored; clippy --all-targets -D warnings clean.

Defer M4 (execution-time mode side-channel; tracked in requirements.md) to its
own ADR.
2026-05-23 21:13:39 +00:00
claude@clouddev1 8d17583fe0 walker: 3i /runda DA round — fix INSERT-target scope confusion (6 cases)
A focused adversarial round (/runda) found a single root cause with
six manifestations, all pre-existing latent false-positives: the
INSERT target is recorded under the `insert_target_table` role, not
as a diagnostic `bindings` entry, so refs that should resolve to the
*target* row were instead checked against the statement's bindings —
which for an `INSERT … SELECT` are the SELECT's *source* tables (the
wrong scope), producing false unknown_column / unknown_qualifier
diagnostics on valid input.

New helper bare_ref_insert_target re-scopes a ref onto the INSERT
target when it sits in a target-referencing region: the UPSERT
DO UPDATE action (byte range) or an INSERT's RETURNING list. Applied
across every ref form:

  1. INSERT column list (insert_column) — validated vs the target,
     skipped in the bare-column branch (was checked vs SELECT source).
  2. ON CONFLICT (col) target (conflict_target_column) — same.
  3. DO UPDATE SET RHS / WHERE bare refs — validated vs the target
     (also closes the #12 residual for VALUES upserts).
  4. RETURNING bare refs — validated vs the target.
  5. target-qualified refs `t.col` in DO UPDATE / RETURNING — the
     unified `excluded` / target-qualifier resolution in the
     qualified-ref None branch.
  6. target-qualified star `t.*` in RETURNING — same re-scoping in
     the qualified-star handler.

Each fix has a positive (resolves cleanly) and negative (genuinely
unknown column / unrelated qualifier still flagged) test; the
`excluded` leak guard and all prior diagnostics remain green.
1613 pass / 0 fail / 1 ignored. Clippy clean.
2026-05-22 22:23:15 +00:00
claude@clouddev1 4fa0aa06e9 db+walker: 3i DA pass — not_null PK false-positive fix + arity hardening
DA pass on 3i. Fix: build_schema_cache set not_null = c.notnull ||
c.primary_key, which would false-flag an omitted `int` PK as a
not_null_missing WARNING — but an int PK is an INTEGER PRIMARY KEY
rowid alias that auto-fills (and SQLite's PK-NULL quirk means a PK
isn't implicitly NOT NULL anyway). Use c.notnull alone (ADR-0033
§8.3 "declared NOT NULL"): faithful and false-positive-free.

Arity-walk hardening (same class as the ON CONFLICT regression the
existing tests caught mid-3i): RETURNING after VALUES is a depth-0
keyword that ends the tuple list (only the real tuple is flagged),
and a comma nested in a function-call value (depth ≥ 2) does not
inflate the tuple's value count.

Tests (+2). 1598 pass / 0 fail / 1 ignored. Clippy clean.
2026-05-22 22:06:04 +00:00
claude@clouddev1 cfd925c24a grammar+db: 3i — DML column-existence + cross-cut verification (ADR-0033 §8)
New dml_target_column_diagnostics pass: an ERROR for an unknown column
in the INSERT column list or the UPSERT DO UPDATE SET (validated
directly against the insert_target_table). The INSERT target isn't a
flat-scope `bindings` entry, so the existing schema-existence pass
didn't cover these; a targeted pass avoids the false INSERT…SELECT
ambiguity a global binding would cause.

Closes the 3i cross-cut "schema-existence fires on INSERT VALUES"
gate item, and closes the DA finding #12 (UPSERT DO UPDATE SET column
now flagged like a top-level UPDATE's SET column). Residual: bare
sql_expr_ident refs in the DO UPDATE SET RHS / WHERE remain
unvalidated for upserts (the documented flat-scope limitation).

Tests (+5): unknown INSERT column flagged + known silent; unknown
DO UPDATE SET column flagged + known/excluded silent; predicate
warning (= NULL) fires on a SQL UPDATE WHERE (cross-cut). 1596 pass /
0 fail / 1 ignored. Clippy clean.
2026-05-22 22:02:33 +00:00
claude@clouddev1 2d1112d0f3 grammar+db: 3i — not_null_missing diagnostic + TableColumn constraints (ADR-0033 §8.3)
Extend SchemaCache TableColumn with not_null + has_default (with a
TableColumn::new constructor for the common no-constraint case),
populated in build_schema_cache from ColumnDescription (a PK column
counts as not-null). New dml_not_null_missing_diagnostics pass: a
WARNING when a SQL INSERT's explicit column list omits a column that
is NOT NULL with no DEFAULT — advisory (the engine enforces it).
serial/shortid (auto-filled) and defaulted columns are excluded.
Anchored on the target-table ident (no token for the omitted column).

Catalog key diagnostic.not_null_missing (engine-neutral). Tests (+4):
fires on omitted required column; silent when included, when
defaulted, and for auto-gen serial/shortid. ~24 TableColumn literal
sites updated for the two new fields (build clean). 1591 pass / 0
fail / 1 ignored. Clippy clean.

All three ADR-0033 §8 DML diagnostics now implemented. Remaining 3i:
cross-cut verification + #12 UPSERT DO UPDATE validation.
2026-05-22 21:58:12 +00:00
claude@clouddev1 6db8253c25 grammar+db: 3i — insert_arity_mismatch diagnostic (ADR-0033 §8.1)
New dml_insert_arity_diagnostics pass (ERROR): when an explicit
(column_name_list) arity disagrees with a row's arity. VALUES tuples
are checked per-row (each offending tuple emits its own diagnostic on
its span; matched rows stay silent). INSERT … SELECT compares the
first SELECT leg's projection arity, anchored on the first projection
item; a WITH-prefixed row source is skipped (engine still reports it —
a false positive would be worse). No-column-list form deferred
(needs schema; outside the 3i gate).

The VALUES walk stops at the first depth-0 keyword so an ON CONFLICT
(col) conflict target / RETURNING tail is not mis-counted as a value
tuple (caught by the existing upsert_excluded tests during dev).

Catalog key diagnostic.insert_arity_mismatch (engine-neutral).
Tests (+7): single-row + matched + per-row multi-row; INSERT…SELECT
mismatch + matched; ON CONFLICT interaction (only the real tuple
flagged, clean case silent). 1587 pass / 0 fail / 1 ignored. Clippy
clean. Remaining 3i: not_null_missing (needs TableColumn
not_null+default), cross-cut verification, #12 UPSERT DO UPDATE
validation.
2026-05-22 21:50:09 +00:00
claude@clouddev1 be63315e61 grammar+db: 3i — auto_column_overridden diagnostic (ADR-0033 §8.2)
New dml_auto_column_diagnostics pass: a WARNING when a SQL INSERT's
explicit column list names a serial/shortid (auto-generated) column —
the explicit value bypasses the auto-counter/generator and may collide
with later auto-generated values. Advisory only (ADR-0027 §1); the
statement still runs. Conflict-target columns (distinct
conflict_target_column role) are not mistaken for inserted columns.

Catalog key diagnostic.auto_column_overridden (engine-neutral).
Tests (+4): serial + shortid fire; omitted is silent; ON CONFLICT
target not falsely flagged. 1580 pass / 0 fail / 1 ignored. Clippy
clean. Remaining 3i: insert_arity_mismatch, not_null_missing (needs
TableColumn not_null+default), cross-cut verification, #12 UPSERT
DO UPDATE validation.
2026-05-22 21:45:02 +00:00
claude@clouddev1 6b8888f105 grammar+db: 3h — UPSERT ON CONFLICT DO NOTHING / DO UPDATE (ADR-0033 §9)
on_conflict_clause on SQL_INSERT_SHAPE: optional (col,…) conflict
target (distinct conflict_target_column role so it never enters
listed_columns), DO NOTHING / DO UPDATE SET … [WHERE …]. `do` is
factored out of the action Choice so nothing/update disambiguate
without tripping the walk_seq/walk_choice shared-prefix trap
(ADR-0033 Amendment 1). Worker runs the UPSERT verbatim (SQLite
native); no new execution path.

build_sql_insert: row_source now stops before the FIRST trailing
clause — ON CONFLICT (3h) or RETURNING (3g) — and do_sql_insert's
shortid auto-fill rewrite re-appends the whole trailing tail, so an
auto-filled INSERT keeps its ON CONFLICT / RETURNING.

excluded pseudo-table (§9): resolves to the target's columns inside
the DO UPDATE action and completes at `excluded.|`, but stays flagged
as unknown_qualifier in VALUES / RETURNING / non-upsert statements.
Diagnostic pass scopes it by the DO UPDATE byte-range (update token →
RETURNING/end); completion resolves it against the INSERT target's
current_table_columns. NOTE: scoping uses byte-range rather than the
plan's prescribed from_scope TableBinding push — same behaviour, no
walker scope-frame change.

Tests (+13): grammar accept/reject; DO NOTHING / DO UPDATE-excluded /
no-target execution + persistence; auto-fill × ON CONFLICT with a
REAL unique conflict (proves the clause survives the rewrite, not a
no-op); excluded resolves in DO UPDATE SET + WHERE, flagged in VALUES
(incl. same statement), unknown column under excluded; excluded.|
completion; conflict-target not in listed_columns. 1576 pass / 0 fail
/ 1 ignored. Clippy clean. Dev sql_insert entry word still removed in
3j.

Known follow-up (tracked for 3i): UPSERT DO UPDATE bare column refs
(SET LHS / WHERE) are not schema-validated, unlike regular UPDATE —
the INSERT target isn't a diagnostic binding. Fits 3i's cross-cut
SET/WHERE validation scope.
2026-05-22 21:28:24 +00:00
claude@clouddev1 fd8b74ba5e grammar+db: 3g — RETURNING on INSERT/UPDATE/DELETE (ADR-0033 §5)
Shared RETURNING_CLAUSE (reuses Phase-2 PROJECTION_LIST, now
pub(crate)) as an optional tail on all three SQL DML shapes.
`returning: bool` on the Command variants, set by the ast-builders
and threaded to the worker. run_returning collects the returned rows
as a DataResult (RETURNING mutates + yields in one pass), reusing
resolve_select_column_types for bare-column type recovery; computed
projections stay typeless. DeleteResult gains a `data` field rendered
alongside the cascade summary.

Follow-set fix: `returning` is added to the table-source and
projection bare-alias follow-sets so an INSERT … SELECT row source
stops before RETURNING instead of reading it as a table alias.

Auto-fill × RETURNING: build_sql_insert stops row_source before the
RETURNING token (keeping it preparable for shortid materialisation),
and plan_shortid_autofill re-appends the RETURNING tail so generated
shortids surface in RETURNING *.

Tests (+17): grammar accept on all three; INSERT/UPDATE/DELETE
RETURNING incl. *, aliases, multi-row, type recovery + computed-
typeless; auto-fill × RETURNING (single + multi-row distinct ids);
INSERT…SELECT…RETURNING execution; UPDATE…RETURNING zero-match;
DELETE…RETURNING cascade+rows; app-level render of both. Dev
sql_insert/sql_update/sql_delete entry words still removed in 3j.
1562 pass / 0 fail / 1 ignored. Clippy clean.
2026-05-22 20:44:55 +00:00