Files
rdbms-playground/docs/adr/0016-pretty-table-rendering.md
T
claude@clouddev1 8ac3537df0 feat(render): incidental-DDL confirmations show structure only, no relationships (#28)
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.
2026-06-12 22:45:18 +00:00

300 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (550). 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 ~70150 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.