From e4ada07dab480c3b60236e21b09d787e4c54483d Mon Sep 17 00:00:00 2001 From: mptyl Date: Fri, 3 Jul 2026 14:13:02 +0200 Subject: [PATCH] feat(viewer): erDiagram shows real column types when present; ArtifactView accepts a {content} sql wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit erDiagram reads data_type/type from the candidate (top-level or signals), falling back to the generic 'col' token Mermaid needs — real types render as soon as schema_linking.json carries them. ArtifactView gains a defensive {content} fallback for sql-kind artifacts, verified against real session shapes. Co-Authored-By: Claude Opus 4.8 --- frontend/src/viewers/ArtifactView.test.tsx | 5 +++++ frontend/src/viewers/ArtifactView.tsx | 1 + frontend/src/viewers/SchemaLinkingViewer.test.tsx | 13 +++++++++++++ frontend/src/viewers/SchemaLinkingViewer.tsx | 15 ++++++++++++++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/viewers/ArtifactView.test.tsx b/frontend/src/viewers/ArtifactView.test.tsx index 03342f4c..ccf02393 100644 --- a/frontend/src/viewers/ArtifactView.test.tsx +++ b/frontend/src/viewers/ArtifactView.test.tsx @@ -10,6 +10,11 @@ test("sql artifact renders the SQL text", () => { expect(screen.getByText("SELECT 1")).toBeInTheDocument(); }); +test("sql artifact accepts a { content } wrapper", () => { + render(); + expect(screen.getByText("SELECT 7")).toBeInTheDocument(); +}); + test("cte_plan renders an ordered list of names", () => { render(); expect(screen.getByText("a_cte")).toBeInTheDocument(); diff --git a/frontend/src/viewers/ArtifactView.tsx b/frontend/src/viewers/ArtifactView.tsx index d1b00643..14903191 100644 --- a/frontend/src/viewers/ArtifactView.tsx +++ b/frontend/src/viewers/ArtifactView.tsx @@ -38,6 +38,7 @@ function toSqlBlocks(raw: unknown): SqlBlock[] | null { const rec = asRecord(raw); if (!rec) return null; if (typeof rec.sql === "string") return [{ name: "SQL", sql: rec.sql }]; + if (typeof rec.content === "string") return [{ name: "SQL", sql: rec.content }]; if (Array.isArray(rec.ctes)) { const blocks = rec.ctes .map((c) => asRecord(c)) diff --git a/frontend/src/viewers/SchemaLinkingViewer.test.tsx b/frontend/src/viewers/SchemaLinkingViewer.test.tsx index 8a706196..a5e1a699 100644 --- a/frontend/src/viewers/SchemaLinkingViewer.test.tsx +++ b/frontend/src/viewers/SchemaLinkingViewer.test.tsx @@ -97,3 +97,16 @@ test("(e) buildErDiagram emits entities, attributes and a relation", () => { expect(def).toContain("col id"); expect(def).toContain("orders }o--o{ customers : join"); }); + +test("(f) buildErDiagram uses a column's real type when the data carries one", () => { + const def = buildErDiagram( + [ + { kind: "table", name: "orders", decision: "promoted" }, + { kind: "column", name: "orders.total", decision: "promoted", signals: { data_type: "numeric" } }, + { kind: "column", name: "orders.id", decision: "promoted" }, // no type -> generic "col" + ], + [], + ); + expect(def).toContain("numeric total"); + expect(def).toContain("col id"); +}); diff --git a/frontend/src/viewers/SchemaLinkingViewer.tsx b/frontend/src/viewers/SchemaLinkingViewer.tsx index cc9c6b1c..b8f18074 100644 --- a/frontend/src/viewers/SchemaLinkingViewer.tsx +++ b/frontend/src/viewers/SchemaLinkingViewer.tsx @@ -33,6 +33,19 @@ function reasonFor(c: Candidate): string { return ""; } +// The real DB type for a promoted column, when the schema-linking data carries it +// (`data_type`/`type`, at top level or under `signals`). Today's schema_linking.json +// has no column types, so this falls back to the generic `col` token that Mermaid +// still needs as the attribute type; a real type shows as soon as the pipeline +// provides one. The token is sanitized to keep the erDiagram valid. +function attrType(c: Candidate): string { + const sig = (c.signals ?? {}) as Record; + const rec = c as unknown as Record; + const raw = sig["data_type"] ?? sig["type"] ?? rec["data_type"] ?? rec["type"]; + const t = typeof raw === "string" && raw.trim() ? raw.trim() : "col"; + return t.replace(/[^a-zA-Z0-9]/g, "_"); +} + export function buildErDiagram(promoted: Candidate[], joins: Join[]): string { const sanitize = (name: string) => name.replace(/[^a-zA-Z0-9]/g, "_"); const tables = promoted.filter((c) => c.kind === "table"); @@ -46,7 +59,7 @@ export function buildErDiagram(promoted: Candidate[], joins: Join[]): string { const cols = columns.filter((col) => col.name.startsWith(t.name + ".")); lines.push(` ${id} {`); for (const col of cols) { - lines.push(` col ${sanitize(col.name.slice(t.name.length + 1))}`); + lines.push(` ${attrType(col)} ${sanitize(col.name.slice(t.name.length + 1))}`); } lines.push(` }`); }