8ac3537df0
Per ADR-0050 (closing issue #28): the confirmation echo after an incidental structural edit — create table, add/drop/rename/change column, add/drop index — now renders the structure only (header + column box + indexes + constraints) and no longer appends the References:/Referenced by: relationship block. Rationale: a confirmation reports the change just made, not the table's relationships, which the user didn't touch. Relationship info is still one `show table <T>` away, and the relationship-subject surfaces (show table, add/drop relationship) keep their ADR-0044 diagrams unchanged. Scope is all incidental DDL (user-confirmed). Mechanism: drop the relationship-block call from render_structure (all its callers are incidental DDL); the handle_dsl_success diagram-vs-structure routing is unchanged. The orphaned relationship_prose_lines + cols_disp helpers are deleted (the prose format survives in ADR-0016 §5 + git history for a future OOS-7 always-prose setting). ADR-0050 supersedes ADR-0044 §1's incidental-DDL prose clause and the relationship-block half of ADR-0016 §5 (both annotated). Tests: prose- presence unit test + snapshot removed; new unit test locks structure- only with inbound+outbound relationships present; the misnamed add- column integration test inverted + renamed. 2458 pass / 0 fail / 0 skip, clippy clean.
300 lines
12 KiB
Markdown
300 lines
12 KiB
Markdown
# ADR-0016: Pretty table rendering for data and structure views
|
||
|
||
## Status
|
||
|
||
Accepted
|
||
|
||
## Context
|
||
|
||
The output panel today renders tabular content with hand-rolled
|
||
pipe-and-dash separators (`render_data_view` in `app.rs`) and
|
||
plain indented lines for table-structure summaries. Both work,
|
||
both are clearly placeholders. The result is functional but
|
||
unmistakably "first pass": a learner pulling up the app for the
|
||
first time sees a default-looking TUI rather than a polished
|
||
teaching tool.
|
||
|
||
The walking skeleton's NFR-4 commits to "distinctive design"
|
||
— a reviewer should be able to identify the app from a
|
||
screenshot of any view — and NFR-5 requires colour to convey
|
||
information. Neither bar is met by the current rendering.
|
||
|
||
This ADR commits to a focused upgrade of two specific output
|
||
surfaces:
|
||
|
||
1. **Data tables.** Output of `show data <T>`, `insert` /
|
||
`update` / `delete` auto-show, and the future query DSL.
|
||
2. **Table structure.** Output of `create table`, `add column`,
|
||
`add 1:n relationship`, and `show table <T>`.
|
||
|
||
Relationship visualization (two structures side-by-side with
|
||
an arrow indicating the relationship) is **deliberately out
|
||
of scope**; the structure renderer here is sized so the future
|
||
relationship view can compose two of them.
|
||
|
||
The bigger UX project — V4's scrollable session log, smart
|
||
structure rendering tied to the items panel selection,
|
||
Markdown export — also remains out of scope. This ADR is a
|
||
narrow visual-quality pass on the existing output panel.
|
||
|
||
## Decision
|
||
|
||
### 1. Border style
|
||
|
||
**Header-only Unicode borders.** Outer frame plus a header
|
||
underline; data rows are separated only by the column
|
||
dividers, no per-row horizontal rule.
|
||
|
||
```
|
||
┌────┬───────┬──────────────┐
|
||
│ id │ Name │ Email │
|
||
├────┼───────┼──────────────┤
|
||
│ 1 │ Alice │ a@example.io │
|
||
│ 2 │ Bob │ b@example.io │
|
||
└────┴───────┴──────────────┘
|
||
```
|
||
|
||
Box-drawing characters: `┌─┐│└─┘├┤┬┴┼`. UTF-8 only; we
|
||
already require UTF-8 throughout the app and crossterm
|
||
handles the encoding. No ASCII fallback in v1: if a
|
||
terminal can't render box-drawing, the user has bigger
|
||
problems than table aesthetics.
|
||
|
||
Rejected:
|
||
|
||
- **Full grid (per-row separators).** Visually noisy for
|
||
typical row counts (5–50). The header underline alone is
|
||
enough to anchor the eye.
|
||
- **Minimal (no outer frame).** Cleaner, but the lack of
|
||
containment makes the table blend into surrounding output
|
||
in a cluttered session.
|
||
|
||
### 2. Column alignment
|
||
|
||
- **Numeric** (`int`, `real`, `decimal`, `serial`): right-aligned.
|
||
- **Boolean**: left-aligned (`true` / `false` are roughly
|
||
equal width and read better left-aligned next to text).
|
||
- **Everything else** (`text`, `date`, `datetime`, `blob`,
|
||
`shortid`): left-aligned.
|
||
|
||
Type is the source of truth: `DataResult` carries the
|
||
user-facing `Type` per column, looked up from the
|
||
column-metadata table (the same lookup `describe_table`
|
||
already does for `ColumnDescription.user_type`). The
|
||
existing `query_data` / `insert` / `update` / `delete`
|
||
paths in `db.rs` populate this alongside the column names
|
||
and rows so the renderer doesn't have to guess and the
|
||
display is deterministic regardless of cell content.
|
||
|
||
`DataResult` extends to:
|
||
|
||
```rust
|
||
pub struct DataResult {
|
||
pub table_name: String,
|
||
pub columns: Vec<String>,
|
||
/// User-facing type per column. Same semantics as
|
||
/// `ColumnDescription.user_type`: always populated for
|
||
/// tables created through the DSL; `None` only for the
|
||
/// edge case of a foreign-attached database we did not
|
||
/// create. The renderer falls back to left-alignment
|
||
/// when `None`.
|
||
pub column_types: Vec<Option<Type>>,
|
||
pub rows: Vec<Vec<Option<String>>>,
|
||
}
|
||
```
|
||
|
||
Structure rendering uses the same `Type` value directly
|
||
from `ColumnDescription.user_type`.
|
||
|
||
### 3. NULL and special values
|
||
|
||
- **NULL** renders as the literal text `(null)`. The
|
||
parentheses distinguish it from a string `"null"` in a
|
||
text column.
|
||
- **Newlines** in cell text are replaced by `↵` for display.
|
||
Preserves the visual single-row-per-record property; the
|
||
underlying CSV still round-trips faithfully.
|
||
- **Tab** characters in cell text are replaced by `→` for
|
||
the same reason.
|
||
- **Other control characters** (anything below `0x20`
|
||
except those handled above) replaced by `·` (middle dot).
|
||
|
||
These substitutions are display-only. The underlying data
|
||
in `playground.db` and `data/<T>.csv` is untouched; the
|
||
round-trip from `insert` → `show data` → CSV → `rebuild`
|
||
preserves the original characters.
|
||
|
||
### 4. Width and truncation
|
||
|
||
The renderer computes intrinsic column widths as
|
||
`max(header_width, max(cell_width))` per column, in
|
||
**Unicode codepoint count** (`chars().count()`). East Asian
|
||
wide characters and grapheme clusters are counted as one
|
||
codepoint each, which under-counts visual width slightly;
|
||
acceptable for v1 and consistent with how the rest of the
|
||
codebase counts widths today.
|
||
|
||
If the rendered table is wider than the output panel,
|
||
ratatui's existing line-truncation handles the right edge.
|
||
**No proactive cell truncation in this iteration.** Adding
|
||
a per-cell ellipsis pass (`…` when a cell exceeds a budget)
|
||
is a clean follow-up once we have a working renderer to
|
||
measure against; doing it up front commits to a width
|
||
budget we can't yet justify.
|
||
|
||
### 5. Structure view
|
||
|
||
Each structure rendering emits one pretty table:
|
||
|
||
```
|
||
┌──────┬────────┬─────────────┐
|
||
│ Name │ Type │ Constraints │
|
||
├──────┼────────┼─────────────┤
|
||
│ id │ serial │ PK │
|
||
│ Name │ text │ NOT NULL │
|
||
└──────┴────────┴─────────────┘
|
||
```
|
||
|
||
Columns:
|
||
|
||
- **Name** — the column name, verbatim.
|
||
- **Type** — `Type::keyword()` (e.g., `serial`, `text`,
|
||
`shortid`). Falls back to the SQLite-reported type if
|
||
metadata is missing — only happens for foreign-attached
|
||
databases we didn't create, not in normal use.
|
||
- **Constraints** — comma-separated list of declared
|
||
constraints in this priority order: `PK`, `NOT NULL`.
|
||
Future constraints (`UNIQUE`, `CHECK`, `DEFAULT` from C3)
|
||
append to the list as they land. Empty when no
|
||
constraints apply (rendered as a single space, which the
|
||
border still frames).
|
||
|
||
Foreign-key references are **not** rendered inside the
|
||
constraint column. They live in a separate `References:` /
|
||
`Referenced by:` block below the structure table, formatted
|
||
as today's plain-text indented list. The rationale is that
|
||
relationships are a multi-table concept and deserve their
|
||
own visualization (the future "two structures + arrow"
|
||
layout); flattening them into a single column would
|
||
foreclose that design.
|
||
|
||
The block-level layout under a single command:
|
||
|
||
```
|
||
[ok] create table Customers
|
||
┌──────┬────────┬─────────────┐
|
||
│ Name │ Type │ Constraints │
|
||
├──────┼────────┼─────────────┤
|
||
│ id │ serial │ PK │
|
||
│ Name │ text │ │
|
||
└──────┴────────┴─────────────┘
|
||
References:
|
||
cust_id → Orders.cust_id (cust_orders, on delete cascade, on update no_action)
|
||
Referenced by:
|
||
Orders.cust_id → id (cust_orders, on delete cascade, on update no_action)
|
||
```
|
||
|
||
The relationship sections retain today's plain-text format
|
||
to leave room for the future relationship-rendering ADR.
|
||
|
||
> **Superseded.** ADR-0044 replaced this prose block with compact
|
||
> diagrams on relationship-subject surfaces (`show table`,
|
||
> `add`/`drop relationship`). **ADR-0050 (2026-06-12, issue #28)** then
|
||
> removed the relationship block entirely from incidental-DDL structure
|
||
> echoes (`create table`, `add`/`drop`/`rename`/`change column`,
|
||
> `add`/`drop index`) — those render structure only — and **deleted the
|
||
> prose renderer**. The `References:` / `Referenced by:` format above is
|
||
> retained here as documentation/provenance should the OOS-7
|
||
> always-prose display setting ever be built.
|
||
|
||
### 6. Theme integration
|
||
|
||
Theme colors apply to the box-drawing characters via the
|
||
existing output-panel rendering path (the lines go through
|
||
`OutputLine` with `OutputKind::System` styling). No
|
||
per-cell theming in this iteration — that's V4 territory
|
||
(NFR-5 mentions "query result types" colouring, which
|
||
implies per-cell awareness; we set that up but don't ship
|
||
it yet).
|
||
|
||
The chosen border characters render legibly on both light
|
||
and dark backgrounds in standard terminals. NFR-7 is
|
||
inherited rather than newly designed for.
|
||
|
||
### 7. Implementation
|
||
|
||
**Hand-rolled.** Consistent with the project's pattern of
|
||
narrow hand-rolled writers (CSV serializer, YAML writer,
|
||
history.log appender). A crate (`tabled`, `comfy-table`)
|
||
would save ~150 lines but cost ~70–150 KB of binary,
|
||
remove direct control over the substitutions in §3, and
|
||
add a dependency that's worth carrying only if our needs
|
||
later outgrow what we hand-roll.
|
||
|
||
The renderer lives in a new module `src/output_render.rs`
|
||
(distinct from `src/ui.rs`, which is the ratatui Frame
|
||
renderer for the whole TUI layout). Public surface:
|
||
|
||
```rust
|
||
pub fn render_data_table(data: &DataResult) -> Vec<String>;
|
||
pub fn render_structure(desc: &TableDescription) -> Vec<String>;
|
||
```
|
||
|
||
Each returns a `Vec<String>`, one display row per element
|
||
— matches the existing `OutputLine`-per-line discipline so
|
||
scroll math remains accurate.
|
||
|
||
Internal helpers (border characters, padding, alignment
|
||
detection, control-character substitution) are private to
|
||
the module.
|
||
|
||
### 8. Testing
|
||
|
||
- **Unit tests** in `output_render.rs` covering: each
|
||
alignment rule, NULL rendering, control-character
|
||
substitution, the structure layout for a representative
|
||
schema, and width computation under multibyte input.
|
||
- **Snapshot tests** via `insta` for the canonical examples
|
||
(a 2-column data table, a 3-column structure with PK +
|
||
NOT NULL, a structure with relationships). The snapshots
|
||
pin the exact rendered output and catch unintended drift.
|
||
- **Existing integration tests** (Tier-3) continue to
|
||
exercise the call sites; their assertions on output text
|
||
may need updates where they currently match
|
||
pipe-separator format.
|
||
|
||
### 9. Out of scope
|
||
|
||
- **OOS-1.** Relationship visualization (two structures +
|
||
arrow). Its own ADR; will compose `render_structure`.
|
||
- **OOS-2.** V4's scrollable session log + Markdown export.
|
||
- **OOS-3.** Per-cell theming for query-result type
|
||
highlighting (NFR-5 partial — sets up the architecture,
|
||
defers the colours until the query DSL ships).
|
||
- **OOS-4.** Cell-level truncation with ellipsis.
|
||
- **OOS-5.** ASCII fallback for terminals that can't
|
||
render box-drawing characters.
|
||
|
||
## Consequences
|
||
|
||
- The two most-frequented output surfaces (data results,
|
||
structure listings) gain a polished, distinctive look
|
||
matching NFR-4.
|
||
- The renderer module is a stable dependency for future
|
||
work: relationship visualization composes
|
||
`render_structure`; query-result rendering reuses
|
||
`render_data_table` (with eventual type-aware
|
||
alignment); V4's session log adds chrome around
|
||
whatever this module produces.
|
||
- `DataResult` gains a `column_types` field; the four
|
||
call sites in `db.rs` (`query_data`, `insert`, `update`,
|
||
`delete`) populate it from the same metadata table
|
||
`describe_table` already consults. No new query paths.
|
||
- Light/dark theme legibility is inherited from the
|
||
current output-panel styling. Per-cell theming is
|
||
deliberately deferred.
|
||
- Shipping this *before* the query-DSL ADR means the new
|
||
command's results land already looking polished, which
|
||
is the right order: pedagogy is worse when the
|
||
visualisation lags the feature.
|