feat: show relationship <name> renders a styled two-table diagram (ADR-0044)

The first wired slice of relationship visualization (V1). `show
relationship <name>` now renders the relationship as two full
structure boxes joined by a width-jogging connector (child-left /
parent-right, n…1 cardinality, on delete/update actions), styled
App-side, with a vertical-stack fallback for narrow terminals.

- db.rs: RelationshipDiagramData + show_relationship worker path
  (structured data: the relationship + both endpoint TableDescriptions)
- runtime.rs: named relationships route to the structured outcome
  (boxed); other show <kind> forms stay prose
- app.rs/event.rs/ui.rs: DslShowRelationshipSucceeded rendered App-side;
  new diagram OutputStyleClass variants; App::last_output_width from ui.rs
- output_render.rs: styled Seg layout engine (boxes, connector routing,
  side-by-side + vertical), composing the ADR-0016 box primitives

Tests: 4 unit + 4 integration; full suite 2201 pass / 0 fail / 1 ignored;
clippy nursery clean. requirements.md V1 stays [/] (show table diagrams,
compound routing, DDL-echo wiring remain).
This commit is contained in:
claude@clouddev1
2026-06-09 22:27:39 +00:00
parent bb02dfb752
commit cad90ec4a5
8 changed files with 756 additions and 1 deletions
+97
View File
@@ -357,3 +357,100 @@ fn app_renders_show_list_lines_as_system_output() {
"item line rendered",
);
}
// =================================================================
// ADR-0044 — `show relationship <name>` renders a diagram
// =================================================================
#[test]
fn show_relationship_worker_returns_structured_diagram_data() {
let (_p, db, _dir) = open_project_db();
let rt = rt();
rt.block_on(seed_schema(&db));
let data = rt
.block_on(db.show_relationship("orders_customer".to_string()))
.expect("show_relationship ok")
.expect("relationship found");
assert_eq!(data.rel.name, "orders_customer");
// child = FK holder, parent = referenced (ADR-0044 left/right).
assert_eq!(data.child.name, "Orders");
assert_eq!(data.parent.name, "Customers");
assert_eq!(data.rel.child_columns, vec!["customer_id".to_string()]);
assert_eq!(data.rel.parent_columns, vec!["id".to_string()]);
}
#[test]
fn show_relationship_worker_returns_none_for_unknown_name() {
let (_p, db, _dir) = open_project_db();
let rt = rt();
rt.block_on(seed_schema(&db));
assert!(
rt.block_on(db.show_relationship("nope".to_string()))
.expect("ok")
.is_none(),
"unknown relationship → None",
);
}
#[test]
fn app_renders_show_relationship_as_a_styled_diagram() {
let (_p, db, _dir) = open_project_db();
let rt = rt();
rt.block_on(seed_schema(&db));
let data = rt
.block_on(db.show_relationship("orders_customer".to_string()))
.expect("ok")
.expect("found");
let mut app = App::new();
app.output.push_back(rdbms_playground::app::OutputLine::echo(
"show relationship orders_customer",
Mode::Simple,
));
app.update(AppEvent::DslShowRelationshipSucceeded {
command: Command::ShowList {
kind: ShowListKind::Relationships,
name: Some("orders_customer".to_string()),
},
data: Some(data),
});
let text: String = app
.output
.iter()
.map(|l| l.text.as_str())
.collect::<Vec<_>>()
.join("\n");
// Both tables, box-drawing, the connector arrow, the actions line.
assert!(text.contains("Orders"), "child box: {text}");
assert!(text.contains("Customers"), "parent box: {text}");
assert!(text.contains('┌') && text.contains('│'), "box drawing: {text}");
assert!(text.contains('▶'), "connector arrow: {text}");
assert!(text.contains("on delete cascade"), "actions: {text}");
// The diagram lines are styled (per-span runs), not plain system.
assert!(
app.output.iter().any(|l| l.styled_runs.is_some()),
"diagram lines carry styled runs",
);
}
#[test]
fn app_show_relationship_not_found_shows_friendly_line() {
let mut app = App::new();
app.output.push_back(rdbms_playground::app::OutputLine::echo(
"show relationship nope",
Mode::Simple,
));
app.update(AppEvent::DslShowRelationshipSucceeded {
command: Command::ShowList {
kind: ShowListKind::Relationships,
name: Some("nope".to_string()),
},
data: None,
});
assert!(
app.output
.iter()
.any(|l| l.text == "No relationship named `nope`."),
"friendly not-found line",
);
}