feat(ui): relationships sidebar panel + schema data (#21, ADR-0046 DB2/DB4)

The left column now stacks a Tables panel over a Relationships panel.
Each relationship renders as three narrow lines — its name, then the
endpoints broken at the arrow (Customers.id -> / indented
Orders.customer_id) — ellipsized past the inner width. The panel is
content-sized within [5 rows ("(none)" when empty), half the column];
the Tables panel keeps the rest (>=3 rows). Phase C adds focus+scroll
for content beyond the cap (clipped for now).

Data path: a new worker Request::ReadAllRelationships +
Database::read_all_relationships returns full RelationshipSchema
records; the runtime posts them via a RelationshipsRefreshed event
alongside the schema-cache refresh, and the App holds them in a new
`relationships` field.

ADR deviation (recorded in ADR-0046 DB2 + index): DB2 specified this
data on SchemaCache; it lives on the App instead — SchemaCache is
walker/completion-facing and needs only relationship names (untouched),
while the full records are UI-only, so App is the cleaner home and it
avoids editing ~23 SchemaCache literals. No behavioural difference.

Tests: panel-height bounds, the three-line render, the empty "(none)"
case, a snapshot, read_all_relationships end-to-end (real DB via the
m:n junction), and the event->field handler.
This commit is contained in:
claude@clouddev1
2026-06-10 18:44:27 +00:00
parent 386627a262
commit 94825d0f36
12 changed files with 324 additions and 26 deletions
@@ -211,17 +211,28 @@ 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. **`SchemaCache` is *extended*, not
retyped:** its existing `relationships: Vec<String>` is left as-is
(`completion.rs` borrows it as `&Vec<String>` via
`IdentSource::Relationships` for relationship-name completion, and
several test fixtures construct it) and a **new field
`relationship_details: Vec<RelationshipSchema>`** is added alongside,
populated by the same cache refresh that runs on schema change (the
refresh is taught to query relationship detail, which today it does not
— it only lists names). Retyping the existing field would break the
completion borrow and the fixtures; adding a field is the
zero-ripple change.
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):
@@ -357,10 +368,11 @@ silently edit an invisible buffer.
- Per-panel scroll offsets for the Tables and Relationships panels, each
clamped against a renderer-reported viewport (DC3), mirroring
`output_scroll` / `note_output_viewport`.
- `SchemaCache` gains **`relationship_details: Vec<RelationshipSchema>`**
(DB2) — *additive*; the existing `relationships: Vec<String>` (names,
used by `completion.rs` `IdentSource::Relationships`) is unchanged. The
cache refresh is extended to populate the new field.
- **`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
@@ -458,11 +470,11 @@ Phase A:
scrolls.
Phase B:
- **Schema-cache enrichment:** after each schema mutation the cache
carries full `relationship_details` (name, parent/child, columns,
actions) *and* the existing `relationships` names; `completion.rs`
`IdentSource::Relationships` still resolves names (the additive field
did not disturb it).
- **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