Commit Graph

49 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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
claude@clouddev1 2c86a1313e grammar+db: 3f — SQL DELETE + cascade summary (ADR-0033 §1/§7)
New src/dsl/grammar/sql_delete.rs (FROM <table> [WHERE] [;]),
Command::SqlDelete, Request::RunSqlDelete, do_sql_delete worker.

do_sql_delete mirrors the DSL do_delete: detect FK cascade by
before/after child row-count diffing, re-persist target + every
cascade-affected child, history-on-success inside the tx. Reuses
CommandOutcome::Delete -> handle_dsl_delete_success, so the
per-relationship cascade summary formatter is shared, not duplicated.

ADR-0033 Amendment 2: supersedes §7's WHERE-injected pre-count. Its
premise (DSL handler builds pre-counts from the typed Expr) was wrong
— do_delete uses count-diff. The pre-count would also have broken the
§2 parity promise by reporting SET NULL the DSL path doesn't. Count-
diff gives exact parity, no WHERE-byte extraction, and withdraws R2.
SET NULL reporting deferred for both paths (user-confirmed).

Tests: +6 grammar unit, +12 integration (cascade parity with DSL,
both R2 subquery cases, before-execute order, no-WHERE, FK-rejection
rollback, childless-parent, two-child cascade). 1542 pass / 0 fail /
1 ignored. Clippy clean. Dev sql_delete entry word removed in 3j.
2026-05-22 14:59:01 +00:00
claude@clouddev1 53808ed9d7 grammar+db: 3e — SQL UPDATE grammar + execution (ADR-0033 §2)
New src/dsl/grammar/sql_update.rs: SQL_UPDATE_SHAPE =
<table> SET col = sql_expr (',' …)* [WHERE sql_expr] [';'], the
__rdbms_* target rejection, and the shared sql_expr on both the
assignment RHS and the predicate. No --all-rows rail — a SQL
UPDATE without WHERE runs as written (ADR-0030 §12). Reuses
sql_select::WHERE_CLAUSE (now pub(crate)) so the predicate
diagnostics are identical. The target uses the shared `table_name`
ident role (not a bespoke one) so the Phase-2 schema-existence and
predicate-warning passes collect it as a scope binding and check
the SET / WHERE columns for free — a bespoke role left them
unchecked (the cross-cut tests caught this).

Command::SqlUpdate { sql, target_table }; Request::RunSqlUpdate +
do_sql_update (execute validated SQL via execute_with_fk_enrichment,
re-persist the target CSV, append history.log). 3e surfaces the
affected-row count only; precise row output is RETURNING (3g), so
the update-success render skips a column-less data set rather than
showing a misleading "(no rows)" band. Behind the dev `sql_update`
entry word until 3j.

Tests: grammar accept/reject; integration (single/multi-col,
no-WHERE all-rows, sql_expr in SET, scalar subquery in SET,
zero-match success, history); walker cross-cut (unknown SET column
→ unknown_column, `= NULL` in WHERE → eq_null warning); app-level
render-guard both ways (column-less → count only; with columns →
table renders). 1524 green, clippy clean.
2026-05-22 13:57:21 +00:00
claude@clouddev1 78ad476d24 db+grammar: 3d — shortid auto-fill for SQL INSERT (ADR-0033 §6)
When an INSERT's column list omits one or more shortid columns,
the worker now fills them. Command::SqlInsert gains listed_columns
and row_source, captured in build_sql_insert from the matched path
(the row source is located by the first values/select/with Word
token, so a string literal like 'select' can't be mistaken for the
keyword). do_sql_insert calls plan_shortid_autofill, which — per
the user-confirmed Option B — materialises the row source by
running it as a query, generates a distinct shortid per row via the
existing generate_shortid_batch (deduped against stored values),
and reconstructs a parameterised multi-row INSERT over the listed
columns plus the omitted shortid columns. Uniform for VALUES and
INSERT…SELECT, and handles multiple omitted shortids in one row
(each gets its own batch). No explicit list, no omitted shortid, or
a zero-row source → execute verbatim (the 3b path). serial stays
engine-filled via rowid. history.log keeps the original line, never
the rewrite (§11).

Tests: VALUES single/multi-row distinct; explicit override
honoured; INSERT…SELECT distinct fills; combined serial(engine) +
shortid(worker); two shortids (PK + non-PK) both fill; one provided
+ one omitted; compound-PK shortid member; mixed-case column name
(ADR-0009 DA gate); original-source-in-history on the rewrite path.
Still behind the dev `sqlinsert` entry word (3j). 1503 green,
clippy clean.
2026-05-22 07:26:54 +00:00
claude@clouddev1 c87363168f grammar+db: 3b — SQL INSERT grammar + minimal execution (ADR-0033 §1)
SQL_INSERT_SHAPE (INTO <table> [(cols)] VALUES tuple(s)) with __rdbms_*
target rejection; Command::SqlInsert{sql,target_table}; Request::RunSqlInsert
+ do_sql_insert worker (tx-guarded: execute, then finalize_persistence for
CSV + history before commit, so failures roll back and don't re-persist).
Auto-show is best-effort via last_insert_rowid range.

Isolated behind a dev `sqlinsert` entry word (Advanced) so the SQL path is
testable without making `insert` a shared word yet (that's 3j, after 3d
auto-fill parity). Command::SqlInsert carries only sql+target_table; the
plan's listed_columns/returning land in 3d/3g where they're read.

6 grammar accept/reject tests + 8 integration tests (single/multi-row,
column-list, full-arity, history, rollback-on-failure, multi-row atomicity,
parse-path reconstruction, internal-table rejection). 1452 baseline green.
2026-05-21 18:51:21 +00:00
claude@clouddev1 6369066fe4 grammar: SQL SELECT end-to-end (ADR-0030 Phase 1)
The first cut of advanced-mode SQL: a `select` line in advanced
mode parses, runs against the database, and renders its rows
through the existing data-table renderer; the same line in
simple mode lights up the precise "this is SQL" hint instead of
running.

Walker mode gate (ADR-0030 §2)
------------------------------
- `WalkContext` gains a `mode: Mode` field; `Mode` derives
  `Default` (= `Simple`, matching the app's startup mode).
- `grammar::is_advanced_only` keys an advanced-only entry-word
  set (Phase 1: just `select`). When the walker matches an
  advanced-only entry word with `ctx.mode == Simple`, it
  short-circuits to a `WalkOutcome::ValidationFailed` carrying
  the `advanced_mode.sql_in_simple` catalog key — the input
  highlights as a keyword, the validity indicator goes ERROR,
  and the parse-error layer renders the "switch with `mode
  advanced`, or prefix the line with `:`" hint.
- `parser::parse_command_with_schema_in_mode` (and the
  schemaless `parse_command_in_mode`) threads the mode into
  `WalkContext`; existing `parse_command*` entry points default
  to `Mode::Advanced` (most permissive) so back-compat callers
  see the full grammar.
- `App::submit` is unified: both modes route through
  `dispatch_dsl(&effective_input, effective_mode)`, which now
  parses with the line's effective mode. The placeholder
  advanced-mode echo branch is gone.

Builder signature sweep (ADR-0031 §2)
-------------------------------------
- `CommandNode.ast_builder` gains a `source: &str` parameter,
  forwarded by the walker. `build_select` reads it to put the
  validated SQL text into `Command::Select`; the 21 existing
  builders accept it as `_source`.

SQL `SELECT` (ADR-0030 §6, ADR-0031)
-------------------------------------
- New `Command::Select { sql: String }` variant. Every
  exhaustive `match Command` updated (`verb`, `target_table`,
  `build_translate_context`, `execute_command_typed`,
  `typing_surface`'s label).
- `grammar::data::SELECT` `CommandNode`: projection (`*` or
  `expr [as alias]` list), optional `FROM <table>`, optional
  `WHERE`/`ORDER BY`/`LIMIT`, optional trailing `;`. The
  expression slots reference the ADR-0031 fragment through
  `Subgrammar(&sql_expr::SQL_OR_EXPR)`. The `FROM` table-name
  slot carries a `reject_internal_table` validator that
  refuses `__rdbms_*` references at parse time.
- The `FROM` clause is optional — `select 1`, `select upper('x')`
  (zero-table constant/function-call SELECTs) work alongside
  the single-table form. Standard SQL admits them and they are
  the canonical learner probe.
- Implicit projection aliasing (`select a x`) is deliberately
  unsupported — `from` is a keyword, the bare alias would be
  ambiguous; only `select a as x` is admitted.

Worker / runtime
----------------
- `Request::RunSelect { sql, source, reply }` + a new
  `Database::run_select` method. `do_run_select_request` runs
  the prepared statement, collects rows into a `DataResult`
  with `column_types: Vec<None>` (Phase-1 SELECT result columns
  carry no playground type per ADR-0030 §6), and appends the
  literal source line to `history.log` so replay re-runs it
  (ADR-0030 §11).
- `runtime::execute_command_typed` gains a `Command::Select`
  arm that calls `database.run_select(sql, src)` and maps to
  `CommandOutcome::Query`, which flows into the existing
  `AppEvent::DslDataSucceeded` → `render_data_table` path.

Catalog (ADR-0019)
------------------
- `advanced_mode.sql_in_simple` — the walker's gate message.
- `select.internal_table` — the `__rdbms_*` rejection.
- `parse.usage.select` — the parse-error usage template.

Tests
-----
Two `app::tests` cases that pinned the pre-ADR-0030 placeholder
echo are updated to pin the new dispatch contract — both verify
that the advanced-mode `select` (one persistent, one via the
`:` one-shot) produces `ExecuteDsl(Command::Select)` with the
submission's effective mode tagged on the echo. The matching
walking-skeleton test is updated likewise.

A separate follow-up commit lands the ambient mode-threading
(completion / live overlay / validity indicator) so simple-mode
users do not see SQL surfaced through Tab or the live error
overlay either — the dispatch-layer gate landed here is the
behavioural foundation that follow-up builds on. Integration
tests for the full end-to-end land in a third commit.
2026-05-19 21:46:56 +00:00
claude@clouddev1 5e97f6ac6a constraints: CHECK-violation friendly error + typing-surface matrix (ADR-0029 §10)
Completes ADR-0029's implementation: the friendly-error layer
now names the rule a CHECK violation broke, and the
typing-surface matrix covers the whole constraint grammar.

CHECK-violation friendly error (ADR-0029 §10):
- enrich_dsl_failure gains a CHECK branch — it reads the column
  from the engine's `CHECK constraint failed: <column>`
  message, then resolves the table, the offending value, and
  the column's compiled CHECK expression.
- FailureContext / TranslateContext carry the resolved
  check_rule; translate_check renders "the value <v> breaks the
  rule `<rule>`" when it is known, falling back to the plain
  hint otherwise.

Typing-surface matrix: a new `constraints` submodule, 14 cells
covering the create-table / add-column constraint suffix and
the add-constraint / drop-constraint commands (174 → 188).

16 tests added (1 translate unit, 1 enrichment integration, 14
matrix cells).
2026-05-19 18:54:48 +00:00
claude@clouddev1 abce1188f2 constraints: add constraint / drop constraint on existing columns (ADR-0029 §2.2)
Adds the two commands for modifying a column's constraints after
creation, completing ADR-0029's §2.2 surface.

Grammar (dsl/grammar/ddl.rs): `add constraint <constraint> to
<T>.<col>` reuses the §2.1 COLUMN_CONSTRAINT choice; `drop
constraint <kind> from <T>.<col>` names only the kind. Both join
the `add` / `drop` choices, discriminated by the `constraint`
form word.

AST (dsl/command.rs): `Command::AddConstraint` / `DropConstraint`
plus the `Constraint` / `ConstraintKind` enums.

Worker (db.rs): `do_add_constraint` / `do_drop_constraint` apply
the change through the rebuild-table primitive. `add` runs the §5
dry-run first — `not null` / `unique` / `check` against a
populated column are refused, before any write, with a
pretty-printed table of offending rows. §9 redundant-on-PK
declarations and §6 `default` on an auto-generated column are
friendly refusals; dropping a constraint the column does not
carry is likewise refused.

Also fixes schema_to_ddl, which suppressed UNIQUE for every PK
column — a compound-PK member is not individually unique, so an
explicit UNIQUE on it must survive the rebuild.

23 tests added (6 grammar, 17 worker); 3 completion-test and 3
matrix snapshots updated for the new `constraint` subcommand.
2026-05-19 18:31:57 +00:00
claude@clouddev1 eff2ee8d14 refactor: ColumnSpec / AddColumn carry constraint fields (ADR-0029 scaffolding)
Expand ColumnSpec and Command::AddColumn with the four
ADR-0029 constraint slots (not_null, unique, default, check),
all defaulting off; `Database::add_column` now takes a
ColumnSpec. No behaviour change — the grammar to set the
fields and the DDL to enforce them land in the following
commits. Isolated here so those commits stay readable.

Adds ColumnSpec::new for the unconstrained case; 110 call
sites updated. 1172 tests pass; clippy clean.
2026-05-19 14:04:36 +00:00
claude@clouddev1 d17addddd7 explain: explain command end to end (ADR-0028 steps 2–3)
Add the `explain` prefix command — `explain show data`,
`explain update`, `explain delete` — from grammar through to a
rendered plan tree.

- Grammar: an `EXPLAIN` CommandNode whose shape is a Choice over
  the three explainable query shapes, referenced (not
  duplicated) through `Subgrammar`. `Command::Explain { query:
  Box<Self> }`; `build_show_data` is extracted so the role-based
  builders serve both standalone and explain-wrapped commands.
- Worker: SQL construction is split out of do_query_data /
  do_update / do_delete into `build_*_sql`, so EXPLAIN QUERY
  PLAN runs the exact same statement. `Request::ExplainPlan` /
  `do_explain_plan` capture the plan; `QueryPlan` / `ExplainRow`
  carry it back. EXPLAIN QUERY PLAN never executes, so
  explaining update/delete changes nothing.
- Display SQL: the executed statement with `?N` parameters
  inlined as standard-SQL literals via a quote-aware scan.
- Render: `render_explain_plan` draws the box-drawing plan tree
  (plain output; ADR-0028 step 4 adds the styled tree).
- Catalog: `parse.usage.explain` and the `help.data.explain`
  entry, so `explain` shows up in the in-app `help` listing.

1151 tests pass (+18); clippy clean.
2026-05-19 12:38:02 +00:00
claude@clouddev1 3a40ae27e7 runtime: don't record an unmodified temp as the --resume target
On launch an empty temp project is created but, by design
(ADR-0015), auto-deleted on quit while still empty. The
unconditional `write_last_project` at startup recorded that
temp's path anyway, so a later `--resume` resolved to a
since-deleted directory and printed a confusing
"recorded project … no longer exists".

All three resume-pointer writes are now gated on
`!project.is_unmodified_temp()`: the startup write, the
on-switch write (a `new`-command switch to a fresh temp no
longer records it), and a new on-quit write. The quit write is
where a launch-temp the user *filled with content* finally
gets remembered — startup skipped it while it was still empty.
An unmodified empty temp is deleted, never recorded; the two
dispositions are mutually exclusive.

The "no previous project" friendly error the user asked for
already exists (`project.resume_no_previous`, wired in the
resume resolution) — verified, no change needed. The gate
predicate `is_unmodified_temp` is covered by existing
integration tests. 1131 passing, clippy clean.
2026-05-19 10:27:01 +00:00
claude@clouddev1 c1c9f6cbc4 runtime: extract the indicator debounce into a tested state machine
The validity-indicator debounce was two locals in the event
loop (indicator_pending + app.input_indicator) with no unit
coverage — ADR-0027's as-built notes flag it as untested async
glue. The decision logic is now an IndicatorDebounce struct:
note_event (a keystroke hides + arms; non-key events leave it
be), settle (the quiet window elapsed → show the verdict +
disarm), is_armed (drives the recv timeout), visible (mirrored
into app.input_indicator for the renderer).

No behaviour change — the tokio timer and terminal stay in the
loop. 7 unit tests cover the debounce contract: the keystroke /
settle cycle, clean verdicts, and that a background event
mid-typing does not cancel the owed recompute. 1125 passing,
clippy clean.
2026-05-19 09:44:28 +00:00
claude@clouddev1 9e10997ffd runtime: debounce the validity indicator (ADR-0027 step E)
The event loop now time-boxes `recv` while an indicator
recompute is owed: every keystroke hides the indicator and
arms an `INDICATOR_DEBOUNCE` (1s) window; once typing pauses
that long the runtime computes `App::input_validity_verdict`
and shows `[ERR]` / `[WRN]`. An idle session (nothing owed)
still blocks plainly on `recv` — no wake-ups.

`update()` stays pure — the debounce timer lives in the
runtime; `App` only holds the resulting `input_indicator`
state, which the runtime clears on a keystroke and sets when
the quiet interval elapses.

`App::input_validity_verdict` is tested directly (a
simple-mode verdict, and silence in advanced mode / the `:`
one-shot); the debounce timing itself is runtime-loop glue,
covered at the integration level.
2026-05-19 07:30:47 +00:00
claude@clouddev1 f75f71bbe4 WHERE expressions: wire into update/delete/show data + SQL gen (ADR-0026 steps 3-4)
Wires the stratified WHERE-expression fragment into the three
filter commands and compiles the resulting Expr to SQL.

Grammar (data.rs): the `update` / `delete` `where` clause is
now the expression fragment (`Subgrammar(&expr::OR_EXPR)`) in
place of the single `col = val` slot; `show data` gains an
optional `where <expr>` and an optional `limit <n>` (a
non-negative integer, validated at parse time). The
expression's right-hand operands are a schema-aware
`DynamicSubgrammar` so the hint panel still narrows to the
left column's type (ADR-0026 §8) — but the inner grammar is
permissive: a type-mismatched literal still parses (§7).

AST: `RowFilter::Where{column,value}` -> `RowFilter::Where(Expr)`;
`ShowData` gains `filter: Option<Expr>` and `limit: Option<u64>`.
A `RowFilter::eq` convenience constructor keeps simple-equality
call sites and tests readable.

SQL (db.rs): `compile_expr` lowers an `Expr` to a
parameterised WHERE — every literal a `?` placeholder,
identifiers `quote_ident`-quoted, `<>` for inequality. A
literal compared against a column binds through that column's
type where compatible and falls back to its syntactic shape on
a mismatch (§7 — permissive). `show data ... limit n` emits
`LIMIT ?` with an implicit primary-key `ORDER BY`, so it is a
stable "first n by primary key".

completion.rs: `invalid_ident_at_cursor` no longer mis-flags a
digit-led literal (`1`) as an unknown column now that the
WHERE operand slot also accepts a column reference; a
`ProseOnly` slot suppresses keyword candidates even when the
expected set also carries a column ident.

11 db integration tests cover AND / OR / NOT, BETWEEN, IN,
LIKE, filtered `show data`, and limit ordering; walker and
expr unit tests cover the parse surface. Type-mismatch /
`= NULL` diagnostic flagging (§7 highlight + hint) is the
remaining ADR-0026 piece.
2026-05-18 23:12:33 +00:00
claude@clouddev1 0dc159fd7e Indexes: add index / drop index, persistence, display (ADR-0025)
Implement ADR-0025 — indexes as a DSL DDL feature.

- Grammar: `add index [as <name>] on <T> (<cols>)`, `drop index
  <name>` / `drop index on <T> (<cols>)`, plus a `--cascade`
  flag on `drop column`.
- db.rs: index operations over the engine's native index
  catalog (no metadata table). The rebuild-table primitive now
  captures and recreates indexes, so `change column` and the
  relationship operations no longer silently drop them.
- `drop column` refuses an indexed column unless `--cascade`,
  which drops the covering indexes and reports each.
- Persistence: additive `indexes:` list in `project.yaml`
  (version unchanged); round-trips through rebuild/export/import.
- Display: an `Indexes:` section in the structure view and a
  nested tables/indexes items panel (S2).

Reconciles requirements.md (C3 index portion, S2 satisfied)
and CLAUDE.md. 1038 tests passing (+31), clippy clean.
2026-05-16 00:15:55 +00:00
claude@clouddev1 f46606b12e Runtime: schema-aware replay parsing
run_replay parsed each line with the schemaless parse_command, so
Phase D typed-slot rejections (wrong-count value lists, wrong-type
column values) fired only at bind time during replay — inconsistent
with the interactive path (handoff-12 §2.1).

run_replay now re-snapshots the schema per line (the schema mutates
as replayed create-table / add-column commands run) and parses with
parse_command_with_schema. Extracted build_schema_cache, shared with
the interactive refresh_schema_cache.

Added a replay integration test asserting a typed-slot violation is
caught at parse time (through the replay.error_parse wrapper).
2026-05-15 22:31:19 +00:00
claude@clouddev1 abebd7944f ADR-0024 Phase D (full): schema-aware value typing
Schema-aware typed value slots — the central design claim of
ADR-0024 §Phase D. Insert / update / delete value slots now
dispatch on the user-facing column type at parse time, rejecting
mis-shaped input with localised wording instead of waiting for
the bind-time error.

What changed:

**SchemaCache extension** (`src/completion.rs`):
- New `TableColumn { name, user_type }` for per-table column
  metadata.
- `SchemaCache.table_columns: HashMap<String, Vec<TableColumn>>`.
- `SchemaCache::columns_for_table(name)` — case-insensitive
  lookup, mirrors the walker's case-insensitive entry-word
  resolution.

**WalkContext schema plumbing** (`src/dsl/walker/context.rs`):
- `WalkContext<'a>` gains a lifetime and a `schema: Option<&'a
  SchemaCache>`. `WalkContext::new()` keeps the schemaless
  default; `with_schema(s)` is the new schema-aware constructor.

**Parser entry point** (`src/dsl/parser.rs`):
- `parse_command_with_schema(input, schema)` is the new public
  schema-aware variant. `parse_command(input)` becomes a thin
  wrapper that delegates with `None` for back-compat.
- Internal `try_walker_route` accepts an `Option<&SchemaCache>`
  and threads it into the WalkContext.

**Node::Ident writes_table/writes_column** (`src/dsl/grammar/mod.rs`):
- Two new fields on `Node::Ident`. When `writes_table: true` and
  `source: Tables`, the walker writes the matched ident's name
  into `current_table` and resolves `current_table_columns`
  against the schema cache. When `writes_column: true` and
  `source: Columns`, the walker writes the resolved
  `TableColumn` into `current_column`.

**Walker driver DynamicSubgrammar dispatch** (`src/dsl/walker/driver.rs`):
- The `Node::DynamicSubgrammar(factory)` branch now resolves the
  factory at walk time and `Box::leak`s the result so its inner
  static-slice fields (Choice/Seq) have the lifetime the walker
  expects (per ADR-0024 §sub-grammars). The leak is bounded by
  command-shape complexity per walk; per-walk arena is a future
  optimisation.
- `walk_ident` extends to perform the schema writes when the
  flags are set.

**Typed value slot factories + dynamic sub-grammars** (`src/dsl/grammar/shared.rs`):
- `int_slot` / `real_slot` / `decimal_slot` / `bool_slot` /
  `text_slot` / `date_slot` / `datetime_slot` / `blob_slot` —
  one per `Type`. Each accepts the appropriate literal kind plus
  `null`; integer-only validator rejects `3.14` at int columns;
  decimal validator pins numeric shape.
- `slot_for_type(ty) -> Node` is the dispatcher.
- `current_column_value(ctx) -> Node` is the dynamic sub-grammar
  for `set col = …` and `where col = …` values; reads
  `current_column` and dispatches via `slot_for_type`.
- `column_value_list(ctx) -> Node` is the dynamic sub-grammar
  for `insert into T values (…)`; reads `current_table_columns`
  and unfolds a Seq of typed slots separated by commas.
- Both fall back to the schemaless `VALUE_LITERAL` choice when
  the context lacks the schema-resolved entries — keeps
  schemaless `parse_command` callers (tests, replay path)
  working.

**Data-command grammar wires the new types** (`src/dsl/grammar/data.rs`):
- `TABLE_NAME_INSERT` / `TABLE_NAME_WRITES` (new): table-name
  slots that set `writes_table: true`. Used by insert / update /
  delete to populate `current_table_columns`.
- `SET_COLUMN` / `FILTER_COLUMN` (new): column-name slots in
  `set col=…` / `where col=…` set `writes_column: true`.
- `INSERT_VALUES_LIST` becomes `DynamicSubgrammar(column_value_list)`.
- `UPDATE_ASSIGNMENT` and `WHERE_CLAUSE` use
  `PER_COLUMN_VALUE = DynamicSubgrammar(current_column_value)`.

**Runtime plumbs schema-with-types** (`src/runtime.rs`):
- `refresh_schema_cache` calls `describe_table` for each table
  and populates `SchemaCache::table_columns` with
  `TableColumn { name, user_type }` entries. Best-effort: a
  `describe_table` miss leaves that table unpopulated and the
  walker falls back to schemaless dispatch.

**App dispatches with schema** (`src/app.rs`):
- `dispatch_dsl` routes through `parse_command_with_schema(&self
  .schema_cache, …)` so live typing/dispatch sees the typed
  slots. The replay path stays schemaless (deferred — replay
  bind-time errors still catch type mismatches).

**Catalog** (`src/friendly/strings/en-US.yaml`, `src/friendly/keys.rs`):
- New `parse.custom.bind_type_mismatch` entry with `{found}` and
  `{expected}` placeholders. Surfaced by the int_slot /
  decimal_slot validators.

Tests:
- 11 new walker-side Phase D tests cover insert / update /
  delete with schemas — typed acceptance per column, decimal
  rejection at int columns, null acceptance at any slot,
  multi-assignment per-column dispatch, schemaless fallback.
- The pre-existing `parse_command(input)` test suite (no
  schema) still passes — the fallback path is behaviour-
  preserving.
- 828 passing total, 0 failing, 1 ignored. Clippy clean.
2026-05-15 17:45:56 +00:00
claude@clouddev1 266b4c2ef4 ADR-0024 Phase F (full) step 3: delete legacy parser modules
Removes the last consumers of `dsl::lexer`, `dsl::keyword`, and
`dsl::ident_slot`, then deletes the modules.

- `Theme::token_color(&TokenKind)` deleted along with its test;
  `Theme::highlight_class_color(HighlightClass)` is the sole
  highlight-colour mapper (the walker's `per_byte_class` feeds
  it directly).
- `IdentSource` (`dsl::grammar`) absorbs the schema-list /
  expected-label / round-trip semantics that previously lived
  on `IdentSlot`. Adds `completes_from_schema`, `expected_label`,
  and `from_expected_label` methods. The walker's
  `Expectation::Ident { source }` and the schema-lookup request
  on the database worker now share one enum.
- `SchemaCache::for_slot(IdentSlot)` → `for_source(IdentSource)`.
- `Database::list_names_for` and the `Request::ListNamesFor`
  worker variant take `IdentSource`. Internal tables and column
  / relationship lookups dispatch on the same enum.
- `InvalidIdent.slot: IdentSlot` → `InvalidIdent.source: IdentSource`.
  The `invalid_ident_at_cursor` rendering branch in
  `input_render.rs::ambient_hint` updates accordingly.
- Completion's keyword filter (`Keyword::from_word`) becomes
  "backticked items whose payload is all ASCII alphabetic" —
  punct and digit literals still surface through their own
  candidate sources (composite-literal, flag, schema-ident);
  the alphabetic filter excludes them from the keyword bucket.
- `friendly::keys::tests::keyword_and_punct_have_complete_token_vocabulary`
  is dropped. It cross-checked `Keyword::ALL` / `Punct::ALL`
  against catalog entries; both enums are gone. The
  `parse.token.keyword.*` / `parse.token.punct.*` catalog
  entries themselves survive for one more commit (catalog
  cleanup, ADR-0024 §cleanup-pass); the
  `keys_validate_against_catalog` test still pins them.
- Modules deleted: `src/dsl/lexer.rs`, `src/dsl/keyword.rs`,
  `src/dsl/ident_slot.rs`.

Tests: 806 passing, 0 failing, 1 ignored. The drop from 852
reflects the removed module-internal tests (~32 lexer, 7
keyword, 4 ident_slot, 1 theme token_color, 1 friendly keys
keyword/punct), and is the expected outcome.

Clippy clean with `nursery` lints + `-D warnings`.
2026-05-15 08:33:59 +00:00
claude@clouddev1 1e06490572 round-5 follow-up: completion + i18n sweep
Four user-reported gaps from the round-4 testing pass:

1. Empty-prompt hint reworded from "(no active hint)" to
   "Type a command — press Tab for options, `help` for a
   list" (6 snapshots updated to reflect 80-col truncation).

2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
   new, load, export, import, mode, messages) now flow through
   the DSL parser:
   - 15 new keywords + catalog token entries
   - new Command::App(AppCommand) AST with 11 variants
   - parse-first dispatch in submit() (app commands work in
     both simple and advanced modes)
   - pre-chumsky source-slice for `export <path>` /
     `import <zip> [as <target>]` mirrors the replay precedent
   - UsageEntry registry entries so parse errors surface
     relevant usage templates
   - `mode <bad>` / `messages <bad>` use try_map for the
     friendly "unknown mode/messages" wording

3. DSL completion gaps:
   - `1:n` surfaces as a composite candidate at `add `
   - --all-rows / --create-fk / --force-conversion /
     --dont-convert surface as new CandidateKind::Flag
     candidates (coloured with tok_flag in hint panel)
   - filter_clause .labelled() wrap removed so chumsky's
     expected-set surfaces the constituent options

4. Hardcoded user-facing strings migrated to catalog:
   - 4 parser custom errors (incl. the known "tables need at
     least one column" wart)
   - UnknownType Display now via parse.custom.unknown_type
   - UI panel titles + mode labels (Output / Hint / SIMPLE /
     ADVANCED / Advanced:)
   - app.rs cascade rendering (action labels + summary)
   - runtime --resume CLI stderr
   - db.rs change-column diagnostic tables (7 headers + 3
     wrapper summaries + force-conversion hint)

Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.

Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
  (DbError, ArgsError, ArchiveError, PersistenceError,
  LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
  implicitly via parse-first dispatch; broader ADR
  amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
  can't offer `as` because `save` parses bare; same shape
  as `--create-fk` after a complete `add relationship`).
2026-05-13 15:58:29 +00:00
claude@clouddev1 7a32c13bd5 ADR-0022 stage 8d: schema cache refresh wiring
New `AppEvent::SchemaCacheRefreshed(SchemaCache)` event +
App handler that stores it on `app.schema_cache`.

Runtime helper `refresh_schema_cache(database, event_tx)`
fetches table / column / relationship names via the
`list_names_for` worker request (added in stage 7) and posts
the assembled cache. Wired into every site that already
posts `TablesRefreshed`:
  - `seed_initial_tables` (initial project load).
  - Project-switch path in `handle_project_switch`.
  - `RebuildSucceeded` path.
  - Post-DDL path (`spawn_command`).
  - Post-replay path.

Result: schema-aware identifier completion (added in 8c)
becomes live — Tab on `show data ` offers the actual table
names from the current project, `drop column from T: ` (or
similar) offers existing columns, etc. The cache stays
fresh across DDL and rebuild without per-keystroke worker
round-trips (one refresh per schema-mutating action is
amortised across many subsequent keystrokes).

Best-effort: a failed `list_names_for` for any individual
slot kind leaves that field empty in the cache rather than
suppressing the whole refresh — partial completion beats
no completion.

Tests: 738 passing, 0 failing, 1 ignored (unchanged
total — this stage is wiring, not new test surface; the
synthetic-cache tests from stage 8c remain the regression
net for the completion logic itself). Clippy clean.
2026-05-11 20:57:09 +00:00
claude@clouddev1 720511ef29 ADR-0019 §9 sweep (2/2): help blocks + modals + system notes
Final pass of the i18n migration sweep. Every user-visible
string in `src/` now flows through the catalog via `t!()`.

## Categories migrated in this commit

- **help.cli_banner** — the entire `cli::HELP_TEXT` const,
  formerly a 40-line `&'static str`, is now a YAML block in
  the catalog. The const is replaced by a thin
  `cli::help_text() -> String` wrapper that performs the
  catalog lookup. `main.rs` calls `help_text()` for both
  `--help` output and the args-parse error path. The two
  integration tests that referenced `HELP_TEXT` directly are
  updated.
- **help.in_app_body** — the in-app `help` command's body is
  one YAML block; `note_help` becomes 5 lines that iterate
  the lines and emit each as its own output row (preserving
  the renderer's "one logical line = one display row"
  invariant for accurate scroll math).
- **modal.*** — load picker, rebuild confirm, and save-as
  path-entry strings: rebuild_cancelled, load_cancelled,
  generic_cancelled, load_picker_nothing,
  path_entry_empty_name, path_entry_empty_path.
- **dsl.failed** — the `"<verb> <subject>" failed: <rendered>`
  wrapper around the friendly-error layer's translated
  message.
- **dsl.running** — the `running: <input>` echo line shown
  above each command's response. (Note: the en-US prefix
  "running: " is hardcoded in the parse-error caret-padding
  calculation. Translators changing the prefix must keep the
  width consistent — documented inline.)
- **advanced_mode.not_implemented** — the placeholder echo
  shown when SQL hits the unimplemented advanced-mode path
  (Q1 territory).
- **fatal.persistence** — the FATAL banner for
  PersistenceFatal events (ADR-0015 §8).
- **project.{load_path_missing,saveas_target_exists,**
  **import_zip_missing}** — runtime-side project-switch
  validation errors that surface via ProjectSwitchFailed.

## Catalog start-up ordering

`main.rs` now calls `friendly::catalog()` at the very top
(before args parsing) so `help_text()` works in both the
success path and the args-error path. A corrupted build
artefact still fails loudly with a useful panic; the
practical risk is essentially zero since the catalog is
`include_str!`'d at compile time and validated by the unit
test before shipping.

## Remaining literals

The only `note_*` calls in `src/` that still pass plain
strings are inside `#[cfg(test)]` modules — synthetic test
fixtures, not user-visible. The codebase passes the "every
user-visible string flows through the catalog" bar.

## Tally

610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints.

## What this closes

ADR-0019 §9 (migration sweep) — done.

ADR-0019 itself is now fully implemented:
- §1-§5: catalog + translator + voice + verbosity ✓ (`eac7e5b`)
- §6: row pinpointing + schema enrichment ✓ (`431645a`)
- §9: migration sweep ✓ (this + `aff528a`)
- §10: anchor phrases preserved throughout ✓
- The five "Out of scope" items remain explicitly bounded
  to future ADRs (advanced-mode SQL, settings persistence,
  pluralisation, runtime locale, value formatting,
  constraint management).
2026-05-09 22:29:28 +00:00
claude@clouddev1 aff528aa3f ADR-0019 §9 sweep (1/2): replay/client_side/ok/mode/messages/project/parse
First half of the catalog migration sweep. Six categories of
user-visible literals moved from inline `format!` calls to the
i18n catalog via `t!()`:

- **replay.*** — `[ok] replay … N command(s) run`,
  `replay … failed at line N: …`, the `> command` echo, and
  the inner `could not open` / `parse error` / `nested replay`
  wordings the runtime constructs inside `ReplayFailed.error`.
- **client_side.*** — the four [client-side] pedagogical notes
  from ADR-0017 §6 / ADR-0018 §9 (transformed,
  transformed_lossy, auto_fill_transition,
  auto_fill_add_serial, auto_fill_add_shortid). The
  `format_auto_fill_add_note` helper in db.rs now routes via
  the catalog too.
- **ok.*** — the `[ok] {verb} {subject}` summary header
  (consolidated through a new `App::note_ok_summary` helper)
  plus the per-operation row-count footers
  (`{count} row(s) inserted/updated/deleted`).
- **mode.*** — `mode: simple/advanced` set/show banners +
  `usage: mode …` + `unknown mode '{value}' …` errors.
- **messages.*** — `messages: short/verbose` set/show + the
  `unknown messages mode` error.
- **project.*** — `[ok] rebuild — {summary}`, `[ok] now
  editing: {display_name}`, `[ok] export — wrote {path}`, plus
  matching failure variants and the `usage: export/import`
  + `import: empty target after as` argument-parsing errors.
- **parse.*** — the `parse error: {detail}` wrapper around
  chumsky's structural output, the `{padding}^` caret pointer,
  and the `empty input` fallback for `ParseError::Empty`.

Catalog total: 99 lines of YAML across the new categories,
44 new entries declared in `keys.rs::KEYS_AND_PLACEHOLDERS`.
The validator (`keys_validate_against_catalog`) walks the
expanded list and confirms placeholder coverage / no format
specifiers / no engine vocabulary across every entry.

Anchor phrases (ADR-0019 §10) preserved verbatim; existing
substring assertions in the test suite hold.

## Tally

610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints. Release builds.

## Still ahead in the sweep

- Sweep 7: HELP_TEXT (CLI banner) + in-app `note_help` —
  large multi-line blocks.
- Sweep 8: modal labels (load picker, rebuild confirm,
  save-as path entry) + any remaining strays. Final pass.

Both shipping in a follow-up commit so this checkpoint
stays reviewable.
2026-05-09 22:20:34 +00:00
claude@clouddev1 431645ae60 ADR-0019 §6: runtime enrichment + row pinpointing
Closes the placeholder-substitution gap reported during manual
testing: FK violations were rendering `<value>` and `<column>`
literally because the App had no schema awareness. With this
change the runtime resolves the schema-dependent facts before
the App ever sees the failure.

## Architecture

- **Database** gains two public methods backed by new worker
  Request variants:
  - `read_relationships(table)` → (outbound, inbound) FK list
    (lifts the previously-private `read_relationships_*` pair
    into the public surface, behind a `RelationshipsReply`
    type alias).
  - `find_rows_matching(table, column, value, limit)` →
    `DataResult` for row pinpoint queries.

- **friendly module** gets:
  - New `FailureContext` struct: schema-resolved facts the
    runtime builds (table, column, value, parent_table,
    parent_column, child_table, optional diagnostic_table).
  - `TranslateContext` loses its lifetime parameter and gains
    `parent_table` / `parent_column` fields. All string fields
    are now `Option<String>` for ownership simplicity.
  - `TranslateContext::from_facts(operation, verbosity, facts)`
    helper.
  - Translator's FK paths now use `ctx.parent_table` /
    `ctx.parent_column` for child-side wording; FK Update gets
    a dedicated `fk_child_side_update` arm.
  - FK dispatch is enrichment-driven first
    (`parent_table` set → child-side; `child_table` set →
    parent-side), with operation as the tiebreaker.
  - The translator forwards `ctx.diagnostic_table` onto the
    `FriendlyError` so pinpointed rows render through the
    existing ADR-0017 §7 bordered renderer.

- **Event** `DslFailed` carries `(command, error, facts)`.
  The runtime populates `facts` via `enrich_dsl_failure`
  before posting the event.

- **Runtime** `enrich_dsl_failure(database, command, error)`
  classifies and resolves:
  - UNIQUE INSERT/UPDATE: parses `T.col` from engine message,
    finds the user's attempted value (with schema fallback
    for natural-order multi-value INSERT — including the
    serial/shortid auto-skip rule from `do_insert`), pinpoints
    the existing conflicting row(s) via `find_rows_matching`
    and renders as a `DiagnosticTable`.
  - NOT NULL INSERT/UPDATE: parses `T.col`; no value
    (definitionally null) and no pinpoint (engine doesn't
    identify the row).
  - FK INSERT/UPDATE: outbound relationship lookup picks the
    FK column the user is touching; resolves
    `parent_table`/`parent_column`/`value`. UPDATE falls back
    to inbound (parent-side) when no outbound match.
  - FK DELETE: inbound relationship lookup picks a child_table
    that references this row.

- **App** drops its old `attempted_value_for` /
  `column_from_qualified_target` helpers (their work moved to
  runtime where the Database is in scope).
  `build_translate_context` combines the runtime-supplied
  facts with the operation derived from the Command and the
  App's verbosity.

## Manual-test fixes folded in

Two issues surfaced during manual testing of the initial
implementation, both fixed:

1. Natural-order multi-value INSERT
   (`insert into Orders values (4, 11.99)`) skipped FK
   enrichment because `user_value_for_column` only knew the
   single-value short form. The schema-aware lookup
   (`user_value_for_column_with_schema`) now mirrors
   `do_insert`'s position-mapping rule (auto-generated
   columns skipped), so positional INSERTs onto tables with
   serial/shortid PKs resolve correctly. Regression test:
   `enrich_fk_insert_natural_order_multi_value_resolves_via_schema`.

2. The arity error on INSERT now lists the columns it
   expected — `expected 3 value(s) for (id, Name, Email), got 2`
   instead of the bare count. Surfaces what the user needs
   to fix without making them go check the schema.

## Tests

`tests/friendly_enrichment.rs` (+8 integration tests):
- UNIQUE INSERT with explicit columns: facts.{table, column,
  value, diagnostic_table} all resolved; pinpoint shows
  conflicting row.
- UNIQUE INSERT natural-order short form: schema fallback
  resolves the value.
- UNIQUE UPDATE: value pulled from assignments.
- NOT NULL INSERT: table+column resolved, value None
  (correct), no pinpoint.
- FK INSERT: parent_table, parent_column, value all resolved
  via outbound relationship lookup.
- FK INSERT natural-order multi-value: schema-aware lookup
  with auto-skip resolves correctly (regression for the
  manual-test bug).
- FK DELETE: child_table resolved via inbound relationship
  lookup.
- DbError::Unsupported: enrichment returns default
  FailureContext (no false positives).

App-level tests updated to populate `FailureContext` directly
(simulating runtime enrichment) for the verbosity / threading
checks.

## Tally

610 tests passing (was 603: +8 enrichment integration tests
minus 1 obsolete App-side helper test that the runtime
absorbed). Clippy clean with nursery lints. Release builds.
2026-05-09 22:10:05 +00:00
claude@clouddev1 eac7e5b81d ADR-0019 implementation: friendly error layer + i18n catalog
All eight implementation steps from ADR-0019's §"Order of
operations":

Step 1 — `src/friendly/` module skeleton; `t!()` macro; YAML
  catalog loader (`include_str!` + `serde_yml`); `{name}`
  substitution helper that rejects format specifiers per §8.4.

Step 2 — `error.*` catalog populated for UNIQUE / FK /
  NOT NULL / CHECK / type-mismatch / not_found / already_exists /
  generic / invalid_value, with verbose hints per
  pedagogical-voice rule (§5). Anchor phrases (§10) preserved
  verbatim.

Step 3 — `FriendlyError { headline, hint, diagnostic_table }`
  + renderer composing the three blocks per §7.

Step 4 — `translate(&DbError, &TranslateContext) → FriendlyError`.
  Classifies by `SqliteErrorKind` first, then by message text
  for the constraint family. `change column` failures route to
  the type-mismatch headline, subsuming the previous
  `friendly_change_column_engine_error` helper.

Step 5 — `DbError::friendly_message()` delegates to the
  translator with default context. Removed
  `friendly_change_column_engine_error` (absorbed) and
  `enrich_fk_message` (FK list moves to the deferred re-query
  step). One test rewritten to assert on the engine-classified
  payload rather than the removed enrichment text.

Step 6 — `messages (short|verbose)` app-level command parallel
  to `mode`. `App::messages_verbosity` (default verbose)
  threaded into `TranslateContext` via
  `App::build_translate_context`. `AppEvent::DslFailed` now
  carries the structured `DbError`, plus the App extracts the
  user's attempted value from `Command::Insert` / `Update`
  to fill the `{value}` placeholder for UNIQUE / NOT NULL.

Step 7 — Catalog validator (§8.6) checks for missing keys,
  unused/undeclared placeholders, format specifiers, and
  forbidden engine vocabulary. `main.rs` parses the embedded
  catalog at startup so a corrupted build artefact fails
  loudly there rather than at the first `t!()` call.

Step 8 — Anchor phrases (§10) held: existing tests asserting
  on "no such table", "already exists", "cannot be converted",
  etc. all pass without rewording.

## Tally

603 tests passing (was 561: +42 net). Clippy clean with
nursery lints. Release binary 7.7 MB.

## Deliberately deferred

- Schema-aware enrichment for FK violations (parent_table /
  parent_column / child_table) and the multi-value
  natural-order INSERT case for UNIQUE. Both need the
  Database handle in scope at translation time, so they
  bundle naturally with the row-pinpoint re-query work
  (ADR-0019 §6) — that follow-on adds runtime-side
  enrichment via a `Database` lookup and a structured
  failure-context carried on `DslFailed`. Until then,
  unfilled placeholders render as their `{name}` form for
  visual consistency with the catalog.
- Migration sweep (§9). Only `error.*` is catalog-driven so
  far; `help.*`, `ok.*`, `client_side.*`, `replay.*`,
  `parse.*`, modal labels, etc. migrate per-PR.
- Settings persistence for `messages`. In-session state for
  now; waits on the future settings ADR.
2026-05-09 12:43:37 +00:00
claude@clouddev1 c4ee264636 replay: new replay <path> command (A3, U4)
Implements the U4 replay command per handoff §A3:

  replay <path>

Reads <path> and dispatches each non-blank, non-`#`-comment
line through the same DSL pipeline as interactive input.
Aborts at the first per-line failure (parse or runtime),
reporting the line number; previously dispatched commands
stay applied (no rollback) — matches the "I'm replaying my
history" mental model where partial replay is a recoverable
state.

Architecture choices and why:

- **Parsed by the DSL parser** (Command::Replay), not as an
  app-level command alongside `import` / `export`. The
  handoff's implementation sketch was explicit and the
  parsed-AST shape gives us a clean test surface for the
  path-lexing rules. A new `path_literal` parser terminal
  accepts either a single-quoted string (escape rules
  mirror `string_literal` — `''` for a literal quote) or a
  bare run of non-whitespace, with explicit refusal of `'`,
  `(`, `)`, `;` in bare form. Empty paths fail at parse
  time so file-system-layer errors aren't shadowed by
  silly inputs.
- **Routed away from the worker thread.** Command::Replay
  is intercepted in `App::dispatch_dsl` and emitted as
  `Action::Replay` rather than `Action::ExecuteDsl`. Two
  reasons: (1) the worker has no filesystem context, and
  (2) the replay invocation must NOT land in
  `history.log` — otherwise `replay history.log` would
  re-trigger itself recursively. Only the individual
  sub-commands write to history.log via the normal
  per-command persistence path.
- **Inner loop separated from spawn.** `runtime::spawn_replay`
  is a thin tokio::spawn wrapper around `runtime::run_replay`,
  which is `pub` and returns a Vec<AppEvent>. The inner
  function is what tests exercise, sidestepping mpsc plumbing.
- **Relative paths resolve under the project root** so
  `replay history.log` works without ceremony from inside
  any project. Absolute paths pass through unchanged.
- **Nested `replay` is refused.** Allowing `replay foo` from
  inside a replay file invites infinite-loop footguns and
  opens design questions (transitive composition, ordering)
  we'd rather not answer right now. Refusal is explicit.

New plumbing:

- `Command::Replay { path }` AST variant + verb/target_table.
- `Action::Replay { path }` runtime action.
- `AppEvent::ReplayCompleted { path, count }` and
  `AppEvent::ReplayFailed { path, line_number, command, error }`.
- `runtime::run_replay` (public) and `runtime::spawn_replay`.
- App handlers render success as
  `[ok] replay <path> — N command(s) run` and failures as
  `replay <path> failed at line N: <error>` with a
  `  > <command>` echo line for line context. Line 0 is the
  "file open failed" signal — header reads
  `replay <path> failed: <error>` and the echo line is
  suppressed.
- In-app `help` lists the new command with a continuation
  describing comment/blank handling and the relative-path
  rule.

Tests (+20):

- 7 parser tests covering bare/quoted/escaped paths,
  case-insensitive keyword, and refusal cases (no path,
  empty quoted path).
- 9 integration tests in `tests/replay_command.rs`:
  - happy 3-line replay → 3 commands run, state mutated;
  - blank lines + `#` comments skipped;
  - empty file + only-comments file → count 0;
  - missing file → ReplayFailed line_number 0;
  - parse failure mid-replay → reports correct line +
    leaves earlier commands applied + does NOT run later
    lines;
  - runtime failure mid-replay (refers to nonexistent
    table) → reports correct line;
  - nested replay refused;
  - history.log contains per-command entries but NOT the
    `replay …` invocation itself.
- 4 App-level tests: Action::Replay dispatch (not
  ExecuteDsl); ReplayCompleted rendering; ReplayFailed
  rendering with and without line-number context.

541 -> 561 passing, clippy clean with nursery lints,
release build successful.

A future ADR on the parser-as-source-of-truth direction
(handoff §"Pending §3") would bring richer error reporting
for replay parse failures (currently uses the same
single-line wording as interactive parse failures, which is
adequate but not great when a script has many lines around
the failing one).
2026-05-08 15:06:56 +00:00
claude@clouddev1 5bb0a147f0 ADR-0018 implementation: auto-fill contracts for serial and shortid
Generalises serial and shortid beyond their previous restricted
forms:

- `serial` is no longer restricted to single-column PK. Non-PK
  serial columns get an emitted UNIQUE constraint and use
  application-side MAX(col)+1 at INSERT time (rowid alias still
  drives the PK case for free; per ADR-0010 worker-thread
  serialisation, the read-then-insert sequence is safe).
- `shortid` columns auto-fill existing null cells when the
  column is materialised — `add column T: x (shortid)` on a
  non-empty table no longer leaves rows in a not-really-valid
  NULL state.
- `int -> serial` joins the type-change matrix as always-clean
  identity (closes the asymmetry vs `text -> shortid`); other
  sources are refused with a route-via-int hint.
- `change column T: x (serial|shortid)` fills null source
  cells with sequence / generated values in the same rebuild
  transaction.

Internal infrastructure:

- ReadColumn gains `unique: bool`; read_schema detects single-
  column UNIQUE indexes via pragma_index_list /
  pragma_index_info; schema_to_ddl emits inline UNIQUE for
  non-PK columns.
- ColumnSchema (persistence) gains `unique: bool` so the flag
  survives YAML round-trip and rebuild-from-text reconstructs
  it faithfully — preserves the "serial -> int leaves UNIQUE
  in place" promise across save/load cycles.
- ChangeColumnTypeResult.client_side now carries `auto_filled`
  + `auto_fill_kind` alongside `transformed` + `lossy`; the
  app handler renders separate note lines when both apply.
- AddColumnResult is a new return type carrying pre-rendered
  [client-side] note lines for the auto-fill paths.

Tests: 519 -> 534 (+15). Clippy clean.
2026-05-08 14:32:19 +00:00
claude@clouddev1 00947b928c ADR-0017 implementation: per-cell type-change with override flags
Replaces the placeholder "trust STRICT" body of do_change_column_type
with the per-cell transformer matrix from ADR-0017. Adds:

- src/type_change.rs: CellOutcome { Clean / Lossy / Incompatible }
  + transform_cell + static_refusal covering every matrix pair
  from §3 (54 unit tests).
- --force-conversion and --dont-convert flags on `change column`
  (mutually exclusive at parse time per §5).
- Refined PK rule (§4.1): refused only when the column has an
  inbound FK and fk_target_type would change. Outbound-FK columns
  still refused outright (§4.2). PK / shortid uniqueness checked
  post-transformation (§4.3).
- Bordered diagnostic tables (lossy / incompatible / collision)
  via the pretty-table renderer (§7) — uses ADR-0016's primitives.
- [client-side] success note (§6) when any cell was rewritten.
- Friendly wrapper for engine-level errors under --dont-convert
  so no engine vocabulary leaks (ADR-0002 user-facing posture).

ADR-0017 §3 + §7 amended in place (with user sign-off): serial->int
added explicitly to the always-clean matrix, and diagnostic rows
identify themselves by PK value(s) rather than positional indices
(SQLite returns rows unordered without ORDER BY, so positional
"row 5" is unaddressable).

Tests: 449 -> 517 (+68). Clippy clean with nursery lints.
2026-05-08 13:21:07 +00:00
claude@clouddev1 7b97786ab7 B2/C2: column drop / rename / change-type DSL commands
Closes B2 (rebuild-table reused outside relationships) and
C2 (full add/drop/rename/change-type column operations).

* drop column [from] [table] <T>: <col>
  - ALTER TABLE DROP COLUMN (SQLite 3.35+) + metadata
    cleanup in __rdbms_playground_columns.
  - Refuses PK columns and columns involved in a declared
    relationship (drop the relationship first).

* rename column [in] [table] <T>: <old> to <new>
  - ALTER TABLE RENAME COLUMN (SQLite 3.25+); SQLite
    cascades the rename through FK declarations on other
    tables.
  - Mirrors the new name into both metadata tables
    (__rdbms_playground_columns, __rdbms_playground_relationships)
    so describes stay accurate after a rename.
  - Refuses identity rename and name collisions.

* change column [in] [table] <T>: <col> (<newtype>)
  - Routes through the rebuild_table primitive (ADR-0013)
    since SQLite ALTER doesn't support type changes.
    INSERT INTO new SELECT FROM old; STRICT typing enforces
    cell compatibility, transaction rolls back on mismatch.
  - Refuses PK columns, relationship-involved columns,
    `serial` target, and no-op same-type changes.

Adds 20 tests (parser + db layer); updates the in-app help
listing. Both prepositions independently optional in each
new command, matching `add column`'s grammar shape.

Total: 449 passing, 0 failing, 0 skipped (up from 429).
Clippy clean.

Known spec gap: column-type-change conversion compatibility
is not yet documented (currently relies on SQLite STRICT
errors); follow-up will close this.
2026-05-08 10:09:24 +00:00
claude@clouddev1 1b27a0c9b1 runtime: suppress silent-rebuild banner for empty projects
A fresh-launch temp project enters run() with no .db, calls
rebuild_from_text on an empty schema, and used to surface a
"[ok] rebuild — 0 tables and 0 rows will be reconstructed"
note that conveyed no information. Now we only emit the note
when the project actually has tables. The explicit `rebuild`
command still always reports its summary — the user asked.
2026-05-08 09:15:19 +00:00
claude@clouddev1 67d68db5f8 Iteration 6: --resume + persistent input history + migration scaffold
Closes out track 2's ADR-0015 backlog.

* `--resume` CLI flag (L1a, ADR-0015 §7) opens the most-
  recently-used project, tracked in <data-root>/last_project.
  Mutually exclusive with a positional <project-path>; errors
  cleanly to stderr (above the shell prompt) on missing file
  or stale recorded path. last_project is rewritten on every
  successful project open (startup, load, new, save as,
  import).
* Persistent input history (I2-persist, ADR-0015 §12). On
  project open, the in-memory navigable history is hydrated
  from the tail of history.log (capped at the in-memory cap).
  ProjectSwitched gains a `history_entries` payload field;
  App::seed_history is the entry point. Pipes inside source
  text round-trip via splitn(3); unknown escape sequences are
  passed through literally.
* Migration framework scaffold (F3, ADR-0015 §9). New
  persistence::migrations module with MigratorRegistry +
  migrate_to_latest + ensure_project_yaml_migrated. Empty
  in v1 (production registry has no migrators); the loader
  runs through it on every project open and is exercised by
  tests with a fake v1→v2 migrator. Writes
  project.yaml.v<N>.bak before any migrator runs; verifies
  each step bumps the version field.

Refreshes docs/requirements.md (A1 / I2 / F3 / E1 / L1a /
test baseline) and adds docs/handoff/20260508-handoff-3.md
covering both Iter 5 and Iter 6.

Total tests: 408 passing, 0 failing, 0 skipped (up from 345
at handoff-2). Clippy clean.
2026-05-08 08:27:50 +00:00
claude@clouddev1 c6cf3df6dc Iteration 5: export / import commands
Implements the `export` and `import` app-level commands per
ADR-0015 §11 + ADR-0007 amendment 1.

- `export [<path>]` writes a zip of project.yaml + data/ to
  <data-root>/YYYYMMDD-<projectname>-export-NN.zip by default,
  preserving the project's directory name as the single
  top-level folder inside the archive.
- `import <zip> [as <target>]` extracts an exported zip into
  a new named project and switches to it. Target name is
  derived from the zip's top-level folder by default; on
  collision the destination auto-suffixes -02, -03, ... up
  to -99 instead of refusing (deviates from §2's refuse-on-
  collision rule for save/save as; recorded as an amendment
  to ADR-0015 §11).
- Excludes playground.db and history.log from the zip.
- Path-traversal protection via zip::enclosed_name + post-
  resolution check that the extraction path stays inside
  the target directory.

Adds the zip = "5" dep with default-features = false +
features = ["deflate"] to keep the binary-size cost modest.

Test baseline: 370 passing, 0 failing, 0 skipped.
2026-05-08 08:24:45 +00:00
claude@clouddev1 58a964da8c Harden temp-project cleanup with stacked safety guards
The previous remove_dir_all on a path returned by Project::path()
was too trusting: an unusual CLI argument or a hand-edited
project.yaml could in principle have steered cleanup into
deleting the wrong directory. Replace it with
safely_delete_temp_project, which refuses unless every one of
the following passes:

1. Path is not a symlink (checked before canonicalize so a
   symlink can't smuggle a different target through).
2. Path is a directory.
3. Canonical path is under <active-data-root>/projects/
   (canonical-prefix containment).
4. Directory basename contains the literal `[temp]` marker.
5. Direct children are exclusively well-known project
   artefacts (project.yaml, data/, history.log,
   playground.db, .gitignore, lock file) plus migration .bak
   files and atomic-write .tmp files. Any stranger file
   (notes.md, .git/, screenshots, etc.) makes the helper
   refuse.

is_unmodified_temp now also requires data/ to be empty, in
addition to project.yaml's tables and relationships being
empty. A hand-edited yaml that drops the schema list but
leaves CSVs in data/ no longer passes.

Failure to delete is non-fatal -- the helper returns
SafeDeleteError, the runtime logs a tracing::warn!, and the
project stays on disk. Leaving an unexpected directory alone
is always preferable to a wrong delete.

Tests: 345 passing (272 lib + 9 + 5 + 6 + 27 + 9 + 17),
0 failing, 0 skipped. 7 new tests covering each guard,
including a unix-only symlink-rejection test.
2026-05-08 06:59:26 +00:00
claude@clouddev1 b7addd6161 Cleanup pass: --help, in-app help, post-rebuild message, unmodified-temp cleanup
Four post-Iteration-4 polish items surfaced by manual testing.

1. `--help` / `-h` CLI flag prints a usage banner (options +
   app-level commands + DSL grammar reference) and exits. Parse
   errors also print the banner to stderr.

2. `help` app-level command notes the same list of supported
   commands to the output panel -- a simple stand-in for the
   richer H3 help system, kept in sync with what's actually
   wired up.

3. The silent rebuild that runs when playground.db is missing
   now surfaces a system message in the output panel ("[ok]
   rebuild -- N tables, M rows reconstructed; ...") via a new
   initial_events plumbing. The user no longer wonders whether
   the .db was magically restored or whether anything happened
   on launch.

4. Unmodified empty temp projects (kind=Temp, project.yaml has
   tables: [] and relationships: []) are now auto-deleted when
   the user switches away (load / new / save as) or quits. This
   addresses the "launch app, load existing project, quit"
   pattern that was leaving an empty temp directory behind
   every time. Modified temps (with any user-created tables or
   relationships) are never auto-deleted; corrupted projects
   are also never auto-deleted (defensive default-to-false on
   yaml read/parse errors).

Tests: 338 passing (272 lib + 9 + 5 + 6 + 20 + 9 + 17),
0 failing, 0 skipped. Clippy clean.
2026-05-08 06:43:49 +00:00
claude@clouddev1 f2198275f0 Iteration 4b: save / save as / new / load with project switching
Adds the rest of the track-2 lifecycle commands (ADR-0015 §11)
and the project-switching machinery they need at runtime.

Temp vs named distinction: replaced the fragile naming heuristic
with an explicit `[temp]` marker in the directory pattern
(`<YYYYMMDD>-[temp]-<word>-<word>-<word>`). validate_user_name
already rejects brackets, so user-typed names can never collide
with a temp marker. The status bar shows `[TEMP] <Display Name>`
for temp projects; the prettifier strips both the date and the
marker so display names are clean.

save / save as: temp project's `save` opens a path-entry modal
(acts as save as); named project's `save` reports "already
auto-saved; use `save as`". `save as` always prompts. Relative
names resolve under <data-root>/projects/; absolute paths used
as-is. Copy excludes the per-process lock file; everything else
(.db, yaml, csvs, history.log) is copied.

new: closes current project, creates a fresh auto-named temp,
switches.

load: opens a picker. List sub-mode shows projects in the active
data root, sorted newest-first by project.yaml mtime; arrow keys
navigate, Enter loads, `b` switches to a path-entry sub-mode for
projects elsewhere, Esc cancels. Empty data root jumps straight
to path entry.

Runtime: `Session` holds Option<Project> + Option<Database> so
project switches can drop old (releasing lock + stopping worker)
before opening new -- required for the "load my own current
project" case. `perform_switch` handles Load / SaveAs / NewTemp
uniformly.

Tests: 332 passing (270 lib + 9 + 5 + 6 + 16 new + 9 + 17),
0 failing, 0 skipped. Clippy clean.
2026-05-08 06:23:46 +00:00
claude@clouddev1 ba93d3c7d8 Iteration 4a: rebuild command with confirmation modal
Adds the explicit `rebuild` app-level command (ADR-0015 §7, §11)
and a modal UI infrastructure to host its confirmation dialog.
Typing `rebuild` emits Action::PrepareRebuild; the runtime reads
project.yaml + data/ to compute a summary ("3 tables and 47 rows
will be reconstructed; the existing playground.db will be
replaced") and posts AppEvent::RebuildPrepared, which opens the
modal. Y confirms, N/Esc cancels. While the modal is open,
normal input is gated.

The worker's do_rebuild_from_text now wipes existing user tables
and metadata before reloading from text, so it works on both
fresh and populated databases. Source text is plumbed through
rebuild_from_text so the explicit rebuild logs to history.log
while the silent on-load rebuild from Iteration 3 stays silent.

Modal infrastructure (App.modal field + key routing + centered
overlay rendering + word-wrap) is reused by Iteration 4b's save
/ save as / load / new flows.

Tests: 314 passing (268 lib + 9 + 5 + 6 new + 9 + 17),
0 failing, 0 skipped. Clippy clean.
2026-05-07 22:27:37 +00:00
claude@clouddev1 f0fc063756 Iteration 3: existence-only load + rebuild from text on missing .db
When the runtime opens a project whose playground.db is missing,
it now rebuilds the database from project.yaml + data/<table>.csv
per ADR-0015 §7. The rebuild path:

1. Parses project.yaml (serde_yml). Unknown versions / types /
   actions surface as PersistenceFatal.
2. Recreates each user table with FK constraints inline
   (PRAGMA foreign_keys=OFF), then populates the column-type,
   relationship, and project metadata tables.
3. Loads each table's CSV via a hand-rolled reader that
   preserves the NULL-vs-empty distinction (the csv crate
   doesn't expose whether a field was quoted; ours does).
4. Runs PRAGMA foreign_key_check before commit; any violation
   aborts.
5. Restores foreign_keys=ON regardless of success.

Row-level failures get DbError::RebuildRowFailed with row
number, file, table, and a friendly per-type detail. They land
in the runtime as a fatal stderr message ("unable to load row N
from `data/T.csv` into table `T`: ...") before the alternate
screen is entered.

created_at from project.yaml overwrites the configure-time
placeholder so timestamps round-trip stably.

Tests: 307 passing (267 lib + 9 + 5 new + 9 + 17), 0 failing,
0 skipped. Clippy clean with nursery lints.
2026-05-07 22:11:45 +00:00
claude@clouddev1 5c076f6d8f Iteration 2: per-command write-through to project.yaml, CSVs, history.log
Every successful user command now persists through to YAML, the
affected CSVs, and history.log inside the same SQLite transaction,
with the commit-db-last ordering from ADR-0015 §6: validate ->
mutate -> stage text + fsync -> atomic rename -> append history ->
commit. A failure in any text-write step rolls back the SQLite tx,
so disk state is unchanged on failure. Persistence failures are
routed through a new AppEvent::PersistenceFatal which sets a
fatal_message on the App, emits Action::Quit, and is printed to
stderr after terminal teardown so the banner remains above the
shell prompt (ADR-0015 §8).

New persistence module owns the file formats: hand-rolled YAML
schema writer, per-type CSV encoder (RFC 4180, NULL distinct from
empty string, base64 blobs), append-only history.log with ISO-8601
timestamps and successful-only entries. Atomic per-file writes via
tmp + fsync + rename.

The db worker holds an Option<Persistence>; tests still use
Database::open(":memory:") with no persistence. Action::ExecuteDsl
gains a source field carrying the user-typed text, threaded
through to history.log.

Tests: 289 passing (256 lib + 7 new integration + 9 lifecycle + 17
walking-skeleton), 0 failing, 0 skipped. Clippy clean with nursery
lints.
2026-05-07 21:09:15 +00:00
claude@clouddev1 601d3b6c51 Iteration 1: file-backed projects with auto-named temps, lock file, and L1 CLI
Replaces the in-memory database with an on-disk project. Startup either
opens a project at the positional CLI path (L1) or creates an auto-named
temp project (<YYYYMMDD>-<word>-<word>-<word>) under the OS-standard
data directory or a --data-dir override. The new project::Project type
owns the directory skeleton and a PID+hostname lock file with
stale-lock takeover via sysinfo. The status bar now shows
"Project: <Display Name>", derived by a small kebab/snake/camel
prettifier. Per-command persistence to YAML/CSV/history.log is NOT
yet wired -- that's Iteration 2; for now playground.db carries the
state across quits.

Tests: 257 passing (231 lib + 9 new integration + 17 existing),
0 failing, 0 skipped. Clippy clean with nursery lints.
2026-05-07 20:21:52 +00:00
claude@clouddev1 305e5083d5 INSERT/UPDATE/DELETE + value model + auto-show, with polish
DSL data operations (ADR-0014):
- insert into T [(cols)] values (vals); short form
  insert into T (vals) omits values keyword for friendlier
  syntax.
- update T set ... where col=val | --all-rows; delete from T
  where col=val | --all-rows; show data T.
- Value AST (Number/Text/Bool/Null) with per-column-type
  validation in the executor: int/real/decimal/bool/date/
  datetime/shortid each accept a documented literal shape
  and produce friendly format errors naming the column.
- INSERT short form fills non-auto-generated columns in
  schema order; auto-fills serial via SQLite and shortid
  via the new generator (T2).
- `add column [to table] T: c (type)` -- `to table` now
  optional.

Database:
- insert/update/delete via prepared statements with bound
  rusqlite::types::Value parameters.
- InsertResult/UpdateResult/DeleteResult: writes return
  rows_affected plus the affected row(s) only (not the whole
  table), so users see exactly what changed.
- INSERT shows the just-inserted row via last_insert_rowid.
- UPDATE captures matching rowids up-front and fetches them
  post-update -- works even if the UPDATE changed the WHERE
  column.
- DELETE reports per-relationship cascade effects by row-
  count diffing inbound child tables; UPDATE-side cascades
  are not yet detected (would need value diffing).
- query_data formats cells (booleans true/false, NULLs as
  None).

FK error enrichment:
- Now lists both outbound (INSERT/UPDATE relevance) and
  inbound (DELETE/UPDATE on parent relevance) FKs from the
  metadata, so RESTRICT errors point at the children
  blocking the delete.
- RelationshipSelector has a proper Display impl -- "no
  such relationship" reads cleanly.

Relationship display:
- target_table for AddRelationship/DropRelationship now
  returns the parent (1-side); structure rendering after
  add/drop shows that side's "Referenced by:" entry,
  matching the `from <Parent>` direction of the command.
- [ok] summary uses display_subject so relationship
  commands show both endpoints (`from P.col to C.col`)
  rather than a single misleading table name.
- Auto-name format `<Parent>_<pcol>_to_<Child>_<ccol>`
  (matches the from..to direction).

Output rendering and scrolling:
- Wrap-aware scroll: renderer reports both visible-row
  count and total wrapped-row count to App; scroll math
  caps against actual displayable rows. Long lines wrap;
  the bottom line is always reachable; PageUp/PageDown work
  correctly even after paging past the buffer top.
- Multi-line messages (FK error enrichment, cascade summary)
  split into single-line OutputLines at creation time so
  wrap/scroll math agree.

Runtime / events:
- New AppEvent variants for Insert/Update/Delete success
  carrying typed result structs; DslDataSucceeded reserved
  for show-data queries.

Docs:
- ADR-0014 covers data-op grammar, value model, --all-rows
  safety, auto-show.
- requirements.md: C5 done, T2 done, V2 partial (basic data
  view), V5 partial (show data added). New entries: C5a
  complex WHERE expressions; H1 progress note for FK
  enrichment; H1a (strong syntax-help in parse errors).

Tests: 200 passing (183 lib + 17 integration), 0 skipped.
Includes parser, type-validation, DB write/read, FK-failure
enrichment, cascade-delete propagation, focused-auto-show
behaviour, scroll-cap invariants. Clippy clean with nursery
enabled.
2026-05-07 16:33:25 +00:00
claude@clouddev1 165068269b Foreign-key relationships, rebuild-table, polish round
DSL:
- add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>
  [on delete <action>] [on update <action>] [--create-fk]
- drop relationship <name> | from <P>.<col> to <C>.<col>
- show table <name> for re-displaying a structure on demand

Database (ADR-0013):
- Rebuild-table primitive following SQLite's
  ALTER-via-rebuild recipe (foreign_keys=OFF outside tx,
  copy-by-name, foreign_key_check before commit). Reusable for
  B2 (column drops/renames/type changes).
- ReferentialAction enum (no action / restrict / set null /
  cascade); SET DEFAULT awaits column DEFAULTs.
- __rdbms_playground_relationships metadata table -- names,
  auto-generated as <Parent>_<pcol>_to_<Child>_<ccol>.
- Type::fk_target_type() validation at declaration; friendly
  errors for type mismatch, non-PK target, missing column,
  duplicate name.
- describe_table populates symmetric outbound + inbound
  relationship lists. drop_table refuses while inbound
  references exist; outbound metadata cleaned up alongside drop.

App / UI:
- In-line cursor editing in the input field: Left, Right,
  Home, End, Delete, Backspace honoring UTF-8 boundaries.
- PageUp / PageDown scrolls the output buffer; viewport row
  count fed back from the renderer via App::note_output_viewport
  so scroll is capped against the actual visible area
  (regression-tested) and snaps to the bottom on new output.
- Failure messages quote the command portion ("verb target"
  failed: ...) for visual clarity; RelationshipSelector has a
  proper Display impl so "no such relationship" reads cleanly.
- Structure rendering shows References / Referenced by sections.

Docs:
- ADR-0013 covers naming, metadata table, symmetric view, and
  the rebuild-table strategy.
- requirements.md updates: C3 (FK done), B2 (primitive in),
  T3 (compound-PK FK still pending). New entries: I1a (cursor
  editing -- landed), I1b (Ctrl-A/E and readline shortcuts --
  pending), V4 partial scroll, V5 (show family), C3a (modify
  relationship -- deferred).

Tests: 154 passing (140 lib + 14 integration), 0 skipped.
Clippy clean with nursery enabled.
2026-05-07 14:52:51 +00:00
claude@clouddev1 c1e52920eb DSL parser, async DB worker, types, history, metadata, polish
Track 1 implementation plus polish round.

Parser (chumsky):
- Grammar-based DSL producing a typed Command AST.
- create table X with pk [name:type[,name:type...]] supports
  arbitrary names, any user type, compound PKs natively. Bare
  form errors with a friendly hint pointing at `with pk`.
- add column to table X: Name (type); drop table X.
- Required clauses use keyword grammar; -- reserved for opt-in
  flags (ADR-0009). Custom Rich reasons preferred when surfacing
  chumsky errors so unknown-type messages list valid alternatives.

Database (ADR-0010, ADR-0012):
- rusqlite + STRICT tables + foreign_keys=ON.
- Dedicated worker thread; mpsc Request inbox, oneshot replies.
- Typed DbError with friendly_message() hook for H1.
- Internal __rdbms_playground_columns metadata table preserves
  user-facing types across schema reads, atomically maintained
  alongside DDL via Connection transactions. list_tables hides
  it via the new __rdbms_ internal-table convention.

Types (ADR-0005, ADR-0011):
- All ten user-facing types: text, int, real, decimal, bool,
  date, datetime, blob, serial, shortid.
- Type::fk_target_type() for FK-side column-type rule
  (Serial->Int, ShortId->Text, others identity) -- foundation
  for the FK iteration.

App / Runtime / UI:
- update() stays pure-sync; runtime dispatches DSL via spawned
  tasks, results post back as AppEvent::Dsl*.
- Items panel renders live tables list; output panel shows the
  user-facing structure of the current table after each DDL.
- In-memory command history (Up/Down, draft preservation,
  consecutive-duplicate dedup) -- I2 partial.
- Mouse capture removed; terminal native text selection
  restored (toggle approach revisited when scroll/click
  features land).

Docs:
- ADRs 0009 (DSL syntax conventions), 0010 (DB worker),
  0011 (FK type compat), 0012 (internal metadata table).
- requirements.md progress notes; new V4 entry for the
  scrollable session-log + inline rich rendering + Markdown
  export direction.

Tests: 103 passing (91 lib + 12 integration), 0 skipped.
Clippy clean with nursery enabled.
2026-05-07 13:32:19 +00:00
claude@clouddev1 25a0f1260f TUI walking skeleton (Phase 4)
First implementation milestone: Cargo project, dependencies,
and a minimal but functional TUI shell built on Ratatui +
Crossterm + Tokio in the Elm-style update/view pattern
(Candidate A from Phase 2/3 selection).

Includes:
- Three-region layout: items list (left), output + input + hint
  (right), bottom status bar with mode-aware shortcuts.
- Two themes (light, dark) plus COLORFGBG auto-detect, per
  NFR-7. CLI: --theme {light,dark}, --log-file <path>.
- Input modes per ADR-0003: simple (default), advanced, with
  the `:` one-shot escape including immediate prompt reaction
  ("Advanced:" label, advanced border) and auto-inserted space
  after a leading `:` in simple mode.
- App-level commands: `quit`/`q`, `mode simple`/`mode advanced`
  (canonical list per ADR-0003 — remaining commands land in
  later iterations).
- File logging via tracing, defaulting to ~/.rdbms-playground/
  playground.log so the TUI is not corrupted by stdio.

Testing per ADR-0008:
- Tier 1: 29 unit tests covering input handling, mode switch,
  one-shot escape, auto-space, output buffering, CLI parsing.
- Tier 2: 4 insta snapshots (default simple/advanced/light,
  one-shot active) of TestBackend frames.
- Tier 3: 7 integration tests driving synthetic events through
  App::update + render path.

All green: 36 tests, 0 failures, 0 skips. Clippy clean with
nursery lints enabled.
2026-05-07 11:17:58 +00:00