d5fb47bcc85b98c887b32af912c5de763d68d1e8
105 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
62f09bebc5 |
db: fix self-referential cascade over-count + SQL-delete render test
A self-referential ON DELETE CASCADE FK (e.g. T.ParentId -> T.id) is returned by read_relationships_inbound as a child whose table IS the delete target. The before/after row-count diff then includes the directly-deleted rows (already in rows_affected), so deleting a chain root reported 3 cascaded rows when only 2 were removed via the self-reference. Fix in both do_delete (DSL) and do_sql_delete (SQL): when the child table equals the target, subtract rows_affected from the diff and guard on the corrected count (a leaf delete no longer reports a phantom 0-row self-cascade); the target's CSV is already queued, so a self-ref child is not re-added to rewritten_tables. Pre-existing in do_delete; surfaced by the 3f DA pass, fixed in both paths to keep DSL/SQL parity. Behaviour: report only the rows removed via the self-reference (user-confirmed). Also adds an app-level render test for the SQL DELETE path (handle_dsl_delete_success via CommandOutcome::Delete) — the shared renderer's ok-summary + per-relationship cascade line were exercised only through the DSL path before. Test-first: self_referential_cascade_counts_only_cascaded_rows added for both paths (asserted 2, failed at 3 before the fix). 1545 pass / 0 fail / 1 ignored. Clippy clean. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
4e16d97fe0 |
walker: 3a — category-grouped mode-aware dispatch (ADR-0033 Amendment 1)
Replaces ADR-0033 §2's original Node::Guard + Choice(SQL,DSL) mechanism,
which was found during 3a to be unworkable: any guard-in-Choice approach
forces a walk_choice change (walk_choice falls through only on NoMatch, so
simple-mode valid-DSL would wrongly surface "this is SQL"), and walk_seq
treats a NoMatch past idx 0 as a hard Failed, breaking advanced-mode DSL
fall-through.
Mechanism (Amendment 1): each REGISTRY entry is tagged
CommandCategory::{Simple, Advanced}, generalising the whole-command
is_advanced_only gate. walk() becomes a thin dispatcher over decide()
(mode-aware candidate selection: simple commits the DSL node or emits the
"this is SQL" hint; advanced tries SQL first, DSL as a full-line fallback)
and an extracted walk_one_command(); speculative match-testing runs on a
scratch WalkContext so the caller's context is only touched by the
committed walk. No Node::Guard, no walk_choice/walk_seq change.
6 dispatch smoke tests on a shared-entry-word smoke registry; 1446 baseline
green; clippy clean.
|
||
|
|
05884bd13a |
2g rework: address DA findings on type recovery + engine routing + UI
Three DA critiques from the Phase-2 verification flagged real gaps; this commit closes them. 1. Type recovery row-independence (critique #1). The all-10-types test left col_blob NULL because the DSL Value enum has no Blob variant. The DA flagged this as a potential row-dependence gap. Added `database_run_select_type_recovery_works_on_empty_table` that proves column-origin metadata works on Text AND Blob columns with zero rows, pinning the invariant. The all-types test now carries an explicit comment referencing it. 2. Engine.* pattern matching against real SQLite output (critique #2). The pre-rework tests fed `translate_generic` hand-coded strings; never verified that the pinned SQLite version actually produces those wordings. Added three engine-routing tests in `tests/sql_select.rs` that produce real engine errors via `run_select` and assert catalog routing. Aggregate-in-WHERE confirms end-to-end. GROUP-BY-required and scalar-subquery are SQLite-permissive (no real error on the natural triggers), so those tests verify the matcher doesn't false-positive on benign queries + that synthetic messages route correctly. 3. Manual TUI verification (critique #3) surfaced an additional gap: `App::input_validity_verdict()` was hard-coded silent in Advanced mode, so SQL predicate warnings emitted but never reached the [WRN] indicator. Wired the verdict through to the active effective mode; updated two pre-existing tests that pinned the now-superseded "silent in Advanced" behavior; added one new test confirming a SQL `LIKE`-on-numeric warning fires the indicator. Launched the TUI, typed a representative warning-triggering SELECT, confirmed SELECT/FROM/WHERE/LIKE highlight as keyword colour AND the [WRN] indicator appears. Test totals: 1441 → 1446 passing (+5). Clippy clean. |
||
|
|
83e0ddc2ff |
app: mode-threaded completion, overlay, and validity indicator
The dispatch-layer mode gate (previous commit) made the submit behaviour correct — `select` runs in advanced mode and shows the SQL hint in simple mode. This commit extends that gating to the ambient assistance layer so simple-mode users do not see SQL leak through Tab completion, the live error overlay, or the `[ERR]`/`[WRN]` validity indicator either. `_in_mode` walker variants -------------------------- - `completion_probe_in_mode`, `expected_at_input_in_mode`, `input_verdict_in_mode`. Each sets `ctx.mode` before walking. The empty-input / unknown-entry fallback in `completion_probe` and `expected_at_input` filters the `REGISTRY` listing by `is_advanced_only` so Tab does not offer `select` in simple mode. Old signatures keep delegating to `Mode::Advanced` (back-compat for tests + other callers). `_in_mode` completion variants ------------------------------ - `candidates_at_cursor_in_mode`, `candidates_at_cursor_with_in_mode`. Internally they route the `parse_command` completeness probe through `parse_command_in_mode(input, mode)`, the `completion_probe` call through `completion_probe_in_mode`, and the `expected_at` fallback through `expected_at_input_in_mode`. Old signatures default to `Mode::Advanced`. `EffectiveMode::as_mode` ------------------------ - Collapses the persistent / one-shot distinction the UI cares about into the plain `Mode` the walker reads from `WalkContext::mode`. App-level call sites that thread mode into the walker chain use this. App / input-render wiring ------------------------- - `App::input_validity_verdict` runs only when effective mode is plain `Simple` (per ADR-0027), so it hardcodes `Mode::Simple` into the new `input_verdict_in_mode` call rather than threading. - `App::start_or_complete_at` / `_last` (the Tab handlers) pass `self.effective_mode().as_mode()` into `candidates_at_cursor_in_mode`, so a `:` one-shot or persistent advanced gives full SQL completion, persistent simple does not offer SQL. - `input_render::render_input_runs` and `ambient_hint` are invoked from `ui.rs` only when effective mode is plain `Simple` (advanced rendering uses `plain_input_spans` and skips ambient hinting per ADR-0022 §12). Their internal `classify_input_with_schema` / `candidates_at_cursor` / `parse_command` calls now go through the mode-aware variants with `Mode::Simple` hardcoded — a SQL form in simple mode surfaces as a definite-error overlay and the hint panel does not offer it. After this commit a simple-mode user typing `select` or `sel<Tab>` sees nothing SQL-shaped: no live highlight, no Tab completion candidate, the `[ERR]` indicator lit, and the on- submit hint that names the recovery paths. An advanced-mode user or a `:` one-shot sees the full SQL surface. |
||
|
|
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.
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
03d8a09457 |
ui: styled-output-line mechanism (ADR-0028 step 1)
OutputLine gains an optional styled-runs payload — a
Vec<OutputSpan> of { byte_range, OutputStyleClass } over the
line text. render_output_line gains a branch: when the payload
is present it renders the text span-by-span, each run's
semantic class (Neutral / Efficient / Expensive /
AutomaticIndex) resolved to a theme colour at render time;
otherwise the existing whole-line kind styling. The echo path
is untouched.
Theme gains `plan_efficient` — a green deliberately distinct
from `system` so green never reads as two things (ADR-0028 §6);
`warning` is reused for expensive steps.
A general per-span output-styling capability (ADR-0016's OOS-3
realized); the query-plan renderer will be its first consumer.
No user-visible change on its own. 1133 passing, clippy clean.
|
||
|
|
151ed084a3 |
hint: show the matching usage template for multi-form commands
A parse error in `add index …` showed the `add column` usage: `add` and `drop` are multi-form commands, and both the ambient hint and the submit-time usage block picked the first-listed form unconditionally. New `grammar::usage_key_for_input` disambiguates by the form word after the entry keyword — `column` / `index` / `table` / `relationship`, or the leading digit of `add 1:n …`. The ambient hint now shows that one form; `render_usage_block` shows the committed form's usage and falls back to the whole family only for a bare `add` / `drop` with no form chosen. |
||
|
|
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. |
||
|
|
1a9d950cc2 |
ui: validity indicator rendering + warning theme colour (ADR-0027 step D)
Adds the `[ERR]` / `[WRN]` validity indicator to the input row. `App` gains `input_indicator: Option<Severity>` (the runtime owns its timing — step E) and a pure `input_validity_verdict()` query that runs `input_verdict` in simple mode only (advanced mode is raw SQL, ADR-0027 §7). `render_input_panel` reserves the rightmost six columns of the input row unconditionally (ADR-0027 §4) — a five-column label plus a one-column gap — so the typed command never shifts sideways when the indicator appears or hides. The label renders only when `input_indicator` is set: `[ERR]` in `theme.error`, `[WRN]` in the new amber `theme.warning` (defined for both light and dark themes). The indicator is not yet wired live — `input_indicator` stays `None` until the debounce lands (step E). Covered by a render test and the theme contrast test; the input-panel snapshot is updated for the six-column reservation. |
||
|
|
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.
|
||
|
|
4e4dbbffe3 |
Input history: reset the nav cursor on every submit
`push_history` skipped its `history_cursor` / `history_draft` reset on the consecutive-duplicate early-return path. Recalling a command with Up and re-submitting it unchanged left the cursor stranded at that entry, so the next Up stepped backwards from there instead of restarting at the newest entry. Move the reset ahead of the early-return guards. Adds a Tier-1 regression test driving the recall/resubmit keystroke sequence. |
||
|
|
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. |
||
|
|
03dd9003df |
Help: consume CommandNode.help_id — REGISTRY-driven in-app help
Every CommandNode declared a help_id that nothing read; the in-app `help` body was a single hand-kept catalog block that drifted from the command set (handoff-12 §2.1). note_help now iterates the command REGISTRY and translates each CommandNode's help_id (`help.<id>`), framed by help.intro / help.dsl_section / help.types_reference. A newly-registered command appears in `help` automatically — no edit to note_help or a hand-kept list. Added 20 per-command help entries plus the 3 framing entries; removed help.in_app_body. Per-command entries use block scalars: a libyml 0.0.5 scanner bug panics on long internal space runs in double-quoted scalars, and the entries are space-aligned. |
||
|
|
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.
|
||
|
|
a41400e532 |
ADR-0024 Phase F (full) step 2: usage via CommandNode.usage_ids
Migrates parse-error usage-block rendering from the legacy `dsl::usage::matched_entry` (which scanned a `Vec<Token>` for the first matched Keyword) to walker-side lookup driven by each `CommandNode`'s `usage_ids` slice. `CommandNode.usage_id: Option<&'static str>` becomes `usage_ids: &'static [&'static str]`. Multi-form families (`drop`, `add`, `show`) carry every variant — `drop` lists table/column/relationship templates; `add` lists column / relationship; `show` lists data / table. The single-shape commands carry their single catalog key. App-lifecycle CommandNodes had pointed at non-existent `parse.usage.app.*` keys (never noticed because the field was unused); they now point at the real catalog entries (`parse.usage.quit`, `parse.usage.help`, …). New helpers in `dsl::grammar`: - `usage_keys_for_input(source) -> Option<(entry_word, usage_ids)>` resolves the first identifier-shape token to a CommandNode and returns its usage_ids list. Used by `app::render_usage_block` and `input_render::ambient_hint`. - `entry_words_alphabetised() -> Vec<&'static str>` replaces `dsl::usage::entry_keywords_alphabetised`. `dsl::usage` is deleted. The "available commands:" fallback in `render_usage_block` now formats entry words as `` `<word>` `` directly (matching the `parse.token.keyword.*` catalog renders); the per-keyword catalog wrappers will collapse in the next step (ADR-0024 §cleanup-pass §F). `parse_command` and `parse_tokens` slim down: - `parse_command(input)` no longer pre-lexes — the walker scans source bytes directly. - `parse_tokens` (internal-only `pub` for "future I3/I4 work") is removed; its body folded into `parse_command`. - `unknown_command_error` reads the walker registry directly. Touched modules also drop their `crate::dsl::lexer::lex` and `crate::dsl::usage` imports: `app.rs`, `input_render.rs`, `completion.rs`. Tests: 852 passing, 0 failing, 1 ignored (down from 860 because the 8 `dsl::usage::tests::*` tests are gone with the module). |
||
|
|
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`).
|
||
|
|
f94a999e66 |
ADR-0022 stage 8 follow-up r2: completion UX fixes from real testing
Two concrete behaviour changes from the user's second testing
round:
1. **Single vs multi commit paths.** Previously every Tab,
even single-candidate, created a memo so Esc/Backspace could
undo. The downside: with one candidate, repeated Tab "cycled"
through the same item invisibly — looked stuck. Now:
- Single candidate → insert with trailing space, no memo.
The user can keep typing or hit Tab again to fresh-complete
at the new cursor. (Trade-off: Esc/Backspace no longer
whole-span undo for unique completions; the user accepted
this for the chained-Tab fluency.)
- Multi candidate → insert WITHOUT trailing space, create
memo for cycling. The natural commit gesture is space —
pressing it clears the memo and inserts the space normally,
producing "<chosen> " ready for the next position.
The "stuck on unique" symptom goes away, and the missing
trailing space on multi-Tab signals "you're picking; press
space when you're done" without needing modal affordances.
2. **Keyword candidates in grammar order.** Dropped the
alphabetical sort in `describe_expected` in favour of
chumsky's native source-order traversal of `or_not`/`choice`
chains — empirically this matches the canonical command
shape. Result: `add column ` now offers `to` before
`table` (as `add column [to] [table] <Table>:…` reads),
not `table` before `to` which previously suggested the
nonsensical `add column table to ...`. Identifiers still
alphabetised within their group; entry-keyword fallback
for the no-prefix case stays alphabetical (no source order
when 10 separate command branches).
Tests: 750 passing, 0 failing, 1 ignored (747 baseline →
+3 net: replaced single-candidate Esc/Backspace tests with
new multi-candidate variants; added the unique-Tab-chains-
naturally case that drove the round-2 fix; kept the
keywords-in-grammar-order test updated to assert
`to`/`table`/identifiers ordering).
|
||
|
|
bd1cce672d |
ADR-0022 stage 8 follow-up: fixes from real-app testing
Three fixes from the user's testing run, plus an investigation note on a fourth. #4 Sticky hint during cycling. The previous code recomputed candidates_at_cursor at the post-Tab cursor position, which made the panel whiplash through "what comes next at the new cursor" between cycles. ambient_hint now short-circuits to the memo's stored candidate list while the memo is alive — so Tab Tab Tab keeps showing the same list with the selection moving, then snaps to the post-Tab ambient state once any non-Tab key clears the memo. #2 Candidate ordering and kind-coloured rendering. New `Candidate { text, kind: Keyword|Identifier }` carries the classification through completion, last-completion memo, and ambient-hint payload. candidates_at_cursor now sorts keywords first (alphabetical), identifiers second (alphabetical), and the hint-panel renderer colours keywords in `tok_keyword` and identifiers in `tok_identifier`. Keyword-vs-identifier name collisions resolve in favour of the keyword (rare; the user can still address their table via different syntax). #3 tok_identifier no longer matches theme.fg. Identifiers in the input pane now render in a distinct cool grey-blue (dark) / dark steel-blue (light), so they stand out from prose-like default text without competing with keyword purple. Same colour drives the identifier candidates in the hint panel for visual consistency input ↔ hint. Limitation worth knowing: "keywords first, alphabetical" is not the same as grammatical order. For "add column " the hint shows `table to` not `to table` — chumsky's expected-set doesn't preserve combinator-source order, and encoding it in the registry adds maintenance overhead the fix doesn't cleanly justify. Marked for future revisit if it bites. #1 (Tab does nothing on "add column ") — not reproduced through App::update. The internal logic works correctly: "add column " + Tab inserts "Customers ", second Tab cycles to "Orders ", third to "Thing ". The most likely explanation is a stale binary or a terminal-level event intercept (tmux focus, kitty-keyboard protocol differences, etc.) — needs user verification with a fresh build. Tests: 747 passing, 0 failing, 1 ignored (744 baseline → +3: 2 new completion-ordering cases including the keyword-wins-on-name-collision edge, plus 1 hint-mid-cycle sticky test). Clippy clean. |
||
|
|
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. |
||
|
|
51a8d9ac44 |
ADR-0022 stage 8c: IdentSlot propagation + SchemaCache API
`IdentSlot` gains `expected_label()` and the round-trip
`from_expected_label()`. The four slot kinds map to the
user-facing labels "identifier" (NewName), "table name",
"column name", "relationship name".
`ident_ctx(slot)` now actually applies `slot.expected_label()`
as the chumsky label (was documentation-only after stage 6).
Parser errors and the hint panel's "expected: …" prose now
read with the slot-specific name: "expected table name"
instead of the generic "expected identifier". One parser
test updated accordingly; the four catalog `parse.token.*`
keys are unaffected (the slot labels are a parallel surface).
New `completion::SchemaCache { tables, columns,
relationships }` struct + `for_slot(slot) -> &[String]`
accessor. Empty by default; runtime wiring lands in a
follow-on substage. NewName slots return `&[]`
unconditionally.
`candidates_at_cursor` extended to accept `&SchemaCache`:
when the parser's expected-set includes a slot label,
schema candidates from the cache are added alongside the
keyword candidates. Both sources are then prefix-filtered,
combined, sorted, deduplicated. App::schema_cache field
threaded into both the App-side completion paths and the
ambient_hint computation in ui.
Tests: 738 passing, 0 failing, 1 ignored (730 baseline →
+8: 2 IdentSlot label round-trip tests, 6 completion-with-cache
cases covering table/column/relationship slots, prefix
filtering, empty cache, and NewName-no-candidates).
Clippy clean.
User-visible: identifier completion infrastructure is in
place but the cache is always empty — runtime wiring (the
next substage) will populate it on project load and after
successful DDL, at which point Tab on identifier slots
starts offering schema names.
|
||
|
|
06e8d1e769 |
ADR-0022 stage 8a: non-modal keyword completion + Esc/Backspace undo
Per the user's framing decision: there is no "completion
mode." Tab is just an action that consumes whatever is
expected at the cursor, and the existing always-on hint
panel (stage 5) tells the user what's available.
New `completion` module: `candidates_at_cursor(input,
cursor)` returns a `Completion { replaced_range,
partial_prefix, candidates }` based on the parser's
expected-token set at the cursor position. Filters to bare
keyword candidates only (no punctuation, no descriptive
labels), narrowed by the typed prefix (case-insensitive).
`LastCompletion` memo struct on `App::last_completion`
carries the cycle state: inserted_range, original_text,
candidates, selection_idx. Wrap-around forward/backward
indices.
App key handling (added before the existing matcher):
- Tab → cycle forward if memo present; else insert first
candidate; create / advance memo.
- Shift-Tab → cycle backward if memo present; else
insert last candidate (alphabetically) so the user can
jump to the end without cycling through everything.
- Esc / Backspace while memo alive → restore
original_text in inserted_range, place cursor at the
pre-Tab position, clear memo.
- Any other key → clear memo, then process normally.
The user's symmetry preference was load-bearing here:
"insert with one keystroke, remove with one keystroke."
Both Esc and Backspace honour that — multiple Tab cycles
collapse into one undo. Documented inline.
A single-candidate completion still creates a memo so
Esc/Backspace can undo it. Multiple Tabs in a row cycle
through the candidate list with wrap-around at both ends
(per the user's #2).
Tests: 728 passing, 0 failing, 1 ignored (705 baseline →
+23: 13 completion module + 10 app integration tests
covering Tab, Shift-Tab, cycling, wrap-around, Esc-undo,
Backspace-undo, multi-Tab-then-Esc, memo invalidation by
typing or cursor movement). Clippy clean.
Stage 8b will add multi-candidate hint-panel rendering
with scroll markers (`<` `>`) per the user's #2. Stage 8c
will plumb in identifier completion + invalid-identifier
detection.
|
||
|
|
11071ae164 |
ADR-0021 implementation: per-command usage templates in parse errors
New `dsl::usage` module: registry pairing each command's
entry-keyword with a `parse.usage.*` catalog key.
`matched_entry()` resolves the entry keyword from the
consumed token prefix; multi-entry families (add, drop,
show) return all matching keys.
Catalog: new `parse.usage.<command>` keys (one per command),
`parse.token.{keyword,punct,...}` vocabulary (one per
Keyword/Punct variant + token-class labels + LexError
kinds), and `parse.available_commands` for the no-prefix
fallback. Catalog grows ~60 entries.
Validator: extended KEYS_AND_PLACEHOLDERS; new completeness
test asserts every Keyword and Punct variant has its
`parse.token.*` entry.
`app::dispatch_dsl` rewritten to compose three blocks per
ADR-0021 §2: caret + structural/custom error + usage block
(or available-commands fallback per §5). Caret math fixed
to use original-input byte position rather than
trimmed-input position (the lexer no longer trims before
lexing). Three pre-existing app tests adjusted to look
across all error lines instead of `output.back()` (the
usage block is now the last line).
`dsl::usage::matched_entry` uses `<=` rather than `<` for
position comparison so custom errors raised by `try_map`
(whose span starts at the first consumed token) still
resolve to the entry keyword.
Tests: 668 passing, 0 failing, 1 ignored (650 baseline →
+18: 8 usage + 1 token-vocab completeness + 9 new
integration tests in tests/parse_error_pedagogy.rs
covering create/add/drop/show/frobulate/update/insert
cases). Clippy clean.
|
||
|
|
a6fd26d15a |
ADR-0019 §9 sweep (3/3): ui.rs prose strings (caught in manual sanity)
Surprise gap from the post-sweep sanity check — `ui.rs` had a substantial set of TUI-rendered strings that the previous two sweep passes didn't cover. Caught by grepping for capitalised literals in `ui.rs` after running the binary smoke check. ## Migrated - **modal.*** — load picker title / empty state / path prompt; rebuild confirm title / "Continue?" prompt. (modal.path_entry's title comes from `save.*` since it's the save / save-as dialog.) - **save.*** — `save` no-op hint, modal titles for Save / Save as, modal prompt body. - **status.*** — status bar `Project:` label and the `(no project)` placeholder. - **panel.*** — `Tables` panel title, `(none yet)` placeholder for empty tables, `(no active hint)` placeholder for the hint panel. - **shortcut.*** — the bottom-bar keyboard hint labels (submit, confirm, cancel, yes, no, load, select, browse_path, back_to_list, switch, advanced_once, cancel_one_shot, quit). Each is a translatable label paired with a key name (Enter / Esc / Ctrl-C / etc.) at the call site. Keystroke names are deliberately left as literals — translating them would mean retraining users away from what their keyboard says. The `push_shortcut` closure's parameter type changed from `&'static str` to `&str` so it accepts the catalog-returned String. ## Deliberately left - **Echo prefix tags**: `[simple] `, `[advanced] `, `[system] `, `[error] `. Their column widths are hardcoded into the wrap-width calculation in `render_output_panel`; translating them would silently break alignment. Worth a follow-up pass if a future locale needs different prefixes (would need `mode.label()` and the echo-tag widths to live behind a single locale-aware function). - **Mode labels**: `SIMPLE` / `ADVANCED` / `Advanced:` rendered in the input panel border. Same alignment reasoning as the echo tags — also they're keywords (the user types `mode simple` to switch), so translating the display label without translating the command word would be confusing. Left as is. - **Visual decoration**: `[Y]`, `[N]`, `[TEMP] `, `>` cursor markers, `█` cursor block, `↑↓` arrow glyph, `›` selection marker. Universal symbols / labels rather than translatable prose. ## Catalog totals The catalog now has ~170 entries across 16 categories. `tests/engine_vocabulary_audit` passes — no engine vocabulary leaks anywhere user-reachable. ## Tally 610 tests passing (no change — pure refactor with identical-output catalog substitutions). Clippy clean with nursery lints. Release builds at 7.8 MB. ADR-0019 §9 is now genuinely complete. |
||
|
|
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).
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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).
|
||
|
|
3dbaedc1da |
help: surface ADR-0017/0018 auto-fill semantics (B1)
ADR-0017 added --force-conversion / --dont-convert as opt-in flags on `change column`; the help text already mentioned the flags but didn't explain when they apply. ADR-0018 generalised serial beyond PK and added auto-fill on `add column ... (serial|shortid)` for non-empty tables; none of that was reflected in user-visible help. This commit: - Annotates the `add column` line with a continuation note that adding serial/shortid to a non-empty table auto-fills existing rows. - Annotates the `change column` line with a continuation note that converting to serial/shortid auto-fills null cells. - Appends an "Auto-generated types" section explaining serial and shortid: how they auto-fill, that they imply UNIQUE outside a PK (serial) or always (shortid), and that adding/converting-to either type on a non-empty table auto-fills existing/null cells. The new test `help_describes_auto_generated_type_behaviour` pins these phrases so a future help-text edit can't silently drop the pedagogical lines. The existing `help_command_lists_supported_commands` and `help_lists_export_and_import` tests still pass — they only assert substring presence. No engine vocabulary leaks (ADR-0002 posture preserved). 536 -> 537 passing, clippy clean. |
||
|
|
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. |
||
|
|
7dfa718c6e |
parser: structural error rendering, source echo, and caret pointer
The old humanise() fell back to chumsky's terse Display for non-
custom errors and appended "(near `X`)", which on top of an
already-cryptic "found 'i' expected ':'" turned the message into
a puzzle. Now humanise() reads the structured RichReason, lists
expected RichPatterns in plain prose, and prefixes the message
with the consumed context.
Before: parse error: found 'i' expected ':' (near `i`)
After: parse error: after `change column Rich`, expected `:`,
found `i`
dispatch_dsl additionally echoes the source line on parse failure
(matching the success path's "running: ...") and prints a `^` caret
under the failure position, so the user can see what got submitted
and where the parser broke without re-reading from scratch.
Known limit: keyword_ci's custom-error mismatches don't aggregate
across choice alternatives, so messages like "expected DATA or
TABLE" (bison-equivalent) aren't yet possible. That's a structural
fix to the keyword matcher, deferred to a future parser-affordances
ADR.
Tests: +2 structural-error regression tests.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
41cef5399b |
parser: make to and table independently optional in add column
Previously the grammar accepted only `to table` together or neither. The user-stated convention is that bare table identifiers are accepted in unambiguous positions (matching how `add 1:n relationship from <T>.<col> to <T>.<col>` takes bare table names). Both `to` and `table` are now or_not'd independently, so all four combinations parse identically. Updates the in-app `help` listing to advertise the new shape: `add column [to] [table] <T>: <col> (<type>)`. 3 new parser tests cover the variants. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|