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 throw. Integer geometry
// only (half-up rounding — JS Math.round parity), so every port draws the
// byte-identical diagram.
//
// Single translation unit, C++17, no dependencies:
// c++ -fsyntax-only -Wall src/tool-sources/schema-visualizer/cpp.cpp
#include <algorithm>
#include <cstddef>
#include <optional>
#include <set>
#include <string>
#include <vector>
namespace schema_visualizer {
// LAYOUT constants: rowHeight/charWidth/padding/layerGap/columnGap = 24/7/8/60/40.
constexpr int kRowHeight = 24;
constexpr int kCharWidth = 7;
constexpr int kPadding = 8;
constexpr int kLayerGap = 60;
constexpr int kColumnGap = 40;
struct Column {
std::string name;
std::string type;
bool nullable = true;
bool isPrimaryKey = false;
};
struct ForeignKey {
std::string fromTable;
std::string fromColumn;
std::string toTable;
std::string toColumn; // empty = omitted, resolved after the parse pass
};
struct Table {
std::string name;
std::vector<Column> columns;
};
struct Schema {
std::vector<Table> tables;
std::vector<ForeignKey> foreignKeys;
std::vector<std::string> notes;
};
// --- small helpers ---------------------------------------------------------
bool isSpaceChar(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
}
char closeQuote(char open) { return open == '[' ? ']' : open; }
bool isOpenQuote(char c) { return c == '\'' || c == '"' || c == '`' || c == '['; }
bool isPunctChar(char c) { return c == '(' || c == ')' || c == ',' || c == '.'; }
std::string trimCopy(const std::string& s) {
std::size_t b = 0, e = s.size();
while (b < e && isSpaceChar(s[b])) b++;
while (e > b && isSpaceChar(s[e - 1])) e--;
return s.substr(b, e - b);
}
std::string toUpper(std::string s) {
for (char& c : s)
if (c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
return s;
}
// Round half up (JS Math.round parity — std::round differs on .5 ties).
int rHalfUp(double v) { return static_cast<int>(v + 0.5); }
bool isModifierWord(const std::string& upper) {
static const std::set<std::string> kModifiers = {
"NOT", "NULL", "PRIMARY", "KEY", "UNIQUE", "DEFAULT", "REFERENCES",
"AUTO_INCREMENT", "AUTOINCREMENT", "ON", "COMMENT", "CHECK", "CONSTRAINT"};
return kModifiers.count(upper) > 0;
}
// --- lexer -----------------------------------------------------------------
enum class Tok { Word, Punct, Str, Qident };
struct Token {
Tok kind;
std::string text;
};
// Split on ';' outside strings/quoted identifiers (depth-agnostic: an
// unterminated paren cannot swallow the statements after it).
std::vector<std::string> splitStatements(const std::string& ddl) {
std::vector<std::string> out;
std::string cur;
std::size_t i = 0, n = ddl.size();
while (i < n) {
char ch = ddl[i];
if (isOpenQuote(ch)) {
char close = closeQuote(ch);
cur += ch;
i++;
while (i < n) {
cur += ddl[i];
if (ddl[i] == close) {
if (close == '\'' && i + 1 < n && ddl[i + 1] == '\'') { // '' escape
cur += ddl[i + 1];
i += 2;
continue;
}
break;
}
i++;
}
i++;
continue;
}
if (ch == ';') {
out.push_back(cur);
cur.clear();
i++;
continue;
}
cur += ch;
i++;
}
if (!trimCopy(cur).empty()) out.push_back(cur);
return out;
}
// Tokens: quoted identifiers/strings carry their text (quotes stripped);
// ( ) , . are punct; everything else is a word.
std::vector<Token> tokenize(const std::string& s) {
std::vector<Token> toks;
std::size_t i = 0, n = s.size();
while (i < n) {
char ch = s[i];
if (isSpaceChar(ch)) {
i++;
continue;
}
if (isOpenQuote(ch)) {
char close = closeQuote(ch);
std::string text;
i++;
while (i < n) {
if (s[i] == close) {
if (close == '\'' && i + 1 < n && s[i + 1] == '\'') {
text += '\'';
i += 2;
continue;
}
break;
}
text += s[i++];
}
i++;
toks.push_back({ch == '\'' ? Tok::Str : Tok::Qident, text});
continue;
}
if (isPunctChar(ch)) {
toks.push_back({Tok::Punct, std::string(1, ch)});
i++;
continue;
}
std::size_t j = i;
while (j < n && !isSpaceChar(s[j]) && !isOpenQuote(s[j]) && s[j] != ']' && !isPunctChar(s[j]))
j++;
if (j == i) { // stray ']' — one-token word so the lexer always advances
j = i + 1;
}
toks.push_back({Tok::Word, s.substr(i, j - i)});
i = j;
}
return toks;
}
// Bounded token access (JS/Python read past the end as null/None).
const Token* g(const std::vector<Token>& toks, int i) {
if (i < 0 || static_cast<std::size_t>(i) >= toks.size()) return nullptr;
return &toks[static_cast<std::size_t>(i)];
}
bool isP(const Token* t, const char* p) {
return t != nullptr && t->kind == Tok::Punct && t->text == p;
}
bool kw(const Token* t, const std::string& w) {
return t != nullptr && t->kind == Tok::Word && toUpper(t->text) == w;
}
// --- name / list helpers ---------------------------------------------------
struct NameRef {
std::string name;
int next; // index just past the name
};
// name[.name]* — dotted qualifiers fold into one name ("sch"."tbl" -> sch.tbl).
std::optional<NameRef> takeName(const std::vector<Token>& toks, int i) {
const Token* first = g(toks, i);
if (first == nullptr || (first->kind != Tok::Qident && first->kind != Tok::Word)) return std::nullopt;
std::string name = first->text;
int j = i + 1;
while (isP(g(toks, j), ".") && g(toks, j + 1) != nullptr &&
(g(toks, j + 1)->kind == Tok::Qident || g(toks, j + 1)->kind == Tok::Word)) {
name += "." + g(toks, j + 1)->text;
j += 2;
}
return NameRef{name, j};
}
struct ListRef {
std::vector<std::string> names;
int next; // index just past the ')'
};
// ( name , name ... ) or nothing.
std::optional<ListRef> parenList(const std::vector<Token>& toks, int i) {
if (!isP(g(toks, i), "(")) return std::nullopt;
std::vector<std::string> names;
int j = i + 1;
for (;;) {
std::optional<NameRef> name = takeName(toks, j);
if (!name) return std::nullopt;
names.push_back(name->name);
j = name->next;
if (isP(g(toks, j), ",")) {
j++;
continue;
}
if (isP(g(toks, j), ")")) return ListRef{names, j + 1};
return std::nullopt;
}
}
// Join type tokens with single spaces, drop spaces around ( ) , , trim,
// uppercase: "DECIMAL ( 10 , 2 )" -> "DECIMAL(10,2)".
std::string joinType(const std::vector<Token>& toks) {
std::string raw;
for (std::size_t k = 0; k < toks.size(); ++k) {
if (k > 0) raw += ' ';
raw += toks[k].text;
}
std::string out;
bool pendingSpace = false;
for (std::size_t k = 0; k < raw.size(); ++k) {
char c = raw[k];
if (isSpaceChar(c)) {
if (!out.empty()) pendingSpace = true;
continue;
}
if (c == '(' || c == ')' || c == ',') {
pendingSpace = false;
out += c;
std::size_t m = k + 1;
while (m < raw.size() && isSpaceChar(raw[m])) m++;
k = m - 1;
continue;
}
if (pendingSpace) {
out += ' ';
pendingSpace = false;
}
out += c;
}
return toUpper(out);
}
// --- column line -----------------------------------------------------------
// Parse one column definition; records inline REFERENCES into fks. Unknown
// modifiers after the type are tolerated (skipped token by token).
std::optional<Column> parseColumn(const std::vector<Token>& line, const std::string& table,
std::vector<ForeignKey>& fks) {
std::optional<NameRef> name = takeName(line, 0);
if (!name) return std::nullopt;
int i = name->next;
std::vector<Token> typeToks;
while (i < static_cast<int>(line.size()) &&
!(line[static_cast<std::size_t>(i)].kind == Tok::Word &&
isModifierWord(toUpper(line[static_cast<std::size_t>(i)].text)))) {
typeToks.push_back(line[static_cast<std::size_t>(i)]);
i++;
}
bool nullable = true, pk = false;
while (i < static_cast<int>(line.size())) {
const Token* t = g(line, i);
if (kw(t, "NOT") && kw(g(line, i + 1), "NULL")) {
nullable = false;
i += 2;
continue;
}
if (kw(t, "NULL")) {
i++;
continue;
}
if (kw(t, "PRIMARY") && kw(g(line, i + 1), "KEY")) {
pk = true;
nullable = false;
i += 2;
continue;
}
if (kw(t, "UNIQUE") || kw(t, "AUTO_INCREMENT") || kw(t, "AUTOINCREMENT")) {
i++;
continue;
}
if (kw(t, "DEFAULT")) {
i++;
if (isP(g(line, i), "(")) { // skip the parenthesized expression whole
int depth = 0;
do {
if (isP(g(line, i), "(")) depth++;
if (isP(g(line, i), ")")) depth--;
i++;
} while (i < static_cast<int>(line.size()) && depth > 0);
} else if (g(line, i) != nullptr) {
i++;
}
continue;
}
if (kw(t, "COMMENT")) {
i++;
if (g(line, i) != nullptr && g(line, i)->kind == Tok::Str) i++;
continue;
}
if (kw(t, "ON")) { // ON UPDATE ... / ON DELETE SET NULL / ON DELETE NO ACTION
i += 2;
if (kw(g(line, i), "SET") || kw(g(line, i), "NO")) i += 2;
else if (g(line, i) != nullptr) i++;
continue;
}
if (kw(t, "REFERENCES")) {
i++;
std::optional<NameRef> target = takeName(line, i);
if (target) {
i = target->next;
std::string toCol; // empty = omitted
if (isP(g(line, i), "(")) {
std::optional<ListRef> list = parenList(line, i);
if (list) {
toCol = list->names.front();
i = list->next;
}
}
fks.push_back(ForeignKey{table, name->name, target->name, toCol});
}
continue;
}
i++; // unknown modifier tolerated
}
return Column{name->name, joinType(typeToks), nullable, pk};
}
// --- DDL parse -------------------------------------------------------------
struct ParseError {};
Schema parseDdl(const std::string& ddl) {
if (trimCopy(ddl).empty()) return Schema{{}, {}, {"No DDL input."}};
Schema schema;
for (const std::string& stmt : splitStatements(ddl)) {
if (trimCopy(stmt).empty()) continue;
std::vector<Token> toks = tokenize(stmt);
try {
int i = 0;
if (!kw(g(toks, i), "CREATE")) throw ParseError{};
i++;
while (kw(g(toks, i), "TEMP") || kw(g(toks, i), "TEMPORARY") || kw(g(toks, i), "UNLOGGED")) i++;
if (!kw(g(toks, i), "TABLE")) {
schema.notes.push_back("Skipped non-table statement.");
continue;
}
i++;
if (kw(g(toks, i), "IF") && kw(g(toks, i + 1), "NOT") && kw(g(toks, i + 2), "EXISTS")) i += 3;
std::optional<NameRef> name = takeName(toks, i);
if (!name || !isP(g(toks, name->next), "(")) throw ParseError{};
i = name->next + 1;
// body = tokens until the matching close paren (depth-aware)
std::vector<Token> body;
int depth = 0;
for (; i < static_cast<int>(toks.size()); i++) {
if (isP(g(toks, i), "(")) depth++;
if (isP(g(toks, i), ")")) {
if (depth == 0) break;
depth--;
}
body.push_back(toks[static_cast<std::size_t>(i)]);
}
if (i >= static_cast<int>(toks.size())) throw ParseError{};
// split body into top-level comma lines
std::vector<std::vector<Token>> lines;
std::vector<Token> line;
depth = 0;
for (const Token& t : body) {
if (isP(&t, "(")) depth++;
if (isP(&t, ")")) depth--;
if (isP(&t, ",") && depth == 0) {
lines.push_back(line);
line.clear();
continue;
}
line.push_back(t);
}
if (!line.empty()) lines.push_back(line);
schema.tables.push_back(Table{name->name, {}});
Table& table = schema.tables.back();
for (const std::vector<Token>& toks2 : lines) {
if (toks2.empty()) continue;
const Token& first = toks2.front();
std::string u = first.kind == Tok::Word ? toUpper(first.text) : "";
if (u == "PRIMARY" && kw(g(toks2, 1), "KEY")) {
std::optional<ListRef> list = parenList(toks2, 2);
if (list) {
for (const std::string& cn : list->names) {
for (Column& col : table.columns) {
if (col.name == cn) {
col.isPrimaryKey = true;
col.nullable = false;
}
}
}
}
continue;
}
if (u == "FOREIGN" && kw(g(toks2, 1), "KEY")) {
std::optional<ListRef> from = parenList(toks2, 2);
if (from && kw(g(toks2, from->next), "REFERENCES")) {
std::optional<NameRef> target = takeName(toks2, from->next + 1);
if (target) {
bool haveToCols = false;
std::vector<std::string> toCols;
if (isP(g(toks2, target->next), "(")) {
std::optional<ListRef> to = parenList(toks2, target->next);
if (to) {
toCols = to->names;
haveToCols = true;
}
}
for (std::size_t idx = 0; idx < from->names.size(); ++idx) {
std::string toCol; // empty = omitted
if (haveToCols && !toCols.empty())
toCol = idx < toCols.size() ? toCols[idx] : toCols.back();
schema.foreignKeys.push_back(
ForeignKey{table.name, from->names[idx], target->name, toCol});
}
}
}
continue;
}
if (u == "UNIQUE" || u == "KEY" || u == "INDEX" || u == "CHECK" || u == "EXCLUDE" ||
u == "CONSTRAINT")
continue;
std::optional<Column> col = parseColumn(toks2, table.name, schema.foreignKeys);
if (col) table.columns.push_back(*col);
}
} catch (...) {
schema.notes.push_back("Skipped unparseable statement.");
}
}
// resolve omitted FK target columns to the referenced table's first PK
for (ForeignKey& fk : schema.foreignKeys) {
if (!fk.toColumn.empty()) continue;
const Table* target = nullptr;
for (const Table& t : schema.tables)
if (t.name == fk.toTable) {
target = &t;
break;
}
const Column* pk = nullptr;
if (target != nullptr)
for (const Column& c : target->columns)
if (c.isPrimaryKey) {
pk = &c;
break;
}
fk.toColumn = pk != nullptr ? pk->name : "id";
}
return schema;
}
// --- layout ----------------------------------------------------------------
struct Box {
int x = 0, y = 0, w = 0, h = 0;
};
struct Edge {
ForeignKey fk;
std::string path;
std::string label;
};
struct LaidTable {
const Table* table;
Box box;
Box titleBar;
std::vector<Box> columnRows;
};
struct Geometry {
int width = 0, height = 0;
std::vector<LaidTable> tables;
std::vector<Edge> edges;
};
// Layered layout: referenced tables above referencing ones (relaxation passes
// over the FK edges, cycle-safe), tables in a layer laid left to right.
Geometry layoutSchema(const Schema& schema) {
Geometry geo;
if (schema.tables.empty()) return geo;
std::vector<std::pair<std::string, int>> indexPairs;
for (std::size_t i = 0; i < schema.tables.size(); ++i) // first occurrence wins
if (std::none_of(indexPairs.begin(), indexPairs.end(),
[&](const std::pair<std::string, int>& p) { return p.first == schema.tables[i].name; }))
indexPairs.emplace_back(schema.tables[i].name, static_cast<int>(i));
auto indexOf = [&](const std::string& name) -> int {
for (const auto& p : indexPairs)
if (p.first == name) return p.second;
return -1;
};
std::vector<Box> boxes(schema.tables.size());
for (std::size_t i = 0; i < schema.tables.size(); ++i) {
const Table& t = schema.tables[i];
std::size_t textLen = t.name.size();
for (const Column& c : t.columns)
textLen = std::max(textLen, c.name.size() + 1 + c.type.size());
if (textLen == 0) textLen = 1;
boxes[i].w = rHalfUp(static_cast<double>(textLen) * kCharWidth + 2 * kPadding);
boxes[i].h = rHalfUp(static_cast<double>(kRowHeight) * static_cast<double>(1 + t.columns.size()) +
kPadding);
}
std::vector<int> layerOf(schema.tables.size(), 0);
for (std::size_t pass = 0; pass < schema.tables.size(); ++pass) {
bool changed = false;
for (const ForeignKey& fk : schema.foreignKeys) {
int ti = indexOf(fk.fromTable), tj = indexOf(fk.toTable);
if (ti < 0 || tj < 0 || ti == tj) continue;
if (layerOf[static_cast<std::size_t>(ti)] < layerOf[static_cast<std::size_t>(tj)] + 1) {
layerOf[static_cast<std::size_t>(ti)] = layerOf[static_cast<std::size_t>(tj)] + 1;
changed = true;
}
}
if (!changed) break;
}
std::vector<std::pair<int, std::vector<int>>> layers; // sorted by layer index
for (std::size_t i = 0; i < layerOf.size(); ++i) {
int l = layerOf[i];
auto it = std::lower_bound(layers.begin(), layers.end(), l,
[](const std::pair<int, std::vector<int>>& p, int key) { return p.first < key; });
if (it == layers.end() || it->first != l) it = layers.insert(it, {l, {}});
it->second.push_back(static_cast<int>(i));
}
int y = 0, width = 0, height = 0;
for (const auto& kv : layers) {
int x = 0, layerH = 0;
for (int i : kv.second) {
Box& b = boxes[static_cast<std::size_t>(i)];
b.x = x;
b.y = y;
x += b.w + kColumnGap;
layerH = std::max(layerH, b.h);
}
width = std::max(width, x - kColumnGap);
height = std::max(height, y + layerH);
y += layerH + kLayerGap;
}
for (const ForeignKey& fk : schema.foreignKeys) {
int fi = indexOf(fk.fromTable), ti = indexOf(fk.toTable);
if (fi < 0 || ti < 0) continue;
const Box& from = boxes[static_cast<std::size_t>(fi)];
const Box& to = boxes[static_cast<std::size_t>(ti)];
int x1 = to.x + rHalfUp(to.w / 2.0), y1 = to.y + to.h;
int x2 = from.x + rHalfUp(from.w / 2.0), y2 = from.y;
int midY = rHalfUp((y1 + y2) / 2.0);
geo.edges.push_back(Edge{fk,
"M " + std::to_string(x1) + " " + std::to_string(y1) + " V " +
std::to_string(midY) + " H " + std::to_string(x2) + " V " +
std::to_string(y2),
fk.fromColumn + " \xE2\x86\x92 " + fk.toColumn});
}
for (std::size_t i = 0; i < schema.tables.size(); ++i) {
LaidTable lt;
lt.table = &schema.tables[i];
lt.box = boxes[i];
lt.titleBar = Box{boxes[i].x, boxes[i].y, boxes[i].w, kRowHeight};
for (std::size_t ci = 0; ci < schema.tables[i].columns.size(); ++ci)
lt.columnRows.push_back(
Box{boxes[i].x, boxes[i].y + kRowHeight * static_cast<int>(1 + ci), boxes[i].w, kRowHeight});
geo.tables.push_back(std::move(lt));
}
geo.width = width;
geo.height = height;
return geo;
}
// --- SVG render ------------------------------------------------------------
std::string esc(const std::string& s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
switch (c) {
case '&': out += "&"; break;
case '<': out += "<"; break;
case '>': out += ">"; break;
case '"': out += """; break;
default: out += c;
}
}
return out;
}
std::string renderSvg(const Geometry& geo) {
std::string out = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 " + std::to_string(geo.width) +
" " + std::to_string(geo.height) +
"\" class=\"sv-root\" role=\"img\"><title>Schema diagram</title>";
std::vector<std::pair<std::string, const Box*>> boxOf;
for (const LaidTable& t : geo.tables)
if (std::none_of(boxOf.begin(), boxOf.end(),
[&](const std::pair<std::string, const Box*>& p) { return p.first == t.table->name; }))
boxOf.emplace_back(t.table->name, &t.box);
for (const Edge& e : geo.edges) {
const Box* from = nullptr;
for (const auto& p : boxOf)
if (p.first == e.fk.fromTable) {
from = p.second;
break;
}
if (from == nullptr) continue;
int ax = from->x + rHalfUp(from->w / 2.0);
out += "<path class=\"sv-edge\" d=\"" + e.path + "\"/>";
out += "<polygon class=\"sv-arrow\" points=\"" + std::to_string(ax - 5) + "," +
std::to_string(from->y - 8) + " " + std::to_string(ax + 5) + "," +
std::to_string(from->y - 8) + " " + std::to_string(ax) + "," + std::to_string(from->y) +
"\"/>";
}
for (const LaidTable& t : geo.tables) {
const Box& b = t.box;
const Box& tb = t.titleBar;
out += "<g class=\"sv-table\"><rect class=\"sv-box\" x=\"" + std::to_string(b.x) + "\" y=\"" +
std::to_string(b.y) + "\" width=\"" + std::to_string(b.w) + "\" height=\"" +
std::to_string(b.h) + "\" rx=\"6\"/>";
out += "<rect class=\"sv-titlebar\" x=\"" + std::to_string(tb.x) + "\" y=\"" + std::to_string(tb.y) +
"\" width=\"" + std::to_string(tb.w) + "\" height=\"" + std::to_string(tb.h) + "\" rx=\"6\"/>";
out += "<text class=\"sv-title\" x=\"" + std::to_string(b.x + 8) + "\" y=\"" +
std::to_string(tb.y + 17) + "\">" + esc(t.table->name) + "</text>";
for (std::size_t ci = 0; ci < t.table->columns.size(); ++ci) {
const Column& c = t.table->columns[ci];
const Box& row = t.columnRows[ci];
out += "<text class=\"" + std::string(c.isPrimaryKey ? "sv-pk" : "sv-col") + "\" x=\"" +
std::to_string(row.x + 8) + "\" y=\"" + std::to_string(row.y + 17) + "\">" + esc(c.name) +
" " + esc(c.type) + "</text>";
}
out += "</g>";
}
out += "</svg>";
return out;
}
// --- entry point -----------------------------------------------------------
struct DdlResult {
std::string svg;
Schema schema;
};
DdlResult ddlToSvg(const std::string& ddl) {
Schema schema = parseDdl(ddl);
Geometry geo = layoutSchema(schema); // borrows `schema` (outlives the render)
DdlResult result;
result.svg = renderSvg(geo);
result.schema = std::move(schema);
return result;
}
} // namespace schema_visualizer
// Example:
// schema_visualizer::ddlToSvg(
// "CREATE TABLE users (id INT PRIMARY KEY);"
// "CREATE TABLE posts (id INT PRIMARY KEY, user_id INT REFERENCES users(id), title TEXT);")
// → users box on layer 0, posts below, one FK edge — byte-identical to the
// TS/Go/… ports (integer geometry, same defaults).
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 →