Skip to content

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 += "&amp;"; break;
      case '<': out += "&lt;"; break;
      case '>': out += "&gt;"; break;
      case '"': out += "&quot;"; 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 →