Convert Mermaid ER to PostgreSQL

Generate production-ready PostgreSQL SQL code from your Mermaid ER diagrams in seconds.

postgres
Dialect: postgresTables: 2
Done.
mermaid
Live preview mermaid

How the Mermaid ER to PostgreSQL conversion works

Mermaid's erDiagram syntax covers structure — entities, PK/FK/UK markers, cardinality arrows — and PostgreSQL is the richest SQL target the generator has: native enum types, partial indexes, timezone-aware timestamps. Everything Mermaid can't say natively (NOT NULL, defaults, CHECK constraints) rides in as %% comment directives, so the diagram stays valid for GitHub and every other Mermaid renderer.

Mermaid erDiagram is a Markdown-friendly notation rendered natively by GitHub, GitLab, Notion, and Obsidian — which makes it the most common way to keep an ER diagram next to the code it describes.

Entities are blocks of `type name marker` lines (markers: PK, FK, UK, INDEX), and relationships use cardinality arrows like `USER ||--o{ POST : writes` — one-to-many from USER to POST. The converter reads both, plus comment directives for everything the base syntax can't express.

Paste your Mermaid ER source into the editor above and the engine parses it into an intermediate schema — tables, columns, relationships, indexes — then generates PostgreSQL DDL from that model. Anything the diagram syntax can't express (NOT NULL, defaults, enums, CHECK constraints, composite indexes) you add with comment directives like %% ::NN, ::DEFAULT(now()) on the line above a column. The Mermaid ER reference documents the full vocabulary.

Worked example: an e-commerce schema

A complete Mermaid ER diagram and the exact PostgreSQL output the generator produces for it — different from the starter diagram in the editor above, so you can see a second real conversion end to end:

erDiagram
    CUSTOMER ||--o{ ORDER : places
    PRODUCT ||--o{ ORDER : "ordered in"
    CUSTOMER {
        int id PK
        string email UK
        %% ::NN
        string name
        %% ::DEFAULT(now())
        timestamp created_at
    }
    PRODUCT {
        int id PK
        string sku UK
        %% ::CHECK(price > 0)
        decimal price
        %% ::ENUM(draft, active, retired)
        string status
    }
    ORDER {
        int id PK
        int customer_id FK
        int product_id FK
        %% ::DEFAULT(0)
        decimal total
    }
CREATE TYPE "product_status_enum" AS ENUM ('draft', 'active', 'retired');

CREATE TABLE "customer" (
    "id" SERIAL PRIMARY KEY,
    "email" TEXT UNIQUE,
    "name" TEXT NOT NULL,
    "created_at" TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE "order" (
    "id" SERIAL PRIMARY KEY,
    "customer_id" INTEGER,
    "product_id" INTEGER,
    "total" NUMERIC DEFAULT 0
);

CREATE TABLE "product" (
    "id" SERIAL PRIMARY KEY,
    "sku" TEXT UNIQUE,
    "price" NUMERIC CHECK (price > 0),
    "status" "product_status_enum"
);

ALTER TABLE "order"
    ADD CONSTRAINT "fk_order_customer_id"
    FOREIGN KEY ("customer_id") REFERENCES "customer"("id");

ALTER TABLE "order"
    ADD CONSTRAINT "fk_order_product_id"
    FOREIGN KEY ("product_id") REFERENCES "product"("id");

Worth noticing: the ::ENUM directive became a real CREATE TYPE before the table; ::NN on name emitted NOT NULL while the UK marker on email produced UNIQUE without NOT NULL (they're independent); ::CHECK and ::DEFAULT landed inline; and both foreign keys arrived as named ALTER TABLE constraints after all tables exist, so declaration order never matters. The reserved word order is safe because every identifier is double-quoted.

What the generator emits for PostgreSQL

  • CREATE TABLE statements with double-quoted identifiers
  • SERIAL primary keys for single integer PK columns
  • CREATE TYPE … AS ENUM for ::ENUM(...) columns
  • ALTER TABLE … ADD CONSTRAINT foreign keys with ON DELETE / ON UPDATE actions
  • CREATE INDEX / CREATE UNIQUE INDEX, including partial indexes with a WHERE clause
  • COMMENT ON TABLE and COMMENT ON COLUMN for documentation carried over from the diagram

Type mapping: Mermaid ER to PostgreSQL

Diagram column types are logical — the generator maps each one to the idiomatic PostgreSQL type. These are the exact mappings the engine uses:

Diagram typePostgreSQL typeNotes
intINTEGERSERIAL when it is the auto-increment primary key
stringTEXTuse varchar(n) in the diagram for VARCHAR(n)
textTEXT
decimalNUMERICdecimal(10,2) passes precision through
booleanBOOLEAN
dateDATE
timestampTIMESTAMPTZtimezone-aware by default
uuidUUIDnative PostgreSQL UUID type
bigintBIGINT
blobBYTEA

Keys, relationships, and enums

A single `int id PK` column becomes `"id" SERIAL PRIMARY KEY`. Composite primary keys (e.g. on junction tables) emit a table-level `PRIMARY KEY (a, b)` clause without auto-increment.

Relationships emit `ALTER TABLE … ADD CONSTRAINT fk_<table>_<column> FOREIGN KEY … REFERENCES …` after all tables are created, so declaration order never matters. `::RELATIONSHIP[onDelete: CASCADE]` adds the corresponding ON DELETE action.

PostgreSQL gets a real enum: `::ENUM(active, inactive)` emits `CREATE TYPE status_enum AS ENUM ('active', 'inactive')` before the table, and the column uses that type. This gives you database-level validation without CHECK constraints.

Frequently asked questions

Why does name get NOT NULL but email doesn't, even though email is UK?

The UK marker maps to UNIQUE only — uniqueness and nullability are independent in SQL. Mermaid erDiagram has no native NOT NULL syntax, so you opt in per column with a %% ::NN directive on the line above it, exactly like name in the example.

Will the %% directive comments break GitHub's Mermaid rendering?

No. %% starts an ordinary Mermaid comment, so GitHub, GitLab, Obsidian, and the Mermaid Live editor all render the diagram unchanged. Only this converter reads the ::directives inside those comments.

How are enums handled in the generated PostgreSQL DDL?

With native CREATE TYPE … AS ENUM statements. The enum type is created before the table that uses it, and the column references the type — not a CHECK constraint or a plain VARCHAR.

Does the converter use SERIAL or IDENTITY columns?

SERIAL. A single integer primary key becomes SERIAL PRIMARY KEY (BIGSERIAL for bigint). Columns that are both PK and FK — the one-to-one child pattern — stay plain INTEGER so the value can mirror the parent key.

Related conversions