Commit Graph

31 Commits

Author SHA1 Message Date
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 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 942222bfc9 constraints: CHECK — check (<expr>) at create table & add column (ADR-0029)
The fourth constraint. `check ( <expr> )` reuses the ADR-0026
WHERE-expression grammar via `Subgrammar`, so a check is
written in the same language as a `where` filter.

- Grammar: a `CHECK_CONSTRAINT` arm joins the shared
  constraint-suffix Choice; `consume_check_expr` extracts the
  parenthesised expression (paren-depth aware) into
  `ColumnSpec.check` / `Command::AddColumn.check`.
- Storage: the parsed `Expr` is compiled once to inline SQL
  (`compile_check_sql` — `compile_expr` + ADR-0028's
  param-inliner) and stored in that form everywhere — a new
  `check_expr` column in `__rdbms_playground_columns`,
  `project.yaml`'s `ColumnSchema.check`, and the column DDL
  emitted by `do_create_table` / `schema_to_ddl`.
- `add column … check` routes through the rebuild primitive
  (SQLite's `ALTER … ADD COLUMN` cannot carry it); a CHECK on
  a serial/shortid column is create-table-only and refused at
  add-column with a friendly message.
- `describe` surfaces the CHECK. ADR-0029 §7/§8 updated to the
  SQL-form decision — double-quoted identifiers, consistent
  with ADR-0028's `explain` display SQL.

1201 tests pass (+8); clippy clean.
2026-05-19 16:42:18 +00:00
claude@clouddev1 58d8958822 add column: column constraints — NOT NULL / UNIQUE / DEFAULT (ADR-0029 §6)
`add column` now accepts the shared constraint suffix and the
worker honours it — the surface where NOT NULL / UNIQUE
actually matter, on non-PK columns.

- Grammar: `ADD_COLUMN_NODES` gains the constraint-suffix
  fragment; `collect_column_constraints` folds it into
  `Command::AddColumn`.
- `do_add_column` routes per ADR-0029 §6: SQLite's `ALTER
  TABLE ADD COLUMN` cannot express `UNIQUE` and requires a
  default for `NOT NULL`, so those go through the rebuild
  primitive (`do_add_constrained_column_via_rebuild`); plain
  cases keep the ALTER path with the constraint suffix
  appended.
- Pre-flight refusals, before any SQL write: a NOT NULL
  column with no default added to a populated table; a UNIQUE
  column with a default added to a multi-row table; a default
  on a `serial` / `shortid` column.

CHECK is still deferred to the next commit. 1193 tests pass
(+9); clippy clean.
2026-05-19 14:50:19 +00:00
claude@clouddev1 12395a9a6c create table: column constraints — NOT NULL / UNIQUE / DEFAULT grammar (ADR-0029)
`create table … with pk` now parses the column-constraint
suffix; combined with the commit-1 db layer, a constrained
table works end to end.

- A shared constraint-suffix grammar fragment — `not null`,
  `unique`, `default <literal>` — sits after each column's
  `(type)` group; `build_create_table` walks the matched path
  per column and folds the constraints into `ColumnSpec`.
- §9 redundancy check: every `with pk` column is a primary-key
  column, so `not null` (any) and `unique` (single-column PK)
  are rejected with a friendly error
  (`parse.custom.constraint_redundant_on_pk`).
- `project.yaml` round-trip: `ColumnSchema` gains `not_null` /
  `default`; the YAML reader/writer and `build_read_schema`
  carry them, so `rebuild` / `export` / `import` preserve
  constraints.
- ADR-0029 §2.1's example corrected — `create table` columns
  are all PK columns, so its suffix is for `default` / `check`;
  `docs/simple-mode-limitations.md` records that non-PK
  columns at create time need advanced mode.

CHECK is deferred to the next commit. 1184 tests pass (+7);
clippy clean.
2026-05-19 14:41:29 +00:00
claude@clouddev1 a60e879f20 db: column-constraint infrastructure — NOT NULL / UNIQUE / DEFAULT (ADR-0029)
The database layer now honours the ColumnSpec constraint
fields end to end, ahead of the grammar that lets users type
them.

- `do_create_table` emits ` NOT NULL` / ` UNIQUE` / ` DEFAULT
  <literal>` per column via the new `column_constraints_sql`
  helper (the default literal bound against the column's type).
- `ReadColumn` gains `default_sql`, read from
  `pragma_table_info.dflt_value`; `schema_to_ddl` emits it, so
  the rebuild-table primitive preserves DEFAULT — it already
  preserved NOT NULL / UNIQUE.
- `ColumnDescription` gains `unique` / `default`;
  `do_describe_table` now sources columns from `read_schema`
  (one source of per-column truth) and `constraints_display`
  lists PK / NOT NULL / UNIQUE / DEFAULT.

No user-facing change yet — no grammar produces constrained
columns. Tests exercise creation, enforcement, describe, and
rebuild-preservation programmatically.

1177 tests pass (+5); clippy clean.
2026-05-19 14:18:45 +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 ae99276283 explain: typing-surface matrix cells (ADR-0028 step 5)
13 matrix cells for the `explain` prefix across all three
wrapped commands — `explain show data` / `explain update` /
`explain delete` — covering each typing position (after the
prefix, the inner entry word, the table, the filter clause)
plus the three complete forms. The cells confirm `explain`
plugs into the inner query grammars cleanly: candidates, hints
and column scoping match the standalone commands, and the
complete forms parse as `Command::Explain`.

Also adds a worker test pinning the display SQL's `<>`
rendering of inequality (ADR-0028 §3).

Matrix: 161 -> 174 cells. 1172 tests pass; clippy clean.
2026-05-19 12:49:58 +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 426e80185f command: Operand carries a source span
Each WHERE-expression Operand now records the byte span of the
terminal it was built from — the precise per-literal highlight
target for an expression WARNING (finishing ADR-0027 §2's
highlight/hint wiring). parse_operand captures MatchedItem::span;
the RowFilter::eq convenience constructor uses Operand::NO_SPAN.

PartialEq is hand-written to ignore the span — it is editor
metadata, so Command equality stays whitespace- and
position-independent, which the Expr test corpus relies on.
No behaviour change; 1100 tests still pass, clippy clean.
2026-05-19 09:20:52 +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 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 6ca297579e round-5 follow-up r2: migrate all thiserror Display attributes to catalog
Completes the i18n sweep started in the previous commit. All
remaining hand-rolled user-facing English strings inside
thiserror #[error(...)] attributes have been moved into the
catalog. Drops the thiserror dependency entirely.

Twelve error types migrated:

- dsl::action::UnknownAction         → parse.custom.unknown_action
- dsl::parser::ParseError            → parse.error_wrapper + parse.empty
- dsl::value::ValueError             → value.{type_mismatch,format}
- persistence::csv_io::CsvError      → persistence.csv.*
- persistence::mod::PersistenceError → persistence.{io,encode}
- persistence::yaml::YamlError       → persistence.yaml.*
- persistence::migrations::MigrateError → persistence.migrate.*
- project::lock::LockError           → project.lock.*
- project::naming::NamingError       → project.naming.*
- project::naming::UserNameError     → project.user_name.*
- project::mod::ProjectError         → project.{path_not_found,...}
- project::mod::SafeDeleteError      → project.safe_delete.*
- archive::ArchiveError              → archive.*
- cli::ArgsError                     → cli.*
- db::DbError                        → db.error.*

Pattern per type: drop thiserror::Error derive, write manual
Display calling crate::t!(), keep #[from] semantics via
explicit From impls, override Error::source() where applicable
so #[source]-style chaining is preserved.

Why this matters (user rationale): "fine to have fallbacks for
errors that are purely technical, but lift the output to a
place where it can be localized later and where an adjustment
with friendly text is easily possible if any of them become
part of the happy path." All surface strings now live in
en-US.yaml and can be reworded or localized without touching
Rust source.

Tests: 769 passing, 0 failed, 1 ignored. Clippy clean with
-D warnings. Cargo.toml: drop thiserror = "2.0.18".
2026-05-13 21:24:51 +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 aea3224da2 ADR-0022 stage 7/8: schema query plumbing
Add `Request::ListNamesFor { slot, reply }` and the public
`Database::list_names_for(slot)` method. The completion
engine in stage 8 calls this on Tab when the cursor sits
on an identifier-typed slot.

Worker dispatch:
  - TableName → user tables (filters __rdbms_*).
  - Column → distinct column names across all user tables
    (v1 simplification per the stage 6 IdentSlot note: no
    table-context binding; the schema-completion engine in
    stage 8 may refine).
  - RelationshipName → relationship names from the
    __rdbms_playground_relationships metadata table.
  - NewName → short-circuited at the public method (no
    worker round-trip).

Names are returned alphabetised + deduplicated. Filters
respect ADR-0002 — internal __rdbms_* tables never reach
the completion menu (covered by a regression test).

Tests: 705 passing, 0 failing, 1 ignored (700 baseline →
+5 list_names_for cases). Clippy clean.

Stage 8 wires this into the App as a Tab-triggered
completion mode. Note for the next session: stage 8 is by
far the largest of the eight stages — it touches App state
(completion mode), event routing (Tab/arrow/Enter/Esc/letter
behaviour while in completion mode), hint-panel render
variant, candidate filtering, integration tests. Several
fine-grained UX decisions (cursor position after accept,
panel height when candidate list overflows, what closes
the mode) want explicit user input rather than agent
guesswork. See "Stage 8 open questions" in the next
handoff for the list.
2026-05-10 17:50:21 +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 0d7a7bcd49 db: end-to-end tests for change_column int -> bool (B2)
The (Int, Bool) entry of the ADR-0017 §3 matrix was already
covered at the per-cell unit-test level in `type_change.rs`,
but the end-to-end change_column path through `db.rs` had no
test exercising it. This closes that gap with the two cases
called out in the handoff:

- `change_column_type_int_to_bool_with_zero_one_succeeds`:
  Rows 0/1/0 succeed, no [client-side] note. The matrix
  returns the same Value::Integer for 0 and 1, so
  is_non_identity reports false for every cell and
  ClientSideNote.transformed stays at 0 — the
  `transformed > 0 || auto_filled > 0` filter therefore
  drops the note.
- `change_column_type_int_to_bool_refuses_other_values`:
  Row with 2 → Incompatible. Verified under both Default
  and ForceConversion modes (per ADR-0017 §5: incompatible
  is not lossy, --force-conversion must not advertise).

No production code change; tests only. 534 -> 536 passing,
clippy clean with nursery lints enabled.
2026-05-08 14:49:34 +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 5b5e08d852 ADR-0016 + Iter 5/6 follow-up: pretty table rendering
Replaces the placeholder pipe-and-dash output with Unicode
box-drawing tables for both data results and table-structure
listings, per ADR-0016.

* New `src/output_render.rs` module with `render_data_table`
  and `render_structure`. Hand-rolled to match the project's
  existing CSV/YAML pattern; ~300 lines.
* Header-only outer-frame border style: outer ┌─┐│└─┘ box +
  ├─┤ header underline, no per-row separators. NULL renders
  as `(null)`; cell newlines/tabs/control chars become
  `↵`/`→`/`·` as display-only substitutions.
* Type-aware column alignment: numeric types right-aligned,
  everything else left. `DataResult` gains a `column_types:
  Vec<Option<Type>>` field, populated from the existing
  metadata lookup at the two query sites in db.rs (no new
  query paths).
* Structure view shows Name | Type | Constraints columns;
  References / Referenced-by sections retain plain-text
  format, leaving room for the future relationship-rendering
  ADR.
* 18 new unit tests in output_render.rs (plus 4 insta
  snapshots for the canonical layouts). Existing assertions
  in app.rs and walking_skeleton.rs updated to match the new
  format.

Total: 426 passing, 0 failing, 0 skipped (up from 408).
Clippy clean.
2026-05-08 09:06:02 +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