# 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 ` 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 ` 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 []` family: `do_show_one` / `show_list` build prose `Vec` **in the worker** from `RelationshipSchema` / index metadata, carrying *no* `TableDescription` — so `show relationship ` is the path that must be restructured (§6). The output buffer is a flat `VecDeque` — 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` 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 `** (V5a) renders **one** diagram: the parent and child tables as **full structure boxes** joined by a connector. The canonical base unit. - **`show table `** 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. *(**Superseded 2026-06-12 by ADR-0050** (issue #28): these incidental DDL echoes now render **structure only** — no relationship block at all, neither prose nor diagram. The prose renderer was deleted. The diagram surfaces below are unchanged.)* 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 `` 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` — `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 ` (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 ` (must be restructured).** Its worker path `do_show_one` currently returns prose `Vec` 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 ` and `show table ` 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 ` (full) and `show table ` (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 ` 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.