🍱 Lunchbox Hands

sql

Generate an ER Diagram From an Existing SQL Schema (No DBML Required)

Most ER-diagram guides assume you are modeling a database from scratch. Usually you already have one — a pg_dump, a migration, a CREATE TABLE block — and just want to see it. How DDL-to-diagram works, where DBML fits in the middle, and why real dumps trip parsers.

Almost every ER-diagram tutorial starts the same way: open a blank canvas, invent some entities, drag arrows between them. Which is lovely, and almost never the situation you’re actually in. The common case is the reverse: the database already exists, its truth lives in DDL — a pg_dump, a migrations folder, a CREATE TABLE block a teammate pasted into Slack — and you just want to see it. You don’t need a modeling methodology for that, and you don’t need to learn a new schema language either. You need a parser that reads the SQL you already have and draws what it finds.

This post is about that direction — DDL in, diagram out — including the part most tools gloss over: why a real-world dump makes schema parsers choke, and what the error messages look like when it does.

Why the diagram should come from the DDL

A hand-drawn diagram is a claim about the database. A generated diagram is an observation of it. That difference is the whole argument.

Hand-maintained diagrams rot in a very specific way: they’re accurate on the day someone draws them, then every migration afterward silently invalidates some box or arrow. Six months later the wiki page shows a users.plan column that was dropped in March and is missing the invoices table entirely — and because the diagram looks authoritative, people trust it over the schema. A wrong diagram is worse than no diagram; at least no diagram sends you to \d users.

Generating from DDL inverts the trust: the input is the schema, so the picture can’t disagree with it. When the schema changes, you don’t update the diagram — you regenerate it, which takes one paste. The diagram becomes a view, not a document.

What an ER diagram actually shows

Strip away the notation wars (Chen, crow’s foot, UML) and an entity-relationship diagram communicates four things:

  • Tables — the entities, one box each.
  • Columns — each table’s fields with their types, plus the constraint flags that matter when reading a schema: primary key, not-null, unique.
  • Foreign-key edges — a line from the referencing column to the referenced column. This is the payload. A schema’s shape lives almost entirely in its FK graph; the boxes are just labeled nodes.
  • Cardinality — which end of each line is “one” and which is “many”. posts.user_id → users.id is many-to-one: many posts, one user.

That last one is worth a beat, because cardinality isn’t stored anywhere explicit in SQL — it’s derived. A plain foreign key is many-to-one. Put a UNIQUE constraint on the FK column and it becomes one-to-one, because each parent row can now be referenced at most once. A many-to-many never appears directly at all: it’s implemented as a join table carrying two many-to-one FKs. A good diagram generator reads these consequences out of the constraints rather than asking you to annotate anything.

DBML: the format in the middle

Here’s the non-obvious implementation detail: most text-to-diagram pipelines don’t go straight from SQL to pixels. They parse the SQL into a neutral, in-memory model first — tables, columns, refs — and render that. And there’s a well-established open-source project for exactly this middle layer: DBML (Database Markup Language), a schema-definition DSL maintained by Holistics, Apache-2.0 licensed, best known as the language behind dbdiagram.io. Its @dbml/core library ships parsers for DBML itself and for SQL dialects, all normalizing into one database model that can be re-exported in any supported dialect.

DBML the language looks like this:

Table users {
  id integer [pk, increment]
  email varchar(255) [not null, unique]
}

Table posts {
  id integer [pk, increment]
  user_id integer [not null]
  title varchar(200) [not null]
}

Ref: posts.user_id > users.id

The > means many-to-one; DBML also has < (one-to-many), - (one-to-one), and <> (many-to-many). It’s a genuinely pleasant format — readable, diffable, unambiguous about relationships in a way SQL sometimes isn’t.

But — and this is the point of the headline — you don’t have to write it. If your schema already exists as SQL, hand-translating CREATE TABLE statements into DBML is busywork a parser should do. Our DBML to SQL + ER diagram tool accepts four input formats: DBML, PostgreSQL, MySQL, and SQL Server DDL. Pick the dialect, paste the dump, and the same @dbml/core parsing machinery builds the model; our renderer then lays it out deterministically and draws an SVG showing every table with its columns and types, PK/FK badges, not-null and unique flags, and relationship edges labeled 1 and at each end. DBML remains available as an input for people who do write it — it’s just no longer the toll booth.

One scoping honesty note: those four are the input formats. On the way out you get five — PostgreSQL, MySQL, SQL Server, Oracle, and DBML. Oracle is export-only: the library can emit Oracle DDL from the parsed model, but there’s no Oracle parser, so you can’t paste Oracle DDL in. Any tool claiming to read “every dialect” deserves suspicion; SQL dialects are different enough that each one needs its own real grammar.

Why real-world dumps trip parsers

Paste the tidy three-table example above and everything works. Paste an actual pg_dump and you may hit a wall, for a structural reason worth understanding: a dialect parser is built from a grammar — a formal definition of every statement it understands — and a real dump is full of statements that aren’t schema structure at all. Session settings, ownership changes, comments, function bodies, trigger definitions, extension boilerplate. A grammar that only covers DDL has no derivation for ALTER TABLE ... OWNER TO, so the parse fails — not because your schema is wrong, but because the dump contains more than schema.

The fix is usually mechanical: trim the paste down to the statements that define structure — CREATE TABLE and the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements that pg_dump emits separately at the bottom of the dump. (Don’t skip those ALTERs: in a Postgres dump most foreign keys live there, not inline in the CREATE, and without them your diagram is boxes with no edges.) Dialect choice matters for the same reason — MySQL’s AUTO_INCREMENT, SQL Server’s IDENTITY(1,1), and Postgres’s SERIAL are three grammars’ worth of spelling for the same idea, so a paste parsed as the wrong dialect fails on the first dialect-specific token.

And when a parse does fail, there’s a second problem: the error itself. The SQL grammars in @dbml/core are ANTLR-generated, and a raw ANTLR syntax error appends the entire set of tokens the parser would have accepted at that position. While building the tool we measured one such error from the SQL Server parser at 14,061 characters — a genuine wall of every keyword in the grammar, attached to a single mistake. Technically complete, humanly useless.

So the tool’s error handling does something deliberately lossy: it keeps the useful head of the message — the line, the column, the offending token — and cuts everything from expecting { onward, clamping what’s left to 200 characters. Instead of fourteen thousand characters of keyword soup you get:

Line 12, column 3: mismatched input 'OWNER' …

which is enough to find line 12 and delete the statement. Multiple errors are sorted by position and de-duplicated, so the first one listed is the earliest problem in your paste — fix top-down. If the paste is so mangled you can’t tell where statements begin, running it through the SQL formatter first makes line numbers a lot more meaningful.

The workflow: paste, see, convert

Putting it together, the practical loop looks like this:

  1. Get DDL. pg_dump --schema-only, mysqldump --no-data, your migrations folder, or a teammate’s snippet:

    CREATE TABLE users (
      id SERIAL PRIMARY KEY,
      email VARCHAR(255) NOT NULL UNIQUE
    );
    
    CREATE TABLE posts (
      id SERIAL PRIMARY KEY,
      user_id INTEGER NOT NULL REFERENCES users(id),
      title VARCHAR(200) NOT NULL
    );
  2. Paste it into the tool with the matching input dialect. The diagram renders as you type (debounced), with a table/relationship count so you can sanity-check that everything parsed — if you pasted nine tables and it says “9 tables · 0 relationships”, your FK constraints didn’t make it into the paste.

  3. Download the SVG. It’s fully self-contained — fonts, colors, and a PK/FK/NN/U legend inlined — so it drops into a wiki, README, or design doc without a rendering dependency, and scales losslessly because it’s vectors, not a screenshot.

  4. Export to another dialect if you need to. Because everything normalizes through the same model, the paste that drew your diagram can be re-emitted as PostgreSQL, MySQL, SQL Server, Oracle, or DBML. That turns the tool into a rough dialect translator: paste MySQL, read it back as Postgres. Treat the output as a starting point, not a certified migration — a normalized model preserves structure (tables, types, keys, references), and dialect-specific storage details are exactly the kind of thing that needs human review on the way back out.

Two notes on trust, since you’re pasting production schemas. First, everything runs client-side: the parser is served as a static asset from this site and executes in your browser, so your DDL never leaves the tab — there’s no server that ever sees your schema. The parser bundle is a chunky download (ANTLR grammars for several dialects), which is why it loads on your first parse rather than with the page. Second, there’s a 500,000-character input cap, which comfortably fits multi-hundred-table schemas while keeping a pathological paste from freezing the parser.

If your schema doesn’t exist yet because the data came first — a CSV export you need to turn into a table — CSV to SQL generates the CREATE TABLE and INSERT statements, and that output pastes straight into the diagram tool. It’s the same move as inferring Pydantic models from a JSON payload, with the same class of edge cases around what inference can and can’t know: we’ve written about those gotchas.

The short version

If you believeThe reality
”I need to learn a modeling language to get an ER diagram”Paste PostgreSQL, MySQL, or SQL Server DDL directly; DBML is an option, not a prerequisite
”I’ll keep the architecture diagram updated by hand”Hand-drawn diagrams drift with every migration; regenerating from DDL takes one paste and can’t disagree with the schema
”Cardinality is something I annotate”It’s derived: plain FK = many-to-one, unique FK = one-to-one, join table = many-to-many
”The parser rejected my pg_dump, so the tool is broken”Dumps carry non-DDL statements a schema grammar can’t accept; trim to CREATE TABLE + FK ALTERs
”Parser errors are unreadable”Raw ANTLR errors can run to 14,061 characters of expected tokens; a usable tool cuts them to the line, column, and offending token
”Any dialect works”Four in (DBML, PostgreSQL, MySQL, SQL Server), five out (those plus Oracle — export only)
“Pasting a schema into a web tool is risky”Here the parse runs entirely in your browser; the schema never leaves the tab

The schema you already have is the most truthful description of your database that exists. Point a parser at it and let the diagram be a consequence, not a chore.