ADR-0052 moved success journaling out of the worker to the dispatch
layer, leaving the `source` that handlers threaded purely for the
worker's old history.log write dead. Remove it:
- drop `_source` from finalize_persistence and do_rebuild_from_text
- inline + delete the three read-only *_request wrappers
- drop the now-unused `source` param from the ~30 forwarding worker
handlers (leaf + composite), compiler-guided
- remove the `source` field from the DescribeTable/QueryData/RunSelect
requests and their DatabaseHandle methods (call sites updated)
The only worker `source` left is the snapshot/undo label
(snapshot_then / stage_pre_mutation / begin_batch). Purely mechanical,
no behaviour change. 2471 pass / 0 fail / 1 ignored, clippy clean.
Record the submission mode per history entry so advanced commands are
reusable in simple mode, and fix the bug where a ':'-one-shot command
lost its ':' across sessions (ADR-0052, closing #30).
Format: the history.log status token gains an optional ':adv' suffix
(ok / ok:adv / err / err:adv); 'source' stays last and canonical, so
replay is unaffected. The in-memory ring (still Vec<String>) stores
advanced entries ': '-prefixed; recall strips the ':' in advanced mode
and keeps it in simple; hydration reconstructs the prefix from the tag.
Journaling moved from the worker to the dispatch layer (spawn_dsl_-
dispatch / run_replay / app-command sites), where the mode is in scope
with no worker plumbing; finalize_persistence writes only yaml/csv
(commit-db-last still atomic for state). The journal write is now
best-effort (command already committed), consistent with the failure
path. App commands journal simple, so they recall bare. Journaling is
now uniform (every successful command, per ADR-0034) — closing a gap
where show tables/relationships/explain didn't journal.
Amends ADR-0034 (status tag + journaling location), ADR-0015 §6
(history.log out of the worker tx), ADR-0040 (journal-write best-effort).
15 worker-level journaling tests retired, re-covered at the new layer
(history.rs format, app.rs recall matrix, iteration6 cross-session
regression, replay). 2471 pass / 0 fail / 0 skip, clippy clean.
Per ADR-0051 (closing issue #27): the bottom status line is now a
keystrokes-only, state-selected strip. A pure status_bar_bindings()
picks the binding set by priority (first match wins):
sidebar focus → Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input
completion live → Tab/Shift-Tab cycle · Esc cancel · Enter run
history nav → ↑↓ browse · Esc clear · Enter run
editing → Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run
default (empty) → Ctrl-O sidebar · Tab complete · ↑ history · Enter run
The editing state surfaces the #29 readline keys (ADR-0049's deferred
advertisement). Typed-command words (mode advanced/simple, the ':'
one-shot) and Ctrl-C quit leave the strip; simple mode's empty-input
hint gains a '`mode advanced` for SQL' pointer (advanced mode shows
none — a switcher knows the way back, and help covers it).
Mechanism: status_bar_bindings + a thin renderer (unit-testable);
App::is_browsing_history() exposes the private history_cursor; the mode
pointer lives in resolve_hint_lines. Catalog: 12 new shortcut labels +
panel.hint_mode_advanced (en-US.yaml + keys.rs, validator 1:1), 5 dead
strip strings removed.
Forks user-chosen: editing state shows #29 keys; quit omitted; no
width-drop machinery (longest strip ~65 cols fits; a width-budget test
keeps it lean). Modal-aware strip is OOS (pre-existing). Tests: 9
Tier-1 unit (per-state key sets, width budget, mode pointer), 1 Tier-3
rewritten, 15 full-panel snapshots re-accepted (reviewed). 2467 pass /
0 fail / 0 skip, clippy clean.
Per ADR-0050 (closing issue #28): the confirmation echo after an
incidental structural edit — create table, add/drop/rename/change
column, add/drop index — now renders the structure only (header +
column box + indexes + constraints) and no longer appends the
References:/Referenced by: relationship block.
Rationale: a confirmation reports the change just made, not the
table's relationships, which the user didn't touch. Relationship info
is still one `show table <T>` away, and the relationship-subject
surfaces (show table, add/drop relationship) keep their ADR-0044
diagrams unchanged.
Scope is all incidental DDL (user-confirmed). Mechanism: drop the
relationship-block call from render_structure (all its callers are
incidental DDL); the handle_dsl_success diagram-vs-structure routing
is unchanged. The orphaned relationship_prose_lines + cols_disp
helpers are deleted (the prose format survives in ADR-0016 §5 + git
history for a future OOS-7 always-prose setting).
ADR-0050 supersedes ADR-0044 §1's incidental-DDL prose clause and the
relationship-block half of ADR-0016 §5 (both annotated). Tests: prose-
presence unit test + snapshot removed; new unit test locks structure-
only with inbound+outbound relationships present; the misnamed add-
column integration test inverted + renamed. 2458 pass / 0 fail / 0
skip, clippy clean.
Implement the deferred I1b readline shortcuts in the command input
field (ADR-0049, closing issue #29):
Esc clear a partly-typed command (only when no completion memo)
Ctrl-A cursor to line start (Home alias)
Ctrl-E cursor to line end (End alias)
Ctrl-W delete the previous word (readline-style, UTF-8 safe)
Ctrl-K kill to end of line
Ctrl-U kill to start of line
Esc precedence is preserved: a live Tab-completion memo still wins
(Esc undoes the completion first, ADR-0022); Esc clears only when no
memo is alive. While a sidebar panel is focused (Ctrl-O), Esc exits
navigation mode upstream and never clears the input draft. Cursor-only
keys leave history navigation intact like Home/End; buffer-mutating
keys end it like Backspace.
New helpers clear_input / delete_prev_word / kill_to_end /
kill_to_start in src/app.rs. 22 new Tier-1 tests (2458 pass / 0 fail
/ 0 skip, clippy clean). ADR-0049 amends ADR-0046's OOS list;
requirements.md I1b marked done.
At `seed <table> ▮` the hint showed only the `set`/`--seed` chips and
never mentioned the optional row count — a bare positional number with no
candidate, on an already-complete command, so neither the candidate
ladder nor the resolver surfaced it. (A prior IntroProse attempt was
reverted: pending_hint_mode is cleared by the trailing optionals.)
Carry a skipped Optional's IntroProse hint: walk_optional stashes the
inner's key into a new WalkContext.surviving_intro_hint (key + position)
before the empty match clears pending_hint_mode; the snapshot keeps it
only when the skip position is the cursor (so it never leaks past a
later-consumed `set …` clause, nor once the count is given); the
resolver returns it ahead of the empty-expected short-circuit. The seed
count is wrapped Hinted{IntroProse("hint.seed_count")}; the prose names
the count (default 20), the `.column` column-fill form, and `set` /
`--seed`. Tab still cycles the keywords.
Only IntroProse is carried; ProseOnly/ForceProse and the CREATE-TABLE
element (a required Repeated) are untouched. No AmbientHint/renderer
change. Fires in both modes.
ADR-0022 Amendment 7; +3 tests.
Two additive D7 catalogue rules, surfaced while writing the website seed
docs. No change to the type fallback, executor, or grammar.
#33 — year-like int columns. `published`/`birth_year` were just `int`, so
they fell to the unbounded int path and produced nonsense (`9419`). Add an
int-gated year rule (after the quantity rule, so `year_count` stays a
count): `year`/`*_year`/`published`/`founded` -> a bounded 1950-2025 year
(new `YearRecent`), or the dob-style birth window 1945-2007 for
`birth`/`born`/`dob` (new `YearBirth`). Plain int; not added to the D9
named-generator vocabulary.
#34 — conventional choice sets. A few enum-ish names have a near-canonical
small set that reads far better than lorem text. Add a type-gated PickFrom
lookup (reusing the existing generator): priority/prio, severity,
rating/stars. `status` is deliberately excluded (values too
domain-specific) and keeps the D12 advisory; a user IN-CHECK still wins.
`priority` leaves ENUM_TOKENS.
ADR-0048 Amendment 1; +8 tests (incl. a column-fill integration test that
also closes a pre-existing gap on that path).
The focused sidebar panel border (ADR-0046 DC3) was bright `fg` plus
`Modifier::BOLD`. Bold box-drawing glyphs render as broken/gapped
line-art in the asciinema cast player and are fragile in some terminals.
`panel_border_style` now marks focus with a non-bold accent colour
(`theme.mode_simple`, blue); the unfocused border stays muted. Bold is
untouched on text spans (titles, key hints) — the constraint is
specifically that box-drawing borders carry no bold attribute.
Pure style change: the Tier-2 snapshots are text-only so none needed
re-accepting; the Tier-1 assertion was updated and a render-level test
now checks the rendered border cells carry the accent and no bold.
ADR-0046 Amendment 1.
`decimal` is stored as exact TEXT, but SQLite has no native decimal type,
so arithmetic/aggregation implicitly coerces it to an IEEE-754 double.
The computed result carries no playground type, so `sum(price * qty)`
rendered the double's full noise — `298.59999999999997` for `298.60` — a
confusing, off-topic float lesson for a teaching tool.
Add `format_real_display`: round REAL values to 15 significant figures
(a double's reliable precision) then take the shortest round-tripping
form, collapsing `298.59999999999997` to `298.6`. Wired into `format_cell`
(result-set / `show data` cells) only — the sole surface where the noise
appears, since it arises from arithmetic.
Every other f64->string path keeps full precision for semantic, not
cosmetic, reasons: CSV persistence stays byte-exact for round-trip;
`render_value` is a canonical identity key for the uniqueness dry-runs
(dry_run_unique, check_uniqueness_collisions), where rounding would
report collisions the exact-valued engine wouldn't; FK-key matching and
EXPLAIN-SQL literals likewise stay exact.
ADR-0005 Amendment 1; +7 tests.
A bare table alias typed where a column is expected — `… GROUP BY o`,
with `o` aliasing `FROM Orders o` — was a blind spot: completion offered
nothing for `o`, and the hint panel called the in-scope alias an unknown
column (`no such column o on table Orders, ...`).
Completion now offers each FROM source's qualifier (alias-if-present-else
table-name) at a bare sql_expr_ident slot, folded into the column
candidate list; on an exact-qualifier partial the alias source steps
aside so the diagnostic can surface. The bare-reference diagnostic arm
emits a targeted `alias_used_as_column` / `table_used_as_column` hint
("`o` is a table alias — write `o.<column>` ...") after the
projection-alias check, so ORDER-BY alias refs still win and a genuine
unknown column still reports `unknown_column`.
Two guards keep the qualified-form advice correct: SQL only (role
`sql_expr_ident`, so the DSL `expr_column` path keeps `unknown_column`
since the DSL has no `table.column` syntax) and effective-qualifier
match (alias-if-present-else-table, so an aliased source referenced by
its shadowed real name falls through rather than being advised as
`name.<column>`). The diagnostic is a drop-in replacement for
`unknown_column` at the same span/Error severity, so verdict/overlay/hint
paths are unchanged.
ADR-0032 Amendment 3; +10 tests.
Captures this session's three committed fixes (flag-completion dash,
table-alias validity, `:` one-shot live feedback), the two
investigated-as-designed findings (comma-FROM, aggregate-without-GROUP
BY), and points the next session at the filed enhancement issues
(#26 seed-count hint, #27 bottom status line, #28 add-column
relationship prose) plus the standing roadmap.
The `:` one-shot escape (ADR-0003) is stripped at submission, but the
*live* feedback kept the leading `:` in the buffer it handed the
walker — so Tab completion, the validity verdict, and the highlight
overlays all bailed at the `:` and treated the SQL as an unknown
command. Effect: in `:`-mode, Tab completed nothing and a valid query
could flash an error, while the identical line in full `mode advanced`
worked. (The ambient hint already stripped it, which is why the hint
showed the right column name while Tab did nothing.)
Add `App::feedback_view()` — the `:`-stripped SQL view, the cursor
mapped into it, and the stripped byte offset — and route all four live
paths through it:
- completion (Tab): complete against the view, then shift the returned
`replaced_range` back by the offset so the edit lands in the buffer;
- validity verdict: verdict the SQL, not the sigil;
- highlight/overlays: new `render_input_runs_feedback` highlights and
diagnoses the view (shifted by the offset) while the `:` renders as
plain text;
- ambient hint: consolidated onto `feedback_view`, replacing the
duplicate local `strip_one_shot_prefix`.
In a SELECT, the projection can reference a table alias whose defining
FROM binding sits textually *after* the cursor — e.g.
`select sum(ol.count*p.price) … from … OrderLines ol …`. The candidate
engine already recovers that scope via the §10.6 full-input lookahead
(ADR-0032), but the typing-time validity indicator
(`invalid_ident_at_cursor`) walked only the text before the cursor,
found `ol` in no scope, and flagged it as an unknown column — a red
"ERR" overlay on an otherwise-valid query. (Other aliases escaped only
by coincidentally prefix-matching real columns.)
Give the validity check the same full-input lookahead: at a SQL
expression slot, recover the from-scope from the whole input and bail
when the partial prefix-matches a binding's alias or table name.
The partial-token walk stopped at `-`, so after typing `-` (or `--`)
the partial was empty and the replaced range was a zero-width point
*after* the dash. Two bugs followed at a flag position (e.g.
`add 1:n relationship … -`): the `on` keyword was offered (it
prefix-matched the empty partial), and accepting a candidate inserted
after the dash — `-on`, `---create-fk`, `----all-rows`.
Detect a dash-prefixed token at a word boundary as a flag-in-progress
and fold the whole dash-run into the partial, gated on a flag actually
being expected there (so `where x = -5` stays a signed number, not a
flag). The flag matcher now strips leading dashes and matches the body
uniformly (empty / `-` / `--` → all flags; `--cr` → create-fk).
Keywords like `on` no longer appear after a dash, and accept replaces
the dash(es) so `-` → `--create-fk` and `--all` → `--all-rows`. Two
partial-flag snapshots updated (they had captured the old behaviour).
Build the two SD2 surfaces Phase 1 deferred:
- `set` override clause (D2): comma-separated per-column pins —
`= 'v'` (fixed), `in ('a','b')` (pick-list), `as <generator>`
(named), `between x and y` (range; numeric and quoted dates).
Type-aware via the typed `current_column_value` slot; an override
drops its column from the generic-fill advisory (D13). Folded from
the flat matched path (build_seed_overrides) and applied to the
per-column plan (apply_seed_overrides).
- `<table>.<column>` column-fill (D1 form 2): an UPDATE over existing
rows. Refuses PK/autogen targets, empty-table no-op, FK-samples the
parent, collision-free for UNIQUE/identifier targets, one undo step;
`set` may only adjust the filled column.
Supporting work: KNOWN_GENERATORS vocabulary + generator_for_name
(src/seed/vocabulary.rs, D9); a range Generator + range_bounds_reason;
IdentSource::Generators and HighlightClass::Function; completion of the
generator vocabulary after `as` and the set/.col column slots; the
typing-time validity indicator for an unknown generator; help,
parse-error pedagogy rows, and the D13 advisory's Phase-2/3 wording.
A bounded override (fixed value / too-short pick-list) on a
single-column-UNIQUE target is a friendly error rather than a silent
uniqueness cap (post-implementation /runda finding, user-chosen).
Dates in the range form are quoted (no date-literal token exists);
ADR-0048 D2 amended accordingly. Both modes (D5); reproducible (D4).
- ADR-0048 status -> Accepted; Phase 1 implemented (commits
202e25a..fbd219b), with the pre-build and post-implementation /runda
passes and the 2358-test green state recorded; index entry updated.
- requirements.md: SD1 [x] (whole-row seed + FK/junction, both modes,
--seed reproducibility with no exceptions), SD2 [/] (core generators /
determinism done; the set override clause + column-fill are Phase 2),
A1 14/15 (only hint/H2 remains unregistered).
- Handoff 65: the full seed Phase-1 build, the two /runda passes, where
the code lives, and Phase-2 / next steps.
P1.4 — user-visible surface:
- Grammar: `seed <table> [count] [--seed <n>]` (the first DSL flag with a
value); build_seed disambiguates the seed value from the positional count.
- Verified the auto-wired surface: table-name completion, --seed offered as
a candidate, validity consistent with `show data`, an ADR-0042 near-miss
row for bare `seed`, and render tests for the seed outcome.
/runda hardening — eight DA findings, all resolved:
- FK sampling now uses ORDER BY so --seed reproducibility no longer relies
on SQLite's unspecified DISTINCT order (D4).
- shortid columns now generate from seed's seeded RNG (new
shortid::generate_with_rng) — D4 now holds with no exceptions.
- Added the missing coverage the DA flagged: undo-one-step (D15), replay
re-runs a seed line (D16), advanced-mode (D5), atomic rollback on a
constraint failure, seed 0 no-op, complex-CHECK advisory (D17), and
FK + shortid reproducibility.
2358 pass / 0 fail / 0 skip, clippy all-targets clean.
do_seed inserted row-by-row through do_insert, re-writing the whole
table CSV each time — O(N^2). Extract do_insert's row core into a new
insert_one_row (bind + serial/shortid autofill + FK-enriched execute,
no tx/persist), shared by:
- do_insert: one row in its own transaction (behaviour unchanged).
- do_seed: all rows in ONE transaction, with a single
finalize_persistence before the single commit — O(N), preserving
ADR-0015 §6 commit-db-last. A mid-batch failure now rolls the whole
seed back atomically; the capped preview is read back by rowid.
A near-max 10000-row seed drops from ~tens of seconds to well under
one. do_insert behaviour unchanged (whole suite green: 2346 pass /
0 fail / 0 skip, clippy clean); seed's existing tests exercise the
batch path.
A dedicated SeedResult replaces the borrowed insert outcome (X5):
- CommandOutcome::Seed + DslSeedSucceeded event + handle_dsl_seed_success
render: the echo, "N row(s) seeded into T", a capped preview table
(D18, first 20 rows; full count always reported), and a Hint-styled
advisory naming enum-ish / un-derivable-CHECK columns filled with
generic text (D12/D13, Phase-1 wording).
- SeedResult carries requested vs produced, so a junction cap is now
reported to the user, not only logged.
- Count cap (D6): a seed over 10000 rows is refused with a friendly error.
- Catalog keys ok.rows_seeded / seed.capped / seed.advisory_generic.
4 new tests (advisory flag, IN-check not flagged, preview cap, excess
count). 2346 pass / 0 fail / 0 skip, clippy clean.
do_seed now enforces value uniqueness and derives enum values:
- Uniqueness groups (D10): the user-fillable PK, compound UNIQUE
constraints, and single-column UNIQUE / identifier columns stay
distinct across the batch and against existing rows (retry per row).
Junction distinct-combos fall out of PK-tuple uniqueness and cap at
the available parent combinations (logged when capped; the
user-facing note arrives with the advisory in P1.3c).
- Identifier-int columns get a monotonic sequence past MAX(col) (D10),
so they never collide.
- IN-CHECK derivation (D17): a simple `col IN ('a','b')` CHECK becomes
the value source via the new, unit-tested seed::parse_in_check_values,
so the enum-as-CHECK pattern just works.
8 parser unit tests + 4 integration tests (unique column, identifier
sequencing, junction cap, IN-check enum). 2343 pass / 0 fail / 0 skip,
clippy all-targets clean.
Deferred to P1.3c: dedicated SeedResult + capped preview (D18) + the
enum/CHECK advisory incl. the cap note (D12/D13); P1.3d: multi-row path.
do_seed fills foreign-key columns by sampling existing parent rows
(D14): sample_parent_key_tuples reads distinct parent keys, and a
compound FK reads all its child columns from one sampled parent row per
child row. An empty parent is refused with a friendly "seed the parent
first" error. The block guard (D1) refuses a NOT NULL blob column (seed
can't generate one); a nullable blob is omitted (-> NULL).
4 integration tests (valid FK references, empty-parent refusal, NOT NULL
blob refusal, nullable-blob omission). 2331 pass / 0 fail / 0 skip,
clippy all-targets clean.
Deferred to P1.3b: identifier/constraint uniqueness incl. junction
distinct-combos (D10), IN-CHECK derivation (D17), dedicated SeedResult +
capped preview (D18) + advisory (D12/D13), and the multi-row path.
The pure generation half of `seed` — no command wiring yet:
- src/seed/: ColumnSpec + Generator model and a seeded StdRng; the
type-gated name-heuristic catalogue (D7) with documented
false-positive guards; table-context name disambiguation (D11);
identifier (D10) and enum-ish (D12) detection; per-type + bounded-date
generators (D8); the hand-rolled product generator (D9); and PickFrom
for IN-CHECK / enum lists.
- Adds the `fake` crate (v5, default features). Verified: single rand
0.10.1 (no duplication), determinism via one seeded StdRng driving
both fake and the hand-rolled generators, security-clean across
osv/grype/trivy.
- ADR-0048 D3 updated to record the dependency verification.
32 Tier-1 tests (exact-value via fixed --seed); 1673 lib tests pass,
clippy all-targets clean.
Dedicated `seed` command: realistic, name-aware fake data via the
`fake` crate + a type-gated heuristic catalogue, table-context name
disambiguation, a hand-rolled product generator, bounded dates, an
identifier-uniqueness rule, a quoted `set` override clause (value /
list / generator / range), `--seed` reproducibility, FK sampling
(empty-parent error, junction distinct combinations), CHECK
derive-or-friendly-fail, undo as one batch step, replay as a
data-write, capped auto-show, and an enum/CHECK advisory.
Design settled across an extended user fork dialogue, then hardened
by a /runda DA pass that found six blockers (undo, replay, set
quoting, CHECK handling, advisory phasing, auto-show flood) — all
folded in, genuine forks re-escalated and user-resolved.
Takes up SD2 ([~]->[ ]); SD1 implementation in progress.
The trailing "multiple tabs" clause of S3 and V2 is dropped from
tracked scope by user decision. The output pane is settling on the
single scrollable V4 journal model rather than switchable result
tabs, so both items are now fully satisfied (the table view and
single-element visualisation were already built). V4 noted as the
sole tracked direction for evolving the output pane. A future
return to tabbed output would be a fresh requirement.
No ADR touched: multi-tab output never had a deciding ADR.
Bring handoff-64 current: it was written just before #22/#24 were closed
and the docs commit landed. §1 now reflects HEAD f0afec3 with both
issues closed on Gitea; §6 removes the completed finalization steps and
points the next session at the open requirements backlog (§7).
Render the keystroke badge and step caption as a solid yellow rectangle
with no border glyphs and a one-cell text margin, instead of a
rounded-border box — deliberately unlike the app's bordered panels so
the demo overlays read as a distinct, eye-catching callout. Shared
fill_overlay_rect helper (borderless Block fill + inset Paragraph).
Snapshots regenerated; ADR-0047 D4 wording updated.
Ctrl+] (decodes to Char('5')+CONTROL) toggles an invisible capture
buffer: typed characters accumulate without touching the input/output,
Backspace edits, every other key is inert, and a second Ctrl+] commits
the text to a caption box (empty commit dismisses). Handled at the top
of handle_key — before the badge and modal gates — so captions can be
authored over the load picker (the #24 cast); an ordinary keystroke
clears a visible caption. The caption renders as a floating
black-on-yellow box at the output panel's bottom-right, wrapped to <=3
lines (then ellipsised), with the keystroke badge stacked directly
above it when both are present.
Tier 1: capture/commit, invisible accumulation, backspace, inert keys
(incl. no badge), empty-commit dismiss, next-key clear, over-modal,
demo-off inert. Tier 2: caption / stacked / wrapped snapshots. Phase C
of ADR-0047 — feature complete.
In --demo mode, an otherwise-invisible key (Tab, Enter, arrows,
Ctrl-O, …) raises a transient [LABEL] badge — a floating
black-on-yellow box inset at the output panel's bottom-right. Set in
App::update before the modal gate (so it shows over the load picker,
the #24 cast); pure demo_badge_label maps the key set. The runtime
expires it on a ~1.5s timer via a new nearest_deadline helper that
extends the existing time-boxed-recv arm condition without disturbing
the ADR-0027 indicator debounce. New App.last_output_area lets the
top-level draw anchor the overlay; overlay colours centralised in
theme.rs.
Tier 1 (label fn, badge set/seq, over-modal), Tier 2 (dark/light
snapshots, black-on-yellow style, too-small clamp), runtime unit
(nearest_deadline). Phase B of ADR-0047; captions land in C.
Add `--demo` (and the RDBMS_PLAYGROUND_DEMO env fallback) to enter
demonstration mode, threaded onto App.demo_mode through run_loop —
mirrors the --no-undo plumbing. Off by default, zero footprint when
off. The --help line advertises only the visible keystroke badges;
the Ctrl+] caption trigger is kept low-profile (ADR-0047 D6 updated).
Phase A of ADR-0047; behaviour (badges/captions) lands in B and C.
Accepted decision record for the in-app demo overlay: a --demo mode
that shows automatic keystroke badges ([TAB], [ENTER], …) and a
stealth Ctrl+]-delimited step-caption buffer, both as floating
black-on-yellow boxes at the output panel's bottom-right. All forks
user-confirmed; a /runda pass contributed 10 tightening findings.
Indexed in docs/adr/README.md.
Add j (down), k (up), g (first) and G (last) to the load picker's
list sub-mode, alongside the existing arrow keys. Typeable keys keep
the picker drivable by autocast in the website's documentation casts,
which cannot emit arrow keys. Footer hint left unchanged.
ADR-0046 status -> Accepted + implemented (8 commits 9f5f76b..22bec61);
README index updated; the two draft-divergent decisions recorded inline
(App.relationships not SchemaCache; nav overlay partial-clear + gutter).
Handoff 63 covers the full UI build across Phases A/B/C; issues
#20/#21/#23 closed on Gitea.
DC3 — navigation-mode scroll: the focused Tables / Relationships panel
scrolls (Up/Down by a line, PageUp/PageDown by its visible-row count).
Per-panel offsets are clamped to content at render time, and the
renderer reports each panel's visible rows for paging — mirroring the
output panel's scroll. render_items_panel / render_relationships_panel
take &mut App, count their rows, and store+clamp the offset before
building the borrowing lines.
DC2 refinement: the expand-on-focus overlay now clears only the sidebar
strip plus a one-column gutter, leaving the base output/input/hint
visible (unchanged) to the right rather than blanking the whole area —
truer to "underneath keeps its layout", with the gutter keeping the
cut-off edge clean (chosen after eyeballing both variants). ADR DC2 and
the overlay snapshot updated to match.
Tests: line/page scroll move only the focused panel and clamp; the
render clamps a past-the-end offset so the last row stays visible.
Ctrl-O enters a navigation mode orthogonal to the input mode, cycling
focus Input -> Tables -> Relationships -> Input (Esc exits). While a
sidebar panel is focused the sidebar is revealed (a peek, even when
width-hidden) and drawn as an expanded 45-column overlay over a cleared
main area, so the schema is browsable without the cramped 26-column
unfocused width. The focused panel gets an accent border.
Routing lives in the main key handler after the modal gate, so Ctrl-O
and nav keys are inert while a modal is open; in nav mode every
non-navigation key (printable/Enter/Tab/Backspace/...) is inert because
the input is occluded. Scroll keys (Up/Down, PageUp/PageDown) are
reserved for DC3 (next).
New App state: NavFocus { Input, SidebarTables, SidebarRelationships }.
Tests: the focus cycle, Esc exit, input-keys-inert, overlay reveal +
expansion, the accent-border style, and an overlay snapshot.
The left column now stacks a Tables panel over a Relationships panel.
Each relationship renders as three narrow lines — its name, then the
endpoints broken at the arrow (Customers.id -> / indented
Orders.customer_id) — ellipsized past the inner width. The panel is
content-sized within [5 rows ("(none)" when empty), half the column];
the Tables panel keeps the rest (>=3 rows). Phase C adds focus+scroll
for content beyond the cap (clipped for now).
Data path: a new worker Request::ReadAllRelationships +
Database::read_all_relationships returns full RelationshipSchema
records; the runtime posts them via a RelationshipsRefreshed event
alongside the schema-cache refresh, and the App holds them in a new
`relationships` field.
ADR deviation (recorded in ADR-0046 DB2 + index): DB2 specified this
data on SchemaCache; it lives on the App instead — SchemaCache is
walker/completion-facing and needs only relationship names (untouched),
while the full records are UI-only, so App is the cleaner home and it
avoids editing ~23 SchemaCache literals. No behavioural difference.
Tests: panel-height bounds, the three-line render, the empty "(none)"
case, a snapshot, read_all_relationships end-to-end (real DB via the
m:n junction), and the event->field handler.
The schema sidebar (the left Tables column) is now shown only when the
terminal is wider than 90 columns; at or below that it is hidden and
the output/input panels span the full width. This reclaims horizontal
space on narrow terminals — notably the 90-column screencasts, where
the sidebar added little and cost the output panel its width.
Visibility is a pure function of terminal width (sidebar_visible);
the Ctrl-O peek-reveal lands in Phase C. render() splits the layout
conditionally — full-width right column when the sidebar is hidden.
Snapshots/tests that rendered at 80 wide now reflect the hidden
sidebar; those whose intent IS the sidebar (populated_with_table, the
items-panel and drop-table integration checks) render at 110 so the
Tables list is actually exercised — one masked-intent integration
check (matched "Customers" in the output, not the panel) is corrected
the same way. New tests cover the width gate and the show/hide
boundary.
On a comfortable terminal (height >= 40) the input panel shows two
rows: the single logical command soft-wraps across them — the first
row stops 6 columns short for the ADR-0027 validity indicator, the
second uses the full width — so a medium command is fully visible
without horizontal scrolling. A line longer than both rows still
scrolls (DA3-style, one column each side reserved for < / > markers)
to keep the cursor visible.
hint_rows generalises to panel_heights(area) -> (input_rows, hint_rows):
compact (<40) stays input 1 / hint 2; comfortable becomes input 2,
degrading hint-then-input on tiny terminals to protect the output
Min(5). render_input_panel splits into render_input_one_row (the
existing DA3 path, unchanged) and render_input_two_rows, with a new
expand_runs_to_cells helper placing styled cells across the rows.
Tests: panel_heights geometry, two-row wrap, overflow-scroll, the
indicator-stays-on-the-first-row case, and a two-row layout snapshot.
Compact one-row snapshots are byte-identical (that path is untouched).
A command longer than the input field used to clip silently at the
right edge, hiding the cursor and the command tail. Now the single
logical input line scrolls horizontally to keep the cursor in view,
with muted `<` / `>` markers at the reserved edge columns signalling
hidden content on either side.
The offset is a pure function of (line length, cursor column, field
width, previous offset) — input_scroll_offset — so the view only moves
when the cursor would leave the window, and one column is held on each
side for the markers so a marker never hides the cursor. The stored
App::input_scroll_offset resets when the buffer is replaced wholesale
(submit, history recall). The ADR-0027 6-column indicator reserve is
preserved.
Tests: pure-offset cases, tail-visible + head-visible render checks,
and the reset-on-submit/history check. One layout snapshot now shows a
long command's tail instead of its clipped head.
The hint panel's height was recomputed every frame from the wrapped
hint content (1–3 rows), so it resized as the user typed and shoved
the input/output panels — the flicker visible in the screencasts.
Make the height a pure function of terminal geometry (new hint_rows),
fixed between resizes: 2 content rows on compact (<40-row) terminals,
3 only on comfortable terminals narrow enough (<54 inner cols) to wrap
the longest catalog hint past two lines, degrading toward 1 on tiny
terminals to protect the output Min(5). resolve_hint_lines clamps to
that fixed budget (long hints ellipsize; short ones leave rows blank).
This reverses issue #12's shrink-to-content "reclaim"; its two tests
are replaced by an anti-jump invariant plus geometry-helper and
third-row tests. Two layout snapshots regenerated.
Accepted; implementation pending, phased A→B→C. Treats the three
coupled UI issues as one decision (shared width/height budget):
- #20 hint jumpiness: hint height becomes a function of terminal
geometry, fixed between resizes, so it no longer shoves the
input/output panels.
- #21 left column: kept but width-optional (hidden by default ≤90),
with a new relationships sibling panel and a Ctrl-O navigation/focus
mode (peek-reveal, expand-on-focus overlay, scroll).
- #23 long input: single-logical-line horizontal scroll plus a 2-row
display when tall, preserving the ADR-0027 indicator reserve.
A pre-build /runda DA pass drove key corrections: Ctrl-B→Ctrl-O (Ctrl-B
is the tmux prefix), an additive SchemaCache.relationship_details field
(retyping would break completion), full nav-mode key disposition +
modal gate, and Tier-2 snapshot coverage. Reconciles requirements
S1 (evolved), S2 (overridden — separate relationships panel), and
S4 (corrected — the stale "keyboard-toggleable" hint claim is struck;
no toggle added).
Updates docs/adr/README.md index and docs/requirements.md S1/S2/S4.
`create m:n relationship from <T1> to <T2> [as <name>]` generates a
junction table with one FK column per parent PK column ({table}_{pkcol},
typed via fk_target_type), a compound PK over them, and two CASCADE 1:n
relationships -- all in one do_create_table call = one undo step.
Auto-named {T1}_{T2} (optional `as`), both modes, compound-parent PKs
supported (ADR-0043). Self-referential m:n / PK-less parent / internal
junction name / name collision all refused.
Wired across every surface: grammar (separate CREATE_M2N node), worker
executor, runtime dispatch, completion ("m:n" composite), hints,
highlighting, help + usage catalog + disambiguator, and the advanced-mode
DSL->SQL teaching echo (render_create_m2n, round-trips as valid SQL).
Generalized/fixed framework assumptions the build + two /runda passes
surfaced (all behaviour-preserving for existing commands):
- simple-mode dispatch committed simple.first() unconditionally -> tries
candidates, so `create table` no longer shadows `create m:n`.
- the completion continuation-merge was advanced-only -> runs in simple
mode too when an entry word has >1 DSL form (gated simple_count>1).
- do_create_table now rejects internal `__rdbms_*` names (closes a
pre-existing hole on the DSL create-table path too, not just m:n).
- usage disambiguator now recognizes the `m:n` opener.
Tests: 14 integration (tests/it/m2n.rs), 7 typing-surface matrix, echo /
highlight / usage / internal-name units. Closes C4.
2237 pass / 0 fail / 1 ignored. Clippy clean.
create m:n relationship from <T1> to <T2> [as <name>] generates a
junction table (compound PK over the two FK column sets, CASCADE FKs)
plus two 1:n relationships, in one do_create_table call = one undo
step. Forks user-confirmed; /runda DA pass verified the reuse against
code and the no-PK-tables-exist-in-advanced-mode fact (parent-PK guard
retained). Self-referential m:n refused; FK cols named {table}_{pkcol}.
Dropping a PK column was already refused in both modes via the shared
do_drop_column guard; this adds end-to-end coverage on the advanced
ALTER surface (single-column + compound PK, asserting refusal for the
right reason) and documents the asymmetry that advanced-mode SQL can
create a PK-less table (SQLite's implicit rowid keys it) while simple
mode forbids it. See issue #19 comment for the full assessment.