Merge branch 'main' into website (V1 relationship visualization)

Brings main's relationship-visualization feature (ADR-0044 in the main
namespace) and Gitea-migration cleanup onto the website branch, so the
docs can be written against the new diagram output:

- show relationship <name> / show table <T> render two-table connector
  diagrams (child-FK-left, parent-right, n…1 cardinality)
- compound-FK bus routing + pairing line
- ~2000 lines across src/{app,db,event,output_render,runtime,ui}.rs,
  new insta snapshots, tests/it/{show_list,compound_fk}.rs

Merged clean — no conflicts. The prior commit moved the website ADR out
of docs/adr/ into its own namespace, so main's ADR-0044
(relationship-visualization) lands with no collision.

Tests on the merged tree: 2207 passed, 0 failed, 0 skipped
(1 ignored doctest, inherited from main).
This commit is contained in:
claude@clouddev1
2026-06-10 11:06:14 +00:00
17 changed files with 1996 additions and 43 deletions
+504
View File
@@ -0,0 +1,504 @@
# ADR-0044: Relationship visualization (two-table connector diagrams)
## Status
Accepted (2026-06-09); **implemented 2026-06-10** (commits: `cad90ec`
`show relationship` full diagram, `a0ee323` `show table` compact
diagrams, + compound-FK bus routing and self-referential diagrams). A
second `/runda` DA pass over the implementation confirmed
ADR-compliance, UTF-8/byte-range safety, and edge-case routing; the §3
last-resort helper line was **considered and rejected** (see §3).
Closes `requirements.md` **V1**.
Resolves **ADR-0016 OOS-1** ("Relationship visualization — two
structures side-by-side with an arrow; its own ADR; will compose
`render_structure`"). Partially **supersedes ADR-0016 §5** (the
plain-text `References:` / `Referenced by:` relationship block) and
**extends ADR-0016 §4 and §6** (adds layout width-awareness and
per-span styling). Builds on ADR-0028 (styled output runs),
ADR-0043 (compound, list-based relationship endpoints), ADR-0013
(named 1:n relationships), and the V5/V5a `show` family. Honours
ADR-0009 (DSL conventions) and ADR-0002 (no engine name in
user-facing strings).
Closes the substantive open half of requirement **V1**
(`docs/requirements.md`): "a selected relationship as two tables
joined by a line."
## Context
The table-structure half of V1 is done: `show table <T>` renders a
box-drawn structure table (columns / types / constraints / indexes),
and relationships appear **as prose** beneath it —
`References:` / `Referenced by:` blocks formatted `A.col → B.col`
(`output_render.rs render_structure`, per ADR-0016 §5). The same
prose appears in V5a's `show relationship <name>` detail view.
The open piece is the **visual** form: drawing a relationship as two
tables side by side joined by a connecting line, with cardinality and
referential actions. ADR-0016 deliberately deferred this (OOS-1) and
**pre-sized the structure renderer to compose two of itself**. This
ADR cashes that in.
Three facts from the current architecture shape the design:
1. **Rendering is App-side; the worker returns structured data.**
*(Corrected from the initial draft after direct verification — an
earlier survey had this backwards.)* The database worker (`db.rs`)
returns `TableDescription` / `DataResult`; the **App** (`app.rs`)
calls the `output_render.rs` helpers to format them into
`OutputLine`s. Verified: every `render_structure` /
`render_data_table` call site is in `app.rs` (557, 1669, 1732,
1752, 1814, 1678, …); **none in `db.rs`**. Width and theme
therefore live App-side. The **one exception** is the V5/V5a
`show <kind> [<name>]` family: `do_show_one` / `show_list` build
prose `Vec<String>` **in the worker** from `RelationshipSchema` /
index metadata, carrying *no* `TableDescription` — so
`show relationship <name>` is the path that must be restructured
(§6). The output buffer is a flat `VecDeque<OutputLine>` — a
historical log of lines, not re-renderable widgets (live
re-rendering is V4 territory).
2. **Relationships are list-based (ADR-0043).** `RelationshipSchema`
/`RelationshipEnd` endpoints are `Vec<String>` column lists,
positionally paired. Single-column FKs are the one-element case;
the diagram must render compound FKs too.
3. **Only 1:n relationships exist.** Relationships are declared 1:n
(ADR-0013); m:n (requirement C4) is unbuilt, and there is no
UNIQUE-target 1:1. So cardinality is always **1 on the parent
(referenced) side, n on the child (referencing/FK) side**.
The user has chosen the design direction across three decisions
(recorded below as made): **visual style**, **where it appears**, and
**multi-relationship layout**.
## Decision
### 1. Scope and trigger — diagrams where relationships are the subject
Relationship diagrams replace the prose relationship form on the
surfaces where **relationships are the subject of the command**; no
new command or sigil is added (ADR-0009: one sigil, keyword grammar).
This is the user-chosen **"relationship-relevant"** reach (over a
global replacement of every structure echo, or a show-commands-only
scope — see the DA pass).
Diagram surfaces:
- **`show relationship <name>`** (V5a) renders **one** diagram: the
parent and child tables as **full structure boxes** joined by a
connector. The canonical base unit.
- **`show table <T>`** keeps T's full structure box at the top
(unchanged from ADR-0016 §5), then a **Relationships** section:
**one compact connector diagram per relationship**, stacked
vertically (§4).
- **Relationship DDL auto-shows** — the structure echo after
`add 1:n relationship`, `drop relationship`, and a future
`modify relationship` (C3a) — render their relationships as
compact diagrams (§4), since the user just acted on a relationship.
Prose-retained surfaces (**unchanged** from ADR-0016 §5):
- **Incidental DDL auto-shows** — the structure echo after
`create table`, `add`/`drop`/`rename`/`change column`,
`add`/`drop index` — keep the terse `References:` /
`Referenced by:` prose. A simple `add column` on a heavily-related
table should not print a wall of diagrams.
So this **partially supersedes ADR-0016 §5**: the prose block is
replaced by diagrams on the relationship-subject surfaces and
retained on incidental ones. No information is lost on either —
relationship name, endpoints, cardinality, and `on delete` /
`on update` actions all appear in both forms.
Mechanically (§6) this is a **render mode** on `render_structure`
(`Diagram` vs `Prose`), selected by the calling command; the items/
side panel (`ui.rs render_items_panel`) is a navigation tree and is
untouched.
A future **user-configurable setting** (e.g. always-prose /
always-diagram / auto-by-width, for small-screen users) is a clean
follow-up and is **out of scope here** (OOS-7) — the per-command
default above is the v1 behaviour.
### 2. The base diagram — Style A (structure + connector)
A relationship diagram is two boxes plus a connector. Reading
convention, applied **uniformly** (this is what makes
multi-relationship lists scannable):
- **Child (FK holder) on the left, parent (referenced) on the
right.** The connector arrow always points **left → right**, in the
direction of the reference (child *references* parent).
- **Cardinality** sits at the connector ends: **`n`** at the child
(left) end, **`1`** at the parent (right) end.
- **Referential actions** (`on delete …`, `on update …`) label the
connector beneath the line. `no action` is shown verbatim
(ADR-0009 wording), abbreviated to the action keyword when space is
tight.
#### 2.1 Box anatomy — the table name must stand out
Every box (full or compact) carries the table name as a **bold title
row separated from the columns by a rule** (`├─┤`), styled in the
table-name theme class (§5). This directly addresses the requirement
that a name never read as just another column row.
Full structure box (used in `show relationship`): title row, then the
ADR-0016 column/type listing, with key markers (§2.2):
```
┌──────────────────────┐
│ orders │ ← bold title row (table name)
├──────────────┬───────┤
│ id (PK) │ int │
│ customer_id ●│ int │ ● marks the FK column in this relationship
│ total │ real │
└──────────────┴───────┘
```
Compact box (used in `show table`'s stacked list): title row, rule,
then **only the column(s) participating in this relationship** — the
focal table's full structure is already shown above, so the compact
box stays small and the focal table is not redrawn in full N times:
```
┌──────────────┐
│ orders │ ← bold title row
├──────────────┤
│ customer_id ●│ only the FK column(s) for this relationship
└──────────────┘
```
#### 2.2 Key markers
- The PK is annotated `(PK)` (consistent with the existing structure
view's constraint column).
- The **endpoint columns of *this* relationship** are marked with a
filled dot `●` adjacent to the column name — on the child FK
column(s) and the parent referenced column(s). The connector
attaches at these `●` rows.
#### 2.3 The connector (single-column)
`show relationship Customers_Orders` (child `orders.customer_id`
→ parent `customers.id`):
```
orders customers
┌──────────────────────┐ ┌─────────────────────┐
│ orders │ │ customers │
├──────────────┬───────┤ 1 ┌──●│ id (PK) │
│ id (PK) │ int │ │ ├─────────────┬───────┤
│ customer_id ●│ int │ n ───────────┘ │ name │ text │
│ total │ real │ │ email │ text │
└──────────────┴───────┘ └─────────────┴───────┘
on delete cascade · on update no action
```
**Connector routing.** The two `●` endpoint rows are generally at
different heights. The connector leaves the child box's right edge at
the child-`●` row, travels horizontally into a **gutter channel**
between the boxes, jogs vertically to the parent-`●` row, then enters
the parent box's left edge with an arrowhead. Box tops are aligned.
(Exact glyphs/spacing are pinned by insta snapshots in
implementation; the mockups here are representative, not normative.)
#### 2.4 Compound foreign keys (ADR-0043)
For an n-column FK, the n child `●` columns and n parent `●` columns
are each listed, and **one connector is routed per positional pair**.
A summary line states the pairing explicitly so it is unambiguous
even if the routed lines are visually close:
```
orders customers
┌──────────────────────────┐ ┌──────────────────────┐
│ orders │ 1 ┌●│ region (PK) │
├──────────────┬───────────┤ │ │ id (PK) │
│ cust_region ●│ text │ n ─────────┤ ├────────────┬─────────┤
│ cust_id ●│ int │ n ─────────┘ │ name │ text │
│ … │ … │ └────────────┴─────────┘
└──────────────┴───────────┘
(cust_region, cust_id) ──► customers.(region, id)
on delete cascade · on update no action
```
### 3. Width handling and the narrow-terminal fallback
The diagram is rendered **once, App-side, at the output-panel width
current when the command runs**. The `App` does **not** track the
panel width today — `note_output_viewport(visible_rows,
total_wrapped_rows)` records only row counts for scroll math
(verified; `App` has no width field). This ADR adds a
`last_output_width: u16` to `App`, set from `ui.rs` where the output
panel's `inner.width` is already computed (next to the existing
`note_output_viewport` call), with an `80` default before the first
render. The App-side render path then chooses layout per diagram:
- **Side-by-side** (§2) when both boxes plus the gutter fit the
available width.
- **Vertical stack** when they do not: parent box above, child box
below, connector running **downward** through a short vertical
channel, cardinality and actions labelling it. This keeps a real
diagram at any width instead of degrading to prose:
```
┌──────────────┐
│ orders │
├──────────────┤
│ customer_id ●│ n
└──────────────┘
│ on delete cascade
▼ on update no action
┌──────────────┐
│ customers │
├──────────────┤
│ id (PK)●│ 1
└──────────────┘
```
- **Last-resort helper — considered and rejected (2026-06-10,
implementation).** The draft proposed that, when even a single
*vertical* diagram cannot fit (a box wider than the pane), `show
table` emit a one-line `run `show relationship <name>`` pointer per
relationship. In implementation we **decided against it**: the
vertical fallback already covers every realistic narrow terminal,
and the only remaining case — a box wider than the *whole* pane —
requires an extreme combination (very long identifiers on a tiny
terminal) and is handled the same way as all other over-wide output
(ratatui's right-edge truncation, ADR-0016 §4). A dedicated pointer
would add a code path and a worse result (less information than even
a truncated diagram) for a near-unreachable case. `show relationship`
itself always renders the diagram; if its box is wider than the pane,
the same truncation applies — no new truncation logic.
**No live reflow.** Because the output buffer is a historical line
log, resizing the terminal *after* a diagram is rendered does not
reflow it — identical to how every other rendered output behaves
today. Reflow-on-resize belongs to V4's re-renderable journal and is
explicitly out of scope (OOS-2).
This **extends ADR-0016 §4**: that ADR added no width-awareness
(relying on ratatui truncation); this ADR adds width-awareness for
the **layout decision** only. It still performs **no per-cell
truncation** (ADR-0016 OOS-4 stands).
### 4. `show table` multi-relationship layout — stacked compact diagrams
After T's full structure box, a **Relationships** heading, then one
**compact** diagram (§2.1) per relationship, stacked vertically.
Ordering matches the current prose order: **outbound** (T is the
child — "References") first, then **inbound** (T is the parent —
"Referenced by"), each group ordered by relationship name. The
child-left/parent-right rule (§2) is applied per diagram, so the
focal table appears on the left for its outbound relationships and on
the right for its inbound ones — consistent with the reference
direction.
```
orders
┌──────────────┬──────┐
│ orders │ │
├──────────────┼──────┤
│ id (PK) │ int │
│ customer_id │ int │
│ total │ real │
└──────────────┴──────┘
Relationships
┌──────────────┐ n 1 ┌─────────────┐
│ orders │────────────────│ customers │
├──────────────┤ on delete ├─────────────┤
│ customer_id ●│ cascade │ id (PK) ●│
└──────────────┘ └─────────────┘
┌──────────────┐ n 1 ┌─────────────┐
│ order_items │────────────────│ orders │
├──────────────┤ on delete ├─────────────┤
│ order_id ●│ cascade │ id (PK) ●│
└──────────────┘ └─────────────┘
```
Stacking (rather than a single focal-centred subgraph with fan-out
connectors) is chosen because: it never produces crossing lines; it
scales to any number of relationships via the pane's existing
vertical scroll; each diagram is only two boxes wide, so it fits
nearly any terminal; and it keeps the per-diagram width logic of §3
trivial. The cost — the focal table name repeats per diagram — is
mitigated by the compact box showing only the participating
column(s).
### 5. Styling
Styling uses the **ADR-0028 styled-run mechanism** (`OutputSpan` /
`OutputStyleClass` on `OutputLine`), not raw text. New style classes
(theme-defined, legible light/dark per ADR-0016 §6 / NFR-7):
- **table name** — bold, accent colour (the "stand out" requirement);
- **key marker** (``, `(PK)`) — a distinct accent;
- **connector** (box-drawing line + arrowhead) — muted;
- **cardinality** (`1` / `n`) — emphasised;
- **referential action** label — muted/secondary.
This **extends ADR-0016 §6** (which set up `OutputKind::System`
styling but no per-element theming) by reusing the per-span
`OutputSpan` path ADR-0028 later introduced — which is **already
produced App-side** (`output_render.rs:299-332` builds styled runs
for the explain-plan tree; `app.rs` constructs `OutputLine`s with
`styled_runs: Some(..)`; `ui.rs:863` renders them). So **no
worker→UI contract change is needed**: the new `output_render`
diagram functions return styled lines directly on the App side
(§6).
### 6. Implementation
All rendering is **App-side** (per the corrected Context fact 1), in
`output_render.rs` (hand-rolled, ADR-0016 §7), returning styled
lines that `app.rs` pushes as `OutputLine`s with `styled_runs` set.
No worker `available_width` and no worker→UI contract change.
- **New renderer functions in `output_render.rs`**, composing the
existing box primitives and emitting styled spans (§5):
- `render_relationship_diagram(child, parent, rel, width, full) ->
Vec<OutputLine>` — `full` selects full vs compact boxes;
internally decides side-by-side vs vertical (§3) from `width`;
routes the connector(s) including compound pairs (§2.4).
- A shared helper for the box title row + key markers.
- The two call paths supply `width` from the new
`App::last_output_width` (§3).
- **`show table <T>` (already App-side).** The focal
`TableDescription` reaches `app.rs` and is rendered by
`render_structure` today. `render_structure` is refactored to emit
the focal structure box, then a **Relationships** section built
from the focal description's `RelationshipEnd`s (which already
carry neighbour table name + participating columns + cardinality +
actions — **enough for the compact box**; no neighbour
full-structure fetch needed). `render_structure` gains a
**relationship render mode** (`Diagram` | `Prose`); the caller
selects it (§1): `show table` and the relationship DDL echoes pass
`Diagram`, incidental DDL echoes pass `Prose`. The generic
`handle_dsl_success(command, description)` (`app.rs:1669`) already
has the `Command`, so the mode is a function of the command
variant; the incidental-only call sites (`app.rs:557/1732/1752/
1814`) pass `Prose`.
- **`show relationship <name>` (must be restructured).** Its
worker path `do_show_one` currently returns prose `Vec<String>`
from a `RelationshipSchema` only. To draw the **full** diagram the
App needs **both** endpoint `TableDescription`s. The relationship
detail reply is upgraded to carry the `RelationshipSchema` plus
both endpoints' `TableDescription`s (the worker already has
`do_describe_table`); the App renders the diagram. The
"No relationship named `X`." not-found line is preserved.
- **Self-referential FKs** (`parent_table == child_table`; supported
in the grammar): render as two boxes bearing the same table name
(child-left copy with the FK column, parent-right copy with the
referenced column), connector as usual — clearer than a self-loop
glyph in a TUI. Covered by a snapshot test.
- **Out of these surfaces:** the items/side panel (`ui.rs:582`
`render_items_panel`) is a navigation tree, not a relationship
view, and is unchanged.
- **Database engine name** never appears in any rendered string
(ADR-0002).
### 7. Testing
- **Insta snapshots** (Tier 2) pin exact rendered output for:
single-column 1:n (`show relationship`); compound FK; the
narrow-terminal vertical fallback; the last-resort helper line;
a self-referential FK; and a `show table` with both an outbound and
an inbound relationship (stacked compact list).
- **Unit tests** (`output_render.rs`): the side-by-side-vs-vertical
**width-threshold** decision at boundary widths; connector routing
for endpoints at differing row heights; compound-pair routing;
key-marker placement.
- **Tier-3 integration**: `show relationship <name>` and
`show table <T>` produce diagram output (not prose) end-to-end
through the worker.
- **Existing tests/code to update** (enumerated from a DA grep — the
supersession of ADR-0016 §5 is not abstract):
- `output_render.rs:121,135` — the `References:` / `Referenced by:`
prose-emitting code in `render_structure`;
- `output_render.rs:78` — the docstring deferral note (OOS-1);
- `output_render.rs:793` — unit test asserting `"Referenced by:"`;
- `src/snapshots/…render_structure_with_relationships.snap` — the
prose snapshot;
- `tests/it/walking_skeleton.rs:477,530` — integration asserts on
`"Referenced by:"` (and the comment at 433);
- `src/dsl/command.rs:984` — a comment referencing the prose (no
behaviour).
If the scope fork resolves to "all `render_structure` call sites,"
the DDL-echo snapshots for create-table / add-column / drop-index /
change-column auto-shows also churn — to be re-recorded with
`cargo insta`. No test is deleted to hide a regression; each change
is a deliberate format update.
### 8. Out of scope
- **OOS-1.** Live reflow-on-resize of already-rendered diagrams —
V4's re-renderable journal (requirement V4).
- **OOS-2.** Whole-database ER diagram / export (requirement V3).
- **OOS-3.** m:n relationships (requirement C4, unbuilt). Only the
existing 1:n form is rendered.
- **OOS-4.** ASCII fallback for terminals without box-drawing
(inherits ADR-0016 OOS-5).
- **OOS-5.** Per-cell colouring of column *data* values (ADR-0016
OOS-3 / NFR-5) — unrelated to relationship structure.
- **OOS-6.** Cell-level truncation with ellipsis (ADR-0016 OOS-4
stands).
- **OOS-7.** A user-configurable relationship-display setting
(always-prose / always-diagram / auto-by-width — useful for small
screens). The §1 per-command default is v1; the setting is a clean
later follow-up (user-flagged 2026-06-09).
## Consequences
- Requirement **V1** is fully satisfied: relationships render as
two-table connector diagrams in both `show relationship <name>`
(full) and `show table <T>` (stacked compact), superseding the
prose form.
- `output_render.rs` grows a relationship renderer that **composes**
the box primitives exactly as ADR-0016 anticipated; the worker→UI
show contract becomes styled-line-based, a small generalisation
reusable by other styled `show` output later.
- The worker gains an `available_width` input for `show` — the first
width-aware formatting in the codebase. The decision is a snapshot
at command time; no resize machinery is introduced.
- Compound FKs (ADR-0043) get their first dedicated visualization,
with explicit positional-pair labelling.
- The historical-log output model is preserved (no widget/reflow
model); V4 remains the home for a re-renderable journal.
- Box-drawing is required (no ASCII fallback), consistent with
ADR-0016.
## Devil's Advocate / runda pass (2026-06-09)
A planning-time DA pass (empirical, against the code) corrected three
foundational errors carried in from an initial code survey, and
surfaced one open fork:
1. **Rendering boundary was inverted** (fixed in Context 1 / §6):
`render_structure` / `render_data_table` run **App-side**
(`app.rs`), not in the worker. Rendering, width, and theme are all
App-side — simpler than the draft assumed.
2. **Width was claimed "already tracked"; it is not** (fixed in §3):
`note_output_viewport` records only rows. A new
`App::last_output_width` (set from `ui.rs`) is required.
3. **`show relationship <name>` renders prose in the worker** (fixed
in §6): `do_show_one` carries only a `RelationshipSchema`. The
detail reply must be upgraded to include both endpoint
`TableDescription`s for the full diagram.
4. **Styled-line contract was over-stated** (fixed in §5): styled
runs are already produced App-side (explain plan); no worker→UI
contract change.
**Resolved fork (user, 2026-06-09): "relationship-relevant" reach.**
Diagrams render on the surfaces where the relationship is the subject
(`show table`, `show relationship`, relationship DDL echoes);
incidental DDL echoes keep prose (§1) — avoiding a wall of diagrams
after an `add column`. A future user-configurable display setting is
noted as OOS-7. This also records V1's **deliberate scope expansion**
beyond its literal "a selected relationship" wording into
multi-relationship surfaces, to be reflected in `requirements.md`.
All design forks are now resolved and the architecture is corrected.
The user accepted this revised ADR on 2026-06-09; status is
**Accepted** and implementation proceeds test-first.
+1
View File
@@ -56,3 +56,4 @@ This directory contains the project's ADRs, recorded per
- [ADR-0041 — Copy the output panel to the system clipboard](0041-copy-output-to-clipboard.md) — **Accepted 2026-06-02 (issue #11)**, amends ADR-0003's app-command registry (adds **`copy`** / `copy all` / `copy last`). The friction it removes: filing a bug report meant terminal-selecting the output panel and fighting wrapping/borders. New **app-level command** (sigil-free, both modes): `copy` / `copy all` copy the whole panel; `copy last` copies from the most recent echo line to the end. **Mechanism — OSC 52 *and* native (`arboard`), always both**, because OSC 52 acceptance is undetectable (no terminal ack), so a true "fall back when unsupported" can't be built: emit the OSC 52 escape (no new dep — `base64`+`crossterm`; works over SSH; tmux-passthrough-wrapped via `$TMUX`), then a best-effort native write whose failure is ignored (headless host — OSC 52 carried it); the two carry identical content. **Format — plain text verbatim as rendered** (tags, `✓`/`✗`, box-drawing) joined by `\n`, without viewport padding/wrapping; a drift-lock test pins `OutputLine::plain_text` to `render_output_line`. `arboard` added **`--no-default-features`** (drops the `image` crate; X11-only on Linux — `wayland-data-control` deliberately omitted as it ~doubles the dep tree and OSC 52 covers native-Wayland). Security: write-only, scans clean for arboard's tree (cargo audit / osv-scanner / grype), 1Password-maintained, minimal surface. OOS: Markdown export, selection/range, a keybinding, OSC 52 read, `screen` passthrough
- [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 ~1520 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)
+211
View File
@@ -0,0 +1,211 @@
# Session handoff — 2026-06-10 (60)
Sixtieth handover. Continues from handoff-59 (tracking reconciliation
+ V5/H3/V5a sweep + T3 compound-PK FK). This session did **three**
things: (1) verified the **Claude Code 2.1.170** upgrade is healthy;
(2) finished the **GitHub→Gitea migration cleanup** and added `tea`
issue-tracking conventions; (3) the big one — **completed requirement
V1, relationship visualization**, designed in **ADR-0044** and
implemented end-to-end with two `/runda` passes.
## §1. State at handoff
**Branch:** `main`. **HEAD `0a34303`.** 5 commits this session
(`b17148b``0a34303`); push is the user's step.
**Tests: 2207 passing / 0 failing / 1 ignored** (lib 1586, it 429,
typing_surface_matrix 192; the 1 ignored is the long-standing
doc-test). **Clippy clean** (nursery, all targets). +14 over the
handoff-59 baseline of 2193.
This session's commits:
```
0a34303 feat: compound-FK bus routing + complete V1 relationship visualization (ADR-0044)
a0ee323 feat: show table renders relationships as compact diagrams (ADR-0044)
cad90ec feat: show relationship <name> renders a styled two-table diagram (ADR-0044)
bb02dfb docs: ADR-0044 relationship visualization (V1); accepted
b17148b docs: scrub GitHub-specifics after Gitea migration; add tea issue conventions
```
## §2. Repo migration: GitHub → Gitea (commit `b17148b`)
The repo moved off GitHub to a self-hosted **Gitea** at
`git.lazyeval.net` (`oli/rdbms-playground`). `tea` auto-detects this
repo correctly off the remote **even though the machine's default
`tea` login is a different host** (`git.oliversturm.com`) — verified.
- **Durable GitHub-specifics scrubbed:** `Cargo.toml` `repository`
URL; `requirements.md` backlog note ("now tracked as Gitea
issues"); **ADR-0001 Amendment 1** reopens the prebuilt-binary
distribution channel (was "GitHub releases") as an undecided choice
for a future distribution ADR (the Decision text was *not*
rewritten, per supersede-don't-rewrite).
- **Left as historical:** all `docs/handoff/*.md` (append-only log).
- **`CLAUDE.md` gained** an *Issue tracking* working-style bullet +
an *Issue tracking — Gitea via `tea`* section (adapted from another
project; repo coordinates corrected, `tea` gotchas kept). **Working
method now: file bugs/enhancements as Gitea issues, cross-reference
in commits/handoffs; `requirements.md` + ADRs remain the source of
truth for scope/decisions; a change to a decided area still earns an
ADR.** No heavyweight planning workflow (we're near completion of
the initial requirements).
- No open Gitea issues; the 18-issue campaign backlog (#1#18) is all
closed. V1 lives in `requirements.md`, not an issue.
## §3. V1 — relationship visualization (the big one)
**ADR-0044** (`docs/adr/0044-relationship-visualization.md`, Accepted
2026-06-09, **implemented 2026-06-10**). Resolves **ADR-0016 OOS-1**
and closes `requirements.md` **V1** (`[x]`). The
relationship-as-line-art half that had been "repeatedly pushed away"
now renders as **Style-A two-table connector diagrams**.
**Design forks (all user-chosen):**
- **Style A** (two structure boxes + connector) over a compact key
card or crow's-foot ER.
- **Reach = "relationship-relevant"** (over global / show-only):
diagrams where the relationship is the *subject*
`show relationship <name>`, `show table <T>`, and
`add`/`drop relationship` echoes; incidental DDL echoes (add column,
drop index, change column, plain create table) keep the prose
`References:` / `Referenced by:` form.
- **`show table` layout:** focal structure box, then a
**Relationships** section of **stacked compact** per-relationship
diagrams (over a focal-centred subgraph — no crossing lines, scales
via scroll, two-boxes-wide fits any terminal).
**Visual conventions:** child (FK holder) on the **left**, parent
(referenced) on the **right**, arrow → (child references parent),
**`n``1`** cardinality, referential actions beneath, and a
**bold title row + rule** on every box so a table name can't read as
a column. Compound FKs route a shared **bus** (each endpoint stub
merges into a vertical channel that splits to the paired endpoints)
plus an explicit `(a, b) ▶ P.(x, y)` **pairing line**.
Self-referential FKs draw two same-named boxes. **Width-aware**:
side-by-side when it fits, else a **vertical stack** fallback.
**Three implementation increments:**
- `cad90ec``show relationship <name>` full diagram (both tables as
full structure boxes).
- `a0ee323``show table` compact stacked diagrams + the
`Diagram|Prose` render mode.
- `0a34303` — compound-FK bus routing + pairing line, self-referential
diagrams, V1 → `[x]`.
**Two `/runda` passes.** The design pass (pre-build) **caught an
inverted-architecture assumption** carried in from a code survey
(rendering is App-side, not worker-side; width was claimed "already
tracked" but wasn't; `show relationship` built prose in the worker).
The implementation pass confirmed ADR-compliance, UTF-8/byte-range
safety, and edge-case routing, and added a compound-from-data glue
test. The §3 **last-resort helper line was considered and rejected**
(vertical fallback + ratatui truncation cover all realistic cases).
## §4. Key implementation facts (read before touching the renderer)
- **Rendering is App-side.** `output_render.rs` helpers
(`render_structure`, `render_data_table`, and the new diagram
functions) are called from **`app.rs`**, never `db.rs`. The worker
returns structured data (`TableDescription`); the App formats it.
*(An Explore survey got this backwards — verify architecture claims
against the code, not a summary.)*
- **The diagram renderer** lives in `output_render.rs` under the
`// ── Relationship visualization (ADR-0044)` banner: a styled `Seg`
engine (text + `OutputSpan` runs that compose by concatenation with
offset-shifting); `render_box` (full or compact box with a title
row); `gutter_seg` + `junction` (the bus connector — routes **all**
endpoint pairs, reduces exactly to the single-column jog);
`compose_side_by_side` / `compose_vertical`;
`render_relationship_layout` (width dispatch + pairing line);
`render_relationship_diagram` (the `show relationship` entry, builds
full `DiagramTable`s from `RelationshipDiagramData`);
`render_structure_with_diagrams` (the `show table` entry: focal box
+ compact diagrams; `render_structure` was refactored into section
helpers — `structure_box_lines` / `relationship_prose_lines` /
`index_lines` / `constraint_lines` — with **byte-identical** prose
output so the old snapshots held).
- **`show relationship` worker path:** `db.rs`
`RelationshipDiagramData` (rel + both endpoint `TableDescription`s)
+ `Database::show_relationship` + `do_show_relationship`; runtime
`CommandOutcome::ShowRelationship` (boxed — two `TableDescription`s
dwarf the other variants) reroutes a named relationship before the
prose `show_list` fallback; event `DslShowRelationshipSucceeded`;
app `handle_dsl_show_relationship_success`.
- **The Diagram/Prose split** is in `handle_dsl_success`: it renders
diagrams for `ShowTable | AddRelationship | DropRelationship`, prose
otherwise.
- **Width:** new `App::last_output_width` (default `80`), set from
`ui.rs` `render_output_panel` next to `note_output_viewport`.
Snapshot at command time — **no live reflow** (that's V4).
- **Styling:** four new `OutputStyleClass` variants
(`DiagramTableName` / `DiagramKey` / `DiagramCardinality` /
`DiagramConnector`), mapped in `ui.rs::output_span_style` to
**existing** theme colours (no new `Theme` fields). `Seg` only ever
pushes whole strings/chars, so styled-run byte ranges are always
valid UTF-8 boundaries (exercised live by the `add relationship`
`rendered_text` test through `ui.rs`'s `text[start..end]` slice).
- **`do_show_one`'s relationship prose branch is now dead-for-users**
(the reroute supersedes it) but **retained** — reachable via the
`Database::show_list` worker API, covered by a worker test, and a
candidate text fallback for a future non-visual display option (cf.
ADR-0044 **OOS-7** relationship-display setting). Documented in
`db.rs`.
- **Two bugs the tests caught** (both in compact boxes, which the
single-relationship full-box tests didn't exercise): an eager
`widths[1]` index panic (`then_some``then`), and body-cell
padding under title-widening (pass the widened `widths[0]`, not the
pre-widening `label_w`).
## §5. Remaining open landscape
**Still `[/]` partial / `[~]` / larger (unchanged from handoff-59):**
- **V2 / S3** multi-result tabs — output-model redesign.
- **V3** whole-DB ER export; **V4** scrollable journal + Markdown
(the home for diagram live-reflow, OOS-1 here).
- **A1** app-commands — blocked on `seed` (SD1) + `hint` (H2).
- **DOC1** reference docs; **X1** logging density.
**`[ ]` not started:** H2 `hint`, SD1 `seed`, C4 m:n, B3
query-timeout, I1 multi-line, I1b readline, I5 cancellation, **TT5
CI** (now Gitea Actions, ties into ADR-0001's reopened distribution
question), TT4 PTY (spec-only), D1D3 distribution, NFR-1…7.
**ADR-0044 OOS for later:** OOS-7 user-configurable
relationship-display setting (always-prose / always-diagram /
auto-by-width); compound display polish if needed.
## §6. Next job — candidates
No forced next step. By readiness:
1. **X1 logging** — mechanical, no ADR; brings instrumentation to the
`CLAUDE.md` "log liberally" bar (~25 `tracing` sites today).
2. **TT5 CI** — test infra solid (2207 green); no pipeline. Now
**Gitea Actions / Woodpecker**, not GitHub Actions — a fresh
decision tied to today's migration + ADR-0001's distribution
question.
3. **T3 residuals** (ADR-0043 §4) — two messaging-only polish items
(inline-FK arity error wording; compound-FK-violation friendly
error names only the first pair).
4. **V2/S3 multi-result tabs** or **V4 journal** — larger,
design-first (own ADR).
## §7. How to take over
1. Read handoffs 58 → 59 → 60, then `CLAUDE.md` (now with the
*Issue tracking — Gitea via `tea`* section), `docs/requirements.md`
(V1 now `[x]`), `docs/adr/README.md`.
2. **For relationship diagrams: read ADR-0044**, then the
`// ── Relationship visualization` block in `src/output_render.rs`
(§4 above maps the functions).
3. **Gitea/`tea`:** plain `tea issues` works here (auto-detects
`git.lazyeval.net`); the gotchas section in `CLAUDE.md` matters
(stdin-hang → `< /dev/null`; multiline bodies via temp file; the
display blind-spot for milestones/comments).
4. Codebase on `main` at `0a34303`, clean, 5 commits unpushed.
5. Process pins that paid off this session: **verify architecture
claims against the code, not a survey** (the §3 inversion);
**`/runda` after design AND after implementation** (both found real
things); **tests on the *compact* path caught bugs the full-box
path missed** — exercise every variant; **escalate genuine forks**
(every V1 design choice was the user's). Commits user-confirmed,
append-only, no AI attribution.
+15 -11
View File
@@ -423,20 +423,24 @@ since ADR-0027.)
## Visualizations
- [/] **V1** Single-element views render in the output pane: a
- [x] **V1** Single-element views render in the output pane: a
selected table as its structure (columns, types, keys,
constraints); a selected relationship as two tables joined by
a line.
*(Partial, verified 2026-06-07: the **table-structure** half is
done — `output_render.rs:82-180` renders columns / types /
constraints / indexes in a box-drawing table, with relationship
metadata as `References:` / `Referenced by:` prose
(`A.col → B.col`). The **relationship-as-line-art** half — two
tables drawn side by side with a connecting line — is **not
implemented** (deferred per `output_render.rs` §5 OOS-1, ADR
pending). This is the relationship-visualisation piece that has
been repeatedly pushed away; it is the substantive open part of
V1. Selection-nav and the broader journal direction live in V4.)*
*(Done 2026-06-10 — **ADR-0044**. The table-structure half shipped
earlier; the **relationship-as-line-art** half (ADR-0016 OOS-1) now
renders as **Style-A two-table connector diagrams** wherever a
relationship is the subject: `show relationship <name>` (full
structure boxes), `show table <T>` and `add`/`drop relationship`
echoes (focal box + compact stacked diagrams). Child-left /
parent-right, `n…1` cardinality, referential actions, bold title
rows; rendered App-side and **width-aware** (side-by-side ↔ vertical
fallback). **Compound** FKs route a shared bus + an explicit
`(a, b) ▶ P.(x, y)` pairing line; **self-referential** FKs draw two
same-named boxes. Incidental DDL echoes keep the prose form (the
"relationship-relevant" reach). The §3 last-resort helper line was
considered and rejected. Two `/runda` passes (design + implementation).
Selection-nav and the broader journal direction remain in V4.)*
- [/] **V2** SQL query results render as a dynamic table view in
the output pane, with multiple result tabs supported.
*(Partial, verified 2026-06-07: the **table view** is done —