feat(viewer): erDiagram shows real column types when present; ArtifactView accepts a {content} sql wrapper

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 14:13:02 +02:00
co-authored by Claude Opus 4.8
parent c3e8f14ad9
commit e4ada07dab
4 changed files with 33 additions and 1 deletions
@@ -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(<ArtifactView artifact={{ kind: "sql", data: { content: "SELECT 7" } }} />);
expect(screen.getByText("SELECT 7")).toBeInTheDocument();
});
test("cte_plan renders an ordered list of names", () => {
render(<ArtifactView artifact={{ kind: "cte_plan", data: { names: ["a_cte", "b_cte"] } }} />);
expect(screen.getByText("a_cte")).toBeInTheDocument();
+1
View File
@@ -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))
@@ -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");
});
+14 -1
View File
@@ -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<string, unknown>;
const rec = c as unknown as Record<string, unknown>;
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(` }`);
}