datamodelingtools.com

worked example · published

Diagram-as-code for databases: one schema in SQL DDL, DBML and Mermaid

The same two-table schema written three ways, with the SQL that DBML actually generates, and what each format is good and bad at.

Diagram-as-code means writing the schema as text and letting a tool draw it. The text goes in Git, changes show up in code review, and nobody drags boxes around. Here is one small schema written three ways.

The schema

A customer places many orders. Each order belongs to exactly one customer.

Version 1: SQL DDL

This is what a PostgreSQL database actually runs. The other two formats exist to describe it.

CREATE TABLE customers (
  id integer PRIMARY KEY,
  email varchar(255) NOT NULL UNIQUE,
  created_at timestamp NOT NULL
);

CREATE TABLE orders (
  id integer PRIMARY KEY,
  customer_id integer NOT NULL REFERENCES customers (id),
  ordered_at timestamp NOT NULL,
  total_cents integer NOT NULL
);

Version 2: DBML

DBML is the open-source language behind dbdiagram.io, also used by ChartDB’s editor. Settings go in square brackets; relationships can be declared separately with Ref.

Table customers {
  id integer [primary key]
  email varchar(255) [not null, unique]
  created_at timestamp [not null]
}

Table orders {
  id integer [primary key]
  customer_id integer [not null]
  ordered_at timestamp [not null]
  total_cents integer [not null]
}

Ref: orders.customer_id > customers.id // many-to-one

We converted that DBML to PostgreSQL with @dbml/core 11.0.0. This is the exact output:

CREATE TABLE "customers" (
  "id" integer PRIMARY KEY,
  "email" varchar(255) UNIQUE NOT NULL,
  "created_at" timestamp NOT NULL
);

CREATE TABLE "orders" (
  "id" integer PRIMARY KEY,
  "customer_id" integer NOT NULL,
  "ordered_at" timestamp NOT NULL,
  "total_cents" integer NOT NULL
);

ALTER TABLE "orders" ADD FOREIGN KEY ("customer_id") REFERENCES "customers" ("id") DEFERRABLE INITIALLY IMMEDIATE;

Two details are worth noticing. The foreign key comes out as a separate ALTER TABLE rather than inline. It is also marked DEFERRABLE INITIALLY IMMEDIATE, which behaves like a normal constraint unless a transaction defers it. Review generated DDL before running it. Generated is not the same as identical.

Version 3: Mermaid erDiagram

Mermaid draws ER diagrams from text inside Markdown and many docs tools. Relationship lines use crow’s-foot markers, and attributes can carry PK, FK or UK keys.

erDiagram
    CUSTOMER ||--o{ ORDER : places
    CUSTOMER {
        int id PK
        string email UK
        timestamp created_at
    }
    ORDER {
        int id PK
        int customer_id FK
        timestamp ordered_at
        int total_cents
    }

||--o{ reads as “exactly one customer to zero or more orders”. Mermaid types are labels, not database types. Nothing checks that string matches varchar(255).

Which format should you use?

SQL DDL DBML Mermaid
Runs on a database yes no (converts to SQL) no
Converts to/from SQL — yes, with @dbml/core no
Renders in Markdown docs no via dbdiagram/dbdocs yes
Shows business relationships weakly yes (Ref) yes (labels)
Stays in sync automatically it is the schema if you regenerate no

One workable split: migrations (SQL) are the source of truth, DBML is generated from the database for reference diagrams, and Mermaid sketches live in design docs before anything exists. Pick one format as the truth and treat the others as views of it.

For editors built around these formats, see the diagram-as-code filter in the comparison table.

Sources

  1. DBML home and syntax overview project documentation · full page read
  2. holistics/dbml repository (Apache-2.0) source repository · full page read
  3. Mermaid entity relationship diagram syntax project documentation · full page read

Update history

  • Published.

Tools in this guide