SQL DDL Generation

Last updated: March 2026

This reference covers everything between parsing your diagram and emitting SQL — column constraints, relationships, dialect-specific DDL, type mapping, directives, and normalization. Input syntax is documented separately in the Mermaid and PlantUML syntax guides.

Column Markers

Markers control constraints generated on each column:

MarkerMermaidPlantUMLEffect
Primary KeyPK<<PK>>Column becomes PRIMARY KEY. If single PK + int type → auto-increment.
Foreign KeyFK<<FK>>Column participates in ALTER TABLE ... FOREIGN KEY from relationships.
UniqueUK<<UNIQUE>>Adds UNIQUE constraint on the column.
Not Null (marker)(implicit for PK)* prefix or <<NOT NULL>>Adds NOT NULL to the column definition.

Relationships

Relationships define foreign key constraints between tables. Both Mermaid and PlantUML use similar cardinality notation:

NotationMeaningSQL Result
||--||One to oneFK + UNIQUE constraint
||--o{One to manyFK on the "many" side
}o--||Many to oneFK on the "many" side
}o--o{Many to manyJunction table (future)
Relationship → Foreign Key
-- 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");
Convention: The FK constraint name is auto-generated as fk_{table}_{column}. The FK column is matched by looking for columns named {referenced_table}_id on the "many" side.

To add ON DELETE / ON UPDATE cascade actions, use a directive comment on the line immediately above the relationship:

Relationship with Cascade — Mermaid
%% ::RELATIONSHIP[onDelete: CASCADE, onUpdate: SET NULL]
USER ||--o{ ORDER : places

-- Generates:
ALTER TABLE "order"
    ADD CONSTRAINT "fk_order_user_id"
    FOREIGN KEY ("user_id") REFERENCES "user"("id")
    ON DELETE CASCADE
    ON UPDATE SET NULL;

PostgreSQL postgres

The PostgreSQL generator produces idiomatic PG DDL:

PostgreSQL — Comments & Types
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:

MySQL — Enums & Comments
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:

SQLite — Simplified Types
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:

Oracle 19c+ — Identity & Types
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';
Oracle note: Oracle does not support 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 TypePostgreSQLMySQLSQLiteOracle
intINTEGERINTINTEGERNUMBER
stringTEXTVARCHAR(255)TEXTVARCHAR2(255)
decimalNUMERICDECIMALREALNUMBER
booleanBOOLEANTINYINT(1)INTEGERNUMBER(1)
dateDATEDATETEXTDATE
timestampTIMESTAMPTZDATETIMETEXTTIMESTAMP
uuidUUIDCHAR(36)TEXTRAW(16)
textTEXTTEXTTEXTCLOB
enumCREATE TYPEENUM(...)TEXTVARCHAR2(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 TypePostgreSQLMySQLSQLiteOracle
bigintBIGINTBIGINTINTEGERNUMBER(19)
smallintSMALLINTSMALLINTINTEGERNUMBER(5)
floatDOUBLE PRECISIONFLOATREALBINARY_DOUBLE
doubleDOUBLE PRECISIONDOUBLEREALBINARY_DOUBLE
realREALREALREALBINARY_FLOAT
varcharVARCHARVARCHARTEXTVARCHAR2
varchar2VARCHARVARCHARTEXTVARCHAR2
charCHARCHARTEXTCHAR
clobTEXTLONGTEXTTEXTCLOB
blobBYTEABLOBBLOBBLOB
serialSERIALINT AUTO_INCREMENTINTEGERNUMBER
datetimeTIMESTAMPTZDATETIMETEXTTIMESTAMP
boolBOOLEANTINYINT(1)INTEGERNUMBER(1)

Parameterized Types

Types with size or precision parameters like varchar2(4000) or number(10,2) are preserved in dialects that support them:

InputPostgreSQLMySQLSQLiteOracle
varchar2(4000)VARCHAR(4000)VARCHAR(4000)TEXTVARCHAR2(4000)
varchar(100)VARCHAR(100)VARCHAR(100)TEXTVARCHAR2(100)
number(10,2)NUMERIC(10,2)NUMERIC(10,2)NUMERICNUMBER(10,2)
char(36)CHAR(36)CHAR(36)TEXTCHAR(36)
SQLite note: SQLite ignores size parameters. All string types become 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:

DialectComment Syntax
PostgreSQLCOMMENT ON COLUMN "table"."col" IS 'text';
MySQL"col" INT COMMENT 'text' (inline)
SQLiteNot supported — comments are omitted
OracleCOMMENT ON COLUMN "table"."col" IS 'text';
Mermaid with comments
erDiagram
    USER {
        int id PK "Primary identifier"
        string email UK "Login email"
    }
PostgreSQL
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:

DialectEnum Strategy
PostgreSQLSeparate CREATE TYPE "table_col_enum" AS ENUM('a','b','c'); before the table. Column type references the created type.
MySQLInline ENUM('a','b','c') directly in the column definition.
SQLiteNo enum support — falls back to TEXT.
OracleNo native enum — falls back to VARCHAR2(255). Consider adding a CHECK constraint manually.

Constraints & Indexes

The following constraints are generated from diagram markers:

Default Values

Default values are set via directive comments and 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)

See the Directives section for full ::DEFAULT(value) syntax and examples.

Directives

Directives are structured comment annotations that extend your ER diagram with metadata that standard Mermaid and PlantUML syntax cannot express — things like NOT NULL on a Mermaid column, default column values, and ON DELETE / ON UPDATE cascade actions on foreign keys.

Syntax

FormatMermaidPlantUML
Column directive%% ::DIRECTIVE' ::DIRECTIVE
Relationship directive%% ::DIRECTIVE' ::DIRECTIVE
Alternate comment style// ::DIRECTIVE
Scoping rule: A directive applies to the very next column (inside an entity block) or the very next relationship line (at top level). It does not persist beyond that single element. Regular comments (without ::) are silently ignored and remain fully backward compatible.

::NN — Not Null

Marks the next column as NOT NULL. This is especially useful in Mermaid, where there is no built-in NOT NULL marker outside of the PK constraint. (In PlantUML, * prefix and <<NOT NULL>> are alternatives.)

Mermaid — ::NN
erDiagram
    USER {
        int id PK
        %% ::NN
        string email
        string bio
    }
PostgreSQL Output
CREATE TABLE "user" (
    "id" SERIAL PRIMARY KEY,
    "email" TEXT NOT NULL,
    "bio" TEXT
);
PlantUML — ::NN
@startuml
entity User {
  * id : int <<PK>>
  ' ::NN
  email : string
  bio : string
}
@enduml
PostgreSQL Output
CREATE TABLE "user" (
    "id" SERIAL PRIMARY KEY,
    "email" TEXT NOT NULL,
    "bio" TEXT
);

::DEFAULT(value) — Default Value

Sets a DEFAULT on the next column. The value is type-aware: numeric literals are emitted unquoted; everything else is single-quoted.

Mermaid — ::DEFAULT
erDiagram
    ORDER {
        int id PK
        %% ::DEFAULT(pending)
        string status
        %% ::DEFAULT(0)
        decimal total
    }
PostgreSQL Output
CREATE TABLE "order" (
    "id" SERIAL PRIMARY KEY,
    "status" TEXT DEFAULT 'pending',
    "total" NUMERIC DEFAULT 0
);
PlantUML — ::DEFAULT
@startuml
entity Order {
  * id : int <<PK>>
  ' ::DEFAULT(pending)
  status : string
  // ::DEFAULT(0)
  total : decimal
}
@enduml
MySQL Output
CREATE TABLE `order` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `status` VARCHAR(255) DEFAULT 'pending',
    `total` DECIMAL DEFAULT 0
);
Combining directives: You can stack ::NN and ::DEFAULT on adjacent lines before a column — both will be applied:
%% ::NN
%% ::DEFAULT(active)
string status
This generates: "status" TEXT NOT NULL DEFAULT 'active'

::RELATIONSHIP[...] — Cascade Actions

Adds ON DELETE and/or ON UPDATE referential actions to the foreign key generated by the next relationship line.

KeySupported Values
onDeleteCASCADE · SET NULL · RESTRICT · NO ACTION · SET DEFAULT
onUpdateCASCADE · SET NULL · RESTRICT · NO ACTION · SET DEFAULT
Mermaid — cascade delete
erDiagram
    USER {
        int id PK
        string name
    }
    ORDER {
        int id PK
        int user_id FK
        decimal total
    }

    %% ::RELATIONSHIP[onDelete: CASCADE]
    USER ||--o{ ORDER : places
PostgreSQL Output
ALTER TABLE "order"
    ADD CONSTRAINT "fk_order_user_id"
    FOREIGN KEY ("user_id")
    REFERENCES "user"("id")
    ON DELETE CASCADE;
PlantUML — both actions
@startuml
entity User {
  * id : int <<PK>>
  name : string
}
entity Order {
  * id : int <<PK>>
  user_id : int <<FK>>
  total : decimal
}

' ::RELATIONSHIP[onDelete: CASCADE, onUpdate: SET NULL]
User ||--o{ Order : places
@enduml
MySQL Output
ALTER TABLE `order`
    ADD CONSTRAINT `fk_order_user_id`
    FOREIGN KEY (`user_id`)
    REFERENCES `user`(`id`)
    ON DELETE CASCADE
    ON UPDATE SET NULL;

Schema Normalization

Before generating SQL, the schema goes through normalization:

Tip: Check warnings in the CLI (stderr) or API response (warnings array) to catch schema issues early.

Full Example: Directives in Action

This example combines all three P0 directives — ::NN, ::DEFAULT, and ::RELATIONSHIP — in a realistic blog schema with users, posts, and comments.

Mermaid Input
erDiagram
    USER {
        int id PK
        %% ::NN
        string username
        %% ::NN
        string email
        %% ::DEFAULT(active)
        string status
        %% ::DEFAULT(0)
        int login_count
        timestamp created_at
    }

    POST {
        int id PK
        int user_id FK
        %% ::NN
        string title
        %% ::DEFAULT(draft)
        string status
        boolean published
        timestamp created_at
    }

    COMMENT {
        int id PK
        int post_id FK
        int user_id FK
        %% ::NN
        text body
        timestamp created_at
    }

    %% ::RELATIONSHIP[onDelete: CASCADE]
    USER ||--o{ POST : writes
    %% ::RELATIONSHIP[onDelete: CASCADE]
    POST ||--o{ COMMENT : has
    %% ::RELATIONSHIP[onDelete: SET NULL]
    USER ||--o{ COMMENT : authors
PostgreSQL Output
CREATE TABLE "user" (
    "id" SERIAL PRIMARY KEY,
    "username" TEXT NOT NULL,
    "email" TEXT NOT NULL,
    "status" TEXT DEFAULT 'active',
    "login_count" INTEGER DEFAULT 0,
    "created_at" TIMESTAMP
);

CREATE TABLE "post" (
    "id" SERIAL PRIMARY KEY,
    "user_id" INTEGER,
    "title" TEXT NOT NULL,
    "status" TEXT DEFAULT 'draft',
    "published" BOOLEAN,
    "created_at" TIMESTAMP
);

CREATE TABLE "comment" (
    "id" SERIAL PRIMARY KEY,
    "post_id" INTEGER,
    "user_id" INTEGER,
    "body" TEXT NOT NULL,
    "created_at" TIMESTAMP
);

ALTER TABLE "post"
    ADD CONSTRAINT "fk_post_user_id"
    FOREIGN KEY ("user_id")
    REFERENCES "user"("id")
    ON DELETE CASCADE;

ALTER TABLE "comment"
    ADD CONSTRAINT "fk_comment_post_id"
    FOREIGN KEY ("post_id")
    REFERENCES "post"("id")
    ON DELETE CASCADE;

ALTER TABLE "comment"
    ADD CONSTRAINT "fk_comment_user_id"
    FOREIGN KEY ("user_id")
    REFERENCES "user"("id")
    ON DELETE SET NULL;

The same diagram with PlantUML syntax using ' directive comments:

PlantUML Input
@startuml
entity User {
  * id : int <<PK>>
  ' ::NN
  username : string
  ' ::NN
  email : string
  ' ::DEFAULT(active)
  status : string
  ' ::DEFAULT(0)
  login_count : int
  created_at : timestamp
}

entity Post {
  * id : int <<PK>>
  user_id : int <<FK>>
  ' ::NN
  title : string
  ' ::DEFAULT(draft)
  status : string
  published : boolean
  created_at : timestamp
}

entity Comment {
  * id : int <<PK>>
  post_id : int <<FK>>
  user_id : int <<FK>>
  ' ::NN
  body : text
  created_at : timestamp
}

' ::RELATIONSHIP[onDelete: CASCADE]
User ||--o{ Post : writes
' ::RELATIONSHIP[onDelete: CASCADE]
Post ||--o{ Comment : has
' ::RELATIONSHIP[onDelete: SET NULL]
User ||--o{ Comment : authors
@enduml
MySQL Output
CREATE TABLE `user` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(255) NOT NULL,
    `email` VARCHAR(255) NOT NULL,
    `status` VARCHAR(255) DEFAULT 'active',
    `login_count` INT DEFAULT 0,
    `created_at` DATETIME
);

CREATE TABLE `post` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT,
    `title` VARCHAR(255) NOT NULL,
    `status` VARCHAR(255) DEFAULT 'draft',
    `published` TINYINT(1),
    `created_at` DATETIME
);

CREATE TABLE `comment` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `post_id` INT,
    `user_id` INT,
    `body` TEXT NOT NULL,
    `created_at` DATETIME
);

ALTER TABLE `post`
    ADD CONSTRAINT `fk_post_user_id`
    FOREIGN KEY (`user_id`)
    REFERENCES `user`(`id`)
    ON DELETE CASCADE;

ALTER TABLE `comment`
    ADD CONSTRAINT `fk_comment_post_id`
    FOREIGN KEY (`post_id`)
    REFERENCES `post`(`id`)
    ON DELETE CASCADE;

ALTER TABLE `comment`
    ADD CONSTRAINT `fk_comment_user_id`
    FOREIGN KEY (`user_id`)
    REFERENCES `user`(`id`)
    ON DELETE SET NULL;
What this example shows:
  • %% ::NN / ' ::NNusername, email, title, and body are required (NOT NULL)
  • ::DEFAULT(active) / ::DEFAULT(draft) — string defaults are single-quoted in the output
  • ::DEFAULT(0) — numeric default is emitted unquoted
  • ::RELATIONSHIP[onDelete: CASCADE] — deleting a user cascades to their posts; deleting a post cascades to its comments
  • ::RELATIONSHIP[onDelete: SET NULL] — deleting a user sets user_id to NULL on orphaned comments (preserving comment history)

Full Example: E-Commerce Schema

Mermaid Input
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"
MySQL Output
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

PlantUML Input
@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

@enduml
Oracle Output
CREATE 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:

Input (Mermaid)
erDiagram
    USER {
        int id PK
        string name
        string email
    }
    ORDER {
        int id PK
        int user_id FK
        decimal total
    }
    USER ||--o{ ORDER : places
PostgreSQL
CREATE 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");
MySQL
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`);
SQLite
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");
Oracle 19c+
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");