Documentation
diagram2code converts Mermaid ER and PlantUML entity-relationship diagrams into production-ready SQL DDL for PostgreSQL, MySQL, SQLite, and Oracle 19c+.
π§ Mermaid ER
Parse erDiagram blocks with tables, columns, types, and relationships.
π± PlantUML
Parse @startuml entity diagrams with stereotypes and cardinalities.
π PostgreSQL
SERIAL PKs, UUID, CREATE TYPE for enums, COMMENT ON syntax.
π¬ MySQL
AUTO_INCREMENT, backtick quoting, inline ENUM(), COMMENT inline.
πͺΆ SQLite
Simplified type affinity (TEXT, INTEGER, REAL, BLOB), IF NOT EXISTS.
ποΈ Oracle 19c+
IDENTITY columns, NUMBER, VARCHAR2, COMMENT ON, no IF NOT EXISTS.
How It Works
The pipeline has three stages:
- Parse β The diagram text (Mermaid or PlantUML) is lexed and parsed into an Intermediate Schema Model (tables, columns, relationships).
- Normalize β The schema is cleaned: tables without a primary key get a synthetic
idcolumn, duplicate columns are removed, and warnings are emitted. - Generate β The normalized schema is emitted as SQL DDL for the chosen database dialect, with dialect-specific type mapping, quoting, constraints, and comments.
ββββββββββββββββ βββββββββββββββββββ ββββββββββββββββββββ
β Mermaid ER ββββββΆβ Intermediate ββββββΆβ PostgreSQL DDL β
β PlantUML β β Schema Model β β MySQL DDL β
ββββββββββββββββ βββββββββββββββββββ β SQLite DDL β
β Oracle DDL β
ββββββββββββββββββββQuick Start Example
Here's a simple diagram and the SQL it generates for each database:
erDiagram
USER {
int id PK
string name
string email
}
ORDER {
int id PK
int user_id FK
decimal total
}
USER ||--o{ ORDER : placesCREATE TABLE "user" (
"id" SERIAL PRIMARY KEY,
"name" TEXT,
"email" TEXT
);
CREATE TABLE "order" (
"id" SERIAL PRIMARY KEY,
"user_id" INTEGER,
"total" NUMERIC
);
ALTER TABLE "order"
ADD CONSTRAINT "fk_order_user_id"
FOREIGN KEY ("user_id") REFERENCES "user"("id");Mermaid ER Syntax
Mermaid ER diagrams begin with erDiagram. Tables are defined as blocks with columns, and relationships connect tables with cardinality notation.
Table & Column Syntax
erDiagram
TABLE_NAME {
type column_name MARKERS "optional comment"
}Each column line has:
- type β Any of the abstract types (
int,string,decimal, etc.) or native database types (varchar2(255),bigint, etc.). - column_name β The column identifier (lowercase, underscored).
- markers β Optional:
PK,FK,UK(unique key), or combinations. - comment β Optional: a quoted string after the markers.
erDiagram
PRODUCT {
int id PK "Primary key"
string name "Product name"
decimal price
boolean active
string sku UK "Stock keeping unit"
int category_id FK
timestamp created_at
}PlantUML Syntax
PlantUML diagrams are wrapped in @startuml / @enduml. Entities use the entity keyword, columns use name : type order (note: reversed from Mermaid), and stereotypes mark PK/FK/UNIQUE.
@startuml
entity User {
* id : int <<PK>>
name : string
email : string <<UNIQUE>>
}
entity Order {
* id : int <<PK>>
user_id : int <<FK>>
total : decimal
}
User ||--o{ Order : places
@enduml- Wrapped in
@startuml/@enduml - Column format:
name : type(name first) - Required columns: prefix with
* - Markers use stereotypes:
<<PK>>,<<FK>>,<<UNIQUE>> - Entity names are
PascalCase(converted tosnake_casein SQL)
Column Markers
Markers control constraints generated on each column:
| Marker | Mermaid | PlantUML | Effect |
|---|---|---|---|
| Primary Key | PK | <<PK>> | Column becomes PRIMARY KEY. If single PK + int type β auto-increment. |
| Foreign Key | FK | <<FK>> | Column participates in ALTER TABLE ... FOREIGN KEY from relationships. |
| Unique | UK | <<UNIQUE>> | Adds UNIQUE constraint on the column. |
| Not Null | (implicit for PK) | * prefix | Adds NOT NULL to the column definition. |
Relationships
Relationships define foreign key constraints between tables. Both Mermaid and PlantUML use similar cardinality notation:
| Notation | Meaning | SQL Result |
|---|---|---|
||--|| | One to one | FK + UNIQUE constraint |
||--o{ | One to many | FK on the "many" side |
}o--|| | Many to one | FK on the "many" side |
}o--o{ | Many to many | Junction table (future) |
-- From this Mermaid relationship:
USER ||--o{ ORDER : places
-- diagram2code generates:
ALTER TABLE "order"
ADD CONSTRAINT "fk_order_user_id"
FOREIGN KEY ("user_id") REFERENCES "user"("id");fk_{table}_{column}. The FK column is matched by looking for columns named {referenced_table}_id on the "many" side.PostgreSQL postgres
The PostgreSQL generator produces idiomatic PG DDL:
- Auto-increment PK:
SERIAL PRIMARY KEY - Identifier quoting: double-quotes (
"user") - IF NOT EXISTS: supported
- Enums: separate
CREATE TYPE ... AS ENUM(...)statements - Comments:
COMMENT ON COLUMN "table"."col" IS '...' - Boolean: native
BOOLEANtype - UUID: native
UUIDtype
CREATE TABLE "product" (
"id" SERIAL PRIMARY KEY,
"name" TEXT NOT NULL,
"price" NUMERIC,
"active" BOOLEAN DEFAULT true,
"created_at" TIMESTAMPTZ
);
COMMENT ON COLUMN "product"."name" IS 'The display name of the product';MySQL mysql
The MySQL generator produces idiomatic MySQL DDL:
- Auto-increment PK:
INT AUTO_INCREMENT PRIMARY KEY - Identifier quoting: backticks (
`user`) - IF NOT EXISTS: supported
- Enums: inline
ENUM('a', 'b', 'c')in column definition - Comments: inline
COMMENT 'text'on column definition - Boolean: mapped to
TINYINT(1) - UUID: mapped to
CHAR(36) - String: mapped to
VARCHAR(255)(not TEXT)
CREATE TABLE `order` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`status` ENUM('pending', 'shipped', 'delivered') COMMENT 'Order status',
`total` DECIMAL,
`user_id` INT
);
ALTER TABLE `order`
ADD CONSTRAINT `fk_order_user_id`
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`);SQLite sqlite
SQLite uses a simplified type system with type affinity:
- Auto-increment PK:
INTEGER PRIMARY KEY(SQLite auto-increments INTEGER PKs) - Identifier quoting: double-quotes (
"user") - IF NOT EXISTS: supported
- Enums: not supported β falls back to
TEXT - Comments: not supported β comments are omitted
- Boolean: mapped to
INTEGER(0/1) - Date/Time: all mapped to
TEXT(ISO-8601 strings) - Parameterized types: parameters are dropped (e.g.
varchar(255)βTEXT)
CREATE TABLE "product" (
"id" INTEGER PRIMARY KEY,
"name" TEXT,
"price" REAL,
"active" INTEGER,
"created_at" TEXT
);
-- Note: No COMMENT syntax, no ENUM, no UUID.
-- boolean β INTEGER, decimal β REAL, timestamp β TEXT.Oracle 19c+ oracle
The Oracle generator targets Oracle Database 19c and later:
- Auto-increment PK:
NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY - Identifier quoting: double-quotes (
"user") - IF NOT EXISTS: not supported β Oracle lacks this syntax
- Enums: not supported β falls back to
VARCHAR2(255) - Comments:
COMMENT ON COLUMN "table"."col" IS '...' - Boolean: mapped to
NUMBER(1) - UUID: mapped to
RAW(16) - String: mapped to
VARCHAR2(255) - Text/CLOB: mapped to
CLOB - Parameterized types: preserved (e.g.
varchar2(4000)βVARCHAR2(4000))
CREATE TABLE "product" (
"id" NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
"name" VARCHAR2(255) NOT NULL,
"price" NUMBER,
"active" NUMBER(1),
"created_at" TIMESTAMP
);
COMMENT ON COLUMN "product"."name" IS 'The display name of the product';CREATE TABLE IF NOT EXISTS. Running the generated DDL twice will produce an error. Use PL/SQL exception handling or check ALL_TABLES before creating.Abstract Type Mapping
Abstract types are the recommended portable types. They map to the best native type for each database:
| Abstract Type | PostgreSQL | MySQL | SQLite | Oracle |
|---|---|---|---|---|
int | INTEGER | INT | INTEGER | NUMBER |
string | TEXT | VARCHAR(255) | TEXT | VARCHAR2(255) |
decimal | NUMERIC | DECIMAL | REAL | NUMBER |
boolean | BOOLEAN | TINYINT(1) | INTEGER | NUMBER(1) |
date | DATE | DATE | TEXT | DATE |
timestamp | TIMESTAMPTZ | DATETIME | TEXT | TIMESTAMP |
uuid | UUID | CHAR(36) | TEXT | RAW(16) |
text | TEXT | TEXT | TEXT | CLOB |
enum | CREATE TYPE | ENUM(...) | TEXT | VARCHAR2(255) |
Native / Pass-through Types
You can also use native database types directly in your diagrams. They are mapped to the closest equivalent in each dialect:
| Native Type | PostgreSQL | MySQL | SQLite | Oracle |
|---|---|---|---|---|
bigint | BIGINT | BIGINT | INTEGER | NUMBER(19) |
smallint | SMALLINT | SMALLINT | INTEGER | NUMBER(5) |
float | DOUBLE PRECISION | FLOAT | REAL | BINARY_DOUBLE |
double | DOUBLE PRECISION | DOUBLE | REAL | BINARY_DOUBLE |
real | REAL | REAL | REAL | BINARY_FLOAT |
varchar | VARCHAR | VARCHAR | TEXT | VARCHAR2 |
varchar2 | VARCHAR | VARCHAR | TEXT | VARCHAR2 |
char | CHAR | CHAR | TEXT | CHAR |
clob | TEXT | LONGTEXT | TEXT | CLOB |
blob | BYTEA | BLOB | BLOB | BLOB |
serial | SERIAL | INT AUTO_INCREMENT | INTEGER | NUMBER |
datetime | TIMESTAMPTZ | DATETIME | TEXT | TIMESTAMP |
bool | BOOLEAN | TINYINT(1) | INTEGER | NUMBER(1) |
Parameterized Types
Types with size or precision parameters like varchar2(4000) or number(10,2) are preserved in dialects that support them:
| Input | PostgreSQL | MySQL | SQLite | Oracle |
|---|---|---|---|---|
varchar2(4000) | VARCHAR(4000) | VARCHAR(4000) | TEXT | VARCHAR2(4000) |
varchar(100) | VARCHAR(100) | VARCHAR(100) | TEXT | VARCHAR2(100) |
number(10,2) | NUMERIC(10,2) | NUMERIC(10,2) | NUMERIC | NUMBER(10,2) |
char(36) | CHAR(36) | CHAR(36) | TEXT | CHAR(36) |
TEXT, and numeric types are reduced to NUMERIC, INTEGER, or REAL based on type affinity.Unknown Types
If you use a type not in the mapping tables (e.g. jsonb, geometry, citext), it is passed through as-is in UPPERCASE:
-- Input: my_custom_type col_name -- Output (all dialects): "col_name" MY_CUSTOM_TYPE
This means you can use database-specific types even if they aren't in the built-in mapping. They will work for your target dialect and pass through unchanged for others.
Comments
Column comments from the diagram are emitted using dialect-specific syntax:
| Dialect | Comment Syntax |
|---|---|
| PostgreSQL | COMMENT ON COLUMN "table"."col" IS 'text'; |
| MySQL | "col" INT COMMENT 'text' (inline) |
| SQLite | Not supported β comments are omitted |
| Oracle | COMMENT ON COLUMN "table"."col" IS 'text'; |
erDiagram
USER {
int id PK "Primary identifier"
string email UK "Login email"
}CREATE TABLE "user" (
"id" SERIAL PRIMARY KEY,
"email" TEXT NOT NULL UNIQUE
);
COMMENT ON COLUMN "user"."id"
IS 'Primary identifier';
COMMENT ON COLUMN "user"."email"
IS 'Login email';Enums
Enum handling varies significantly by dialect:
| Dialect | Enum Strategy |
|---|---|
| PostgreSQL | Separate CREATE TYPE "table_col_enum" AS ENUM('a','b','c'); before the table. Column type references the created type. |
| MySQL | Inline ENUM('a','b','c') directly in the column definition. |
| SQLite | No enum support β falls back to TEXT. |
| Oracle | No native enum β falls back to VARCHAR2(255). Consider adding a CHECK constraint manually. |
Constraints & Indexes
The following constraints are generated from diagram markers:
- PRIMARY KEY β from
PKmarker. Single integer PKs get auto-increment. Composite PKs usePRIMARY KEY ("col1", "col2"). - FOREIGN KEY β from relationships. Generated as
ALTER TABLE ... ADD CONSTRAINT. - UNIQUE β from
UK/<<UNIQUE>>marker. AddsUNIQUEinline. - NOT NULL β Non-PK, non-nullable columns get
NOT NULL. - INDEX β Indexed columns generate
CREATE INDEX "idx_{table}_{col}" ON "table"("col").
Default Values
Default values are emitted with automatic quoting: numeric values are unquoted, string values are single-quoted.
-- Numeric default: DEFAULT 0 -- String default: DEFAULT 'active' -- Boolean default: DEFAULT true (postgres) / DEFAULT 1 (mysql/sqlite/oracle)
Schema Normalization
Before generating SQL, the schema goes through normalization:
- Synthetic PK: Tables without any
PKcolumn automatically get anid int PKcolumn added. A warning is emitted. - Duplicate columns: If a table has duplicate column names, only the first is kept. A warning is emitted.
- Warning severity: Normalization issues produce warnings (not errors), so SQL is still generated.
stderr) or API response (warnings array) to catch schema issues early.Full Example: E-Commerce Schema
erDiagram
CUSTOMER {
int id PK
string name
string email UK
timestamp created_at
}
PRODUCT {
int id PK
string title
decimal price
text description
boolean in_stock
}
ORDER {
int id PK
int customer_id FK
timestamp ordered_at
decimal total
}
ORDER_ITEM {
int id PK
int order_id FK
int product_id FK
int quantity
decimal unit_price
}
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : "listed in"CREATE TABLE `customer` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255),
`email` VARCHAR(255) NOT NULL UNIQUE,
`created_at` DATETIME
);
CREATE TABLE `product` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(255),
`price` DECIMAL,
`description` TEXT,
`in_stock` TINYINT(1)
);
CREATE TABLE `order` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`customer_id` INT,
`ordered_at` DATETIME,
`total` DECIMAL
);
CREATE TABLE `order_item` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`order_id` INT,
`product_id` INT,
`quantity` INT,
`unit_price` DECIMAL
);
ALTER TABLE `order`
ADD CONSTRAINT `fk_order_customer_id`
FOREIGN KEY (`customer_id`)
REFERENCES `customer`(`id`);
ALTER TABLE `order_item`
ADD CONSTRAINT `fk_order_item_order_id`
FOREIGN KEY (`order_id`)
REFERENCES `order`(`id`);
ALTER TABLE `order_item`
ADD CONSTRAINT `fk_order_item_product_id`
FOREIGN KEY (`product_id`)
REFERENCES `product`(`id`);Full Example: Blog Platform
@startuml
entity Author {
* id : int <<PK>>
name : string
email : string <<UNIQUE>>
bio : text
}
entity Post {
* id : int <<PK>>
author_id : int <<FK>>
title : string
body : text
published : boolean
created_at : timestamp
}
entity Tag {
* id : int <<PK>>
name : string <<UNIQUE>>
}
Author ||--o{ Post : writes
@endumlCREATE TABLE "author" (
"id" NUMBER GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
"name" VARCHAR2(255),
"email" VARCHAR2(255) NOT NULL UNIQUE,
"bio" CLOB
);
CREATE TABLE "post" (
"id" NUMBER GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
"author_id" NUMBER,
"title" VARCHAR2(255),
"body" CLOB,
"published" NUMBER(1),
"created_at" TIMESTAMP
);
CREATE TABLE "tag" (
"id" NUMBER GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
"name" VARCHAR2(255) NOT NULL UNIQUE
);
ALTER TABLE "post"
ADD CONSTRAINT "fk_post_author_id"
FOREIGN KEY ("author_id")
REFERENCES "author"("id");Same Diagram β All 4 Dialects
Here's how the same simple diagram renders across all supported databases:
erDiagram
USER {
int id PK
string name
string email
}
ORDER {
int id PK
int user_id FK
decimal total
}
USER ||--o{ ORDER : placesCREATE TABLE "user" (
"id" SERIAL PRIMARY KEY,
"name" TEXT,
"email" TEXT
);
CREATE TABLE "order" (
"id" SERIAL PRIMARY KEY,
"user_id" INTEGER,
"total" NUMERIC
);
ALTER TABLE "order"
ADD CONSTRAINT "fk_order_user_id"
FOREIGN KEY ("user_id")
REFERENCES "user"("id");CREATE TABLE `user` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255),
`email` VARCHAR(255)
);
CREATE TABLE `order` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT,
`total` DECIMAL
);
ALTER TABLE `order`
ADD CONSTRAINT `fk_order_user_id`
FOREIGN KEY (`user_id`)
REFERENCES `user`(`id`);CREATE TABLE "user" (
"id" INTEGER PRIMARY KEY,
"name" TEXT,
"email" TEXT
);
CREATE TABLE "order" (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER,
"total" REAL
);
ALTER TABLE "order"
ADD CONSTRAINT "fk_order_user_id"
FOREIGN KEY ("user_id")
REFERENCES "user"("id");CREATE TABLE "user" (
"id" NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
"name" VARCHAR2(255),
"email" VARCHAR2(255)
);
CREATE TABLE "order" (
"id" NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
"user_id" NUMBER,
"total" NUMBER
);
ALTER TABLE "order"
ADD CONSTRAINT "fk_order_user_id"
FOREIGN KEY ("user_id") REFERENCES "user"("id");