ER/Schema Visualizer — C source
Paste CREATE TABLE DDL and get an ER diagram as SVG: tables with typed columns, primary keys, and foreign-key arrows in a deterministic layered layout. Pan and zoom the live diagram; export the SVG.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* schema-visualizer — pure CREATE TABLE DDL → layered ER diagram as SVG.
* C port (canonical TS: src/lib/schema-visualizer.ts; Go twin:
* cli/schema-visualizer). Tolerant common subset of Postgres/MySQL/SQLite:
* unparseable statements degrade to notes, never abort. Integer geometry only
* (half-up rounding — JS Math.round parity), so every port draws the
* byte-identical diagram. Caller frees the result with sv_free_result(). */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- small growable string (house style: see csv-to-sql/c.c) ---- */
typedef struct { char *buf; size_t len, cap; } Str;
static void str_reserve(Str *s, size_t need) {
if (need >= s->cap) {
size_t cap = s->cap ? s->cap : 64;
while (need >= cap) cap *= 2;
s->buf = realloc(s->buf, cap);
s->cap = cap;
}
}
static void str_push(Str *s, char c) {
str_reserve(s, s->len + 1);
s->buf[s->len++] = c;
s->buf[s->len] = '\0';
}
static void str_app(Str *s, const char *t) {
size_t tl = strlen(t);
str_reserve(s, s->len + tl);
memcpy(s->buf + s->len, t, tl + 1);
s->len += tl;
}
/* ---- shared helpers ---- */
/* Layout constants (LAYOUT in the TS/JS/… ports). */
enum { ROW_H = 24, CHAR_W = 7, PAD = 8, LAYER_GAP = 60, COL_GAP = 40 };
/* Half-up rounding: (int)(v + 0.5), JS Math.round parity. */
static int sv_round(double v) { return (int)(v + 0.5); }
/* ASCII case-insensitive equality (JS/Python toUpperCase + == on keywords). */
static int ci_eq(const char *a, const char *b) {
while (*a && *b) {
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) return 0;
a++; b++;
}
return *a == *b;
}
/* JS \s for the ASCII range (space, \t, \n, \v, \f, \r). */
static int sv_isspace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static const char *sv_trim(const char *s, size_t n, size_t *out_n) {
size_t b = 0, e = n;
while (b < e && sv_isspace(s[b])) b++;
while (e > b && sv_isspace(s[e - 1])) e--;
*out_n = e - b;
return s + b;
}
/* ---- statement splitting: `;` outside strings/quoted identifiers ---- */
typedef struct { const char *s; size_t n; } Slice;
typedef struct { Slice *v; size_t n; } Slices;
static void slices_push(Slices *l, Slice s) {
l->v = realloc(l->v, (l->n + 1) * sizeof(Slice));
l->v[l->n++] = s;
}
static int is_quote_open(char c) { return c == '\'' || c == '"' || c == '`' || c == '['; }
static Slices split_statements(const char *ddl) {
Slices out = {0};
size_t i = 0, n = strlen(ddl), start = 0;
while (i < n) {
char ch = ddl[i];
if (is_quote_open(ch)) {
char close = ch == '[' ? ']' : ch;
i++;
while (i < n) {
if (ddl[i] == close) {
if (close == '\'' && i + 1 < n && ddl[i + 1] == '\'') { i += 2; continue; }
break;
}
i++;
}
i++;
continue;
}
if (ch == ';') {
slices_push(&out, (Slice){ ddl + start, i - start });
i++;
start = i;
continue;
}
i++;
}
size_t tail;
sv_trim(ddl + start, n - start, &tail);
if (tail) slices_push(&out, (Slice){ ddl + start, n - start });
return out;
}
/* ---- tokenizer ---- */
typedef enum { TK_WORD, TK_STRING, TK_QIDENT, TK_PUNCT } TokKind;
typedef struct { TokKind kind; char *text; } Tok;
typedef struct { Tok *v; size_t n; } TokOwn; /* owns array + texts */
typedef struct { const Tok *v; size_t n; } Toks; /* borrowed view */
static void tok_push(TokOwn *l, TokKind kind, char *owned_text) {
l->v = realloc(l->v, (l->n + 1) * sizeof(Tok));
l->v[l->n].kind = kind;
l->v[l->n].text = owned_text;
l->n++;
}
static void free_tokens(TokOwn *t) {
for (size_t i = 0; i < t->n; i++) free(t->v[i].text);
free(t->v);
*t = (TokOwn){0};
}
static int word_break(char c) {
return sv_isspace(c) || c == '\'' || c == '"' || c == ',' || c == '(' ||
c == ')' || c == '.' || c == '`' || c == '[' || c == ']';
}
/* Quoted identifiers/strings carry their text (quotes stripped); ( ) , . are
* punct; everything else is a word. */
static TokOwn tokenize(const char *s, size_t n) {
TokOwn toks = {0};
size_t i = 0;
while (i < n) {
char ch = s[i];
if (sv_isspace(ch)) { i++; continue; }
if (is_quote_open(ch)) {
char close = ch == '[' ? ']' : ch;
Str text = {0};
i++;
while (i < n) {
if (s[i] == close) {
if (close == '\'' && i + 1 < n && s[i + 1] == '\'') {
str_push(&text, '\'');
i += 2;
continue;
}
break;
}
str_push(&text, s[i++]);
}
i++;
tok_push(&toks, ch == '\'' ? TK_STRING : TK_QIDENT, text.buf);
continue;
}
if (ch == '(' || ch == ')' || ch == ',' || ch == '.') {
char *p = malloc(2);
p[0] = ch; p[1] = '\0';
tok_push(&toks, TK_PUNCT, p);
i++;
continue;
}
Str w = {0};
while (i < n && !word_break(s[i])) str_push(&w, s[i++]);
tok_push(&toks, TK_WORD, w.buf);
}
return toks;
}
static const Tok *g(const Toks *t, size_t i) { return i < t->n ? &t->v[i] : NULL; }
static int is_p(const Tok *t, const char *p) {
return t && t->kind == TK_PUNCT && strcmp(t->text, p) == 0;
}
static int kw(const Tok *t, const char *w) {
return t && t->kind == TK_WORD && ci_eq(t->text, w);
}
/* ---- schema model ---- */
typedef struct { char *name, *type; int nullable, is_pk; } Column;
typedef struct { char *name; Column *cols; size_t ncols; } Table;
typedef struct { Table *v; size_t n; } Tables;
typedef struct { char *from_table, *from_col, *to_table, *to_col; } Fk; /* to_col NULL = omitted */
typedef struct { Fk *v; size_t n; } Fks;
typedef struct { Tables tables; Fks fks; char **notes; size_t nnotes; } Schema;
static void tables_push(Tables *l, Table t) {
l->v = realloc(l->v, (l->n + 1) * sizeof(Table));
l->v[l->n++] = t;
}
static void fks_push(Fks *l, Fk f) {
l->v = realloc(l->v, (l->n + 1) * sizeof(Fk));
l->v[l->n++] = f;
}
static void notes_push(Schema *sc, const char *msg) {
sc->notes = realloc(sc->notes, (sc->nnotes + 1) * sizeof(char *));
sc->notes[sc->nnotes++] = strdup(msg);
}
static void table_add_col(Table *t, Column c) {
t->cols = realloc(t->cols, (t->ncols + 1) * sizeof(Column));
t->cols[t->ncols++] = c;
}
typedef struct { char **v; size_t n; } Names;
static void names_push(Names *l, char *owned) {
l->v = realloc(l->v, (l->n + 1) * sizeof(char *));
l->v[l->n++] = owned;
}
static void free_names(Names *l) {
for (size_t i = 0; i < l->n; i++) free(l->v[i]);
free(l->v);
*l = (Names){0};
}
/* ---- name / paren-list consumers ---- */
/* Consume an identifier (quoted or bare, dotted). Returns 0 when absent. */
static int take_name(const Toks *t, size_t i, Str *name, size_t *end) {
const Tok *first = g(t, i);
if (!first || (first->kind != TK_QIDENT && first->kind != TK_WORD)) return 0;
str_app(name, first->text);
size_t j = i + 1;
while (is_p(g(t, j), ".") && g(t, j + 1) &&
(g(t, j + 1)->kind == TK_QIDENT || g(t, j + 1)->kind == TK_WORD)) {
str_push(name, '.');
str_app(name, g(t, j + 1)->text);
j += 2;
}
*end = j;
return 1;
}
/* Comma-separated identifiers inside a paren group at tokens[i] === '('.
* On failure the out list is left empty. */
static int paren_list(const Toks *t, size_t i, Names *out, size_t *end) {
if (!is_p(g(t, i), "(")) return 0;
size_t j = i + 1;
for (;;) {
Str nm = {0};
size_t next;
if (!take_name(t, j, &nm, &next)) {
free(nm.buf);
free_names(out);
return 0;
}
names_push(out, nm.buf);
j = next;
if (is_p(g(t, j), ",")) { j++; continue; }
if (is_p(g(t, j), ")")) { *end = j + 1; return 1; }
free_names(out);
return 0;
}
}
/* ---- column line ---- */
static const char *const MODIFIERS[] = {
"NOT", "NULL", "PRIMARY", "KEY", "UNIQUE", "DEFAULT", "REFERENCES",
"AUTO_INCREMENT", "AUTOINCREMENT", "ON", "COMMENT", "CHECK", "CONSTRAINT", NULL,
};
static int is_modifier(const Tok *t) {
if (!t || t->kind != TK_WORD) return 0;
for (size_t i = 0; MODIFIERS[i]; i++) if (ci_eq(t->text, MODIFIERS[i])) return 1;
return 0;
}
/* Join type tokens with single spaces, collapse whitespace around ( ) and ,
* (the ports' three regex subs), trim, then upper-case in place. */
static void join_type(const Toks *toks, Str *out) {
Str raw = {0};
for (size_t i = 0; i < toks->n; i++) {
if (i) str_push(&raw, ' ');
str_app(&raw, toks->v[i].text);
}
for (size_t i = 0; i < raw.len; i++) {
char c = raw.buf[i];
if (c == '(' || c == ')' || c == ',') {
while (out->len && sv_isspace(out->buf[out->len - 1])) out->buf[--out->len] = '\0';
str_push(out, c);
while (i + 1 < raw.len && sv_isspace(raw.buf[i + 1])) i++;
} else str_push(out, c);
}
while (out->len && sv_isspace(out->buf[0])) {
memmove(out->buf, out->buf + 1, out->len);
out->buf[--out->len] = '\0';
}
while (out->len && sv_isspace(out->buf[out->len - 1])) out->buf[--out->len] = '\0';
for (char *p = out->buf ? out->buf : ""; *p; p++) *p = (char)toupper((unsigned char)*p);
free(raw.buf);
}
static int parse_column(const Toks *line, const char *table, Fks *fks, Column *out) {
Str name = {0};
size_t i = 0;
if (!take_name(line, 0, &name, &i)) { free(name.buf); return 0; }
size_t m = i;
while (m < line->n && !is_modifier(&line->v[m])) m++;
Toks type_toks = { line->v + i, m - i };
Str type = {0};
join_type(&type_toks, &type);
int nullable = 1, pk = 0;
i = m;
while (i < line->n) {
const Tok *t = &line->v[i];
if (kw(t, "NOT") && kw(g(line, i + 1), "NULL")) { nullable = 0; i += 2; continue; }
if (kw(t, "NULL")) { i++; continue; }
if (kw(t, "PRIMARY") && kw(g(line, i + 1), "KEY")) { pk = 1; nullable = 0; i += 2; continue; }
if (kw(t, "UNIQUE") || kw(t, "AUTO_INCREMENT") || kw(t, "AUTOINCREMENT")) { i++; continue; }
if (kw(t, "DEFAULT")) {
i++;
if (is_p(g(line, i), "(")) {
int depth = 0;
while (i < line->n) {
if (is_p(&line->v[i], "(")) depth++;
if (is_p(&line->v[i], ")")) depth--;
i++;
if (depth == 0) break;
}
} else if (i < line->n) i++;
continue;
}
if (kw(t, "COMMENT")) { i++; if (i < line->n && line->v[i].kind == TK_STRING) i++; continue; }
if (kw(t, "ON")) {
i += 2;
if (kw(g(line, i), "SET") || kw(g(line, i), "NO")) i += 2;
else if (i < line->n) i++;
continue;
}
if (kw(t, "REFERENCES")) {
i++;
Str target = {0};
size_t j;
if (take_name(line, i, &target, &j)) {
i = j;
char *to_col = NULL;
if (is_p(g(line, i), "(")) {
Names list = {0};
size_t k;
if (paren_list(line, i, &list, &k)) {
to_col = list.v[0];
i = k;
for (size_t q = 1; q < list.n; q++) free(list.v[q]);
free(list.v);
}
}
Fk fk = { strdup(table), strdup(name.buf), strdup(target.buf), to_col };
fks_push(fks, fk);
}
free(target.buf);
continue;
}
i++; /* unknown modifier tolerated */
}
out->name = name.buf;
out->type = type.buf;
out->nullable = nullable;
out->is_pk = pk;
return 1;
}
/* ---- statement ---- */
/* First token is a word equal (case-insensitively) to w. */
static int line_starts_with(const Toks *line, const char *w) {
return line->n > 0 && line->v[0].kind == TK_WORD && ci_eq(line->v[0].text, w);
}
/* Half-open token index range [start, end). */
typedef struct { size_t start, end; } Range;
static void parse_statement(const Toks *toks, Schema *sc) {
size_t i = 0, nj = 0, body_start = 0, body_end = 0, depth = 0, start = 0;
Str name = {0};
Table tb = {0};
Range *lines = NULL;
size_t nlines = 0;
if (!kw(g(toks, i), "CREATE")) goto fail;
i++;
while (kw(g(toks, i), "TEMP") || kw(g(toks, i), "TEMPORARY") || kw(g(toks, i), "UNLOGGED")) i++;
if (!kw(g(toks, i), "TABLE")) { notes_push(sc, "Skipped non-table statement."); return; }
i++;
if (kw(g(toks, i), "IF") && kw(g(toks, i + 1), "NOT") && kw(g(toks, i + 2), "EXISTS")) i += 3;
if (!take_name(toks, i, &name, &nj) || !is_p(g(toks, nj), "(")) goto fail;
i = nj + 1;
/* body = tokens until the matching close paren (depth-aware) */
body_start = i;
for (; i < toks->n; i++) {
if (is_p(&toks->v[i], "(")) depth++;
if (is_p(&toks->v[i], ")")) {
if (depth == 0) break;
depth--;
}
}
if (i >= toks->n) goto fail;
body_end = i;
/* split the body into top-level comma lines */
depth = 0;
start = body_start;
for (size_t k = body_start; k < body_end; k++) {
if (is_p(&toks->v[k], "(")) depth++;
if (is_p(&toks->v[k], ")")) depth--;
if (is_p(&toks->v[k], ",") && depth == 0) {
lines = realloc(lines, (nlines + 1) * sizeof(Range));
lines[nlines].start = start;
lines[nlines].end = k;
nlines++;
start = k + 1;
}
}
if (body_end > start) {
lines = realloc(lines, (nlines + 1) * sizeof(Range));
lines[nlines].start = start;
lines[nlines].end = body_end;
nlines++;
}
tb.name = strdup(name.buf);
for (size_t li = 0; li < nlines; li++) {
Toks line = { toks->v + lines[li].start, lines[li].end - lines[li].start };
if (line.n == 0) continue;
if (line_starts_with(&line, "PRIMARY") && kw(g(&line, 1), "KEY")) {
Names list = {0};
size_t end;
if (paren_list(&line, 2, &list, &end)) {
for (size_t q = 0; q < list.n; q++)
for (size_t c = 0; c < tb.ncols; c++)
if (strcmp(tb.cols[c].name, list.v[q]) == 0) {
tb.cols[c].is_pk = 1;
tb.cols[c].nullable = 0;
break; /* first matching column, as the TS/JS ports */
}
free_names(&list);
}
continue;
}
if (line_starts_with(&line, "FOREIGN") && kw(g(&line, 1), "KEY")) {
Names from = {0}, to = {0};
size_t fend = 0, tend = 0;
Str target = {0};
if (paren_list(&line, 2, &from, &fend) && kw(g(&line, fend), "REFERENCES") &&
take_name(&line, fend + 1, &target, &tend)) {
/* omitted target columns fall back per-index, last one repeating */
if (is_p(g(&line, tend), "(")) paren_list(&line, tend, &to, &tend);
for (size_t q = 0; q < from.n; q++) {
char *tc = NULL;
if (to.n) tc = strdup(q < to.n ? to.v[q] : to.v[to.n - 1]);
Fk fk = { strdup(tb.name), strdup(from.v[q]), strdup(target.buf), tc };
fks_push(&sc->fks, fk);
}
}
free_names(&from);
free_names(&to);
free(target.buf);
continue;
}
if (line_starts_with(&line, "UNIQUE") || line_starts_with(&line, "KEY") ||
line_starts_with(&line, "INDEX") || line_starts_with(&line, "CHECK") ||
line_starts_with(&line, "EXCLUDE") || line_starts_with(&line, "CONSTRAINT")) continue;
Column col;
if (parse_column(&line, tb.name, &sc->fks, &col)) table_add_col(&tb, col);
}
tables_push(&sc->tables, tb);
free(name.buf);
free(lines);
return;
fail:
free(name.buf);
free(lines);
notes_push(sc, "Skipped unparseable statement.");
}
/* ---- DDL → schema ---- */
/* Index of the first table named `name`, or -1 (the ports' index map). */
static long index_of(const Schema *sc, const char *name) {
for (size_t i = 0; i < sc->tables.n; i++)
if (strcmp(sc->tables.v[i].name, name) == 0) return (long)i;
return -1;
}
static void parse_ddl(const char *ddl, Schema *sc) {
size_t trimmed = 0;
sv_trim(ddl, strlen(ddl), &trimmed);
if (trimmed == 0) { notes_push(sc, "No DDL input."); return; }
Slices stmts = split_statements(ddl);
for (size_t i = 0; i < stmts.n; i++) {
size_t n = 0;
const char *s = sv_trim(stmts.v[i].s, stmts.v[i].n, &n);
if (n == 0) continue;
TokOwn own = tokenize(s, n);
Toks toks = { own.v, own.n };
parse_statement(&toks, sc);
free_tokens(&own);
}
free(stmts.v);
/* resolve omitted FK target columns to the referenced table's first PK */
for (size_t i = 0; i < sc->fks.n; i++) {
Fk *fk = &sc->fks.v[i];
if (fk->to_col) continue;
long t = index_of(sc, fk->to_table);
const char *pk = "id";
if (t >= 0)
for (size_t c = 0; c < (size_t)sc->tables.v[t].ncols; c++)
if (sc->tables.v[t].cols[c].is_pk) { pk = sc->tables.v[t].cols[c].name; break; }
fk->to_col = strdup(pk);
}
}
/* ---- layout ---- */
typedef struct { int x, y, w, h; } Box;
/* The edge's `fk "col → col"` label is computed but never rendered, so the C
* port carries only the path plus a borrowed FK pointer. */
typedef struct { const Fk *fk; char *path; } Edge;
typedef struct { const Table *table; Box box; Box title_bar; Box *rows; } Laid;
typedef struct { Laid *tables; size_t ntables; Edge *edges; size_t nedges; int w, h; } Geo;
static void geo_free(Geo *geo) {
for (size_t i = 0; i < geo->ntables; i++) free((void *)geo->tables[i].rows);
free(geo->tables);
for (size_t i = 0; i < geo->nedges; i++) free(geo->edges[i].path);
free(geo->edges);
geo->tables = NULL;
geo->edges = NULL;
}
static Geo layout_schema(const Schema *sc) {
Geo geo = {0};
size_t n = sc->tables.n;
if (n == 0) return geo;
Box *boxes = calloc(n, sizeof(Box));
for (size_t i = 0; i < n; i++) {
const Table *t = &sc->tables.v[i];
size_t text_len = strlen(t->name);
for (size_t c = 0; c < t->ncols; c++) {
size_t cl = strlen(t->cols[c].name) + 1 + strlen(t->cols[c].type);
if (cl > text_len) text_len = cl;
}
if (text_len < 1) text_len = 1;
boxes[i].w = sv_round((double)text_len * CHAR_W + 2 * PAD);
boxes[i].h = sv_round((double)ROW_H * (double)(1 + t->ncols) + PAD);
}
/* layer assignment: longest FK chain, fixed point over at most n passes */
int *layer_of = calloc(n, sizeof(int));
for (size_t pass = 0; pass < n; pass++) {
int changed = 0;
for (size_t k = 0; k < sc->fks.n; k++) {
const Fk *fk = &sc->fks.v[k];
long ti = index_of(sc, fk->from_table), tj = index_of(sc, fk->to_table);
if (ti < 0 || tj < 0 || ti == tj) continue;
if (layer_of[ti] < layer_of[tj] + 1) { layer_of[ti] = layer_of[tj] + 1; changed = 1; }
}
if (!changed) break;
}
int max_layer = 0;
for (size_t i = 0; i < n; i++) if (layer_of[i] > max_layer) max_layer = layer_of[i];
int y = 0;
for (int li = 0; li <= max_layer; li++) {
int x = 0, layer_h = 0;
for (size_t i = 0; i < n; i++) {
if (layer_of[i] != li) continue;
boxes[i].x = x;
boxes[i].y = y;
x += boxes[i].w + COL_GAP;
if (boxes[i].h > layer_h) layer_h = boxes[i].h;
}
if (x - COL_GAP > geo.w) geo.w = x - COL_GAP;
if (y + layer_h > geo.h) geo.h = y + layer_h;
y += layer_h + LAYER_GAP;
}
free(layer_of);
geo.tables = calloc(n, sizeof(Laid));
geo.ntables = n;
for (size_t i = 0; i < n; i++) {
Laid *l = &geo.tables[i];
const Table *t = &sc->tables.v[i];
l->table = t;
l->box = boxes[i];
l->title_bar = (Box){ boxes[i].x, boxes[i].y, boxes[i].w, ROW_H };
l->rows = t->ncols ? calloc(t->ncols, sizeof(Box)) : NULL;
for (size_t c = 0; c < t->ncols; c++)
l->rows[c] = (Box){ boxes[i].x, boxes[i].y + ROW_H * (int)(1 + c), boxes[i].w, ROW_H };
}
geo.edges = sc->fks.n ? calloc(sc->fks.n, sizeof(Edge)) : NULL;
for (size_t k = 0; k < sc->fks.n; k++) {
const Fk *fk = &sc->fks.v[k];
long fi = index_of(sc, fk->from_table), ti = index_of(sc, fk->to_table);
if (fi < 0 || ti < 0) continue;
const Box *fb = &boxes[fi], *tb = &boxes[ti];
int x1 = tb->x + sv_round(tb->w / 2.0), y1 = tb->y + tb->h;
int x2 = fb->x + sv_round(fb->w / 2.0), y2 = fb->y;
int mid = sv_round((y1 + y2) / 2.0);
Edge *e = &geo.edges[geo.nedges++];
e->fk = fk;
e->path = malloc(96);
snprintf(e->path, 96, "M %d %d V %d H %d V %d", x1, y1, mid, x2, y2);
}
free(boxes);
return geo;
}
/* ---- SVG rendering ---- */
static void sv_esc(Str *out, const char *s) {
for (const char *p = s; *p; p++) {
if (*p == '&') str_app(out, "&");
else if (*p == '<') str_app(out, "<");
else if (*p == '>') str_app(out, ">");
else if (*p == '"') str_app(out, """);
else str_push(out, *p);
}
}
/* Box of the table named `name` — last occurrence wins, as in the ports'
* boxOf map built from the laid-out tables. */
static const Box *box_of(const Geo *geo, const char *name) {
for (size_t i = geo->ntables; i > 0; i--)
if (strcmp(geo->tables[i - 1].table->name, name) == 0) return &geo->tables[i - 1].box;
return NULL;
}
static char *render_svg(const Geo *geo) {
Str out = {0};
char head[160], buf[256];
snprintf(head, sizeof head,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 %d %d\" "
"class=\"sv-root\" role=\"img\"><title>Schema diagram</title>", geo->w, geo->h);
str_app(&out, head);
for (size_t i = 0; i < geo->nedges; i++) {
const Edge *e = &geo->edges[i];
const Box *fb = box_of(geo, e->fk->from_table);
if (!fb) continue;
int ax = fb->x + sv_round(fb->w / 2.0);
snprintf(buf, sizeof buf,
"<path class=\"sv-edge\" d=\"%s\"/><polygon class=\"sv-arrow\" "
"points=\"%d,%d %d,%d %d,%d\"/>",
e->path, ax - 5, fb->y - 8, ax + 5, fb->y - 8, ax, fb->y);
str_app(&out, buf);
}
for (size_t i = 0; i < geo->ntables; i++) {
const Laid *t = &geo->tables[i];
const Box *b = &t->box, *tb = &t->title_bar;
snprintf(buf, sizeof buf,
"<g class=\"sv-table\"><rect class=\"sv-box\" x=\"%d\" y=\"%d\" "
"width=\"%d\" height=\"%d\" rx=\"6\"/>"
"<rect class=\"sv-titlebar\" x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" rx=\"6\"/>"
"<text class=\"sv-title\" x=\"%d\" y=\"%d\">",
b->x, b->y, b->w, b->h, tb->x, tb->y, tb->w, tb->h, b->x + 8, tb->y + 17);
str_app(&out, buf);
sv_esc(&out, t->table->name);
str_app(&out, "</text>");
for (size_t c = 0; c < t->table->ncols; c++) {
const Column *col = &t->table->cols[c];
snprintf(buf, sizeof buf, "<text class=\"%s\" x=\"%d\" y=\"%d\">",
col->is_pk ? "sv-pk" : "sv-col", t->rows[c].x + 8, t->rows[c].y + 17);
str_app(&out, buf);
sv_esc(&out, col->name);
str_push(&out, ' ');
sv_esc(&out, col->type);
str_app(&out, "</text>");
}
str_app(&out, "</g>");
}
str_app(&out, "</svg>");
return out.buf;
}
/* ---- main entry ---- */
typedef struct { char *svg; Schema schema; } SvResult;
SvResult sv_ddl_to_svg(const char *ddl) {
SvResult r;
r.schema = (Schema){0};
parse_ddl(ddl, &r.schema);
Geo geo = layout_schema(&r.schema);
r.svg = render_svg(&geo);
geo_free(&geo);
return r;
}
/* Caller frees: sv_free_result(&res). */
void sv_free_result(SvResult *r) {
free(r->svg);
for (size_t i = 0; i < r->schema.tables.n; i++) {
Table *t = &r->schema.tables.v[i];
for (size_t c = 0; c < t->ncols; c++) { free(t->cols[c].name); free(t->cols[c].type); }
free(t->cols);
free(t->name);
}
free(r->schema.tables.v);
for (size_t i = 0; i < r->schema.fks.n; i++) {
Fk *fk = &r->schema.fks.v[i];
free(fk->from_table);
free(fk->from_col);
free(fk->to_table);
free(fk->to_col);
}
free(r->schema.fks.v);
for (size_t i = 0; i < r->schema.nnotes; i++) free(r->schema.notes[i]);
free(r->schema.notes);
r->svg = NULL;
r->schema = (Schema){0};
}
/* Example:
* SvResult r = sv_ddl_to_svg(
* "CREATE TABLE users (id INT PRIMARY KEY);"
* "CREATE TABLE posts (id INT PRIMARY KEY, "
* "user_id INT REFERENCES users(id), title TEXT);");
* puts(r.svg); → users box on layer 0, posts below, one FK edge —
* byte-identical to the TS/Go/… ports (integer geometry, same defaults).
* sv_free_result(&r); */
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →