Merge branch 'main' into website (m:n, logging, UI nav, demo overlays, vi-nav)
Brings a large batch of app work onto the website branch so the docs (and casts) can reflect it: - #24 vi-style j/k/g/G navigation in the load picker (ADR-0047 era) — unblocks a scriptable projects cast (autocast can send j/k; not arrows) - #22 demonstration overlay layer (ADR-0047): `--demo` mode, keystroke badges, and step-caption info banners — usable from casts to highlight key moments - C4 m:n convenience command (ADR-0045): `add m:n relationship … via <junction>` - ADR-0046 UI: width-derived schema sidebar + Ctrl-O nav mode, responsive two-row input + horizontal scroll, geometry-fixed hint panel - X1 comprehensive logging sweep across worker/parser/app/persistence/runtime - FK fixes: compound-FK violation message names every column pair; inline FK referencing a compound PK points at the table-level form Merged clean — no conflicts (the docs/website/ ADR namespace split kept the new main ADRs 0045–0047 from colliding). Tests on the merged tree: 2290 passed, 0 failed (1 ignored doctest, inherited from main).
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# ADR-0045: `create m:n relationship` convenience command (C4)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-06-10); **implemented 2026-06-10**. Closes
|
||||
`requirements.md` **C4**.
|
||||
|
||||
**Implementation note — a corrected ADR premise.** The plan claimed
|
||||
"the walker already dispatches multiple nodes per entry word" and used
|
||||
that to justify a *separate* `CREATE_M2N` node. That is true only in
|
||||
**advanced** mode. The build hit **two** places hard-coded to assume
|
||||
**≤1 DSL form per entry word in simple mode**: (1) the dispatcher
|
||||
(`decide`) committed `simple.first()` unconditionally, so `create
|
||||
table` shadowed `create m:n`; (2) the completion continuation-merge was
|
||||
gated `if mode == Advanced`, so simple mode never surfaced `m:n` as a
|
||||
candidate. Both were generalized to support multiple DSL forms per
|
||||
entry word — **behaviour-preserving for every existing single-form
|
||||
command** (the dispatch reduces to the old single-candidate commit; the
|
||||
completion merge is gated on `simple_count > 1`). Verified: zero ripple
|
||||
beyond the new command's own surfaces. The teaching echo (advanced-mode
|
||||
DSL→SQL, ADR-0038) was also wired: `render_create_m2n` emits the
|
||||
generated `CREATE TABLE … FOREIGN KEY …` from the post-exec junction
|
||||
description (round-trips as valid SQL).
|
||||
|
||||
A second `/runda` DA pass (pre-commit) closed five coverage gaps
|
||||
(highlighting, persistence round-trip, junction rename, name-collision,
|
||||
missing-parent — the first two had been wrongly claimed verified) and
|
||||
found two more issues: (a) `create m:n … as __rdbms_*` was accepted —
|
||||
a hidden-orphan hole the new `as` slot **exposed**, but rooted in the
|
||||
simple-mode `TABLE_NAME_NEW` slot having no internal-name guard (so
|
||||
plain `create table __rdbms_*` had it too). Fixed at the **root** —
|
||||
a `reject_internal_table_name` guard in the shared `do_create_table`,
|
||||
closing every path (the advanced-SQL path already rejected at parse).
|
||||
(b) the usage disambiguator (`usage_key_for_input`) handled the `1:n`
|
||||
opener but not `m:n`, so `create m:n …` resolved to no usage form —
|
||||
fixed with an explicit `m:n` branch. All four
|
||||
design forks were escalated and user-confirmed at the recommended
|
||||
option (compound-over-FKs junction PK; `CASCADE` actions; auto-name +
|
||||
optional `as`; both modes). Two follow-up points were also confirmed
|
||||
in a `/runda` DA pass: **self-referential m:n is refused outright**
|
||||
(user: "refuse — full stop"; it is a beginner-facing convenience, not
|
||||
the place for directional-naming complexity), and the FK column naming
|
||||
is **`{parent_table}_{pk_column}`**. The DA pass additionally
|
||||
established — against the user's initial assumption — that **PK-less
|
||||
tables *are* reachable** (advanced-mode SQL `create table t (a int)`
|
||||
declares no PK; `sql_create_table.rs` asserts `pk.is_empty()`), so the
|
||||
parent-PK guard (D7) is retained as a correctness check.
|
||||
|
||||
Builds on ADR-0013 (named 1:n relationships, the relationship
|
||||
metadata table, the rebuild-table primitive), ADR-0043 (compound,
|
||||
list-based FK references — the junction may reference compound parent
|
||||
PKs), ADR-0011 (`Type::fk_target_type()` for FK column typing), and
|
||||
the existing `do_create_table` executor (which already accepts
|
||||
`foreign_keys: Vec<SqlForeignKey>` and writes relationship metadata
|
||||
per FK). Honours ADR-0003 (mode model), ADR-0009 (DSL conventions),
|
||||
ADR-0002 (no engine name in user-facing strings), and ADR-0024
|
||||
(unified grammar / `CommandNode` registration, completion, hints,
|
||||
help-id, usage-id wiring).
|
||||
|
||||
## Context
|
||||
|
||||
A many-to-many relationship is modelled in a relational database by a
|
||||
**junction table** (a.k.a. associative / bridge table) that holds one
|
||||
foreign key to each of the two parents. Today a learner can build this
|
||||
by hand: `create table` the junction, then `add 1:n relationship`
|
||||
twice. That is three commands and requires the learner to already know
|
||||
the junction-table pattern — exactly the concept C4 is meant to
|
||||
*teach*.
|
||||
|
||||
C4 (`requirements.md`): *"`create m:n relationship from <T1> to <T2>`
|
||||
produces an auto-named junction table the user can rename; pulls
|
||||
primary keys and FK definitions automatically."*
|
||||
|
||||
The relationship machinery this builds on is freshly solid: ADR-0043
|
||||
made the relationship model list-based (compound-aware) across six
|
||||
layers, and ADR-0044 gave relationships a visual representation. C4 is
|
||||
the natural convenience layer on top.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a DSL command:
|
||||
|
||||
```
|
||||
create m:n relationship from <T1> to <T2> [as <name>]
|
||||
```
|
||||
|
||||
It generates a **junction table** with one FK column per primary-key
|
||||
column of each parent, a **compound primary key** over all of those FK
|
||||
columns, and **two 1:n relationships** (junction → T1, junction → T2),
|
||||
all in a single transaction (= one undo step). The junction is a
|
||||
normal table: `rename table`, `drop table`, `show table`, `insert`,
|
||||
etc. all work on it afterward.
|
||||
|
||||
### D1 — Junction primary key: compound over the FK columns (fork, user-chosen)
|
||||
|
||||
The junction's PK is the **combination of all its FK columns**. For
|
||||
`create m:n relationship from Students to Courses` (both PK `id`):
|
||||
|
||||
```
|
||||
Students_Courses
|
||||
Students_id int ┐ PRIMARY KEY (Students_id, Courses_id)
|
||||
Courses_id int ┘
|
||||
FOREIGN KEY (Students_id) REFERENCES Students(id)
|
||||
FOREIGN KEY (Courses_id) REFERENCES Courses(id)
|
||||
```
|
||||
|
||||
This is the textbook junction: the `(Students_id, Courses_id)` pair is
|
||||
unique, so a student cannot be linked to the same course twice. It is
|
||||
the most pedagogically correct model and needs no surrogate key.
|
||||
|
||||
*Rejected:* a surrogate `serial` PK + `UNIQUE` over the FK pair (adds a
|
||||
key the learner did not ask for); two FK columns with no PK (allows
|
||||
duplicate links — wrong lesson).
|
||||
|
||||
### D2 — Referential actions: `CASCADE` on delete and update (fork, user-chosen)
|
||||
|
||||
Both generated FKs default to `ON DELETE CASCADE ON UPDATE CASCADE`. A
|
||||
junction row is meaningless without both ends, so deleting a parent
|
||||
(a Student or a Course) removes its link rows automatically — the
|
||||
natural junction semantics, and a clean teaching demonstration of
|
||||
cascade. There is no syntax in this command to override the actions;
|
||||
the learner who wants different actions builds the junction by hand
|
||||
(or drops + re-adds a relationship). This keeps the convenience
|
||||
command convenient.
|
||||
|
||||
### D3 — Naming: auto-name `<T1>_<T2>`, optional `as <name>` (fork, user-chosen)
|
||||
|
||||
The junction table is auto-named `{T1}_{T2}` (e.g. `Students_Courses`).
|
||||
An optional `as <name>` clause overrides it — consistent with
|
||||
`add 1:n relationship [as <name>]` and saving a follow-up
|
||||
`rename table`. The two generated relationships are auto-named by the
|
||||
existing relationship-name resolver (e.g.
|
||||
`Students_id_to_Students_Courses_Students_id`), exactly as `create
|
||||
table` with inline FKs already names them.
|
||||
|
||||
### D4 — FK column naming: `{parent_table}_{pk_column}`
|
||||
|
||||
Each FK column is named `{parent_table}_{pk_column}` — one per PK
|
||||
column of each parent. This disambiguates the common case where both
|
||||
parents share a PK column name (both `id` → `Students_id`,
|
||||
`Courses_id`), and generalises to compound parent PKs: a parent
|
||||
`Sections(course_id, term)` contributes `Sections_course_id` and
|
||||
`Sections_term`. Column types come from each parent PK column's
|
||||
`Type::fk_target_type()` (ADR-0011): `serial → int`, `shortid → text`,
|
||||
others identity — so the junction columns are plain storable types,
|
||||
never auto-generating.
|
||||
|
||||
### D5 — Mode availability: both simple and advanced
|
||||
|
||||
`create m:n relationship` is a `CommandCategory::Simple` DSL command,
|
||||
reachable in **both** input modes — the same posture as the sibling
|
||||
relationship commands (`drop relationship … Mode::Advanced` is a tested
|
||||
path today). There is no SQL spelling for it; an advanced-mode user who
|
||||
prefers raw SQL still has `CREATE TABLE` + `FOREIGN KEY`. The command
|
||||
is purely additive teaching sugar, so making it available everywhere is
|
||||
harmless and consistent.
|
||||
|
||||
### D6 — Implementation: one `do_create_table` call, not a batch
|
||||
|
||||
The junction table and both relationships are created by **building a
|
||||
single `Command`/worker request that `do_create_table` already
|
||||
handles**: `do_create_table` accepts `columns`, `primary_key`, and
|
||||
`foreign_keys: Vec<SqlForeignKey>`, and already inserts relationship
|
||||
metadata for each FK. So the executor:
|
||||
|
||||
1. Canonicalises `T1`, `T2` (`require_canonical_table`) and reads each
|
||||
parent's PK (`read_schema().primary_key`). **D7 — parent-PK guard:**
|
||||
if either parent's `primary_key` is empty, error with a friendly
|
||||
"`<table>` has no primary key, so it cannot anchor an m:n
|
||||
relationship" *before* building any FK. This is a real case — a
|
||||
table created via advanced-mode SQL `create table t (a int)` has no
|
||||
PK (`sql_create_table.rs` asserts `pk.is_empty()` for that form) —
|
||||
not a theoretical one, so the guard is a correctness requirement,
|
||||
not defensive padding.
|
||||
2. Builds the junction `ColumnSpec`s (one per parent PK column, typed
|
||||
via `fk_target_type`), the compound `primary_key` list, and two
|
||||
`SqlForeignKey` values (`on_delete = on_update = Cascade`).
|
||||
3. Calls the create-table path, which creates the table + both FKs +
|
||||
all metadata in **one transaction** — naturally one undo step, no
|
||||
`BeginBatch`/`EndBatch` bracketing needed.
|
||||
|
||||
This reuses the most-tested machinery and inherits its persistence,
|
||||
metadata, and FK-validation behaviour for free.
|
||||
|
||||
A new typed command variant `Command::CreateM2nRelationship { t1, t2,
|
||||
name }` carries the parsed form; the runtime/executor expands it to the
|
||||
junction definition. (We do **not** lower it to `Command::CreateTable`
|
||||
at parse time — keeping a distinct command preserves command identity
|
||||
per the X5 "unique commands for every unique case" principle, and lets
|
||||
the teaching echo speak in m:n terms.)
|
||||
|
||||
## Grammar, AST, and cross-cutting wiring
|
||||
|
||||
A new command is not done when it parses — it must light up every
|
||||
surface a learner touches. Enumerated here so none is missed
|
||||
(verification in **Testing**).
|
||||
|
||||
- **Grammar node** `CREATE_M2N` (`ddl.rs`): a separate `CommandNode`
|
||||
with `entry: create`, shape `m:n relationship [as <name>] from <T1>
|
||||
to <T2>`, registered in `REGISTRY` as `CommandCategory::Simple`. A
|
||||
separate node (not a branch inside `CREATE`) keeps the tested
|
||||
create-table builder untouched; the walker already dispatches
|
||||
multiple nodes per entry word. The `m:n` opener mirrors `1:n`
|
||||
(`Word("m")`, `Punct(':')`, `Word("n")`).
|
||||
- **AST builder** `build_create_m2n` → `Command::CreateM2nRelationship`.
|
||||
- **Command + worker plumbing:** `Command::CreateM2nRelationship`
|
||||
variant; `Request::CreateM2nRelationship`; runtime
|
||||
`execute_command_typed` arm; `Database::create_m2n_relationship`
|
||||
public API; executor `do_create_m2n_relationship` (per D6).
|
||||
- **Completion:** add `("m", "m:n")` to `COMPOSITE_CANDIDATES`
|
||||
(`completion.rs`) so Tab on `create m` offers `m:n` as one fluent
|
||||
piece (exactly as `("1", "1:n")` does for `add`). Identifier slots
|
||||
for `<T1>`/`<T2>` inherit table-name completion from the walker's
|
||||
`IdentSource::Tables` automatically.
|
||||
- **Hints:** set `HintMode`s on the `CREATE_M2N` nodes so the ambient
|
||||
hint panel guides `from` / table / `to` / table / optional `as`,
|
||||
matching the `add 1:n relationship` hinting.
|
||||
- **Highlighting:** automatic — `walker/highlight.rs` is grammar-driven
|
||||
with no per-command special-casing; verify the line highlights.
|
||||
- **Help:** `help_id: Some("ddl.create_m2n")` → the command appears in
|
||||
`help` automatically (REGISTRY iteration) and under `help create`;
|
||||
add the `help.ddl.create_m2n` catalog string.
|
||||
- **Usage / parse errors:** `usage_ids: &["parse.usage.create_m2n"]`
|
||||
→ a malformed `create m:n …` shows the form in the usage block; add
|
||||
the `parse.usage.create_m2n` catalog string. Add the near-miss cases
|
||||
to the `parse_error_pedagogy` matrix (ADR-0042) for the `create`
|
||||
entry word.
|
||||
- **Teaching echo** (`echo.rs` + `build_schema_echo`): a
|
||||
`CreateM2nRelationship` arm that echoes the generated junction —
|
||||
the `create table` it built plus the two `FOREIGN KEY` lines — so the
|
||||
learner sees the pattern the convenience expanded to.
|
||||
- **Structure render:** the executor returns the junction's
|
||||
`TableDescription`; the ADR-0044 render path already draws its
|
||||
relationships as diagrams on the create echo? No — incidental
|
||||
`create table` echoes keep prose (ADR-0044 reach); the m:n echo shows
|
||||
the junction structure with its outbound FKs in the standard prose
|
||||
form. (A future enhancement could draw both relationships as
|
||||
diagrams; out of scope here.)
|
||||
|
||||
## Genuine forks (escalated, all resolved 2026-06-10)
|
||||
|
||||
1. **Junction PK** — compound-over-FKs (chosen) vs surrogate serial +
|
||||
UNIQUE vs no PK. → D1.
|
||||
2. **Referential actions** — `CASCADE` (chosen) vs `NO ACTION` vs
|
||||
`RESTRICT`. → D2.
|
||||
3. **Naming** — auto-name + optional `as` (chosen) vs auto-name only.
|
||||
→ D3.
|
||||
4. **Mode** — both (chosen by default, unobjected) vs simple-only. → D5.
|
||||
|
||||
## Testing
|
||||
|
||||
Integration (`tests/it/`), test-first:
|
||||
|
||||
- **Functional:** `create m:n relationship from A to B` creates table
|
||||
`A_B` with the two FK columns, compound PK over them, and two
|
||||
enforced FKs; inserting a junction row with a non-existent parent is
|
||||
refused; the `(fk1, fk2)` pair is unique (duplicate link refused).
|
||||
- **`as <name>`** overrides the junction name.
|
||||
- **Compound parent PK:** a parent with a 2-column PK contributes two
|
||||
FK columns; the junction PK spans all of them; FKs enforce per pair.
|
||||
- **Cascade:** deleting a parent row removes its junction rows.
|
||||
- **Undo:** one `create m:n` is exactly one undo step (table + both
|
||||
relationships gone after `undo`).
|
||||
- **Persistence round-trip:** the junction + both relationships survive
|
||||
a save → rebuild-from-text.
|
||||
- **Errors:** missing parent table; parent without a PK; junction-name
|
||||
collision with an existing table; (self-m:n → OOS error, below).
|
||||
|
||||
Cross-cutting (the surfaces a new command must light up):
|
||||
|
||||
- **Completion:** Tab after `create ` surfaces `m:n relationship`;
|
||||
table-name completion fires at the `<T1>`/`<T2>` slots.
|
||||
- **Help:** `help` lists the command; `help create` includes the m:n
|
||||
form; the catalog string renders.
|
||||
- **Usage / parse pedagogy:** a bare/half `create m:n` shows the usage
|
||||
block; near-miss matrix entries added (`parse_error_pedagogy`).
|
||||
- **Hints + highlighting:** ambient hint progression through the form;
|
||||
the line highlights (snapshot or assertion as the sibling commands
|
||||
use).
|
||||
|
||||
All tiers green, zero skips; clippy clean (nursery).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Self-referential m:n** (`from T to T`) — **refused outright**
|
||||
(user-confirmed, "full stop"): the two FK column sets would collide
|
||||
on `{T}_{pkcol}`, and directional disambiguation (`from_*`/`to_*`)
|
||||
is more complexity than this beginner-facing convenience warrants.
|
||||
The executor detects `t1 == t2` (on the canonical names) and errors
|
||||
with a friendly pointer ("an m:n relationship needs two different
|
||||
tables — to link a table to itself, add the junction by hand").
|
||||
Not a deferred follow-up; a deliberate non-goal.
|
||||
- **Per-relationship action overrides** in the command syntax (D2 fixes
|
||||
`CASCADE`); use a hand-built junction for other actions.
|
||||
- **Extra junction columns** (payload attributes on the link, e.g. an
|
||||
enrolment date) — add them afterward with `add column`.
|
||||
- **m:n visualization as diagrams** on the create echo (ADR-0044 reach
|
||||
keeps incidental create echoes in prose).
|
||||
- **Renaming the auto-generated *relationships*** (only the table is
|
||||
`as`-nameable); drop + re-add covers it.
|
||||
@@ -0,0 +1,556 @@
|
||||
# ADR-0046: Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20 / #21 / #23)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-06-10); **implemented 2026-06-10**, phased **A → B → C**
|
||||
(see *Decision — phasing*) across commits `9f5f76b` (DA1/DA2) · `e0b9470`
|
||||
(DA3) · `41bae99` (DA4) · `386627a` (DB1) · `94825d0` (DB2/DB4) ·
|
||||
`c9da6ff` (DC1/DC2/DC4) · `22bec61` (DC3 + DC2 refinement). Closes Gitea
|
||||
issues **#20** (hint-panel height jumpiness), **#21** (database-structure
|
||||
/ left-column improvements), and **#23** (long command input). Issue
|
||||
#23's own note ("handle after #21 is decided") is honoured: the input
|
||||
work is split so the part that depends on the sidebar's width budget
|
||||
lands with it.
|
||||
|
||||
Two decisions landed differently from the original draft and are
|
||||
recorded inline: the relationship data lives on **`App`, not
|
||||
`SchemaCache`** (DB2), and the navigation overlay clears **only the
|
||||
sidebar strip + a one-column gutter** (panels stay visible behind),
|
||||
not the whole area (DC2).
|
||||
|
||||
Builds on and honours: **ADR-0003** (the persistent Simple/Advanced
|
||||
mode model — navigation mode is *not* a third input mode, see DC1),
|
||||
**ADR-0027** (the input validity indicator's reserved 6 right columns —
|
||||
horizontal scroll and 2-line display preserve that reserve),
|
||||
**ADR-0044** (relationship visualization — the relationships panel
|
||||
renders the same `RelationshipSchema` data the `show relationship`
|
||||
diagram already consumes), **ADR-0013 / ADR-0043** (the
|
||||
`RelationshipSchema` model: name, parent/child tables, list-based
|
||||
compound columns, referential actions), **ADR-0015** (project file
|
||||
format — sidebar visibility is **session-only**, so the format is
|
||||
untouched), and **ADR-0002** (no engine name in user-facing strings).
|
||||
Preserves the **pure-render-from-`App`-state** invariant (CLAUDE.md):
|
||||
visual changes here are driven either by new `App` *state* fields
|
||||
(mutated in `update()`) or by pure *render-time* functions of the frame
|
||||
geometry (see State section); `update()` stays pure-sync.
|
||||
|
||||
**Requirements & issues touched (verified against `requirements.md`).**
|
||||
Evolves **S1** (the always-present three-region layout — the left items
|
||||
region becomes width-optional, DB1). **Overrides S2**, which planned
|
||||
additional element kinds as *nested* items in the tables list;
|
||||
relationships get their own panel instead (DB2/DB4 — see Genuine forks
|
||||
§11). **Corrects S4**: its "keyboard-toggleable hint area" was never
|
||||
implemented (no toggle keybinding exists in the code) and is not wanted
|
||||
— the hint panel became indispensable once completion moved into it
|
||||
(ADR-0022) — so the toggle phrase is struck from `requirements.md` and
|
||||
no toggle is added here. Extends **I1a** (single-line cursor editing →
|
||||
horizontal scroll, DA3) and honours **S6 / ADR-0027** (the 6-column
|
||||
validity-indicator reserve, DA3/DA4). The PageUp/PageDown context-rebind
|
||||
(DC3) does **not** regress **V4**'s output scroll, which stays live in
|
||||
input mode. Adjacent but separate: Gitea **#22** (an in-app
|
||||
overlay/annotation layer for casts and guided lessons — its own ADR)
|
||||
shares the overlay-render and screencast context with DC2's `Clear`
|
||||
overlay; the two are meant to coexist, not merge.
|
||||
|
||||
## Context
|
||||
|
||||
Three UI issues were raised together because they are coupled through
|
||||
the terminal's width and height budget; treating them as one decision
|
||||
avoids three conflicting partial fixes.
|
||||
|
||||
**Current layout (verified in `src/ui.rs`).** `render()` splits
|
||||
vertically into `Min(8)` main / `Length(1)` project label / `Length(1)`
|
||||
status. The main area splits horizontally into a **fixed
|
||||
`Length(28)`** left column (`render_items_panel`, a "Tables" list with
|
||||
indented index names) and a `Min(20)` right column. The left column's
|
||||
block has `Borders::ALL`, so its **usable inner width is 26 columns**
|
||||
(28 − 2 borders). The right column splits vertically into output
|
||||
(`Min(5)`), input (`Length(3)`), and hint (`Length(hint_content)`).
|
||||
|
||||
**#20 — hint jumpiness.** `hint_content` is recomputed **every frame**
|
||||
as `clamp(wrapped_lines, 1, MAX_HINT_ROWS=3) + 2`, i.e. 3–5 rows. As the
|
||||
user types and hint strings appear, grow, and vanish, the hint panel
|
||||
resizes and **shoves the input and output panels**, producing the
|
||||
flicker visible in screencasts. The root cause is that height tracks
|
||||
*content* rather than terminal *geometry*.
|
||||
|
||||
The hint catalog (`src/friendly/strings/en-US.yaml`) was measured: the
|
||||
two longest strings are `value_literal_slot` (106 chars) and
|
||||
`create_table_element` (102); four more are 50–57; the rest ≤ 50. The
|
||||
wrapping consequence is sharp: at a right-column inner width ≥ ~54
|
||||
columns the worst string needs **at most 2 lines**; a **3rd line is
|
||||
only ever required when the right column is narrower than ~54** (a
|
||||
sub-~83-column terminal *with the sidebar shown*, or a sub-~55-column
|
||||
terminal). On the project's screencasts (90 columns wide, sidebar
|
||||
hidden — see DB1) two lines are provably sufficient.
|
||||
|
||||
**#21 — the left column.** A persistent 26-column Tables list is rarely
|
||||
filled even half-way by a teaching database, yet it permanently costs
|
||||
horizontal space the output and input panels want — acutely so on the
|
||||
90-column screencasts. The pedagogical value of an *always-visible*
|
||||
schema overview is real (CLAUDE.md "pedagogy wins ties"), so the column
|
||||
is **kept but made optional and more useful**, not deleted.
|
||||
|
||||
**#23 — long input.** The command input is a **single logical `String`**
|
||||
rendered by a `Paragraph` with no wrap and no horizontal scroll
|
||||
(`render_input_panel`); text past the panel width **clips silently**.
|
||||
The cursor is a byte offset on a char boundary; Up/Down drive history.
|
||||
The fix needs the width the sidebar's removal frees, hence the coupling.
|
||||
|
||||
**Keybinding space (verified).** Taken: Tab/Shift-Tab (completion),
|
||||
Enter (submit), Up/Down (history), Left/Right/Home/End (cursor),
|
||||
PageUp/PageDown (output scroll), Backspace/Delete, Esc
|
||||
(completion-undo / modal cancel), Ctrl-C (quit). Reserved-but-deferred
|
||||
(I1b readline): Ctrl-A/E/W/K/U. Printable keys all route to the input.
|
||||
Terminal-hijacked and therefore unusable: Ctrl-S/Q (flow control),
|
||||
Ctrl-Z (suspend), Ctrl-H (backspace), Ctrl-I/M (Tab/Enter), Ctrl-G
|
||||
(BEL). This leaves a narrow band of safe combinations for new controls.
|
||||
|
||||
## Decision — phasing
|
||||
|
||||
The work ships in three phases so the screencasts benefit from the
|
||||
least-controversial part first and the riskiest part (the focus/scroll
|
||||
model) is isolated:
|
||||
|
||||
- **Phase A — input & hint (DA1–DA4):** self-contained, no sidebar
|
||||
dependency; fixes #20 and the baseline of #23.
|
||||
- **Phase B — optional, richer sidebar (DB1–DB3):** visibility model +
|
||||
relationships panel + schema-cache enrichment.
|
||||
- **Phase C — navigation mode (DC1–DC4):** the Ctrl-O focus/scroll/
|
||||
expand model that makes the sidebar browsable.
|
||||
|
||||
Each phase is independently shippable and independently green.
|
||||
|
||||
## Decision — Phase A: responsive input & hint heights
|
||||
|
||||
### DA1 — Hint height is a function of terminal geometry, fixed between resizes
|
||||
|
||||
The hint panel's height is **decoupled from hint content**. It is
|
||||
computed from the terminal's width and height **once per resize** and
|
||||
held constant as the user types. Because the panel no longer resizes on
|
||||
every keystroke, it never shoves the input/output panels — the #20 jump
|
||||
is eliminated at the source, not damped. Content that exceeds the fixed
|
||||
height is ellipsized (the existing `clamp_wrapped` truncation), which is
|
||||
now a rare, width-driven event rather than a per-keystroke one.
|
||||
|
||||
### DA2 — Responsive height buckets
|
||||
|
||||
Heights are chosen by terminal **height** (rows), with the hint's
|
||||
optional 3rd line gated on right-column **width** (per the Context
|
||||
measurement):
|
||||
|
||||
| Terminal height | Input content rows | Hint content rows |
|
||||
| --- | --- | --- |
|
||||
| **Compact** (`H < 40` — covers the 25-row screencasts) | 1 (+ horizontal scroll, DA3) | 2 |
|
||||
| **Comfortable** (`H ≥ 40` — fullscreen terminals) | 2 (soft-wrap, DA4) | 2 (→ 3 only if right-column inner < ~54) |
|
||||
|
||||
A safety degradation protects tiny terminals: the output panel's
|
||||
`Min(5)` is honoured first; if rows are insufficient, the hint shrinks
|
||||
to 1, then the input to 1. The `40`-row threshold is a tunable constant.
|
||||
|
||||
### DA3 — Input horizontal scroll (single logical line)
|
||||
|
||||
The input keeps its **single-`String`** model (no embedded newlines —
|
||||
this is explicitly *not* multi-line input, see Out of scope). A new
|
||||
`App` field `input_scroll_offset: usize` tracks the first visible
|
||||
column; the renderer shows a window of the line and keeps the cursor in
|
||||
view, mirroring the candidate-line horizontal-scroll markers already in
|
||||
`render_candidate_line`. The ADR-0027 6-column indicator reserve is
|
||||
preserved (the scroll window is the text area = `inner.width − 6`, not
|
||||
the full inner width). Because `update()` does not know the panel width,
|
||||
the renderer feeds it back via a `note_input_viewport(text_width)` call
|
||||
(the analogue of the existing `note_output_viewport`), against which the
|
||||
offset is clamped to keep the cursor visible. `input_scroll_offset`
|
||||
**resets to 0** whenever the buffer is replaced wholesale — on `submit`,
|
||||
on history navigation (Up/Down), and on any clear. This is the baseline
|
||||
#23 fix and is sufficient on its own for the compact (1-row) layout.
|
||||
|
||||
### DA4 — Two-line input display when tall (`H ≥ 40`)
|
||||
|
||||
On comfortable terminals the input renders across **2 visual rows** by
|
||||
soft-wrapping the single logical line, with the cursor mapped to a
|
||||
(row, col) within the two rows. Content longer than two rows scrolls
|
||||
the two-row window horizontally (DA3) so the cursor stays visible. The
|
||||
**ADR-0027 `[ERR]`/`[WRN]` indicator stays anchored to the right edge
|
||||
of the *first* row** (its 6-column reserve applies to row 1; the soft-
|
||||
wrap on row 1 stops 6 columns short, row 2 uses the full text width) —
|
||||
S6 is preserved.
|
||||
|
||||
This is display-only over the same single-`String` model — distinct
|
||||
from the deferred true multi-line-input feature (I1, which adds
|
||||
*multiple logical lines* with Enter-inserts-newline). **Forward-compat
|
||||
note:** I1, when built, should reuse DA4's row-rendering and cursor
|
||||
(row, col) mapping rather than introduce a parallel one — DA4 is the
|
||||
substrate, not a competitor.
|
||||
|
||||
## Decision — Phase B: optional, richer sidebar
|
||||
|
||||
### DB1 — Width-derived visibility plus transient peek (session-only)
|
||||
|
||||
Sidebar visibility is **derived, not stored**: the sidebar is visible
|
||||
iff the terminal **width > 90** *or* navigation mode is currently
|
||||
focused on a sidebar panel (the Ctrl-O peek, DC1). It is recomputed
|
||||
every frame from terminal width and `NavFocus`; nothing persists to
|
||||
`project.yaml` (ADR-0015 untouched), so it is session-only by
|
||||
construction — and there is no stored visibility field to keep in sync.
|
||||
|
||||
At ≤ 90 columns the sidebar is hidden by default — so the 90-column
|
||||
screencasts never show it and the output panel gets the full width it
|
||||
needs there — but `Ctrl-O` temporarily reveals it for the duration of a
|
||||
browse and re-hides it on exit (DC1).
|
||||
|
||||
**No persistent show/hide toggle (resolved 2026-06-10, user).** Issue
|
||||
#21's original wording asked for "a keystroke to show and hide it"; the
|
||||
Ctrl-O peek covers that need, so no separate toggle and no
|
||||
force-shown/force-hidden override is added. Visibility stays a pure
|
||||
function of `(terminal width, NavFocus)` — the simplest model that
|
||||
satisfies the requirement. Should pinning ever prove necessary, a
|
||||
persistent override is an additive follow-up (see Out of scope).
|
||||
|
||||
### DB2 — Add a relationships panel; enrich the schema cache
|
||||
|
||||
The left column gains a **second panel** below Tables: a list of the
|
||||
project's relationships. This is a deliberate **override of S2**, whose
|
||||
note proposed additional element kinds (relations, views) as *nested*
|
||||
items inside the existing tables list. Relationships are *cross-table*,
|
||||
not per-table, so nesting them under a single table reads wrong; a
|
||||
sibling panel is the honest shape (user-confirmed 2026-06-10). S2's
|
||||
"without restructuring" intent is still met — the items column simply
|
||||
holds two stacked panels (DB4) instead of one.
|
||||
|
||||
The panel needs the full `RelationshipSchema` (name, parent/child
|
||||
tables, list-based columns, on-delete/on-update actions) that the `show
|
||||
relationship` path already fetches.
|
||||
|
||||
**Data home — `App`, not `SchemaCache` (revised at implementation,
|
||||
2026-06-10).** The design first proposed an additive
|
||||
`SchemaCache.relationship_details: Vec<RelationshipSchema>` field.
|
||||
Implementation revised this to a **parallel `App.relationships:
|
||||
Vec<RelationshipSchema>`** field for two reasons: (1) `SchemaCache` is
|
||||
*walker/completion-facing* — it needs only relationship **names**
|
||||
(unchanged in `SchemaCache.relationships`, still borrowed as
|
||||
`&Vec<String>` by `IdentSource::Relationships`); the full records are
|
||||
**UI-only**, so `App` is the architecturally correct home, mirroring
|
||||
`app.tables` (which the items panel already reads alongside the cache).
|
||||
(2) Adding a field to `SchemaCache` would force edits to ~23 full
|
||||
struct literals across the test suite, whereas `App` gains one field.
|
||||
The /runda guard it answered — *don't break completion by retyping
|
||||
`relationships`* — is fully honoured either way. Delivery: a worker
|
||||
`Request::ReadAllRelationships` (→ `Database::read_all_relationships`,
|
||||
returning `Vec<RelationshipSchema>` via the existing
|
||||
`read_all_relationships(conn)`); the runtime's `refresh_schema_cache`
|
||||
posts a new `AppEvent::RelationshipsRefreshed` alongside
|
||||
`SchemaCacheRefreshed`, and the `App` stores it. No behavioural
|
||||
difference from the original design.
|
||||
|
||||
The panel has **two display states** keyed off focus (DC2):
|
||||
|
||||
- **Unfocused (26-col)** — an ambient glance. Per relationship: the
|
||||
name (ellipsized past the inner width), and the endpoints broken at
|
||||
the arrow to fit a narrow column:
|
||||
|
||||
```
|
||||
Customers_Orders
|
||||
Customers.id ->
|
||||
Orders.customer_id
|
||||
```
|
||||
|
||||
- **Focused + expanded (40–50 col, DC2)** — a browse view. At the wider
|
||||
width the endpoints fit on one line
|
||||
(`Customers.id -> Orders.customer_id`); the arrow-break is used only
|
||||
when even the expanded width cannot hold a (possibly compound)
|
||||
endpoint pair. The wider width minimises horizontal truncation so the
|
||||
panel needs **mainly vertical scrolling** (DC3).
|
||||
|
||||
### DB3 — Sidebar width unchanged when unfocused
|
||||
|
||||
The unfocused sidebar keeps `Length(28)` / 26 inner columns. Widening
|
||||
happens only on focus (DC2), as an overlay, so the unfocused layout and
|
||||
the right-column reflow are unchanged from today.
|
||||
|
||||
### DB4 — Vertical split of the two left-column panels
|
||||
|
||||
The items column stacks **Tables (top)** and **Relationships (bottom)**.
|
||||
The Relationships panel's height is content-driven within bounds, so it
|
||||
stays small when there is little to show and never dominates the column
|
||||
(user-chosen 2026-06-10):
|
||||
|
||||
- **No relationships:** fixed at **5 rows** (3 content + 2 border),
|
||||
rendering a single `None` line. This is the floor.
|
||||
- **With relationships:** grows with content (`content_rows + 2`, where
|
||||
the unfocused format is ~3 rows per relationship) up to a **cap of
|
||||
50 % of the column height**; beyond the cap the panel **scrolls**
|
||||
(DC3). Formally `rel_h = clamp(content_rows + 2, 5, ⌊col_h / 2⌋)`.
|
||||
- **Tables** takes the remainder (`col_h − rel_h`) and scrolls if it
|
||||
overflows (it, too, is a focusable, scrollable panel — DC3).
|
||||
- **Degradation:** on a column too short to honour the 5-row floor plus
|
||||
a usable Tables panel (`col_h < ~10`), the floor yields first so
|
||||
Tables keeps at least its border + one row; both panels stay
|
||||
renderable. The `50 %` cap and `5`-row floor are tunable constants.
|
||||
|
||||
Heights are a pure render-time function of the column height and the
|
||||
cached relationship count, so they are unit-testable without a terminal
|
||||
(see Testing).
|
||||
|
||||
## Decision — Phase C: navigation mode
|
||||
|
||||
### DC1 — `Ctrl-O` navigation mode: a focus cycle, not an input mode
|
||||
|
||||
`Ctrl-O` enters a **navigation mode** that is orthogonal to the
|
||||
Simple/Advanced input mode (ADR-0003) — it changes *where keystrokes
|
||||
go*, not *how commands parse*. It drives a focus cycle:
|
||||
|
||||
1. **Press 1 →** focus the **Tables** panel (revealing the sidebar if
|
||||
it is currently hidden — a temporary peek).
|
||||
2. **Press 2 →** focus the **Relationships** panel.
|
||||
3. **Press 3 →** leave navigation mode: restore the sidebar width,
|
||||
re-hide it if the peek revealed it, and return focus to the command
|
||||
input.
|
||||
|
||||
`Esc` exits navigation mode directly from any focused panel (a
|
||||
short-cut for step 3); `Esc` is otherwise only completion-undo, which
|
||||
does not apply while browsing.
|
||||
|
||||
**Why `Ctrl-O` and not `Ctrl-B`.** `Ctrl-B` is the *default tmux prefix*
|
||||
and `Ctrl-A` is *screen's* — a multiplexer eats them before the app
|
||||
sees them, so either would make navigation mode unreachable for the many
|
||||
students who run inside tmux/screen. `Ctrl-O` is not a multiplexer
|
||||
prefix; in the raw mode the TUI sets, its legacy line-discipline meaning
|
||||
(discard-output) is disabled, so it reaches the app. It is free in the
|
||||
app today (the main key handler's catch-all, `app.rs:1001`). The
|
||||
mnemonic is weak ("**O**utline"); reachability won over mnemonic.
|
||||
|
||||
**Routing.** Navigation mode is handled inside the **main** key handler,
|
||||
which runs only when no modal is open (`app.rs:919` gates on
|
||||
`self.modal.is_some()`). So `Ctrl-O` and the nav keys are **inert while
|
||||
a modal dialog is active** — modals keep full keyboard ownership. Within
|
||||
the main handler, a `NavFocus != Input` branch precedes the normal
|
||||
input-editing arms and routes keys per DC3/DC4.
|
||||
|
||||
### DC2 — Expand-on-focus as an overlay
|
||||
|
||||
A focused sidebar panel widens to a **45-column** overlay
|
||||
(`NAV_EXPANDED_WIDTH`): the renderer `Clear`s the strip the expanded
|
||||
panel occupies **plus a one-column gutter** (`NAV_OVERLAY_GUTTER`) and
|
||||
paints the wide panel on top. The output/input/hint panels underneath
|
||||
keep their exact layout — **unused and unchanging** while browsing,
|
||||
**still visible to the right** of the overlay (just partially occluded
|
||||
on the left) — and are restored fully by the next frame on exit. The
|
||||
gutter keeps them from butting against the expanded panel's border so
|
||||
the overlay edge reads cleanly. This is cheap because the renderer is a
|
||||
pure function of `App` state: focus state selects the width and the
|
||||
overlay path. (The input underneath is inactive in navigation mode.)
|
||||
|
||||
*Implementation note (2026-06-10):* a full-area clear (hiding the base
|
||||
panels entirely during browse) was tried first and rejected — leaving
|
||||
the base visible is truer to "underneath keep their layout," and the
|
||||
one-column gutter resolves the only wrinkle (the panels' left edges
|
||||
being cut by the overlay reading harshly without separation).
|
||||
|
||||
### DC3 — Scroll the focused panel; focus highlight
|
||||
|
||||
While a sidebar panel is focused it scrolls, reusing the output panel's
|
||||
proven mechanism (a `usize` offset clamped against a renderer-reported
|
||||
viewport via a `note_*_viewport` call):
|
||||
|
||||
- **Up / Down — line-by-line** scroll (the lazygit `j`/`k` feel;
|
||||
user-chosen 2026-06-10).
|
||||
- **PageUp / PageDown — page** scroll.
|
||||
|
||||
This is a context-sensitive rebind: Up/Down drive *history* and
|
||||
PageUp/PageDown scroll the *output* in input mode, whereas in navigation
|
||||
mode they scroll the *focused sidebar panel*. The two contexts never
|
||||
apply simultaneously (`NavFocus` selects which). The focused panel shows
|
||||
an **accent border** so it is obvious where keys are going (lazygit
|
||||
convention).
|
||||
|
||||
### DC4 — Other keys are inert in navigation mode
|
||||
|
||||
The command input is visibly occluded by the overlay while browsing, so
|
||||
keys that have no navigation meaning are **inert** rather than acting on
|
||||
the hidden input. Specifically, **only** `Ctrl-O` (advance focus),
|
||||
Up/Down + PageUp/PageDown (scroll, DC3), and `Esc` (exit) are live;
|
||||
printable characters, Enter, Tab, Backspace/Delete, Left/Right, and
|
||||
Home/End all do nothing until navigation mode is exited. The occlusion
|
||||
signals "not typing," so swallowing these is clearer than letting them
|
||||
silently edit an invisible buffer.
|
||||
|
||||
## State, keybindings, and cross-cutting wiring
|
||||
|
||||
**Stored `App` state** (mutated in `update()`, read by the renderer):
|
||||
|
||||
- `input_scroll_offset: usize` (DA3) — reset on submit / history-nav /
|
||||
clear.
|
||||
- `NavFocus { Input, SidebarTables, SidebarRelationships }` (DC1) — the
|
||||
navigation-mode focus cursor; `Input` ≙ not in navigation mode.
|
||||
- Per-panel scroll offsets for the Tables and Relationships panels, each
|
||||
clamped against a renderer-reported viewport (DC3), mirroring
|
||||
`output_scroll` / `note_output_viewport`.
|
||||
- **`App.relationships: Vec<RelationshipSchema>`** (DB2) — the full
|
||||
relationship records for the sidebar panel, delivered by
|
||||
`AppEvent::RelationshipsRefreshed` from the runtime's schema refresh.
|
||||
`SchemaCache.relationships: Vec<String>` (names, for completion) is
|
||||
unchanged. (See DB2 for why this lives on `App`, not `SchemaCache`.)
|
||||
|
||||
**Render-time derived** (pure functions of `frame.area()` + cached
|
||||
counts — *not* stored fields; this keeps the pure-render invariant and
|
||||
makes the geometry logic unit-testable without a terminal):
|
||||
|
||||
- Sidebar visibility — `(width > 90) || NavFocus is a sidebar panel`
|
||||
(DB1).
|
||||
- Input/hint row counts — a pure helper `panel_heights(area) ->
|
||||
(input_rows, hint_rows)` (DA1/DA2), the same helper the renderer and
|
||||
the Tier-1 tests call.
|
||||
- Left-column split `rel_h = clamp(content_rows + 2, 5, ⌊col_h/2⌋)`
|
||||
(DB4).
|
||||
- Input width fed back to `update()` via `note_input_viewport`
|
||||
(DA3), since `update()` cannot read `frame.area()`.
|
||||
|
||||
Keybindings introduced/affected:
|
||||
|
||||
| Key | Input mode | Navigation mode |
|
||||
| --- | --- | --- |
|
||||
| `Ctrl-O` | enter nav mode, focus Tables (peek-reveal) | advance focus (Tables → Relationships → exit) |
|
||||
| `Up` / `Down` | history (unchanged) | line-scroll focused panel |
|
||||
| `PageUp` / `PageDown` | scroll output (unchanged) | page-scroll focused panel |
|
||||
| `Esc` | completion-undo (unchanged) | exit nav mode directly |
|
||||
| printable / Enter / Tab / Backspace / Left / Right / Home / End | edit/submit input (unchanged) | inert |
|
||||
|
||||
All nav keys are inert while a modal is open (the main handler is gated
|
||||
on `!modal.is_some()`, `app.rs:919`).
|
||||
|
||||
Renderer changes (`src/ui.rs`): geometry-driven hint/input height
|
||||
(DA1/DA2), input window + cursor windowing (DA3) and 2-row soft-wrap
|
||||
with row-1 indicator (DA4), the relationships panel + two-panel split
|
||||
(DB2/DB4), the focus accent border and expand-on-focus `Clear` overlay
|
||||
(DC2/DC3); `note_input_viewport` feedback added alongside the existing
|
||||
`note_output_viewport`.
|
||||
|
||||
## Genuine forks (escalated, resolved 2026-06-10)
|
||||
|
||||
1. **Left column fate** — remove entirely vs narrow vs **keep + make
|
||||
optional and richer** (chosen, user). → DB1/DB2.
|
||||
2. **Focus/scroll model** — a navigation mode (chosen, user) vs
|
||||
modeless modifier-key scroll vs deferring scroll. → DC1.
|
||||
3. **Navigation shortcut** — **`Ctrl-O`** (chosen, user); `Ctrl-B`
|
||||
*rejected on review* (it is the default tmux prefix → unreachable
|
||||
inside tmux); Ctrl-T also viable; terminal-hijacked combos excluded.
|
||||
→ DC1.
|
||||
4. **Expand-on-focus rendering** — **overlay with `Clear`** (chosen,
|
||||
keeps the right panels unchanging) vs re-splitting the layout (would
|
||||
reflow output). → DC2.
|
||||
5. **Navigation-mode printables** — **ignore** (chosen, user) vs
|
||||
drop-to-input-and-type. → DC4.
|
||||
6. **Hint anti-jump** — **fix height to terminal geometry** (chosen)
|
||||
vs damping/hysteresis vs always-reserve-max. → DA1.
|
||||
7. **Height thresholds** — `H < 40` compact / `H ≥ 40` comfortable, with
|
||||
1/2 and 2/2 splits (chosen, user). → DA2.
|
||||
8. **Visibility persistence** — **session-only** (chosen, user) vs
|
||||
per-project in `project.yaml`. → DB1.
|
||||
9. **Persistent show/hide toggle** — **deferred** (chosen, user): the
|
||||
Ctrl-O peek covers #21's "keystroke to show and hide", so visibility
|
||||
stays width-derived with no override. → DB1.
|
||||
10. **Nav-mode Up/Down** — **line-scroll the focused panel** (chosen,
|
||||
user) vs leaving scroll to PageUp/PageDown only. → DC3.
|
||||
11. **Relationships placement** — **a separate sibling panel** (chosen,
|
||||
user — *overrides S2*) vs nesting relations inside the tables list
|
||||
per S2's documented extension model. → DB2/DB4.
|
||||
12. **Hint-area toggle (S4)** — **no toggle** (chosen, user): the hint
|
||||
panel is indispensable since completion moved into it; S4's stale
|
||||
"keyboard-toggleable" claim (never implemented) is struck from
|
||||
`requirements.md`. → Status (Requirements & issues touched).
|
||||
|
||||
## Testing
|
||||
|
||||
Tier-1 (`app.rs` pure `update()` unit tests), **Tier-2 (`insta`
|
||||
snapshots, `src/snapshots/`) for the visual surfaces** — this change is
|
||||
heavily render-side, so the geometry/format/overlay assertions belong in
|
||||
snapshots, not only behavioural tests — and Tier-3 integration.
|
||||
Test-first per CLAUDE.md. The geometry helpers (`panel_heights`, the
|
||||
DB4 split, visibility) are **pure functions** exercised directly in
|
||||
Tier-1 without a terminal.
|
||||
|
||||
Phase A:
|
||||
- **Hint anti-jump:** `panel_heights(area)` is invariant under changing
|
||||
hint content at a fixed terminal size (assert it does not change as
|
||||
`app.hint` varies); it *does* change across the `H < 40` / `H ≥ 40`
|
||||
boundary and the width-< 54 boundary.
|
||||
- **Height buckets:** compact → input 1 row / hint 2; comfortable →
|
||||
input 2 / hint 2 (3 only when right-column inner < ~54); tiny-terminal
|
||||
degradation honours output `Min(5)`.
|
||||
- **Input horizontal scroll:** a line longer than the panel keeps the
|
||||
cursor visible while moving Left/Right/Home/End; ADR-0027's 6-column
|
||||
reserve is intact; no characters are lost (buffer = full string);
|
||||
`input_scroll_offset` resets on submit / history-nav / clear.
|
||||
- **Two-line input:** at `H ≥ 40` a line wrapping to two rows renders
|
||||
both rows with correct cursor (row, col), the `[ERR]`/`[WRN]`
|
||||
indicator on row 1's right edge (Tier-2 snapshot); a longer line
|
||||
scrolls.
|
||||
|
||||
Phase B:
|
||||
- **Relationship data path:** `Database::read_all_relationships`
|
||||
returns full records through the worker thread (integration test, real
|
||||
DB via an m:n junction); `AppEvent::RelationshipsRefreshed` populates
|
||||
`App.relationships`; `SchemaCache.relationships` names are undisturbed
|
||||
(completion still resolves them).
|
||||
- **Relationships panel render (Tier-2):** empty → a single `None` line
|
||||
at the 5-row floor; the unfocused narrow format (name + arrow-break,
|
||||
ellipsis past inner width); a compound endpoint pair arrow-breaks
|
||||
correctly.
|
||||
- **Two-panel split (DB4):** `rel_h = clamp(content_rows + 2, 5,
|
||||
⌊col_h/2⌋)` — 5 when empty; grows with content; capped at 50 %;
|
||||
Tables takes the remainder; degrades sanely at `col_h < 10`.
|
||||
- **Width-derived visibility:** width ≤ 90 hides, > 90 shows, recomputed
|
||||
on resize (the peek interaction is covered under Phase C).
|
||||
|
||||
Phase C:
|
||||
- **Focus cycle:** `Ctrl-O` cycles Input → Tables → Relationships →
|
||||
Input; `Esc` exits directly; a peek-revealed sidebar re-hides on exit;
|
||||
a width-shown (> 90) sidebar stays shown on exit; `Ctrl-O` is inert
|
||||
while a modal is open.
|
||||
- **Expand overlay (Tier-2):** focusing widens to the expanded width;
|
||||
the underlying output/input/hint state is unchanged across enter/exit
|
||||
(no reflow); the focus accent border marks the focused panel.
|
||||
- **Scroll rebind:** in nav mode Up/Down line-scroll and PageUp/PageDown
|
||||
page-scroll the focused panel (clamped to its viewport); in input mode
|
||||
Up/Down still drive history and PageUp/PageDown still scroll output
|
||||
(no V4 regression); inert keys (printable/Enter/Tab/Backspace) do
|
||||
nothing in nav mode.
|
||||
|
||||
All tiers green, zero skips; clippy clean (nursery).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **True multi-line input (I1)** — Enter-inserts-newline / Ctrl-Enter-
|
||||
submits over a multi-logical-line buffer. DA3/DA4 keep a single
|
||||
logical line; this remains a separate, deferred feature.
|
||||
- **Readline shortcuts (I1b)** — Ctrl-A/E/W/K/U stay reserved-deferred;
|
||||
not touched here.
|
||||
- **Cross-session sidebar persistence** — visibility is session-only
|
||||
(DB1); persisting it would amend ADR-0015.
|
||||
- **The output panel as a third navigation focus target** — navigation
|
||||
mode cycles the two sidebar panels only; output keeps its input-mode
|
||||
PageUp/PageDown scroll.
|
||||
- **Relationship search / filtering within the panel** — the panel is a
|
||||
scrollable list; no query box.
|
||||
- **Relationship rename / edit from the panel** — it is read-only;
|
||||
mutation stays with the DSL/SQL commands.
|
||||
- **A persistent show/hide toggle / force-shown override** (DB1,
|
||||
resolved deferred) — visibility is width-derived + Ctrl-O peek; a
|
||||
pin/force override is an additive follow-up if ever needed.
|
||||
- **A hint-area toggle (old S4 wording)** — not implemented today and
|
||||
not wanted (the hint panel is indispensable since completion moved in;
|
||||
fork §12). The stale "keyboard-toggleable" phrase is removed from S4.
|
||||
- **In-app overlay / keystroke-annotation layer (Gitea #22)** — a
|
||||
separate feature with its own ADR; DC2's `Clear` overlay is built to
|
||||
coexist with it, not to provide it.
|
||||
|
||||
## Accepted consequences
|
||||
|
||||
- **Width-threshold discontinuity.** Because `Auto` visibility flips at
|
||||
width 90 and the sidebar costs 28 columns, widening a terminal across
|
||||
the boundary (89 → 91) makes the *output* narrower (≈ 89 → 63 inner)
|
||||
as the sidebar appears. This is inherent to any width-gated auto-hide
|
||||
and is accepted: 90 is the screencast width, real terminals sit well
|
||||
to one side of it, and `Ctrl-O` peek covers the in-between case. The
|
||||
`90` threshold is a tunable constant.
|
||||
@@ -0,0 +1,418 @@
|
||||
# ADR-0047: Demonstration overlay layer — keystroke badges and step captions
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-06-10); **implemented 2026-06-11**, phased A→B→C (closes
|
||||
Gitea **#22**). Addresses Gitea **#22**. Builds the in-app
|
||||
overlay/annotation primitive that screencast recording (ADR-website-001
|
||||
§2, the `autocast` pipeline) and a future guided-lesson system both
|
||||
need. Adjacent to ADR-0046 (the nav-mode sidebar overlay it must
|
||||
coexist with) and unblocks the polished version of the assistive-editor
|
||||
and projects (`#24`) casts.
|
||||
|
||||
**Implementation (commits `f879d54` → `2d0f4b2`).** Phase A
|
||||
(`f879d54`): `--demo` flag + `RDBMS_PLAYGROUND_DEMO` env →
|
||||
`App.demo_mode`, mirroring the `--no-undo` plumbing; help text mentions
|
||||
only the visible badges (the `Ctrl+]` caption trigger stays
|
||||
low-profile, D6). Phase B (`2584e76`): automatic keystroke badges — pure
|
||||
`demo_badge_label`, set in `App::update` before the modal gate, expired
|
||||
by a ~1.5 s runtime timer via the new `nearest_deadline` helper that
|
||||
extends the time-boxed-`recv` arm condition **without** regressing the
|
||||
ADR-0027 indicator debounce (the rewrite tracks `Instant` deadlines;
|
||||
verified equivalent). Phase C (`241f60c`): the stealth `Ctrl+]`
|
||||
caption buffer in `App::update`, intercepted before the modal gate so
|
||||
captions work over the load picker. Post-build (`2d0f4b2`, user
|
||||
decision): the overlays render as **flat filled yellow rectangles** (no
|
||||
border glyphs, one-cell text margin) to read as a distinct callout. A
|
||||
whole-implementation `/runda` pass returned **PASS** with no blockers;
|
||||
the only untested wiring is the `run_loop` badge timer (not unit-testable
|
||||
in isolation — same posture as the existing `IndicatorDebounce`; the
|
||||
pure pieces are all tested). One intentional, user-acknowledged
|
||||
behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is,
|
||||
by spec; exit capture with `Ctrl+]`). Tests: 2290 passing / 0 failing /
|
||||
0 skipped (Tier-1 label fn + caption FSM + `nearest_deadline`, Tier-2
|
||||
dark/light/stacked/wrapped/clamp snapshots + black-on-yellow style,
|
||||
CLI parse/env); clippy clean.
|
||||
|
||||
All primary forks and the visual placement were **user-confirmed** —
|
||||
including the two follow-ups settled after the first draft: the trigger
|
||||
key (**`Ctrl+]`**, the maximally-obscure valid single-byte code, over
|
||||
`Ctrl+!` which autocast cannot send) and caption sizing (**wrap to 3
|
||||
lines**). A `/runda` pass over this ADR ran before implementation and
|
||||
tightened it — its findings are folded in below (caption/badge
|
||||
interception placement, in-capture key disposition, badge suppression
|
||||
during capture, the timer arm-condition, box clamping, the new
|
||||
output-rect field, and the control-code decode note).
|
||||
|
||||
**Requirements traceability.** There is **no `requirements.md` item**
|
||||
for this work — verified by sweep. It is tracked as Gitea issue **#22**
|
||||
plus this ADR, consistent with the project's convention ("issues are
|
||||
the lightweight tracker; ADRs are the decisions"). The website-side cast
|
||||
scope lives in **ADR-website-001** (website branch), not main's
|
||||
`requirements.md`.
|
||||
|
||||
## Context
|
||||
|
||||
The website records its demos as asciinema `.cast` files driven by
|
||||
**`autocast`** (ADR-website-001 §2; STYLE.md): source step-lists in
|
||||
`casts-src/casts.mjs` (`type` / `wait` / `key`) expand to **one key per
|
||||
character, Enter = `^M`**, recorded against the real `target/debug`
|
||||
binary. The hard constraint — the same one that drove `#24` — is that
|
||||
autocast can only emit **typeable characters, ASCII control codes
|
||||
(`^X`), and waits**. It cannot send arrow keys, function keys, or any
|
||||
multi-byte escape sequence.
|
||||
|
||||
Two classes of on-screen event are therefore invisible or
|
||||
unexplained in a cast:
|
||||
|
||||
1. **Keystrokes that cause a visible change but render no glyph of
|
||||
their own** — most acutely **Tab** completion: the command line
|
||||
jumps from `show data bo` to `show data books` with no sign a key
|
||||
was pressed. Enter, the arrows, Ctrl-O, Esc are the same.
|
||||
2. **Step structure / "what just happened" narration** — a cast is a
|
||||
silent moving picture; there is no channel to separate or explain
|
||||
steps for a visual learner.
|
||||
|
||||
asciinema-player has no inline keystroke overlay, and a website-side
|
||||
HTML overlay layered on the player would be fragile (its timings would
|
||||
have to track every recording and break on each re-record). The robust
|
||||
place to solve this is **in the app**: if the app renders the overlay,
|
||||
the cast captures it natively and it re-records for free. The same
|
||||
primitive is exactly what a future **guided-lesson** system needs to
|
||||
point at things and narrate steps — so it is built as a general
|
||||
capability, not a cast-only hack (the issue's "pays off twice"). It is
|
||||
also directly useful for a **teacher demonstrating the playground
|
||||
live** — pressing Tab in front of a class has the same
|
||||
invisible-keystroke problem as a cast.
|
||||
|
||||
The app's renderer is a pure function of `App` state and already draws
|
||||
two kinds of last-pass overlay over the base render with **no layout
|
||||
reflow**: modals and the ADR-0046 nav-mode sidebar overlay. The event
|
||||
loop already **time-boxes `event_rx.recv()`** with a `tokio` timeout
|
||||
(the ADR-0027 `IndicatorDebounce`) and redraws when the timer elapses —
|
||||
the exact mechanism a self-expiring badge needs. These two existing
|
||||
seams make the feature cheap.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 — Activation: a `--demo` flag (+ env var), off by default
|
||||
|
||||
Demonstration mode is entered with a **`--demo`** CLI flag, or
|
||||
equivalently the **`RDBMS_PLAYGROUND_DEMO`** environment variable (set
|
||||
truthy) — mirroring the existing `--log-file` / `RDBMS_PLAYGROUND_LOG_FILE`
|
||||
pair. It combines freely with every other flag (`--resume`, `--mode`, a
|
||||
positional path); there are no exclusions.
|
||||
|
||||
When the flag is **off** (the default), none of the key handling or
|
||||
rendering below is active and the app behaves exactly as today — **zero
|
||||
footprint for real users** (R8). `autocast` sets the flag when it
|
||||
launches the binary; a teacher sets it on their own command line.
|
||||
|
||||
It is framed as a general **demonstration mode**, not "cast mode" — the
|
||||
honest name for what it does, and it reads sensibly in `--help`. The
|
||||
flag is documented in the CLI banner (one line); obscurity is not a
|
||||
security property here and a harmless opt-in flag is better surfaced
|
||||
than hidden. What stays "low-profile" (per #22) is that there is **no
|
||||
normal in-app command** for it and **no persistent on-screen indicator**
|
||||
(see D7) — so a cast frame is never polluted by a `[DEMO]` marker.
|
||||
|
||||
### D2 — Keystroke badges: automatic, app-detected
|
||||
|
||||
In demo mode the app shows a transient badge **automatically** whenever
|
||||
it handles one of a curated set of *otherwise-invisible* keys. The cast
|
||||
does nothing special — it presses the key it was going to press anyway,
|
||||
and the badge re-records for free. The set:
|
||||
|
||||
| Key | Badge | | Key | Badge |
|
||||
|-----|-------|-|-----|-------|
|
||||
| Tab | `[TAB]` | | Home | `[HOME]` |
|
||||
| Shift-Tab | `[SHIFT-TAB]` | | End | `[END]` |
|
||||
| Enter | `[ENTER]` | | PageUp | `[PGUP]` |
|
||||
| Esc | `[ESC]` | | PageDown | `[PGDN]` |
|
||||
| ↑ | `[UP]` | | Backspace | `[BKSP]` |
|
||||
| ↓ | `[DOWN]` | | Delete | `[DEL]` |
|
||||
| ← | `[LEFT]` | | Ctrl-O | `[CTRL-O]` |
|
||||
| → | `[RIGHT]` | | | |
|
||||
|
||||
Plain character keys render a glyph on the input line already, so they
|
||||
produce **no** badge (that is the definition of the set — "invisible"
|
||||
keys). The badge fires on **key press**, regardless of whether the key
|
||||
had an effect in the current state (e.g. `↑` with no history still shows
|
||||
`[UP]`): simpler, and the demo author controls the script. Badge text is
|
||||
bracketed ASCII (`[TAB]`) per the user's preference — renders on every
|
||||
terminal and is cast-safe, unlike the `⇥` glyph mocked earlier.
|
||||
|
||||
The label mapping is a **pure function** `demo_badge_label(&KeyEvent) ->
|
||||
Option<&'static str>` (Tier-1 testable). The badge **auto-expires on a
|
||||
timer** (D5).
|
||||
|
||||
### D3 — Step captions: a stealth, control-code-delimited input buffer
|
||||
|
||||
Caption text must arrive through typeable input only (R4). A **single
|
||||
toggle control code — `Ctrl+]`** (byte `0x1D`) drives a **stealth
|
||||
capture buffer**. `Ctrl+]` was chosen (over the bound `Ctrl-O`/`Ctrl-C`,
|
||||
the readline-reserve letters Ctrl-A/E/W/K/U, the tmux-prefix Ctrl-B, the
|
||||
signal/flow-control codes Ctrl-\\=SIGQUIT and Ctrl-S/Q=XON/XOFF, and a
|
||||
plain letter chord like Ctrl-G) because it is **maximally non-obvious**
|
||||
— the classic telnet escape, almost never pressed by accident — while
|
||||
still being a single ASCII control byte autocast can emit. It has **no
|
||||
signal or flow-control baggage** and is **multiplexer-safe**. Note
|
||||
collision risk is already near-zero in casts (a fresh `--demo` binary
|
||||
sees only scripted keys); the obscurity mainly protects a live teacher
|
||||
from a stray trigger.
|
||||
|
||||
- First `Ctrl+]` **opens** capture. The command input line and the
|
||||
output are untouched. If a caption is already visible, opening clears
|
||||
it (you are starting a new annotation).
|
||||
- Subsequent typed characters **accumulate into the caption buffer
|
||||
invisibly** — they do **not** appear on the prompt, do not execute,
|
||||
and do not enter history. **`Backspace`** deletes the last buffered
|
||||
character. **Every other key while capturing — Enter, the arrows,
|
||||
Tab, … — is inert** (swallowed, no effect): only typing and `Ctrl+]`
|
||||
do anything.
|
||||
- A second `Ctrl+]` **commits** the buffer to the caption box (D4).
|
||||
An **empty** commit (toggle-toggle with nothing typed) clears any
|
||||
visible caption — the author's explicit dismiss.
|
||||
|
||||
Because nothing about the capture shows on the prompt, the caption
|
||||
"pops" into its box with no ugly typing artifact, while the caption text
|
||||
still lives **inline in `casts.mjs`** at the right spot (one source of
|
||||
truth, no separate notes file to keep ordered).
|
||||
|
||||
This is all keyboard-stream interpretation, so it lives in the
|
||||
pure-sync `App::update()` (Tier-1 testable) and is **only active in demo
|
||||
mode** — when off, `Ctrl+]` is inert and characters reach the input
|
||||
line normally.
|
||||
|
||||
**Placement in `handle_key` — before the modal gate (runda finding).**
|
||||
The capture interception (`Ctrl+]` and the accumulating characters)
|
||||
**and** the "clear a visible caption on the next keystroke" check sit at
|
||||
the **very top of `handle_key`, before the `self.modal.is_some()`
|
||||
gate** — *not* alongside the `Ctrl-O` handler, which is gated behind it.
|
||||
This is required so captions can be authored **while a modal is open** —
|
||||
specifically the load-picker, which is exactly the **projects / `#24`
|
||||
cast** (annotating "press j/k to move", with an `[ENTER]` badge as the
|
||||
selection is made). While capturing, the modal is frozen (capture
|
||||
swallows keys), which is the intended behaviour. `App` exposes
|
||||
`demo_capturing` so the runtime can read it (see D5).
|
||||
|
||||
The control-code path is sound end to end, verified against our
|
||||
crossterm (0.29, `event/sys/unix/parse.rs:110-113`): `autocast` emits
|
||||
`^]` = byte `0x1D`; crossterm decodes `0x1C..=0x1F` →
|
||||
`KeyCode::Char('4'..='7') + CONTROL`, so **`Ctrl+]` (0x1D) arrives in
|
||||
the app as `KeyCode::Char('5') + KeyModifiers::CONTROL`** — that is the
|
||||
pattern `handle_key` matches. (The same routine decodes `0x09`/`0x0D`/
|
||||
`0x1B`/`0x7F` to the named `Tab`/`Enter`/`Esc`/`Backspace` keys and
|
||||
`0x01..=0x1A` to `Ctrl+a..z`, so `0x1D` is unambiguously distinct.) The
|
||||
canonical way to produce it is **Ctrl+]**; on some layouts `Ctrl+5`
|
||||
yields the same byte. *(This is the Unix/Linux decode path — the
|
||||
cast-recording platform; crossterm's separate Windows backend would be
|
||||
confirmed by test if live `--demo` on Windows is exercised.)*
|
||||
|
||||
### D4 — Both overlays are floating boxes at the output panel's inner bottom-right
|
||||
|
||||
The badge and the caption both render as **floating, flat filled
|
||||
rectangles anchored to the inside of the output panel's bottom-right
|
||||
corner** (inset one cell from the panel's inner edge), drawn **last over
|
||||
the base render** — after modals, so they remain visible while the
|
||||
load-picker (the `#24` cast) or any modal is up, and with **no layout
|
||||
reflow** (consistent with the modal / nav-overlay precedent; honours
|
||||
R8).
|
||||
|
||||
**Flat rectangle, not a bordered box (user decision, post-build).** The
|
||||
overlays draw as a **solid yellow rectangle with no border glyphs** and
|
||||
a one-cell margin around the text — deliberately *unlike* the app's
|
||||
rounded-border panels, so they read as a distinct callout that "stands
|
||||
out nicely" rather than as another panel. Implemented with a borderless
|
||||
`Block` fill (the `paint_background` mechanism) plus a `Paragraph` inset
|
||||
into a one-cell `Margin`.
|
||||
|
||||
The top-level `render()` does not currently know the output-panel rect
|
||||
(it is computed inside `render_right_column`), so a **new field
|
||||
`App.last_output_area: Rect`** is set in `render_output_panel` and read
|
||||
at the top-level draw pass to anchor the overlay — the established
|
||||
"renderer reports metrics back to `App`" pattern (sibling to
|
||||
`note_output_viewport`, which stores row counts, not a rect).
|
||||
|
||||
When **both** are present, the **keystroke badge stacks directly above
|
||||
the caption box** (both right-aligned in the corner) so they never
|
||||
overlap.
|
||||
|
||||
**Styling — deliberately high-contrast:** **bold black text on a yellow
|
||||
fill** — hard to overlook, identical in light and dark themes (a fixed
|
||||
high-contrast pair centralised in `theme.rs`, not theme-derived).
|
||||
|
||||
**Caption sizing (user-confirmed).** The caption is **word-wrapped to at
|
||||
most 3 lines** within a content width of `min(40, output_inner_width −
|
||||
4)` columns, ellipsised beyond the third line. So the caption rectangle
|
||||
is **3–5 rows** tall (1–3 text rows + a one-cell margin top and bottom),
|
||||
its height varying with the text — a full sentence fits without forcing
|
||||
the author to split it, while the 3-line cap keeps it corner-sized. The
|
||||
**badge** rectangle is always a single short token (`[TAB]` …
|
||||
`[SHIFT-TAB]`), so it is a fixed **3 rows** (1 text row + the margin),
|
||||
narrow.
|
||||
|
||||
**Clamping (runda finding).** Stacked, the two boxes are up to 8 rows
|
||||
(5 caption + 3 badge); the output panel's inner height is only `Min(5)`,
|
||||
so on a short terminal they could exceed it. Both boxes are **clamped to
|
||||
the output inner area**: width to `output_inner_width`, the caption's
|
||||
wrap-line count reduced so the stack fits the available height (badge
|
||||
first — it is the time-critical one), and if a box cannot fit at all
|
||||
(pathologically small terminal) it is **not drawn** rather than
|
||||
overflowing. Cast geometry (90×26) leaves ~18 output rows — ample; the
|
||||
guard only protects a real user who runs `--demo` in a tiny window.
|
||||
|
||||
### D5 — Timing: badges expire on a ~1.5 s timer; captions persist until the next keystroke
|
||||
|
||||
- **Keystroke badge:** auto-expires on a **time-based TTL**, default
|
||||
**1.5 s** (a single tunable constant; the user asked for 1–2 s). This
|
||||
matters for both media: in a cast the badge fades on its own so a
|
||||
trailing `wait` ends on a clean frame, and in live teaching the badge
|
||||
clears without the presenter needing another key. A new badge replaces
|
||||
the current one and resets the timer.
|
||||
- **Caption:** persists **until the next keystroke**, which clears it
|
||||
and is then processed normally (or until an explicit empty-`Ctrl+]`
|
||||
dismiss, or replacement by a new caption).
|
||||
|
||||
The timer reuses the runtime's existing time-boxed-`recv` pattern: the
|
||||
loop already arms a `tokio::time::timeout` for the indicator debounce.
|
||||
|
||||
**Arm-condition extension (runda finding).** Today the loop time-boxes
|
||||
`recv` **only while `debounce.is_armed()`** — and the debounce settles
|
||||
at `INDICATOR_DEBOUNCE` (1000 ms), shorter than the 1500 ms badge TTL.
|
||||
So the arm condition becomes **`debounce.is_armed() || badge_pending`**,
|
||||
and the loop waits on the **nearest deadline** of the two. On a wake it
|
||||
checks each independently: at the 1000 ms debounce deadline it settles
|
||||
the indicator **without clearing the badge**; at the 1500 ms badge
|
||||
deadline it clears the badge; then redraws. The pure "nearest deadline"
|
||||
computation is unit-testable on its own.
|
||||
|
||||
The badge's expiry `Instant` lives in the **runtime** (so `App` stays
|
||||
clock-free and Tier-1-pure, exactly as `IndicatorDebounce` keeps timing
|
||||
out of `App`); `App.demo_badge: Option<&'static str>` is the render
|
||||
mirror, **set by the runtime** on a significant key and cleared on timer
|
||||
elapse.
|
||||
|
||||
**Badge suppression during capture (runda finding).** Because the
|
||||
runtime sets badges from the raw key independently of `App` state, it
|
||||
must **not** badge a key that capture swallowed (e.g. an inert `Tab`
|
||||
while a caption is being typed would otherwise flash `[TAB]` for a
|
||||
no-op). The runtime sets a badge only when **`!app.demo_capturing`**.
|
||||
|
||||
**Ownership note.** `demo_caption` is mutated inside `update()`
|
||||
(input-driven) while `demo_badge` is mutated by the runtime
|
||||
(timing-driven). This split is deliberate and mirrors the existing
|
||||
`input` (set in `update()`) vs `input_indicator` (set by the runtime
|
||||
from `IndicatorDebounce`) pair — not an inconsistency.
|
||||
|
||||
### D6 — Help text and strings
|
||||
|
||||
The CLI banner (`help.cli_banner` in `en-US.yaml`) gains a `--demo`
|
||||
line. User-facing wording obeys the house rules (no engine name, no
|
||||
"DSL"): *"Demonstration mode — show on-screen badges for otherwise-
|
||||
invisible keys (Tab, Enter, …), for screencasts and live teaching."*
|
||||
|
||||
The help text **deliberately mentions only the visible badges, not the
|
||||
`Ctrl+]` step-caption mechanism** (user decision): the caption trigger
|
||||
stays low-profile, true to #22's "secret trigger" framing — a cast
|
||||
author or lesson script knows it; a casual `--help` reader is not
|
||||
pointed at it. Badge labels and the `[…]` chrome are fixed ASCII, not
|
||||
localised; caption content is author-supplied free text and likewise
|
||||
not a catalog string.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Scripted badges** (cast pushes each badge explicitly) — rejected:
|
||||
the app already sees every key, so automatic detection (D2) is more
|
||||
robust and re-records for free. *(User-confirmed.)*
|
||||
- **Typed hidden command for captions** (a secret-prefixed line) —
|
||||
rejected: the command is briefly visible being typed on the prompt.
|
||||
**Preloaded notes file + advance key** — rejected: a separate file
|
||||
that must stay ordered/in-sync with the cast. The **stealth buffer**
|
||||
(D3) is self-contained in the cast script *and* leaves the prompt
|
||||
clean. *(User-confirmed.)*
|
||||
- **Fixed-corner HUD badge / badge by the input line** — rejected in
|
||||
favour of a floating box at the output panel's bottom-right; **top
|
||||
banner / subtitle band** for captions — rejected in favour of the
|
||||
matching floating box. *(User-confirmed via mockups.)*
|
||||
- **A persistent `[DEMO]` status-bar marker** — rejected: it would show
|
||||
in every cast frame. Demo mode is silent except for the transient
|
||||
overlays (D7).
|
||||
- **Caption persists for a fixed time** (instead of until next
|
||||
keystroke) — noted as a one-constant change if the next-keystroke rule
|
||||
proves too eager in practice; the user chose next-keystroke.
|
||||
- **Trigger via `Ctrl+!` / a Kitty-protocol chord** — rejected: not
|
||||
representable as a single ASCII control byte, so autocast cannot send
|
||||
it (fails R4, the same wall as arrow keys). **`Ctrl+G` / a letter
|
||||
chord** — workable but less non-obvious; the user chose the
|
||||
maximally-obscure `Ctrl+]` from the valid single-byte set.
|
||||
- **Single-line ellipsised caption** — rejected in favour of wrap-to-3-
|
||||
lines so a full sentence fits. *(User-confirmed via mockups.)*
|
||||
|
||||
## Consequences
|
||||
|
||||
- A general overlay primitive exists that the cast pipeline uses now and
|
||||
the guided-lesson system can reuse later (`App.demo_caption` and the
|
||||
badge channel are the seam).
|
||||
- `autocast` casts gain a real Tab-completion moment, key indicators for
|
||||
the projects/`#24` round-trip, and step captions — all by adding
|
||||
`key: ^G` / `type:` / `key: ^G` and ordinary keys to `casts.mjs`, then
|
||||
re-running `pnpm casts`. No website-side overlay machinery.
|
||||
- Teachers get the same affordance live via `--demo`.
|
||||
- One new control-code binding (`Ctrl+]`) is consumed, but only inside
|
||||
demo mode — normal sessions are unaffected, so it does not encroach on
|
||||
the reserved readline chords (I1b).
|
||||
- The renderer must expose the output-panel rect to `App`; a small,
|
||||
pattern-consistent addition.
|
||||
|
||||
## Scope / non-goals (OOS)
|
||||
|
||||
- **Manual/scripted badge push** and **badges for plain character
|
||||
keys** — out; badges are automatic over the fixed invisible-key set.
|
||||
- **Configurable overlay styling or placement** — out; fixed
|
||||
black-on-yellow boxes at the output panel's bottom-right.
|
||||
- **The guided-lesson / tutorial system itself** — out (its own ADR);
|
||||
this ADR only builds the primitive it will reuse.
|
||||
- **Persisting demo mode across project switches / sessions** — out;
|
||||
it is a per-run flag.
|
||||
- **Localising caption content** — out; captions are author-supplied
|
||||
free text.
|
||||
- **Output-pane scroll-in-casts** and other arrow-only interactions —
|
||||
out (separate enhancement; same autocast limitation as noted in #24).
|
||||
|
||||
## Testing
|
||||
|
||||
Per ADR-0008 and the project's test discipline (test-first; green, no
|
||||
skips):
|
||||
|
||||
- **Tier 1 (`app.rs` units):** `demo_badge_label` mapping over the full
|
||||
key set **and** the no-badge cases (plain chars, `Ctrl+]`, `Ctrl-C`);
|
||||
the stealth-caption state machine — open on `Ctrl+]`; characters
|
||||
accumulate with the **input line unchanged**; `Backspace` edits the
|
||||
buffer; **non-typing keys inert while capturing**; commit sets the
|
||||
caption; empty commit clears; opening over a visible caption clears
|
||||
it; next keystroke clears a visible caption **then processes
|
||||
normally**; capture works **with a modal open** (caption set while the
|
||||
load-picker modal is up, picker state untouched); the **demo-off
|
||||
gate** (`Ctrl+]` inert, characters reach the input, no caption/badge
|
||||
state ever set); the pure "nearest deadline" helper.
|
||||
- **Tier 2 (insta snapshots, `ui.rs`):** badge box, caption box, both
|
||||
stacked, at 90×26 in light and dark — verifying the bottom-right
|
||||
anchor, the stack order, and the black-on-yellow styling; plus a
|
||||
short-terminal case exercising the clamp/skip guard.
|
||||
- **Tier 3 (integration):** `--demo` plumbs `app.demo_mode`; a
|
||||
significant-key event sets `app.demo_badge` and a swallowed key during
|
||||
capture does **not**; a `Ctrl+]` / type / `Ctrl+]` sequence sets
|
||||
`app.demo_caption` without touching `app.input`.
|
||||
- **CLI (`cli.rs` units):** `--demo` parses (mirrors `--no-undo`); the
|
||||
`RDBMS_PLAYGROUND_DEMO` env fallback; default-off.
|
||||
|
||||
**Honest coverage limit.** The badge **timer-expiry wiring** runs inside
|
||||
`run_loop` (terminal + db worker), which is not unit-testable in
|
||||
isolation; it is a thin reuse of the already-proven `IndicatorDebounce`
|
||||
time-boxed-`recv` path. We therefore test the **pure pieces**
|
||||
exhaustively (label fn, capture state machine, nearest-deadline helper)
|
||||
and assert plumbing via Tier-3, rather than over-claiming an integration
|
||||
test of the `tokio` timeout itself.
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -57,3 +57,6 @@ This directory contains the project's ADRs, recorded per
|
||||
- [ADR-0042 — H1a parse-error pedagogy in the grammar-tree era](0042-h1a-parse-error-pedagogy-grammar-tree.md) — **Accepted 2026-06-03.** Continues **H1a** from ADR-0021 against the ADR-0024 grammar tree (ADR-0021's chumsky mechanism is dead). Records the **baseline already shipped** — per-command `usage:` block (38 `parse.usage.*` templates), available-commands fallback, structural "after `…`, expected …" wording, source-derived ident slot labels ("table name"/"column name"), curated `parse.custom.*` near-miss messages, and the ADR-0027/0033/0036 schema-aware `[ERR]` diagnostics — so H1a is *substantially* delivered at the intent level. Defines the remaining work as **(1)** a verified per-command **near-miss matrix** (`tests/typing_surface/` + `tests/it/parse_error_pedagogy.rs`) as the definition of done, test-first; **(2)** **friendlier literal expectation labels** — optional prose glosses on `Word`/`Punct`/`Flag` positions that *add* role context while always keeping the exact literal visible (e.g. "a filter clause: `where …` or `--all-rows`"); **(3)** **advanced-mode SQL** near-miss parity (RETURNING scope, CTE-arity positioning, `CROSS JOIN … ON`, INSERT…SELECT count) — **in scope**, kept distinct from ADR-0019 §OOS-2 which covers advanced-SQL *engine*-error sanitisation, a different layer. Catalog/anchor-phrase discipline (ADR-0019) preserved; no public API change. OOS: I3/I4, spell-correction, multi-error reporting, verbosity-gating the usage block
|
||||
- [ADR-0043 — Compound-primary-key foreign-key references (T3)](0043-compound-pk-foreign-key-references.md) — **Accepted + implemented 2026-06-09** (all four forks confirmed at the recommended option: full-PK matching, house-style uniform lists, parenthesized DSL syntax, bare-SQL-FK auto-expansion). Closes `requirements.md` **T3** `[x]` — the relationship model went list-based across six layers (single-column preserved, no migration), DSL `from P.(a,b) to C.(x,y)` + SQL `FOREIGN KEY (a,b) REFERENCES P(x,y)` parse/execute/enforce, 12 tests in `tests/it/compound_fk.rs`. Closes the open leg of `requirements.md` **T3**: a foreign key that *references* a parent's compound primary key. A 2026-06-09 audit found single-column FK woven through ~15–20 sites (metadata table, `RelationshipSchema`, `project.yaml` `RawEndpoint`, both grammar surfaces, executor FK-DDL emission, per-column type-compat, display) — earns an ADR, not an inline build. **Decision:** reference the parent's **full** compound PK, matched **positionally** to an equal-length child column list, per-pair `fk_target_type` compat (ADR-0011, element-wise); DSL `from <P>.(a, b) to <C>.(x, y)` (single form unchanged), SQL `FOREIGN KEY (x, y) REFERENCES P(a, b)` (extend the existing one-cap lists; bare table-level FK auto-expands to the parent PK when arities match). **Storage — no migration (back-compat not required, user-confirmed 2026-06-09; no installed base):** the relationship endpoint joins the list convention `project.yaml` *already* uses — `columns: [a, b]` like `primary_key: [id]` and index `columns: [...]` (the endpoint was the lone scalar `column:` holdout); the metadata `TEXT` columns are unchanged and store the list **comma-joined** (`a,b`; the bare name for single — safe because identifiers are `[A-Za-z0-9_]+`). No F3 migrator, no version bump; accepted trade-off is that a pre-change `project.yaml` with relationships won't load (clean cutover). In-memory model goes list-based (`Vec<String>`) through all six layers; the enforced FK is the rebuilt child-table DDL (`FOREIGN KEY (a,b) REFERENCES P(x,y)`), one relationship = one undo step (ADR-0013). Genuine forks escalated: matching policy (full-PK vs subset), storage (house-style uniform lists vs normalized table), DSL syntax (parenthesized vs repeated-dotted), bare-SQL-FK auto-expansion. OOS: subset/non-PK (UNIQUE-targeted) FK references; any single-column behaviour change
|
||||
- [ADR-0044 — Relationship visualization (two-table connector diagrams)](0044-relationship-visualization.md) — **Accepted 2026-06-09; implemented 2026-06-10** (closes `requirements.md` V1; second `/runda` pass over the implementation; §3 last-resort helper line considered and rejected). Resolves **ADR-0016 OOS-1** and closes the open half of `requirements.md` **V1** ("a selected relationship as two tables joined by a line"). Renders a relationship as **Style A** (two structure boxes + connector). **Reach = "relationship-relevant"** (user-chosen over global / show-only): diagrams on the surfaces where the relationship is the *subject* — `show relationship <name>` (one full diagram), `show table <T>` (T's structure box then a **Relationships** section of **stacked compact** per-relationship diagrams — chosen over a focal-centred subgraph: no crossing lines, scales via scroll, two-boxes-wide fits any terminal), and relationship DDL echoes (`add`/`drop`/`modify relationship`); incidental DDL echoes (`add column`, `drop index`, `change column`, plain `create table`) keep the terse prose, via a `Diagram`|`Prose` render mode on `render_structure`. Reading convention **child(FK)-left / parent-right, arrow →, `n`…`1` cardinality**, applied uniformly; every box gets a **bold title row + rule** so the name can't read as a column. **Compound FKs** (ADR-0043) route one connector per positional pair + an explicit pairing line. **Width-aware** (first in the codebase) but **App-side**: `render_structure`/diagram rendering runs in `app.rs` (the worker only returns `TableDescription`s), a new `App::last_output_width` (set from `ui.rs`) drives side-by-side vs a **vertical-stack** fallback + last-resort "run `show relationship`" pointer; rendered once at command time, **no live reflow** (V4). `show relationship`'s worker path (`do_show_one`, prose-only) is restructured to return both endpoint `TableDescription`s. Styling reuses **ADR-0028** App-side styled runs (new classes: table-name/key/connector/cardinality/action) — no worker→UI contract change. **Partially supersedes ADR-0016 §5** (prose block replaced on relationship-subject surfaces, retained on incidental ones); extends §4 (layout width-awareness, still no cell truncation) and §6 (per-span theming). Tests: insta snapshots (single, compound, vertical fallback, helper line, self-referential, multi-rel `show table`) + width-threshold/routing unit tests + Tier-3 wiring; enumerated prose-fallout updates (`output_render.rs:121/135/793`, the relationships snapshot, `walking_skeleton.rs:477/530`). A `/runda` DA pass corrected three inverted-architecture claims (App-side rendering, untracked width, prose-in-worker show-relationship) before acceptance. OOS: user-configurable display setting (OOS-7), live reflow (V4), whole-DB ER export (V3), m:n (C4), ASCII fallback (ADR-0016 OOS-5)
|
||||
- [ADR-0045 — `create m:n relationship` convenience command (C4)](0045-mn-convenience.md) — **Accepted + implemented 2026-06-10** (closes `requirements.md` **C4**; all forks user-confirmed + a `/runda` DA pass that verified the `do_create_table` reuse against code and corrected the "no PK-less tables" assumption — advanced SQL `create table t (a int)` has none, so a parent-PK guard is retained). Implementation corrected a second ADR premise: "the walker already dispatches multiple nodes per entry word" held only in *advanced* mode — two simple-mode spots (dispatcher `decide`, completion continuation-merge) assumed ≤1 DSL form per entry word and were generalized **behaviour-preservingly** (dispatch reduces to the old single-candidate commit; completion merge gated on `simple_count > 1`). Junction echo wired (`render_create_m2n`, round-trips as SQL). `create m:n relationship from <T1> to <T2> [as <name>]` generates a junction table with one FK column per parent PK column, a **compound PK over all the FK columns** (the textbook junction — the pair is unique, no duplicate links), and **two 1:n relationships**, all in **one transaction = one undo step** (built by reusing `do_create_table`, which already takes `foreign_keys` + writes relationship metadata — no batch bracketing). Forks all user-chosen: junction PK = compound-over-FKs (vs surrogate serial / no PK); referential actions = **`CASCADE`** on delete+update (vs NO ACTION / RESTRICT); naming = auto `{T1}_{T2}` + optional `as` (vs auto-only); available in **both modes** (Simple-category DSL, like the sibling relationship commands). FK columns named `{parent_table}_{pk_column}` (disambiguates shared `id`; generalises to compound parents via ADR-0043), typed via `fk_target_type` (ADR-0011). A distinct `Command::CreateM2nRelationship` (not lowered to `CreateTable`) preserves command identity (X5) and lets the teaching echo speak in m:n terms. Cross-cutting wiring enumerated: separate `CREATE_M2N` `CommandNode` (own `help_id`/`usage_ids`), `("m","m:n")` completion composite, `HintMode`s, grammar-driven highlighting, `help`/`help create`, `parse_error_pedagogy` near-miss matrix, teaching echo. OOS: **self-referential m:n** (`from T to T`) refused outright (user-confirmed "full stop" — directional column-naming is more than this beginner convenience warrants); per-relationship action overrides; extra junction payload columns; m:n diagram echo; renaming the auto-generated relationships
|
||||
- [ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)](0046-sidebar-navigation-and-responsive-input-hint.md) — **Accepted + implemented 2026-06-10, phased A→B→C** (8 commits `9f5f76b`…`22bec61`; closes Gitea **#20** hint jumpiness, **#21** left-column improvements, **#23** long input — all forks user-confirmed, including the persistent show/hide toggle which is **deferred**: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on **`App`** not `SchemaCache` (DB2); the nav overlay clears **only the sidebar strip + a one-column gutter**, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. **Phase A (input & hint):** the hint panel's height becomes a function of **terminal geometry, fixed between resizes** (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets `H<40` compact (input 1 row + horizontal scroll / hint 2) vs `H≥40` comfortable (input 2 rows soft-wrap / hint 2), output `Min(5)` honoured first under degradation; input gains horizontal scroll (`input_scroll_offset`, single logical `String` — **not** I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. **Phase B (sidebar):** the 26-col Tables column is **kept but made optional and richer** (not deleted — pedagogy wins ties) — **width-derived session-only** visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a **relationships panel** rendered narrow with endpoints broken at the arrow, ellipsized — a **separate sibling panel** that **overrides S2**'s nested-list extension model (relationships are cross-table). the full records live on a new **`App.relationships`** field (revised from the ADR's original `SchemaCache.relationship_details` at implementation — `SchemaCache` is walker-facing and needs only the names, kept in `relationships: Vec<String>`; details are UI-only, so `App` mirrors `app.tables` and avoids ~23 fixture edits), delivered by `Database::read_all_relationships` + an `AppEvent::RelationshipsRefreshed`; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). **Phase C (navigation mode):** **`Ctrl-O`** enters a focus cycle (Input → Tables → Relationships → Input; `Esc` exits) orthogonal to the ADR-0003 input mode — **`Ctrl-B` was rejected on review as the default tmux prefix** (unreachable inside tmux); the focused panel **expands to ~40–50 cols as a `Clear` overlay** (right panels stay unchanging underneath) and scrolls via **Up/Down (line) + PageUp/PageDown (page)** (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); `Ctrl-O` (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; `H<40/≥40` thresholds; session-only persistence; Up/Down line-scroll; **separate relationships panel overriding S2**; **no hint-area toggle** (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build `/runda` DA pass drove these corrections: caught the `Ctrl-B`/tmux collision, the `SchemaCache` retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue **#22** (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output *narrower* when widened across the boundary (sidebar appears)
|
||||
- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54`→`2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# Session handoff — 2026-06-10 (61)
|
||||
|
||||
Sixty-first handover. Continues from handoff-60 (Gitea migration
|
||||
cleanup + V1 relationship visualization, ADR-0044). This session was
|
||||
a **list-trimming pass on "easy wins"**: it closed **X1**
|
||||
(comprehensive logging, full sweep) and both **T3 residuals** (the two
|
||||
ADR-0043 messaging-polish items). Four commits, all green, all
|
||||
user-confirmed.
|
||||
|
||||
## §1. State at handoff
|
||||
|
||||
**Branch:** `main`. **HEAD `5a33f2a`.** 4 commits this session
|
||||
(`a8ad0c6` → `5a33f2a`) on top of session-60's 5; push is the user's
|
||||
step.
|
||||
|
||||
**Tests: 2211 passing / 0 failing / 1 ignored** (lib 1588, it 431,
|
||||
typing_surface_matrix 192; the 1 ignored is the long-standing
|
||||
doc-test). **Clippy clean** (nursery, all targets). +4 over the
|
||||
handoff-60 baseline of 2207 (one test per residual at each of the
|
||||
enrichment + render layers, plus the two grammar/worker tests).
|
||||
|
||||
This session's commits:
|
||||
```
|
||||
5a33f2a fix(fk): compound-FK violation message names every column pair
|
||||
6985a43 fix(fk): inline FK referencing a compound PK points at the table-level form
|
||||
0a7612e feat: comprehensive logging across parser, app, persistence, runtime (X1)
|
||||
a8ad0c6 feat(db): comprehensive logging across worker + executors (X1)
|
||||
```
|
||||
|
||||
## §2. X1 — comprehensive logging (closed, `[x]`)
|
||||
|
||||
The full-sweep instrumentation pass the "log liberally" standard
|
||||
called for. **~75 → 135 `tracing` sites** under a documented level
|
||||
discipline now living in the **`src/logging.rs` module doc** (read it
|
||||
before adding logs — it is the durable convention).
|
||||
|
||||
**Levels:** `error` = unrecoverable; `warn` = recoverable / fallback
|
||||
taken; `info` = low-volume lifecycle (worker start/exit, project
|
||||
open); `debug` = the bulk, one line per *executed* command + its
|
||||
decision points (off by default, opt-in `RDBMS_PLAYGROUND_LOG=debug`);
|
||||
`trace` = hot paths only (per-keystroke parse, per-key input).
|
||||
|
||||
**Where logs go (was a point of confusion):** always a **file**
|
||||
(stdout/stderr would corrupt the TUI). Path precedence: `--log-file`
|
||||
> `RDBMS_PLAYGROUND_LOG_FILE` > default `~/.rdbms-playground/
|
||||
playground.log` (append mode). Level filter is the *separate*
|
||||
`RDBMS_PLAYGROUND_LOG` env var, default `info`.
|
||||
|
||||
**Coverage by commit:**
|
||||
- `a8ad0c6` **db.rs** (26→67): entry-`debug!` on all 34 `do_*`
|
||||
executors (DDL/DML/relationship/index/read), matching the existing
|
||||
`do_sql_delete`/`do_run_select` style — so the route through
|
||||
*delegating* executors (e.g. `add_column` →
|
||||
`add_constrained_column_via_rebuild`) is visible in the log
|
||||
*sequence*. Decision-point logs: `rebuild_table_with_copy`
|
||||
begin/commit (+ FK-check-failure and `foreign_keys` re-enable
|
||||
failure as `warn`), `do_insert` autofill summary, `do_delete`
|
||||
cascade summary, `do_create_table` FK resolution. Worker
|
||||
start/exit `debug!`→`info!`.
|
||||
- `0a7612e` **rest**: `persistence/mod.rs` logs every yaml/CSV/history
|
||||
write (the silent-failure disk paths); `runtime.rs`
|
||||
`execute_command_typed` dispatch; `app.rs` submit /
|
||||
`dispatch_app_command` / ADR-0044 diagram-vs-prose render choice;
|
||||
`dsl/parser.rs` parse begin/outcome at **`trace`** (the
|
||||
`parse_command_inner` choke point — `completion.rs` re-parses
|
||||
per-keystroke, probing candidates in a loop, so `debug` would
|
||||
flood).
|
||||
|
||||
**Verification:** emission proven end-to-end through the *real* worker
|
||||
thread + real `logging::init` via two throwaway smoke tests (db path
|
||||
and persistence path), both since deleted. The DA-honest gap: a few
|
||||
internal read-only helpers (`do_find_rows_matching`,
|
||||
`do_read_relationships`, `do_list_names_for`) and the thin `*_request`
|
||||
wrappers are not *individually* instrumented — the wrappers delegate
|
||||
to logged executors (skipped to avoid double-logging), the helpers are
|
||||
low-value. Effective coverage is complete via logged entry points; it
|
||||
is not literally 44/44.
|
||||
|
||||
## §3. T3 residuals — both closed (ADR-0043)
|
||||
|
||||
Two messaging-only items carried since handoff-59 §4; FK
|
||||
correctness/enforcement was never affected.
|
||||
|
||||
**#1 — inline-FK arity wording (`6985a43`).** `col REFERENCES P(a,b)`
|
||||
referencing a compound PK gave the generic arity error. An inline
|
||||
column-level FK is single-column by construction, so it now points at
|
||||
the table-level form: *"an inline column reference can only name one
|
||||
column … Use the table-level form instead: `FOREIGN KEY (<columns>)
|
||||
REFERENCES P (a, b)`."* Mechanism: new **`inline: bool` on
|
||||
`SqlForeignKey`**, set by the single shared grammar builder
|
||||
`consume_fk_reference` (true for the inline path at `ddl.rs:1560`,
|
||||
false for table-level `1590` and `build_alter_fk`); threaded into
|
||||
`resolve_fk_parent_columns`, which tailors the arity-mismatch message
|
||||
when `inline && parent_key.len() > 1`. 6 construction sites total (2
|
||||
grammar + 1 ALTER delegate + 3 test literals) — hand-edited, **not**
|
||||
the scripted sweep handoff-59 §4 warned about. The bare inline form
|
||||
(`col REFERENCES P`, no parens) hits the same arity branch, so it is
|
||||
covered by the same code (tested via the explicit-parens form).
|
||||
|
||||
**#2 — compound-FK violation names every pair (`5a33f2a`).**
|
||||
`enrich_fk_violation` (`runtime.rs`) picked only `local_columns
|
||||
.first()` / `other_columns.next()`. It now gathers all pairs of the
|
||||
matched relationship and carries them **comma-joined in the existing
|
||||
single-column facts slots** (`column`, `parent_column`, `value`), so
|
||||
the headline reads *"no parent row in `Region` has `country, code` =
|
||||
`7, 8`."* No facts-model or catalog change — joined strings flow
|
||||
through the existing `{parent_column}`/`{value}` placeholders.
|
||||
Single-column behaviour is byte-identical (a one-element join is the
|
||||
element). **Known minor awkwardness:** the *verbose hint* interpolates
|
||||
`{parent_table}.{parent_column}` → `Region.country, code`, which reads
|
||||
a touch oddly; the headline is clean. A perfectly-formatted compound
|
||||
hint would need catalog work, out of scope for a messaging-polish
|
||||
residual — flagged, not fixed.
|
||||
|
||||
## §4. Remaining open landscape (unchanged except X1)
|
||||
|
||||
**Closed this session:** X1 → `[x]`; both T3 residuals (ADR-0043 fully
|
||||
wrapped — no residuals left).
|
||||
|
||||
**Still `[/]` / `[~]` / larger (design-first, own ADR):**
|
||||
- **V2 / S3** multi-result tabs — output-model redesign.
|
||||
- **V3** whole-DB ER export; **V4** scrollable journal + Markdown
|
||||
(also the home for diagram live-reflow, ADR-0044 OOS-1).
|
||||
- **A1** app-commands — blocked on `seed` (SD1) + `hint` (H2).
|
||||
- **H1a** parse-error syntax help (partial; ADR-0021).
|
||||
- **DOC1** reference docs.
|
||||
|
||||
**`[ ]` not started:** H2 `hint`, SD1 `seed`, C4 m:n convenience, B3
|
||||
query-timeout, I1 multi-line input, I1b readline shortcuts, I5
|
||||
cancellation, **TT5 CI** (now Gitea Actions / Woodpecker — a fresh
|
||||
decision tied to the migration + ADR-0001's reopened distribution
|
||||
question), TT4 PTY (spec-only), D1–D3 distribution, NFR-1…7.
|
||||
|
||||
**ADR-0044 OOS for later:** OOS-7 user-configurable relationship-
|
||||
display setting (always-prose / always-diagram / auto-by-width).
|
||||
|
||||
## §5. Next job — candidates (by readiness)
|
||||
|
||||
No forced next step. Recommended order:
|
||||
1. **TT5 CI** — test infra is solid (2211 green) and now there is real
|
||||
logging to surface failures; no pipeline yet. A fresh **Gitea
|
||||
Actions / Woodpecker** decision (earns a short ADR; ties into
|
||||
ADR-0001's reopened distribution question). Highest leverage:
|
||||
protects everything else.
|
||||
2. **SD1 `seed`** then **H2 `hint`** — the two unblockers for **A1**
|
||||
app-commands; both are net-new, self-contained features (each its
|
||||
own ADR).
|
||||
3. **C4 m:n convenience** — auto-generate a junction table; depends on
|
||||
relationships, which are now solid (ADR-0043/0044 done).
|
||||
4. **V2/S3 tabs** or **V4 journal** — larger output-model redesign;
|
||||
design-first, own ADR. V4 also unlocks diagram live-reflow.
|
||||
|
||||
## §6. How to take over
|
||||
|
||||
1. Read handoffs 59 → 60 → 61, then `CLAUDE.md` (Gitea/`tea` section),
|
||||
`docs/requirements.md` (X1 now `[x]`), `docs/adr/README.md`.
|
||||
2. **Before adding any logging:** read the level-discipline block in
|
||||
the `src/logging.rs` module doc (the X1 convention).
|
||||
3. **For FK/relationship work:** ADR-0043 (compound FKs) + ADR-0044
|
||||
(visualization) are both fully landed; `SqlForeignKey` now carries
|
||||
`inline`.
|
||||
4. Codebase on `main` at `5a33f2a`, clean, 9 commits unpushed (5 from
|
||||
session 60 + 4 this session).
|
||||
5. Process pins that paid off: **verify log emission end-to-end, not
|
||||
just that it compiles** (throwaway smoke tests through the real
|
||||
worker thread caught nothing broken but proved the stack);
|
||||
**hot-path logging belongs at `trace`, not `debug`** (the parser);
|
||||
**test-first on both residuals** (red → green at every layer);
|
||||
**hand-edit struct-field ripples, never script them** (handoff-59
|
||||
§4's scare avoided). Commits user-confirmed, append-only, no AI
|
||||
attribution.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Session handoff — 2026-06-10 (62)
|
||||
|
||||
Sixty-second handover. Continues from handoff-61 (X1 logging full sweep
|
||||
+ T3 residuals). This session was a **list-trimming + one-feature run**:
|
||||
it closed **C4** (the `create m:n relationship` convenience command,
|
||||
**ADR-0045**) and, in passing, resolved **Gitea issue #19** (drop-PK
|
||||
guard). Handoff-61 itself was written mid-session, so the X1 / T3 work
|
||||
it describes is also part of this session's commit range.
|
||||
|
||||
## §1. State at handoff
|
||||
|
||||
**Branch:** `main`. **HEAD `8bd43cc`.** Push is the user's step.
|
||||
|
||||
**Tests: 2237 passing / 0 failing / 1 ignored** (the 1 ignored is the
|
||||
long-standing doc-test). **Clippy clean** (nursery, all targets). +30
|
||||
over the handoff-60 baseline of 2207.
|
||||
|
||||
**This session's commits** (8, on top of session-60's 5):
|
||||
```
|
||||
8bd43cc feat: create m:n relationship convenience command (C4, ADR-0045)
|
||||
e598008 docs: ADR-0045 m:n convenience command (C4); accepted
|
||||
e44d298 test+docs: lock drop-PK-refused on advanced surface; document no-PK advanced mode (#19)
|
||||
b803468 docs: session handoff 61 — X1 logging full sweep + T3 residuals closed
|
||||
5a33f2a fix(fk): compound-FK violation message names every column pair
|
||||
6985a43 fix(fk): inline FK referencing a compound PK points at the table-level form
|
||||
0a7612e feat: comprehensive logging across parser, app, persistence, runtime (X1)
|
||||
a8ad0c6 feat(db): comprehensive logging across worker + executors (X1)
|
||||
```
|
||||
|
||||
**Requirements closed this session:** **X1** `[x]` (logging), **T3**
|
||||
residuals (both ADR-0043 messaging items), **C4** `[x]` (m:n). Gitea
|
||||
**#19 closed**.
|
||||
|
||||
## §2. X1 — comprehensive logging (closed) — see handoff-61 §2
|
||||
|
||||
Full detail in handoff-61. In brief: ~75 → **137** `tracing` sites under
|
||||
a documented level discipline (read the **`src/logging.rs` module doc**
|
||||
before adding logs). Logs go to a **file** (`--log-file` >
|
||||
`RDBMS_PLAYGROUND_LOG_FILE` > `~/.rdbms-playground/playground.log`);
|
||||
level via the separate `RDBMS_PLAYGROUND_LOG` env (default `info`).
|
||||
`debug` = per-command detail (off by default), `trace` = hot paths
|
||||
(per-keystroke parse).
|
||||
|
||||
## §3. T3 residuals (both closed) — see handoff-61 §3
|
||||
|
||||
`6985a43` inline-FK arity wording (points at the table-level form;
|
||||
added `inline: bool` to `SqlForeignKey`). `5a33f2a` compound-FK
|
||||
violation names every column pair (comma-joined in the single-column
|
||||
facts slots; `enrich_fk_violation`). ADR-0043 now has no residuals.
|
||||
|
||||
## §4. Issue #19 — drop-PK guard (closed, `e44d298`)
|
||||
|
||||
A parallel check the user requested. **Finding: dropping a PK column is
|
||||
already refused in both modes** via the shared `do_drop_column` guard
|
||||
(*"cannot drop primary-key column …"*) — simple `drop column` and
|
||||
advanced `ALTER … DROP COLUMN` both route through it. Added end-to-end
|
||||
coverage (`tests/it/sql_alter_table.rs`: single + compound PK, refusal
|
||||
for the right reason). **Corrected a long-standing misconception:** the
|
||||
issue's premise ("we don't support creating a table with no PK") is true
|
||||
only in **simple** mode — advanced SQL `create table t (a int)` makes a
|
||||
real **PK-less** table (SQLite's implicit `rowid` keys it; only
|
||||
`WITHOUT ROWID` lacks one, which this app never creates). The simple-mode
|
||||
`with pk` requirement is **pedagogical** (ADR-0029), not an engine
|
||||
constraint. Documented in `docs/simple-mode-limitations.md`.
|
||||
|
||||
## §5. C4 — `create m:n relationship` (the feature, ADR-0045)
|
||||
|
||||
`create m:n relationship from <T1> to <T2> [as <name>]` generates a
|
||||
**junction table**: one FK column per parent PK column
|
||||
(`{table}_{pkcol}`, typed via `fk_target_type` — ADR-0011), a **compound
|
||||
PK** over all of them, and **two `CASCADE` 1:n relationships** — all in
|
||||
**one `do_create_table` call = one undo step** (no batch needed;
|
||||
`do_create_table` already takes `foreign_keys` + writes per-FK
|
||||
relationship metadata). Auto-named `{T1}_{T2}` (optional `as`), available
|
||||
in **both modes**, compound-parent PKs supported (ADR-0043).
|
||||
|
||||
**Forks (all user-confirmed):** compound-over-FKs PK (vs surrogate /
|
||||
none); `CASCADE` actions; auto-name + optional `as`; both modes; FK
|
||||
columns `{table}_{pkcol}`. **Refused:** self-referential m:n (`from T to
|
||||
T` — full stop, OOS); PK-less parent; internal `__rdbms_*` junction
|
||||
name; name collision.
|
||||
|
||||
**Where the code lives:**
|
||||
- Grammar: a **separate `CREATE_M2N` `CommandNode`** in
|
||||
`dsl/grammar/ddl.rs` (entry `create`, opener `Node::Literal("m")` —
|
||||
not a keyword, so it never shadows an identifier), registered Simple
|
||||
in `grammar/mod.rs` `REGISTRY`. `build_create_m2n` →
|
||||
`Command::CreateM2nRelationship { t1, t2, name }`.
|
||||
- Worker: `Request::CreateM2nRelationship`,
|
||||
`Database::create_m2n_relationship`, executor
|
||||
`do_create_m2n_relationship` (reads each PK, guards self-ref /
|
||||
PK-less, builds columns + compound PK + 2 `SqlForeignKey`s, calls
|
||||
`do_create_table`).
|
||||
- Runtime: `execute_command_typed` arm. Echo:
|
||||
`echo::render_create_m2n` (advanced-mode DSL→SQL teaching echo, ADR-
|
||||
0038 — the generated `CREATE TABLE … FOREIGN KEY …`, round-trips as
|
||||
valid SQL), wired in `build_schema_echo`.
|
||||
- Surfaces: completion `("m","m:n")` composite; `help.ddl.create_m2n` +
|
||||
`parse.usage.create_m2n` catalog (+ `keys.rs` declarations);
|
||||
highlighting is grammar-driven (automatic).
|
||||
|
||||
**Tests:** 14 integration (`tests/it/m2n.rs`), 7 typing-surface matrix
|
||||
(`tests/typing_surface/create_m2n.rs` — completion/hint/highlight/parse),
|
||||
plus echo / highlight / usage-disambiguator / internal-name units.
|
||||
|
||||
## §6. Framework fixes the C4 build + two `/runda` passes surfaced
|
||||
|
||||
C4's "separate node" design rested on an ADR premise that proved **only
|
||||
half true**: *"the walker already dispatches multiple nodes per entry
|
||||
word"* held in **advanced** mode but not **simple**. Three latent
|
||||
simple-mode assumptions ("≤1 DSL form per entry word") were generalized,
|
||||
**all behaviour-preserving for existing single-form commands**:
|
||||
|
||||
1. **Dispatch** (`walker/mod.rs` `decide`) committed `simple.first()`
|
||||
unconditionally → now tries simple candidates (so `create table` no
|
||||
longer shadows `create m:n`). Reduces to the old single-candidate
|
||||
commit when there is one.
|
||||
2. **Completion continuation-merge** (`walker/mod.rs`) was gated
|
||||
`if mode == Advanced` → now runs in simple mode too, **gated on
|
||||
`simple_count > 1`** so single-form entry words are untouched.
|
||||
3. **Usage disambiguator** (`grammar/mod.rs` `usage_key_for_input`)
|
||||
knew the `1:n` opener but not `m:n` → added an explicit branch.
|
||||
|
||||
Plus a **root-cause bug fix** (user-chosen scope): `do_create_table`
|
||||
now rejects internal `__rdbms_*` names. This closed both the C4 `as
|
||||
__rdbms_*` hole **and a pre-existing hole** — simple-mode DSL `create
|
||||
table __rdbms_*` was accepted at parse (the `TABLE_NAME_NEW` slot had no
|
||||
guard; only the advanced-SQL path rejected internal names). The shared
|
||||
executor is the single choke point; the SQL path still rejects earlier
|
||||
at parse.
|
||||
|
||||
**Process note:** the two `/runda` passes were worth it. The first
|
||||
(pre-build) corrected the inverted "no PK-less tables" assumption and
|
||||
confirmed the `do_create_table` reuse against code. The second
|
||||
(pre-commit) closed **five** test-coverage gaps — two of which
|
||||
(highlighting, persistence round-trip) had been **wrongly claimed
|
||||
verified** (the typing-surface `Assessment` has no highlight field;
|
||||
"transitively covered" was a hand-wave) — and found the two bugs above.
|
||||
Lesson re-confirmed: verify a claimed-tested surface actually has an
|
||||
assertion; "transitively covered" is a DA red flag.
|
||||
|
||||
## §7. Remaining open landscape
|
||||
|
||||
**Closed since handoff-60:** X1, both T3 residuals, C4, #19. ADR-0043 and
|
||||
ADR-0045 fully landed.
|
||||
|
||||
**Still open (by readiness, unchanged otherwise):**
|
||||
1. **TT5 CI** — test infra solid (2237 green); no pipeline. **Gitea
|
||||
Actions / Woodpecker** (a fresh decision tied to the migration +
|
||||
ADR-0001's reopened distribution question). **Friction:** the
|
||||
requirement is Linux/macOS/Windows on stable — self-hosted Gitea can
|
||||
do Linux easily, but mac/Windows runners need machines that may not
|
||||
exist; likely needs a Linux-first scope decision.
|
||||
2. **SD1 `seed`** then **H2 `hint`** — the two unblockers for **A1**
|
||||
app-commands; both net-new, own ADR (SD2 is the seed-generator design
|
||||
ADR). SD1 should now seed **m:n junctions** too (valid FK refs from
|
||||
parent rows) — C4 makes that concrete.
|
||||
3. **V2/S3 multi-result tabs** or **V4 journal** — larger output-model
|
||||
redesign, design-first, own ADR. V4 also unlocks diagram live-reflow.
|
||||
4. **C3a modify relationship** — small follow-up (drop+add covers it
|
||||
today; ADR pending).
|
||||
|
||||
**ADR-0045 OOS for later:** self-referential m:n (deliberate non-goal);
|
||||
per-relationship action overrides; extra junction payload columns;
|
||||
m:n-as-diagram echo. **Pre-existing, now-fixed:** the internal-name hole
|
||||
(§6) — no separate issue needed, it's closed.
|
||||
|
||||
## §8. How to take over
|
||||
|
||||
1. Read handoffs 60 → 61 → 62, then `CLAUDE.md`, `docs/requirements.md`
|
||||
(X1/C4 now `[x]`), `docs/adr/README.md`.
|
||||
2. **Before adding logging:** the level discipline in the
|
||||
`src/logging.rs` module doc.
|
||||
3. **For grammar/command work:** an entry word can now carry **multiple
|
||||
DSL forms** in simple mode (C4 generalized the dispatch + completion +
|
||||
usage paths). `create` is the first such entry word (table + m:n).
|
||||
4. **For relationship/FK work:** ADR-0013/0043/0044/0045 are all landed;
|
||||
`SqlForeignKey` carries `inline`; `do_create_table` now guards
|
||||
internal names.
|
||||
5. Codebase on `main` at `8bd43cc`, clean. Commits user-confirmed,
|
||||
append-only, no AI attribution. Process pins that paid off: **two
|
||||
`/runda` passes per feature** (design + pre-commit) — both found real
|
||||
bugs and gaps every time; **verify a claimed-tested surface has an
|
||||
actual assertion**; **escalate genuine forks** (every C4 design choice
|
||||
+ the internal-name fix scope was the user's).
|
||||
@@ -0,0 +1,159 @@
|
||||
# Session handoff — 2026-06-10 (63)
|
||||
|
||||
Sixty-third handover. Continues from handoff-62 (C4 m:n + #19). This
|
||||
was a **single-ADR, full-build session**: it designed and implemented
|
||||
**ADR-0046** end to end — the UI work for the three sidebar/input
|
||||
issues **#20 / #21 / #23**, all now **closed** on Gitea.
|
||||
|
||||
## §1. State at handoff
|
||||
|
||||
**Branch:** `main`. **HEAD `22bec61`** (plus an uncommitted docs
|
||||
finalization — ADR status flip + this handoff — see §7). Push is the
|
||||
user's step.
|
||||
|
||||
**Tests: 2263 passing / 0 failing / 1 ignored** (the 1 ignored is the
|
||||
long-standing doc-test). **Clippy clean** (nursery, all targets). +26
|
||||
over the handoff-62 baseline of 2237.
|
||||
|
||||
**This session's commits** (8 + the docs finalization):
|
||||
```
|
||||
22bec61 feat(ui): scroll the focused sidebar panel + refine the nav overlay (DC3 + DC2)
|
||||
c9da6ff feat(ui): Ctrl-O navigation mode — peek + expand the schema sidebar (DC1/DC2/DC4)
|
||||
94825d0 feat(ui): relationships sidebar panel + schema data (DB2/DB4)
|
||||
386627a feat(ui): width-derived sidebar visibility — hide at <=90 cols (DB1)
|
||||
41bae99 feat(ui): two-row input display on tall terminals (DA4)
|
||||
e0b9470 feat(ui): horizontal-scroll long input so the cursor stays visible (DA3)
|
||||
9f5f76b fix(ui): geometry-fixed hint-panel height kills the typing jump (DA1/DA2)
|
||||
93266b9 docs: ADR-0046 UI sidebar nav-mode + responsive input/hint
|
||||
```
|
||||
|
||||
**Issues closed:** **#20**, **#21**, **#23** (all via ADR-0046).
|
||||
**#22** (in-app overlay/keystroke-annotation layer for casts/lessons)
|
||||
remains **open** — its own future ADR; adjacent but out of scope here.
|
||||
|
||||
## §2. What shipped — ADR-0046 (read it; it's the source of truth)
|
||||
|
||||
Three coupled UI issues, treated as one decision because they share the
|
||||
terminal width/height budget. Phased A → B → C.
|
||||
|
||||
**Phase A — input & hint (#20, #23).**
|
||||
- **DA1/DA2 (#20):** the Hint panel height is now a pure function of
|
||||
terminal geometry (`hint_rows` → later `panel_heights`), **fixed
|
||||
between resizes** — it no longer resizes as you type, killing the
|
||||
jump. Compact (`<40` rows) = hint 2; comfortable = hint 2, or 3 only
|
||||
when the column is narrow (`inner < 54`). This **reverses issue #12's**
|
||||
shrink-to-content sizing (its two tests were replaced by an anti-jump
|
||||
invariant). Long hints ellipsize at the fixed budget.
|
||||
- **DA3 (#23):** long input **horizontally scrolls** to keep the cursor
|
||||
visible (`input_scroll_offset`, pure `input_scroll_offset()` helper),
|
||||
with muted `<` / `>` edge markers; resets on submit / history.
|
||||
Preserves ADR-0027's 6-col indicator reserve.
|
||||
- **DA4 (#23):** on a tall terminal (`>=40` rows) the input renders
|
||||
across **two visual rows** (soft-wrap of the single logical line;
|
||||
indicator stays on row 1). Distinct from deferred multi-line **I1**;
|
||||
`expand_runs_to_cells` is the substrate I1 should reuse.
|
||||
|
||||
**Phase B — the sidebar (#21).**
|
||||
- **DB1:** the left column is **width-optional** — `sidebar_visible() =
|
||||
width > 90`, so it's hidden at <=90 (the 90-col screencasts) and the
|
||||
right column takes the full width. (Resize a terminal below ~90 to see
|
||||
it; in a normal wide terminal it shows, by design.)
|
||||
- **DB2/DB4:** a **Relationships panel** stacks below Tables — each
|
||||
relationship is name + endpoints broken at the arrow
|
||||
(`Customers.id ->` / indented `Orders.customer_id`), ellipsized. The
|
||||
panel floors at 5 rows ("(none)") and grows to a 50%-of-column cap
|
||||
(`relationships_panel_height`). **Overrides S2** (relations were to be
|
||||
*nested* in the tables list; a sibling panel is the honest shape).
|
||||
|
||||
**Phase C — navigation mode (#21).**
|
||||
- **DC1/DC4:** **`Ctrl-O`** enters a navigation mode orthogonal to the
|
||||
input mode, cycling focus **Input → Tables → Relationships → Input**
|
||||
(`Esc` exits). It's routed in the main key handler *after* the modal
|
||||
gate, so it's inert behind a modal; in nav mode every non-nav key is
|
||||
inert (the input is occluded). `NavFocus` enum on `App`.
|
||||
- **DC2:** the focused panel is revealed (peek, even when width-hidden)
|
||||
and drawn as a **45-col expanded overlay**, clearing the sidebar strip
|
||||
**+ a one-column gutter** and leaving the base output/input/hint
|
||||
visible (unchanged) to the right. *(Two variants were eyeballed; this
|
||||
partial-clear-with-gutter was chosen over a full-area clear.)*
|
||||
- **DC3:** the focused panel **scrolls** — Up/Down by a line,
|
||||
PageUp/PageDown by its visible rows; per-panel offsets clamped to
|
||||
content at render time, mirroring the output-panel scroll.
|
||||
|
||||
**`Ctrl-B` was rejected** for nav mode (it's the tmux prefix →
|
||||
unreachable inside tmux); `Ctrl-O` is multiplexer-safe.
|
||||
|
||||
## §3. Two decisions that landed differently from the draft
|
||||
|
||||
Both recorded inline in the ADR (and called out in its Status):
|
||||
1. **Relationship data on `App`, not `SchemaCache`** (DB2). `SchemaCache`
|
||||
is walker/completion-facing and needs only relationship *names*
|
||||
(untouched); the full records are UI-only, so `App.relationships`
|
||||
mirrors `app.tables`, and it avoided editing ~23 `SchemaCache`
|
||||
literals. Delivered via `Database::read_all_relationships` (new worker
|
||||
request) + `AppEvent::RelationshipsRefreshed` from the runtime's
|
||||
schema refresh.
|
||||
2. **Nav overlay = partial clear + 1-col gutter** (DC2), not a full-area
|
||||
clear — truer to "underneath keeps its layout."
|
||||
|
||||
## §4. Process notes
|
||||
|
||||
- **The pre-build `/runda` pass earned its keep again.** It caught the
|
||||
`Ctrl-B`/tmux collision, a `SchemaCache` retype that would have broken
|
||||
completion, the 2-row-input/indicator placement, the missing nav-mode
|
||||
key disposition + modal gate, and **three unreferenced requirements**
|
||||
(S1 evolved, S2 overridden, S4 corrected — `requirements.md` updated).
|
||||
- **Snapshot discipline:** DB1's 90-col threshold collided with the
|
||||
test-suite's 80-col convention — many snapshots/tests were retuned
|
||||
(sidebar-dependent ones now render at 110; input tests at narrower
|
||||
widths so the now-wider input still overflows). One masked-intent
|
||||
integration check (matched "Customers" in output, not the panel) was
|
||||
corrected.
|
||||
- Each phase was committed green + clippy-clean, user-confirmed message,
|
||||
no AI attribution, append-only.
|
||||
|
||||
## §5. Requirements / S-items touched
|
||||
|
||||
`requirements.md` annotated: **S1** (three-region layout → left region
|
||||
width-optional), **S2** (*overridden* — relationships get a sibling
|
||||
panel, not nested), **S4** (*corrected* — the "keyboard-toggleable" hint
|
||||
claim was never implemented and is struck; the panel is always-on).
|
||||
|
||||
## §6. Remaining open landscape (unchanged from handoff-62, minus the closed items)
|
||||
|
||||
1. **TT5 CI** — test infra solid (2263 green); no pipeline. Gitea Actions
|
||||
/ Woodpecker decision + likely a Linux-first scope call.
|
||||
2. **SD1 `seed`** then **H2 `hint`** — the two unblockers for **A1**
|
||||
app-commands; both net-new, own ADR. SD1 should seed m:n junctions.
|
||||
3. **V2/S3 multi-result tabs** or **V4 journal** — larger output-model
|
||||
redesign, design-first, own ADR.
|
||||
4. **C3a modify relationship** — small follow-up (drop+add covers it).
|
||||
5. **#22 overlay/annotation layer** — own ADR; shares the cast + overlay
|
||||
space with DC2 (designed to coexist).
|
||||
6. **Tutorial/lesson system** — acknowledged in scope; needs its own ADR.
|
||||
|
||||
**ADR-0046 OOS (deferred):** true multi-line input (I1); readline
|
||||
shortcuts (I1b); cross-session sidebar persistence; a persistent
|
||||
show/hide toggle (Ctrl-O peek covers it); output as a third nav focus;
|
||||
relationship search/edit from the panel; a hint-area toggle.
|
||||
|
||||
## §7. How to take over
|
||||
|
||||
1. Read handoffs 61 → 62 → 63, then `CLAUDE.md`, `docs/requirements.md`,
|
||||
`docs/adr/README.md`, and **ADR-0046** (fully landed).
|
||||
2. **Pending:** an uncommitted docs finalization (ADR-0046 status →
|
||||
*implemented*; README index status; this handoff). Commit it as
|
||||
`docs: session handoff 63` (the user confirms commit messages).
|
||||
3. **For UI/layout work:** `src/ui.rs` now has `panel_heights`,
|
||||
`sidebar_visible`, `relationships_panel_height`, the nav overlay, and
|
||||
`&mut App` sidebar panels (they report scroll viewports). `App` gained
|
||||
`input_scroll_offset`, `nav_focus`, `relationships`, and the
|
||||
`tables_scroll` / `relationships_scroll` (+ `last_*_visible`) fields.
|
||||
4. **For relationship/schema-cache work:** relationship *names* are in
|
||||
`SchemaCache.relationships` (completion); full records are on
|
||||
`App.relationships` via `Database::read_all_relationships` +
|
||||
`RelationshipsRefreshed`.
|
||||
5. **Eyeball reminder honoured:** the user reviewed the nav overlay
|
||||
appearance and chose the partial-clear + 1-col-gutter variant.
|
||||
6. Run a `cargo sweep` at some point — `target/` has grown across this
|
||||
build-heavy session.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Session handoff — 2026-06-11 (64)
|
||||
|
||||
Sixty-fourth handover. Continues from handoff-63 (ADR-0046 sidebar/nav).
|
||||
This session closed **two unrelated, website-screencast-enabling Gitea
|
||||
issues**: **#24** (vi-style load-picker navigation) and **#22**
|
||||
(in-app demonstration overlay layer — its own **ADR-0047**, built end
|
||||
to end across three phases + a restyle).
|
||||
|
||||
## §1. State at handoff
|
||||
|
||||
**Branch:** `main`. **HEAD `2d0f4b2`** plus an **uncommitted docs
|
||||
finalization** (ADR-0047 status → implemented, README index, this
|
||||
handoff — see §6). Push is the user's step.
|
||||
|
||||
**Tests: 2290 passing / 0 failing / 0 skipped / 1 ignored** (the 1
|
||||
ignored is the long-standing `friendly` doctest). **Clippy clean**
|
||||
(nursery, all targets). +27 over the handoff-63 baseline of 2263.
|
||||
|
||||
**This session's commits:**
|
||||
```
|
||||
2d0f4b2 feat(ui): flat filled rectangles for demo overlays (#22, ADR-0047 D4)
|
||||
241f60c feat(ui): demo-mode step-caption stealth buffer (#22, ADR-0047 D3/D4)
|
||||
2584e76 feat(ui): demo-mode keystroke badges (#22, ADR-0047 D2/D4/D5)
|
||||
f879d54 feat(cli): --demo demonstration mode flag + app plumbing (#22, ADR-0047 D1)
|
||||
e9eb1b1 docs: ADR-0047 — demonstration overlay layer for casts/teaching (#22)
|
||||
638b4c9 feat(app): vi-style j/k/g/G navigation in the load picker (#24)
|
||||
```
|
||||
|
||||
**Issues closed:** **#24** (vi nav) and **#22** (demo overlays) — close
|
||||
#22 once the docs finalization commit lands.
|
||||
|
||||
## §2. #24 — vi-style load-picker navigation (commit `638b4c9`)
|
||||
|
||||
Purely additive to the ADR-0015 load picker (`handle_load_picker_key`,
|
||||
`LoadPickerSubMode::List`): **`j`/`k`** mirror Down/Up (bounds-
|
||||
respecting, no wrap), **`g`/`G`** jump to first/last. Existing keys
|
||||
(`↑↓`/`Enter`/`Esc`/`b`) unchanged; the footer hint is **left as-is** at
|
||||
the user's request (the new keys are not advertised). No ADR (additive).
|
||||
Motivation: `autocast` (the website cast driver) can only send typeable
|
||||
characters — not arrow keys — so the projects demo needs `j`/`k` to
|
||||
drive the picker. Tests: `load_picker_jk_navigates_like_arrows`,
|
||||
`load_picker_g_jumps_to_first_and_last` (test-first).
|
||||
|
||||
## §3. #22 — ADR-0047 demonstration overlay layer (read the ADR)
|
||||
|
||||
An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO`
|
||||
env, **off by default, zero footprint when off**) that renders two
|
||||
transient overlays so `autocast` screencasts — and live teaching, and a
|
||||
future guided-lesson system — can show otherwise-invisible interactions.
|
||||
|
||||
- **Phase A (`f879d54`):** `--demo` + env → `App.demo_mode`, threaded
|
||||
through `run_loop` like `--no-undo`. `--help` line mentions **only the
|
||||
visible badges**; the `Ctrl+]` caption trigger is kept low-profile
|
||||
(user decision, D6).
|
||||
- **Phase B (`2584e76`):** **automatic keystroke badges**
|
||||
(`[TAB]`/`[ENTER]`/`[UP]`/…) over a fixed set of glyph-less keys —
|
||||
pure `demo_badge_label(&KeyEvent)`, set in `App::update` **before the
|
||||
modal gate** (so they fire over the load picker), expired by a **~1.5 s
|
||||
runtime timer**. The timer extends the event loop's time-boxed-`recv`
|
||||
via a new pure `nearest_deadline` helper; the rewrite tracks `Instant`
|
||||
deadlines and was **verified not to regress the ADR-0027 indicator
|
||||
debounce**. New `App.last_output_area: Rect` (set in
|
||||
`render_output_panel`) anchors the overlays.
|
||||
- **Phase C (`241f60c`):** the **stealth `Ctrl+]` caption buffer** —
|
||||
`Ctrl+]` (byte `0x1D` → `Char('5')+CONTROL`, verified vs crossterm
|
||||
0.29) toggles an invisible buffer; typed chars accumulate without
|
||||
touching input/output, `Backspace` edits, other keys inert, a second
|
||||
`Ctrl+]` commits (empty commit dismisses). In pure-sync `App::update`,
|
||||
intercepted **before the modal gate**; an ordinary keystroke clears a
|
||||
visible caption.
|
||||
- **Restyle (`2d0f4b2`):** the overlays render as **flat filled yellow
|
||||
rectangles** (no border glyphs, one-cell text margin) — user decision,
|
||||
deliberately unlike the bordered panels so they pop. Shared
|
||||
`fill_overlay_rect` (borderless `Block` fill + inset `Paragraph`).
|
||||
|
||||
**Placement:** both float at the output panel's inner bottom-right,
|
||||
drawn **last over modals**, badge **stacked directly above** the caption
|
||||
when both show; caption **wraps to ≤ 3 lines** then ellipsises; clamp/
|
||||
skip guard for tiny terminals.
|
||||
|
||||
**Process:** ADR-first (user chose), pre-build `/runda` (10 findings,
|
||||
folded in) + whole-implementation `/runda` (**PASS, no blockers**). Every
|
||||
fork user-confirmed via mockups/questions, incl. the two post-draft
|
||||
follow-ups: `Ctrl+]` trigger (over `Ctrl+!`, which `autocast` cannot
|
||||
send — not a single ASCII byte) and wrap-to-3-line captions.
|
||||
|
||||
## §4. Two things to know about the implementation
|
||||
|
||||
1. **Ownership split (intentional, mirrors `input`/`input_indicator`):**
|
||||
`demo_caption`/`demo_caption_capturing`/`demo_caption_buffer` are
|
||||
driven by `App::update` (input); `demo_badge` is **set** by
|
||||
`App::update` but its expiry is **timed by the runtime**
|
||||
(`demo_badge_seq` bumps so a repeated key restarts the timer).
|
||||
2. **`Ctrl-C` is inert while capturing** — by spec ("every other key is
|
||||
inert"); exit capture with `Ctrl+]`. User-acknowledged; flagged in
|
||||
the ADR. The only behaviour worth a second look if it ever annoys.
|
||||
|
||||
## §5. Honest coverage note
|
||||
|
||||
Everything *testable* is tested (label fn, full caption FSM incl.
|
||||
over-modal + demo-off, `nearest_deadline`, all rendering, CLI parse/env).
|
||||
The **only** untested wiring is inside `run_loop` (the badge-timer
|
||||
arm/clear and `app.demo_mode = demo_mode`) — `run_loop` is not
|
||||
unit-testable (terminal + DB + channels), exactly the posture the
|
||||
existing `IndicatorDebounce` already takes. A future Tier-4 PTY harness
|
||||
(ADR-0008 TT4, still unwired) would close it.
|
||||
|
||||
## §6. How to take over
|
||||
|
||||
1. Read handoffs 62 → 63 → 64, `CLAUDE.md`, `docs/requirements.md`,
|
||||
`docs/adr/README.md`, and **ADR-0047** (fully landed).
|
||||
2. **Pending:** the docs finalization commit (ADR-0047 status →
|
||||
implemented; README index; this handoff). Commit as
|
||||
`docs: session handoff 64 + ADR-0047 implemented (#22/#24)` (the user
|
||||
confirms commit messages). Then close **#22** on Gitea.
|
||||
3. **For demo-overlay work:** `App` has `demo_mode`, `demo_badge`,
|
||||
`demo_badge_seq`, `demo_caption`, `demo_caption_capturing`,
|
||||
`demo_caption_buffer`, `last_output_area`. Rendering:
|
||||
`render_demo_overlays` / `render_badge_box` / `render_caption_box` /
|
||||
`fill_overlay_rect` in `ui.rs`; colours `DEMO_OVERLAY_FG/BG` in
|
||||
`theme.rs`; key handling `handle_demo_caption_key` + the top-of-
|
||||
`handle_key` gate; timer in `runtime.rs` (`nearest_deadline`,
|
||||
`DEMO_BADGE_TTL`).
|
||||
|
||||
## §7. Remaining open landscape (from handoff-63, minus the closed items)
|
||||
|
||||
1. **Wire the overlays into the website casts** — `casts.mjs` on the
|
||||
`website` branch can now emit `^]`/text/`^]` for captions and rely on
|
||||
automatic badges. Website-branch follow-up (OOS for #22's app scope).
|
||||
2. **TT5 CI** — 2290 green, no pipeline yet.
|
||||
3. **SD1 `seed`** then **H2 `hint`** — the unblockers for **A1**
|
||||
app-commands; own ADRs.
|
||||
4. **V2/S3 multi-result tabs** / **V4 journal** — larger output-model
|
||||
redesign, own ADR.
|
||||
5. **C3a modify relationship** — small (drop+add covers it).
|
||||
6. **Tutorial/lesson system** — acknowledged in scope; needs its own
|
||||
ADR; ADR-0047's overlay primitive is what it will reuse.
|
||||
|
||||
Run a `cargo sweep` at some point — `target/` grew across this
|
||||
build-heavy session.
|
||||
+53
-15
@@ -73,24 +73,38 @@ since ADR-0027.)
|
||||
panel (right), input field (bottom).
|
||||
*(Verified 2026-06-07: `ui.rs:26-58` lays out a horizontal
|
||||
split — items panel left, right column subdivided into output
|
||||
panel / input field / hint panel; rendered every frame.)*
|
||||
panel / input field / hint panel; rendered every frame.
|
||||
**ADR-0046 evolves this:** the left items region becomes
|
||||
width-optional — hidden by default at ≤ 90 columns, peek-revealed
|
||||
via `Ctrl-O` navigation mode — so the three-region layout is the
|
||||
wide-terminal default, not an invariant.)*
|
||||
- [x] **S2** Items list shows tables and per-table indexes;
|
||||
designed to extend to additional element kinds (relations,
|
||||
views, etc.) without restructuring.
|
||||
*(ADR-0025: the items panel renders a nested list — each
|
||||
table with its index names indented beneath it. The nested
|
||||
model is the extension point for future element kinds.)*
|
||||
model is the extension point for future element kinds.
|
||||
**ADR-0046 overrides the nesting approach for relationships:**
|
||||
because relationships are cross-table rather than per-table, they
|
||||
get their own sibling panel stacked below the tables list, not
|
||||
nested items within it — user-confirmed 2026-06-10.)*
|
||||
- [/] **S3** Output panel renders a visualization of the
|
||||
currently selected item and supports multiple tabs.
|
||||
*(Partial, verified 2026-06-07: single-element structure
|
||||
visualisation renders (`output_render.rs:82-180`); **multiple
|
||||
tabs are not implemented** — the output is one line buffer, no
|
||||
tab abstraction. Same multi-tab gap as V2.)*
|
||||
- [x] **S4** Hint area below the input field; keyboard-toggleable
|
||||
for inspecting hints about the current input or last error.
|
||||
- [x] **S4** Hint area below the input field, showing hints about
|
||||
the current input or last error.
|
||||
*(Verified 2026-06-07: `ui.rs:1088-1110` `render_hint_panel` /
|
||||
`resolve_hint_lines` — a dynamic 1–`MAX_HINT_ROWS` panel below
|
||||
the input showing ambient hints, candidates, or the last error.)*
|
||||
the input showing ambient hints, candidates, or the last error.
|
||||
**Correction (2026-06-10, ADR-0046):** the original wording said
|
||||
the area was "keyboard-toggleable"; that was never implemented and
|
||||
is deliberately dropped — the panel became indispensable once
|
||||
completion moved into it (ADR-0022), so it is always on. ADR-0046
|
||||
replaces its content-driven height with a geometry-driven one to
|
||||
stop the resize jump (#20); no toggle is added.)*
|
||||
- [x] **S5** Mode label and distinct border style on the input
|
||||
field communicate the current input mode at all times.
|
||||
*(Verified 2026-06-07: `ui.rs:896-934` `render_input_panel` —
|
||||
@@ -276,9 +290,23 @@ since ADR-0027.)
|
||||
the same via drop + add today; one-step modify is a small
|
||||
follow-up using the existing rebuild-table machinery. ADR
|
||||
pending.
|
||||
- [ ] **C4** Convenience: `create m:n relationship from <T1> to
|
||||
- [x] **C4** Convenience: `create m:n relationship from <T1> to
|
||||
<T2>` produces an auto-named junction table the user can rename;
|
||||
pulls primary keys and FK definitions automatically.
|
||||
*(Done 2026-06-10 via **ADR-0045**. `create m:n relationship from
|
||||
<T1> to <T2> [as <name>]` builds 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`), available in both modes, compound-parent
|
||||
PKs supported (ADR-0043). Self-referential m:n refused; PK-less parent
|
||||
refused. Wired across every surface — completion (`m:n` composite),
|
||||
hints, highlighting, `help`/usage, and the advanced-mode DSL→SQL
|
||||
teaching echo (the generated `CREATE TABLE … FOREIGN KEY …`). 9
|
||||
integration + 7 typing-surface + echo/parse unit tests. The build
|
||||
surfaced — and fixed — two latent simple-mode dispatch/completion
|
||||
assumptions ("≤1 DSL form per entry word"), now generalized
|
||||
behaviour-preservingly.)*
|
||||
- [x] **C5** Data operations: insert / update / delete via DSL.
|
||||
*(ADR-0014. INSERT short and long forms, UPDATE/DELETE with
|
||||
required WHERE plus `--all-rows` opt-in, `show data <T>`,
|
||||
@@ -804,17 +832,27 @@ since ADR-0027.)
|
||||
|
||||
## Cross-cutting
|
||||
|
||||
- [/] **X1** Comprehensive logging via the project's logging
|
||||
- [x] **X1** Comprehensive logging via the project's logging
|
||||
infrastructure per `CLAUDE.md` (decision points, parameter
|
||||
values, fallback paths).
|
||||
*(Partial, verified 2026-06-07: the logging **harness** is
|
||||
wired — `src/logging.rs` sets up file-backed `tracing` with an
|
||||
env filter — but instrumentation is **sparse**: ~25 `tracing::`
|
||||
call sites across the tree, concentrated in `runtime.rs` and
|
||||
`undo.rs` and mostly error/warning on failure paths. The
|
||||
decision-point / parameter-value / fallback-path coverage the
|
||||
`CLAUDE.md` "log liberally" standard calls for — especially in
|
||||
`db.rs`, the parser, and the executors — is largely absent.)*
|
||||
*(Done 2026-06-10 via a full-sweep instrumentation pass. The
|
||||
prior state (verified 2026-06-07) was a wired harness
|
||||
(`src/logging.rs`) but sparse instrumentation — failure-path
|
||||
heavy, nothing in `db.rs`/parser/executors. The sweep brought
|
||||
every layer to the "log liberally" bar under a documented level
|
||||
discipline (see the `logging.rs` module doc): **`db.rs`** gained
|
||||
entry-level `debug!` on all 34 `do_*` executors plus decision-point
|
||||
logs (rebuild-table primitive, insert auto-fill, delete cascade,
|
||||
FK resolution) — so the route through delegating executors is
|
||||
visible in the log sequence; **persistence** logs every
|
||||
yaml/CSV/history write (the silent-failure paths); **runtime**
|
||||
logs `execute_command_typed` dispatch; **`app.rs`** logs
|
||||
submit / app-command dispatch / render-mode choice; the **parser**
|
||||
logs parse begin/outcome at `trace` (it is a per-keystroke hot
|
||||
path). Levels: `debug` for per-command detail (off by default,
|
||||
`RDBMS_PLAYGROUND_LOG=debug`), `info` for lifecycle, `warn` for
|
||||
fallbacks, `trace` for hot paths. Emission verified end-to-end
|
||||
through the real worker thread + `logging::init`. ~75 → ~135 sites.)*
|
||||
- [~] **X2** Language: English-only for v1; multi-language is an
|
||||
open question to revisit later.
|
||||
- [~] **X3** Accessibility: TUI screen-reader support is
|
||||
|
||||
@@ -41,6 +41,22 @@ entry names the ADR that drew the boundary.
|
||||
|
||||
## Table creation (ADR-0029)
|
||||
|
||||
- **A simple-mode table always has a primary key; an advanced-mode
|
||||
table need not.** `create table … with pk …` is mandatory in simple
|
||||
mode (ADR-0029) — the bare `with pk` even defaults to `id serial`.
|
||||
Advanced-mode SQL follows standard SQL and permits a *PK-less* table:
|
||||
`create table t (a int)` declares no primary key. This is **not** a
|
||||
storage problem — every ordinary table (STRICT included) carries
|
||||
SQLite's implicit `rowid`, which keys it internally; only a
|
||||
`WITHOUT ROWID` table (which this app never creates) would lack one.
|
||||
So the simple-mode requirement is a *pedagogical* boundary (teach that
|
||||
tables should have a key), not an engine constraint. Consequences in a
|
||||
PK-less table, all handled: `show data … limit` falls back to rowid
|
||||
order (no stable user-facing key to order by); `update` / `delete`
|
||||
still target the affected rows by rowid; and there is no "PK column"
|
||||
to drop — dropping a *declared* PK column is refused in **both** modes
|
||||
(the shared `do_drop_column` guard: *"cannot drop primary-key column
|
||||
…"*).
|
||||
- **`create table` declares only primary-key columns.**
|
||||
`create table T with pk …` makes every listed column part
|
||||
of the primary key; there is no simple-mode syntax for a
|
||||
|
||||
Reference in New Issue
Block a user