Mermaid to SQL: Generate PostgreSQL DDL From an ER Diagram (and Keep It in Sync)

I keep drawing the same database twice. Once as a Mermaid erDiagram in a design doc, because diagrams are how humans agree on a data model. Then again as CREATE TABLE statements, because that's what Postgres actually runs. The two drift apart almost immediately, and the diagram quietly becomes a lie.

So I built diagram2code to delete that second step. You write the diagram once, and it generates the SQL DDL, an ORM model, and even an API schema from the same source. This is the fastest ER diagram to SQL workflow I've found, and it's a hands-on walkthrough: Mermaid in, PostgreSQL out, then MySQL, then an ORM, then a few directives for the things diagrams can't say โ€” and finally a GitHub Action so the schema stays honest on every pull request.

Everything below runs in your browser at diagram2code.com or with a single curl. There's nothing to install.

Step 1: Write a Mermaid erDiagram

Start with a familiar shape โ€” users and the orders they place:

erDiagram
    USER ||--o{ ORDER : places
    USER {
        int id PK
        string email
        string name
    }
    ORDER {
        int id PK
        int user_id FK
        decimal total
        datetime created_at
    }

That's standard Mermaid syntax. ||--o{ reads as "one USER places zero-or-many ORDERs," PK marks a primary key, and FK marks a foreign key. If you've ever sketched a schema in a README, you already know this language.

Step 2: Convert it to PostgreSQL DDL

Paste that diagram into the converter on the web app. The left pane renders a live Mermaid preview so you can confirm the shape; the right pane shows the generated SQL.

The PostgreSQL output looks like this:

CREATE TABLE "user" (
    "id" SERIAL PRIMARY KEY,
    "email" VARCHAR(255),
    "name" VARCHAR(255)
);

CREATE TABLE "order" (
    "id" SERIAL PRIMARY KEY,
    "user_id" INTEGER,
    "total" NUMERIC,
    "created_at" TIMESTAMP
);

ALTER TABLE "order"
    ADD CONSTRAINT "fk_order_user_id"
    FOREIGN KEY ("user_id") REFERENCES "user" ("id");

Notice it picked up the relationship and wrote the foreign key for you โ€” the ||--o{ edge becomes an actual ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY, emitted as its own statement after the tables exist.

Prefer the terminal? The same conversion is one REST call. SQL conversion is free and needs no account:

curl -X POST https://diagram2code.com/api/v1/convert/sql \
  -H "Content-Type: application/json" \
  -d '{"diagramType":"mermaid","database":"postgres","diagram":"erDiagram\nUSER {\n  int id PK\n}"}'

That endpoint is the same engine the web app uses, so the web UI and the API always agree.

Step 3: Switch the dialect to MySQL

Here's where a single source of truth pays off. Change "database":"postgres" to "mysql" (or flip the dialect dropdown in the web app), and the same diagram produces MySQL-flavored DDL:

CREATE TABLE `user` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `email` VARCHAR(255),
    `name` VARCHAR(255)
);

CREATE TABLE `order` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT,
    `total` DECIMAL,
    `created_at` DATETIME
);

ALTER TABLE `order`
    ADD CONSTRAINT `fk_order_user_id`
    FOREIGN KEY (`user_id`) REFERENCES `user` (`id`);

Look at what changed: backtick identifiers instead of double quotes, AUTO_INCREMENT instead of SERIAL, and the column types adapt per dialect too โ€” total is DECIMAL here where Postgres emitted NUMERIC, and the timestamp is DATETIME instead of TIMESTAMP. Four dialects are supported today โ€” PostgreSQL, MySQL, SQLite, and Oracle โ€” and the diagram never changes. This is the whole point: the diagram is the model; the dialect is just a rendering of it.

Step 4: Generate an ORM model

You usually don't talk to the database in raw SQL โ€” you talk to it through an ORM. Point the same diagram at an ORM target instead. For Prisma, call the ORM endpoint with the same minimal payload:

curl -X POST https://diagram2code.com/api/v1/convert/orm \
  -H "Content-Type: application/json" \
  -d '{"diagramType":"mermaid","framework":"prisma","diagram":"erDiagram\nUSER {\n  int id PK\n}"}'

You get a Prisma schema with the models and relations wired up โ€” ready to drop into schema.prisma. There are eight ORM frameworks in total: GORM (Go), SQLAlchemy (Python), TypeORM and Prisma (TypeScript), JPA (Java), Django (Python), Sequelize (JavaScript), and EF Core (C#). GORM is available on the free tier; the rest are part of the Developer tier.

One diagram, and you can hand a Go team a GORM model and a Python team a SQLAlchemy one from the identical design.

Step 5: Add directives for what diagrams can't say

Mermaid is great at shape and weak at constraints. It can't express "not null," "this must be positive," or "index these two columns." So diagram2code reads directives โ€” annotations you write right in the diagram source as comments. Here's the PRODUCT table with a check constraint, a not-null, and a default:

PRODUCT {
    int id PK
    %% ::CHECK(price > 0), ::NN
    decimal price
    %% ::DEFAULT(0)
    int stock
}

::NN makes the column NOT NULL, ::CHECK(price > 0) emits a real check constraint, and ::DEFAULT(0) sets a default. You can comma-chain directives on one line, as shown above with ::CHECK(...) and ::NN together.

Need an index? Add an index directive, for example:

%% ::INDEX([user_id, created_at], name:"idx_order_user_created")

There's a fuller vocabulary โ€” ::ENUM(a,b), ::UNIQUE([cols]), ::RELATIONSHIP[onDelete:CASCADE, ...], named constraints, table-level comments, display labels โ€” but ::NN, ::CHECK, and ::INDEX cover most of what a diagram leaves unsaid. The metadata lives with the diagram, so it travels into every dialect and every ORM you generate.

Step 6: Keep the schema in sync on every PR

Generating SQL once is nice. The real win is never letting the diagram and the database drift apart again. That's the GitHub Action.

Drop this into a workflow file, and on any pull request that touches a .mmd or .puml diagram, it calls the hosted API and posts the generated SQL plus a human-readable schema diff as a sticky PR comment:

- uses: diagram2code/diagram2code@v1
  with:
    database: postgres
    fail-on-destructive: true

The fail-on-destructive: true line is the part I rely on most. The diff engine flags destructive changes โ€” dropping a column, dropping a table, narrowing a column type โ€” and with that flag set, the build fails on them. A DROP COLUMN "email" slipped into a diagram edit becomes a red check, not a 2 a.m. incident.

Reviewers see exactly what the schema change means in SQL, right next to the diagram edit, on every PR.

Try it

That's the loop: write a Mermaid erDiagram once, generate Postgres or MySQL DDL from it, hand your ORM the same model, annotate the gaps with directives, and let the Action keep everyone honest.

You can run every step in this article right now โ€” the converter and SQL generation are free, no account needed:

Draw the database once. Let the SQL follow.