In this guide

FROM SCHEMA TO DIAGRAM

SQL to ERD guide

Turn your database schema into a diagram. Learn supported SQL, preview data, and query your model.

SQL DDL → ER diagramOpen SQL to ERD
GETTING STARTED

AI-Ready SQL Syntax Contract

Use this page as a compact contract when asking an AI to write or edit SQL for Erd Go. Prefer the syntax patterns below, keep statements complete and semicolon-terminated, and avoid unsupported database objects unless you only need raw SQL outside Erd Go. SQLite, PostgreSQL, MySQL, and MSSQL syntax are supported across the shared subset below. SQL-to-ERD conversion runs entirely in the browser, and its parser and preview engines are locked in by thousands of unit and integration tests covering real-world production schemas (SaaS, banking, social, analytics, EHR).

SQLite

INTEGER PRIMARY KEY rowid rules, typeless columns, AUTOINCREMENT, WITHOUT ROWID, validated STRICT tables, FK actions, and migration replay

PostgreSQL

ENUM & composite types, arrays, JSONB, SERIAL, identity columns, NULLS NOT DISTINCT, expression indexes, schema identity, and quoted-name case sensitivity

MySQL

AUTO_INCREMENT, inline ENUM & SET, backticks, COMMENT, UNSIGNED/SIGNED, inline indexes, CHANGE/MODIFY COLUMN, and FIRST/AFTER placement

MSSQL

IDENTITY, NVARCHAR(MAX), bracketed names, clustered indexes, GO batches, ALTER COLUMN, computed columns, named defaults, and temporal periods

DDL

CREATE TYPE, CREATE TABLE, ALTER TABLE migration replay, CREATE INDEX, and safe DROP statements for schema shape

DML / SELECT

INSERT, UPDATE, DELETE for preview data, plus read-only SELECT syntax for joins, CTEs, aggregates, windows, and reports

Quick Start

Use this as a model response when asking an AI for Erd Go-compatible SQL: custom types first, tables next, relationship constraints after tables when useful, then sample DML. Keep read-only SELECT queries separate.

-- Define custom types
CREATE TYPE status_enum AS ENUM (
  'active', 'inactive', 'suspended'
);

-- Create tables with constraints
CREATE TABLE departments (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE employees (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email TEXT UNIQUE NOT NULL,
  status status_enum DEFAULT 'active',
  department_id INTEGER,
  salary NUMERIC(10, 2) DEFAULT 0
);

-- Add constraints via ALTER TABLE
ALTER TABLE employees
  ADD CONSTRAINT fk_dept
  FOREIGN KEY (department_id)
  REFERENCES departments(id);

-- Add sample data after schema DDL
INSERT INTO departments (id, name) VALUES
  (1, 'Engineering'),
  (2, 'Design');

INSERT INTO employees (id, name, email, status, department_id, salary) VALUES
  (1, 'Alice', '[email protected]', 'active', 1, 1500.00),
  (2, 'Bob', '[email protected]', 'inactive', 2, 1200.00);

UPDATE employees
  SET status = 'active'
  WHERE id = 2;
Schema syntax: Relationships can be declared with either inline REFERENCES or ALTER TABLE ADD FOREIGN KEY. Both forms contribute the same relationship metadata.
DML syntax: Put INSERT, UPDATE, and DELETE after the schema DDL. The script is evaluated top-to-bottom, so sample rows should reference tables and types that already exist.
SELECT syntax: Keep read-only reporting queries separate from mutation scripts. They should only reference tables and rows produced by the schema/DML script.
SELECT e.name, d.name AS department, e.salary
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.status = 'active'
ORDER BY e.salary DESC;

SQL tabs & file import

Start a new SQL file with a sample, a blank Schema tab, or Import SQL Files. The sample separates schema and data into tabs. Add SQL tab creates another editor tab; double-click its title to rename it.

Active tabs compile together in their visible order for the ERD, Data View, Query View, and AI requests. An inactive tab remains a saved draft and is excluded from those workflows. Place types and tables before statements that depend on them.

Import SQL files accepts multiple text SQL files and reviews parsing issues together before import. This helps resolve references between files. Use the tab order to keep dependencies and migration steps intentional.

Use arrow keys and Home/End in the tab strip to switch tabs, Alt+Left/Right to reorder them, and Shift+Alt+F to format SQL in the editor.

Edit and review the diagram

SQL is the source of schema meaning. Use Add Table to construct a table, or select table and column actions on the canvas to edit columns and connections. Canvas schema edits update the owning SQL tabs as well as the diagram.

Removing a column previews affected connections, keys, indexes, and dependent SQL. Review the removal or cascade before confirming. Complex migrations or unsupported dependent statements can require direct SQL editing.

Use the ERD health score to inspect structural findings and opt-in relationship suggestions. Suggested relationships need review; they do not replace declared foreign keys or establish business rules automatically.

Arrange tables with Layout Mode, zoom or Fit to Screen, find tables with Table Navigator, and use Table Colors or the minimap to navigate a large diagram. Hover over or focus a toolbar control for its help text.

AI review inside the SQL editor

  1. Enter a specific change in Ask AI to edit SQL and choose an available model. Set Gemini API key is available when you want to use your own Gemini key.
  2. Review the proposed changes and temporary diagram preview. The request uses the active-tab snapshot captured when it was sent; a response is not saved automatically.
  3. Accept or reject individual changes, or use the actions for the remaining changes. Navigate code changes with F7 and Shift+F7. Protected inactive tabs remain drafts.
  4. Inspect the accepted SQL, then save the document. Copy AI Prompt in this guide also lets you use the documentation with an assistant outside the app.

Data, queries, exports & saved files

Use the ERD, Data, and Query buttons to switch workbenches. Data View materializes supported DDL and DML in the browser and exposes tables, rows, types, and an execution log. Download a populated table as CSV from its data panel.

Query View runs read-only SELECT queries against the current in-memory tables. It supports the syntax documented below; it does not connect to an external database. Query text is saved with the SQL document context.

Choose Export ERD to select PNG, image-backed SVG, or PDF with HD, 2K, or 4K sizing. Exported images do not replace an editable saved document.

Sign in and use Save SQL and ERD to save the SQL and diagram context, including tabs and layout. Open saved files from the file list, and use Share on an owned saved document to configure public access and distribute its read-only link.

SQL, Prisma, and Builder documents open in their own tools. Prisma and Builder have separate file lists and cloud-only saving. Unsaved-change prompts protect edited documents when leaving a workspace; save your changes before sharing or switching away.

Troubleshoot the SQL workspace

Troubleshoot the SQL workspace
SymptomWhat to check
A table is missingCheck parsing diagnostics, active tabs, and statement order. A later DROP or rename can change the final diagram.
A relationship is missingCheck both tables and columns exist, foreign-key column counts match, and the referenced key is globally unique. Schema-qualify ambiguous names.
Data View shows no rowsPut INSERT after its CREATE TABLE in an active tab. Inspect the execution log for rejected rows or a transaction rollback.
Query results no longer match the SQLRun the query again after changing the schema or data; use the current in-memory tables.
Add Table suggests a different dialectDetected active-workspace syntax takes priority over the manually selected fallback. Review the SQL language indicator and table-specific defaults.
A saved file cannot be editedCheck sign-in and ownership. A public link grants read-only access; use Retry or Open files for recoverable loading errors.

AI Syntax Workflow

When using an AI assistant, ask it to produce SQL in this order. This keeps the result easy to parse, easy to diagram, and easy to preview with sample rows or read-only queries.

1. Define domains first

Put CREATE TYPE enum/composite definitions before tables that reference them. Prefer explicit names over ad hoc inline types when a domain is shared.

2. Create base tables

Use CREATE TABLE for columns, primary keys, uniqueness, defaults, checks, generated columns, and inline foreign keys.

3. Replay migrations

Use ALTER TABLE for added constraints, renames, dropped columns, column type/nullability changes, and circular foreign keys.

4. Add data and queries last

Put INSERT/UPDATE/DELETE after all DDL. Keep SELECT queries separate from schema mutation when you want result sets.

AI prompt tip: Ask for one complete, semicolon-terminated SQL script. Tell the AI to avoid unsupported objects such as stored procedures, triggers, recursive CTEs in read-only SELECT queries, and ALTER TYPE. Expression indexes are preserved with a warning; request simple column keys when you need ERD index badges.

DDL & DML Syntax

These statement forms are safe for AI-generated schema scripts and sample data. The same syntax drives ERD structure and in-memory preview rows, so keep DDL before DML and avoid external database-specific side effects.

Supported Statements

-- Schema statements
CREATE TYPE type_name AS ENUM (...);
CREATE TYPE type_name AS (...);
CREATE TABLE table_name (...);
CREATE OR REPLACE TABLE table_name (...);
CREATE VIEW view_name AS SELECT ...;
CREATE OR REPLACE VIEW view_name AS SELECT ...;
CREATE OR ALTER VIEW view_name AS SELECT ...;
ALTER TABLE table_name ADD ...;
ALTER TABLE table_name DROP COLUMN column_name;
ALTER TABLE table_name RENAME COLUMN old_name TO new_name;
CREATE INDEX index_name ON table_name (column_name);
DROP INDEX index_name;
TRUNCATE TABLE table_name;
DROP TABLE [IF EXISTS] table_name;
DROP VIEW [IF EXISTS] view_name;
DROP TYPE [IF EXISTS] type_name;

-- Data statements
INSERT INTO table_name [(col1, col2, ...)] VALUES (...), (...);
UPDATE table_name SET col1 = value WHERE condition;
DELETE FROM table_name WHERE condition;
WHERE support: =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN, NOT BETWEEN, IS NULL, IS NOT NULL, plus AND / OR combinations. NULL comparisons follow SQL-standard UNKNOWN logic — col = NULL never matches; use col IS NULL instead. Unknown assignment targets, right-hand column references, and filter columns reject the complete mutation before any row changes.

Preview Semantics for AI-Generated DML

Generate sample data locally: In Data View, choose Generate sample data, choose auto detection or a SQL dialect, a name dictionary from 26 countries or general English, an optional nullable-value percentage, and table row counts, and expand a table to customize its generators, ranges, or allowed values. Preview first, then copy/download SQL or insert a new Sample Data tab. No AI key or external data request is needed. Generation preserves existing rows, copies complete foreign-key tuples with balanced parent coverage, and validates supported constraints and scalar types. The catalog has over 3,200 built-in entries, including more than 2,600 names with native spelling and ASCII email handles, plus companies, products, jobs, streets, and location examples across more than 40 countries, alongside currencies, IP addresses, SKUs, statuses, colors, dates, and JSON. Batches support up to 5,000 rows per table, 10,000 rows and 150,000 cells overall, and 5 MB of SQL. Required empty relationship cycles, exhausted unique domains, colliding table names across schemas, and unsupported required complex types need adjustments before generation can finish. Nullable complex values and cycle-breaking references can become NULL with a preview note. Seeds repeat generated dictionary values; database defaults still follow their normal behavior. Finish AI review first, and reopen the generator if the SQL workspace changes.

Rows are replayed from scratch

DDL and DML statements are applied together in source order; each data statement uses the schema live at that point.

Types stay inspectable

Named enums and composite types created with CREATE TYPE remain part of the parsed schema.

Constraints are enforced

PK, FK, NOT NULL, UNIQUE, DEFAULT, CHECK, generated columns, and enum domains affect preview rows.

Errors should be fixable

Prefer explicit column lists and concrete WHERE clauses so diagnostics can point at the exact table or column.

Do not ask AI to rely on: standalone SELECT result sets in the main schema/DML script, unsupported procedural bodies, or external database calls. CREATE VIEW, CREATE OR REPLACE VIEW, and MSSQL CREATE OR ALTER VIEW are materialized from their SELECT result for preview; keep ad hoc read-only queries separate.

Transactions in Data View

Data View supports BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO, and RELEASE in the in-memory preview. These statements affect preview rows; they never commit to an external database.

SQLite, PostgreSQL, and SQL Server retain transactional DDL in the preview. MySQL DDL commits the active transaction. SQLite PRAGMA foreign_keys and MySQL SET FOREIGN_KEY_CHECKS affect subsequent DML validation.

The ERD renderer ignores transaction wrappers and represents schema statements in source order. A transaction rollback in Data View can therefore produce a different set of tables from the diagram. Check each workbench for the behavior you are testing.

CREATE TABLE preview_items (
  id INTEGER PRIMARY KEY,
  label TEXT NOT NULL
);
INSERT INTO preview_items (id, label) VALUES (1, 'Before transaction');

BEGIN;
INSERT INTO preview_items (id, label) VALUES (2, 'Keep this row');
SAVEPOINT before_trial;
INSERT INTO preview_items (id, label) VALUES (3, 'Undo this row');
ROLLBACK TO before_trial;
RELEASE before_trial;
COMMIT;
In this example, Data View retains items 1 and 2. Item 3 is removed by ROLLBACK TO before COMMIT. Query View reads those final preview rows.

SELECT Query Syntax

Ask AI to keep read-only reporting queries as SELECT statements. These can reference the tables and rows produced by the schema/DML script and are useful for joins, filters, totals, and report-shaped result sets without requiring a live database.

Supported Query Shape

WITH dept_totals AS (
  SELECT department_id, COUNT(*) AS employee_count, SUM(salary) AS payroll
  FROM employees
  GROUP BY department_id
)
SELECT d.name,
       dt.employee_count,
       dt.payroll
FROM dept_totals dt
JOIN departments d ON d.id = dt.department_id
WHERE dt.employee_count >= 1
ORDER BY dt.payroll DESC
LIMIT 10;

SELECT only

The query editor accepts SELECT statements, including SELECT without FROM and WITH CTEs that resolve to SELECT.

Joins & filters

INNER, LEFT, RIGHT, FULL, and CROSS joins, WHERE conditions, EXISTS, IN subqueries, BETWEEN, LIKE, ILIKE, and NULL checks.

Aggregates

GROUP BY, HAVING, COUNT, SUM, AVG, MIN, MAX, ARRAY_AGG, GROUP_CONCAT, STRING_AGG, and DISTINCT variants.

Ordering & paging

ORDER BY names or ordinals, ASC/DESC, NULLS FIRST/LAST, LIMIT/OFFSET, TOP, and FETCH FIRST.

Advanced SELECT Support

CTEs & derived tables

WITH clauses, nested SELECTs in FROM, scalar subqueries, EXISTS, NOT EXISTS, IN, and NOT IN are supported.

Set operators

UNION, UNION ALL, INTERSECT, and EXCEPT can combine compatible result sets.

Window functions

ROW_NUMBER, RANK, DENSE_RANK, NTILE, LAG, LEAD, FIRST_VALUE, and LAST_VALUE with OVER and PARTITION BY. Ordered windows use the peer-aware SQL default frame, and NTILE assigns larger buckets first.

Dialect helpers

Common SQLite, PostgreSQL, MySQL, and MSSQL functions are normalized for preview, including JSON and date/string helpers.

Read-only by design: Keep INSERT, UPDATE, DELETE, CREATE, ALTER, and DROP statements in the schema/DML script. Query syntax should stay SELECT-only.

Diagnostics & Errors

Syntax diagnostics surface schema-parse, DDL/DML execution, and read-only SELECT issues with clear messages. Parse errors include line/column positions when possible, and execution messages point back to the table, column, or statement shape that needs correction.

Schema Parse Errors

The schema parser (SQLite, PostgreSQL, MySQL, and MSSQL) reports problems with two severities: error stops a single statement from contributing to the schema; warning keeps valid syntax but flags something the user should know about.

// Every entry the editor receives has this shape:
{
  message:  "Column 'a' missing data type",
  severity: "error" | "warning",
  position: { line: 1, column: 17, index: 16 },
  token?:   { type, value, raw }   // present for parser-side errors
}

Parser errors

Empty PRIMARY KEY / UNIQUE / FOREIGN KEY column lists, missing required data types (including SQLite STRICT columns), unmatched parentheses, unexpected commas, and malformed constraints.

Tokenizer warnings

Unclosed strings, unclosed block comments, mismatched parentheses, and suspicious cross-line CHECK expressions.

Foreign-key warnings

FKs that point to an unknown table, unknown column, ambiguous schema leaf, or non-PK/non-UNIQUE target are dropped from the diagram with a descriptive warning.

Grouped diagnostics

Warnings are grouped by cause, including Dropped relation, Unknown table, Unknown column, Non-unique FK target, Warnings, and Parse errors.

Empty SQL

Empty input is valid and produces an empty schema. Only invalid non-string inputs throw.

DDL/DML Execution Log

Every executed DDL or DML statement appends one structured entry to the in-memory execution log. The message stream preserves chronological order and classifies each statement as error, warning, or success.

// Each log entry is:
{
  type:      "success" | "warning" | "error",
  message:   "Inserted 3 row(s) into \"users\"",
  timestamp: 1734567890123  // ms since epoch, monotonic
}
-- Examples ────────────────────────────────────────────────
-- success
CREATE TABLE users (id INT PRIMARY KEY);
-- → success: Table "users" created with 1 columns

-- warning (no WHERE clause)
UPDATE users SET id = 99;
-- → warning: UPDATE "users": no WHERE clause — all 2 row(s) will be affected
-- → success: Updated 2 row(s) in "users"

-- error (unknown table)
INSERT INTO ghost VALUES (1);
-- → error: INSERT INTO "ghost": table does not exist
Always actionable. Every error and warning quotes the offending table/column name, so you can pinpoint the issue without re-reading the script.

Common Diagnostics

error    : INSERT INTO "x": unknown column(s): foo
warning  : INSERT INTO "x" row 3: expected 4 values but got 3
warning  : UPDATE "x": setting NOT NULL column "name" to NULL
warning  : DELETE FROM "x": no WHERE clause — all 1200 row(s) will be removed
error    : DROP: table "ghost" does not exist
warning  : ALTER "x": column "old_col" does not exist
warning  : Relation dropped: foreign key 'posts.user_id' references column
           'users.no_such' which does not exist on table 'users'.
warning  : Relation dropped: foreign key 'events.email' references 'users.email',
           but the target is only partial-unique, not globally unique.
Crash-resistant. Schema parsing, DDL/DML execution, and SELECT execution recover malformed SQL into visible errors so you can keep refining the same script.

CREATE TYPE

Define custom data types for reuse across your database schema. Named enum and composite types should appear before tables that reference them, including schema-qualified references.

1.1. Composite Type

Define custom composite types with multiple fields of various data types.

CREATE TYPE type_name AS (
  field1 data_type1,
  field2 data_type2,
  ...
);

Example:

CREATE TYPE address_type AS (
  street VARCHAR(100),
  city TEXT,
  postal_code INTEGER
);

CREATE TYPE coordinates AS (
  latitude DECIMAL(10, 8),
  longitude DECIMAL(11, 8)
);
Supported Data Types: INTEGER, BIGINT, SMALLINT, TEXT, VARCHAR(n), CHAR(n), DECIMAL(p,s), NUMERIC, BOOLEAN, DATE, TIME, TIMESTAMP, FLOAT, REAL, DOUBLE PRECISION, UUID, JSON, JSONB, BYTEA, MONEY, and more.

1.2. Enum Type

Create enumerated types with predefined values for better data integrity and readability.

CREATE TYPE enum_name AS ENUM (
  'value1',
  'value2',
  ...
);

Example:

CREATE TYPE status_enum AS ENUM (
  'pending',
  'active',
  'completed',
  'cancelled'
);

CREATE TYPE priority_level AS ENUM (
  'low', 'medium', 'high', 'critical'
);
Important: Enum values must be enclosed in single quotes ('value'), not double quotes.

1.3. Schema-Qualified Types

User-defined types are resolved with the same schema awareness as tables. If two schemas define the same leaf name, the ERD keeps them distinct; if a type is unique, the display name stays simple.

CREATE TYPE s1.status AS ENUM ('draft', 'active');
CREATE TYPE s2.status AS ENUM ('queued', 'done');

CREATE TABLE s1.items (
  id    INT PRIMARY KEY,
  state s1.status
);

CREATE TABLE s2.items (
  id    INT PRIMARY KEY,
  state s2.status
);
Resolution rule: s1.status never resolves to s2.status just because both share the leaf name status. Qualified type names are strict, while unqualified names resolve only when unambiguous.

CREATE TABLE

Define database tables with columns, data types, and constraints.

2.1. Basic Table Structure

The general syntax for creating a table:

CREATE TABLE table_name (
  column_name data_type [constraints],
  ...
  -- table-level constraints (optional)
);

-- Also supports:
CREATE TABLE IF NOT EXISTS table_name ( ... );
CREATE TABLE schema_name.table_name ( ... );
Note: Table and column names can use double quotes ("name"), backticks (\`name\`), or square brackets ([name]) for escaping special characters or reserved keywords.

2.2. Column-Level Definitions

Each column can include various constraints and properties:

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  username VARCHAR(50) UNIQUE NOT NULL,
  email TEXT NOT NULL CHECK (email LIKE '%@%'),
  status VARCHAR(20) DEFAULT 'active',
  created_at TIMESTAMP DEFAULT now(),
  department_id INTEGER REFERENCES departments(id)
);

PRIMARY KEY

Uniquely identifies each row

UNIQUE

Ensures column values are distinct

NOT NULL

Prevents null values

DEFAULT

Sets default values (literals or functions)

CHECK

Validates data with boolean expressions

REFERENCES

Creates foreign key relationships

DEFAULT values support:
  • Literals: numbers (123), strings ('text')
  • PostgreSQL functions: now(), CURRENT_TIMESTAMP, CURRENT_DATE, nextval('seq'::regclass)
  • MSSQL functions: GETDATE(), NEWID()
REFERENCES (inline): Recognizes REFERENCES table(column), schema-qualified REFERENCES schema.table(column), omitted target columns such as REFERENCES departments, and FK actions such as ON DELETE CASCADE, ON UPDATE SET NULL, MATCH FULL, and DEFERRABLE INITIALLY DEFERRED.

2.3. Generated / Computed Columns

Quick reference. See the dedicated Computed Columns section below for the full per-dialect breakdown including the MSSQL bare-AS form.

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  price NUMERIC(10, 2),
  quantity INTEGER,

  -- PostgreSQL / MySQL: parenthesized expression
  total NUMERIC GENERATED ALWAYS AS (price * quantity) STORED,

  -- MySQL VIRTUAL column
  discount NUMERIC GENERATED ALWAYS AS (price * 0.1) VIRTUAL
);

2.4. Inline ENUM Types

You can declare an ENUM directly in a column definition (MySQL style). The parser will register it as a named enum and replace the column type automatically.

CREATE TABLE tasks (
  id INTEGER PRIMARY KEY,
  status ENUM('todo', 'doing', 'done'),
  priority ENUM('low', 'medium', 'high')
);
How it works: Inline ENUMs are auto-registered as named types. The first simple name uses column_enum (for example, status_enum); later tables with a different definition for the same column name get a table-qualified name such as tasks_status_enum so domains never collapse accidentally.
Tip: If you need shared enums or custom names, define them explicitly with CREATE TYPE ... AS ENUM instead.

2.5. Table-Level Constraints

Define constraints at the end of the table body. Useful for composite keys and multi-column constraints:

-- Syntax:
[CONSTRAINT name] PRIMARY KEY (col1, col2, ...)

[CONSTRAINT name] FOREIGN KEY (col1, col2, ...)
  REFERENCES ref_table(ref_col1, ref_col2, ...)
  [MATCH FULL|PARTIAL|SIMPLE]
  [ON DELETE CASCADE|RESTRICT|SET NULL|SET DEFAULT|NO ACTION]
  [ON UPDATE CASCADE|RESTRICT|SET NULL|SET DEFAULT|NO ACTION]
  [DEFERRABLE|NOT DEFERRABLE] [INITIALLY DEFERRED|IMMEDIATE]

[CONSTRAINT name] UNIQUE (col1, col2, ...)

[CONSTRAINT name] CHECK (expression)

Example:

CREATE TABLE order_items (
  order_id INTEGER,
  product_id INTEGER,
  quantity INTEGER CHECK (quantity > 0),
  unit_price NUMERIC(10, 2),

  CONSTRAINT pk_order_items
    PRIMARY KEY (order_id, product_id),
  CONSTRAINT fk_order
    FOREIGN KEY (order_id) REFERENCES orders(id),
  CONSTRAINT fk_product
    FOREIGN KEY (product_id) REFERENCES products(id),
  CONSTRAINT uq_order_product
    UNIQUE (order_id, product_id),
  CONSTRAINT chk_price
    CHECK (unit_price >= 0)
);
Supported Constraints: PRIMARY KEY, FOREIGN KEY, UNIQUE, and CHECK are all supported at table level. The constraint name (CONSTRAINT name) is optional.
Foreign-key correctness: If REFERENCES ref_table omits the target column list, Erd Go resolves it to the referenced table's primary key, including composite primary keys. If you specify target columns explicitly, they must be backed by a PRIMARY KEY, UNIQUE constraint, or global CREATE UNIQUE INDEX; filtered / partial unique indexes do not count as FK targets. Invalid relations are dropped with a warning.

2.6. Reserved-Word Column Names

Words like key, index, value, and name are reserved in some dialects but commonly used as column names in others. The parser disambiguates automatically when the next token is a known data type, so bare unquoted use works:

CREATE TABLE feature_flags (
  id        BIGSERIAL PRIMARY KEY,
  org_id    BIGINT NOT NULL,
  key       VARCHAR(128) NOT NULL,   -- 'key' as column name → OK
  index     INT,                     -- 'index' as column name → OK
  value     TEXT,                    -- 'value' as column name → OK
  enabled   BOOLEAN NOT NULL DEFAULT FALSE,
  UNIQUE (org_id, key)
);

-- The MySQL "KEY ... (cols)" index form is still recognized correctly
CREATE TABLE posts (
  id   INT PRIMARY KEY,
  body TEXT,
  KEY idx_body (body),                 -- treated as index, adds IDX badge
  FULLTEXT KEY ft_body (body)          -- treated as fulltext index badge
);
Disambiguation rule: KEY / INDEX / FULLTEXT / SPATIAL followed by a data-type keyword (VARCHAR, INT, …) is a column. Followed by an identifier and a parenthesized column list, it's an index. For absolute safety, quote the column with "key", `key`, or [key].

Identity / Auto-Increment Columns

Use the native identity syntax for the selected dialect. All supported forms produce a primary-key column with the AUTO_INCREMENT marker, but SQLite has one strict rule: AUTOINCREMENT requires a single INTEGER PRIMARY KEY column.

SQLite

-- Rowid alias; auto assigns a value when omitted
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE
);

-- AUTOINCREMENT prevents rowid reuse after DELETE
CREATE TABLE events (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL
);

-- Invalid for SQLite AUTOINCREMENT:
-- 1. BIGINT PRIMARY KEY AUTOINCREMENT
-- 2. INT PRIMARY KEY AUTOINCREMENT
-- 3. AUTOINCREMENT on a composite primary key
SQLite rule: use exactly INTEGER PRIMARY KEY AUTOINCREMENT. Do not use BIGINT, INT, or a composite primary key with AUTOINCREMENT.

PostgreSQL

-- Classic SERIAL family
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  big_id BIGSERIAL,
  small_id SMALLSERIAL
);

-- Modern SQL-standard identity columns
CREATE TABLE events (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT
);

CREATE TABLE invoices (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
);
Important: GENERATED ALWAYS AS IDENTITY is an identity column (auto-increment), distinct from GENERATED ALWAYS AS (expression) which is a computed column. The parser keeps them apart automatically.

MySQL

CREATE TABLE users (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE
) ENGINE=InnoDB;

MSSQL / SQL Server

-- IDENTITY(seed, increment)
CREATE TABLE [dbo].[Customers] (
  [CustomerID] BIGINT IDENTITY(1,1) NOT NULL CONSTRAINT pk_customer PRIMARY KEY,
  [Email] NVARCHAR(255) NOT NULL
);

-- Bare IDENTITY (defaults to (1,1))
CREATE TABLE [dbo].[Orders] (
  [OrderID] INT IDENTITY NOT NULL PRIMARY KEY
);
ERD output: Every form above shows up identically in the diagram — PK key icon plus AUTO_INCREMENT tag — so you can mix dialects in one schema without losing the auto-increment hint.

Computed Columns (per-dialect)

A computed column derives its value from an expression over other columns in the same row. Supported in all four dialects with their respective syntaxes.

PostgreSQL & MySQL — GENERATED ALWAYS AS

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  price NUMERIC(10,2),
  qty INTEGER,
  total NUMERIC GENERATED ALWAYS AS (price * qty) STORED,
  discount NUMERIC GENERATED ALWAYS AS (price * 0.1) VIRTUAL
);

-- Complex expressions, including CASE
CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  qty INT,
  flag INT GENERATED ALWAYS AS (CASE WHEN qty > 100 THEN 1 ELSE 0 END) STORED
);
Database modes: In a native database, STORED stores the computed value and VIRTUAL computes it on read. Erd Go keeps preview values in memory; the ERD preserves each mode as column metadata.

MSSQL / SQL Server — column AS expression [PERSISTED]

MSSQL has its own syntax: column_name AS expression, optionally followed by PERSISTED (and optionally NOT NULL). Both parenthesized and bare expression forms are supported.

CREATE TABLE [dbo].[Customers] (
  [CustomerID] BIGINT IDENTITY(1,1) NOT NULL CONSTRAINT pk_c PRIMARY KEY,
  [FirstName] NVARCHAR(50) NOT NULL,
  [LastName]  NVARCHAR(50) NOT NULL,

  -- Parenthesized form
  [FullName] AS ([FirstName] + ' ' + [LastName]) PERSISTED,

  -- Bare form (no parens) — also accepted
  [Initials] AS LEFT([FirstName], 1) + LEFT([LastName], 1)
);

CREATE TABLE [dbo].[Loans] (
  [LoanID] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY,
  [StartDate] DATE NOT NULL,
  [TermMonths] INT NOT NULL,

  -- Function call in a bare AS expression
  [MaturityDate] AS DATEADD(MONTH, [TermMonths], [StartDate]) PERSISTED,

  -- CASE expression in a bare AS
  [SignedAmount] AS CASE WHEN [TermMonths] > 12 THEN 1 ELSE 0 END
);
Bare AS: The parser accepts col AS expression (no surrounding parentheses) — including function calls like DATEADD(...), table-qualified references like c.GrowthDurationDays, and CASE expressions — terminating at the next column-comma, PERSISTED, or end of the table body.
ERD output: All computed columns show COMPUTED as the type and the expression preserved in the column extras / tooltip.

ALTER TABLE

Common migration-style ALTER TABLE statements are replayed after table creation, so the final schema reflects added columns, constraints, renames, dropped columns, and column type/nullability changes. PostgreSQL dump syntax using ALTER TABLE ONLY preserves the qualified table identity.

Add & constrain

ADD COLUMN, ADD PRIMARY KEY, ADD UNIQUE, ADD CHECK, and ADD FOREIGN KEY are reflected in the ERD.

Rename & drop

Table, column, constraint, and index renames—including MySQL RENAME TABLE, PostgreSQL ALTER INDEX, and SQL Server sp_rename—stay bound to the correct object incarnation before later DROP operations.

Column changes

PostgreSQL ALTER COLUMN TYPE/SET DATA TYPE/SET or DROP DEFAULT/nullability, MSSQL ALTER COLUMN, and MySQL MODIFY/CHANGE COLUMN are replayed.

Relationship safety

Foreign keys are validated against the final table state after migration replay, including PK/UQ/index changes and exact source/target column counts.

3.1. Add Foreign Key

ALTER TABLE table_name
  ADD [CONSTRAINT name] FOREIGN KEY (cols...)
    REFERENCES ref_table[(ref_cols...)]
    [ON DELETE action] [ON UPDATE action]
    [DEFERRABLE|NOT DEFERRABLE] [INITIALLY DEFERRED|IMMEDIATE];

Example:

ALTER TABLE orders
  ADD CONSTRAINT fk_orders_customer
  FOREIGN KEY (customer_id)
  REFERENCES customers(id)
  ON DELETE CASCADE;

3.2. Add Primary Key

ALTER TABLE table_name
  ADD [CONSTRAINT name] PRIMARY KEY (cols...);

Example:

ALTER TABLE products
  ADD CONSTRAINT pk_products
  PRIMARY KEY (product_id);

3.3. Add Unique Constraint

ALTER TABLE table_name
  ADD [CONSTRAINT name] UNIQUE (cols...);

Example:

ALTER TABLE users
  ADD CONSTRAINT uq_users_email
  UNIQUE (email);

3.4. Add Check Constraint

ALTER TABLE table_name
  ADD [CONSTRAINT name] CHECK (expression);

Example:

ALTER TABLE employees
  ADD CONSTRAINT chk_salary
  CHECK (salary >= 0);

3.5. Migration Replay Example

CREATE TYPE app.ticket_status AS ENUM ('new', 'open', 'closed');

CREATE TABLE app.accounts (
  tenant_id  INT NOT NULL,
  account_no TEXT NOT NULL,
  status     app.ticket_status NOT NULL DEFAULT 'new',
  PRIMARY KEY (tenant_id, account_no)
);

CREATE TABLE app.contacts (
  tenant_id   INT NOT NULL,
  account_no  TEXT NOT NULL,
  owner_email TEXT,
  CONSTRAINT fk_contacts_account
    FOREIGN KEY (tenant_id, account_no)
    REFERENCES app.accounts
    ON DELETE CASCADE
    DEFERRABLE INITIALLY DEFERRED
);

ALTER TABLE app.contacts ADD COLUMN audit_state TEXT;
ALTER TABLE app.contacts ALTER COLUMN owner_email TYPE VARCHAR(320);
ALTER TABLE app.contacts ALTER COLUMN owner_email SET NOT NULL;
ALTER TABLE app.contacts RENAME COLUMN owner_email TO contact_email;
ALTER TABLE app.contacts DROP COLUMN audit_state;

CREATE UNIQUE INDEX ux_contacts_email
  ON app.contacts (contact_email);
What the ERD does: The contact FK resolves to the composite primary key on app.accounts, owner_email becomes contact_email, audit_state is removed, and the unique index gives contact_email a UQ badge.

3.6. MySQL CHANGE / MODIFY COLUMN

CREATE TABLE `customers` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `email` VARCHAR(320) NOT NULL,
  UNIQUE INDEX `ux_customers_email` (`email`)
) ENGINE=InnoDB;

CREATE TABLE `orders` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `customer_email` VARCHAR(320),
  `status` ENUM('new','paid') NOT NULL DEFAULT 'new',
  `old_note` VARCHAR(50),
  CONSTRAINT `fk_orders_customer`
    FOREIGN KEY (`customer_email`)
    REFERENCES `customers`(`email`)
    ON DELETE SET NULL
) ENGINE=InnoDB;

ALTER TABLE `orders`
  CHANGE COLUMN `old_note` `note` VARCHAR(255) NOT NULL;
ALTER TABLE `orders`
  MODIFY COLUMN `status` ENUM('new','paid') NOT NULL DEFAULT 'paid';

3.7. MSSQL / SQL Server ALTER COLUMN

CREATE TABLE [dbo].[Customers] (
  [CustomerID] INT IDENTITY(1,1) NOT NULL,
  [Email] NVARCHAR(320) NOT NULL,
  CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ([CustomerID]),
  CONSTRAINT [UQ_Customers_Email] UNIQUE NONCLUSTERED ([Email])
);

CREATE TABLE [dbo].[Orders] (
  [OrderID] INT IDENTITY(1,1) NOT NULL,
  [CustomerEmail] NVARCHAR(320),
  [LegacyNote] NVARCHAR(50),
  CONSTRAINT [PK_Orders] PRIMARY KEY NONCLUSTERED ([OrderID]),
  CONSTRAINT [FK_Orders_Customers]
    FOREIGN KEY ([CustomerEmail])
    REFERENCES [dbo].[Customers]([Email])
    ON DELETE SET NULL
);

ALTER TABLE [dbo].[Orders]
  ALTER COLUMN [LegacyNote] NVARCHAR(255) NOT NULL;
ALTER TABLE [dbo].[Orders]
  ADD [CreatedAt] DATETIME2 NOT NULL DEFAULT '2026-01-01';
Syntax boundary: SQL-to-ERD migration replay is intentionally richer than Data View execution. Parsing the final diagram remains browser-only and does not require a database connection; unsupported execution-only statements do not weaken the ERD lifecycle model.

CREATE INDEX

Index DDL is parsed across all four dialects. Simple column keys are surfaced as small ERD badges: IDX for non-unique speed-ups, UQ for global unique indexes, and PUQ for filtered / partial unique indexes. Expression entries are preserved in parser output and reported when they cannot honestly map to a column badge.

IDX badge

Non-unique index. Solid pill on the column. Composite indexes mute the pill (gray) on every key column so single-col vs multi-col is obvious at a glance.

UQ badge

From a global CREATE UNIQUE INDEX. Behaves like a UNIQUE constraint for FK validation and column badging.

PUQ badge

From CREATE UNIQUE INDEX ... WHERE. Shows the partial uniqueness predicate but does not validate FK targets as globally unique.

Suppression rule

Stronger constraints win: PK ⟶ UQ ⟶ PUQ ⟶ IDX. A stronger key is never demoted by a later index.

Multi-dialect

PostgreSQL CONCURRENTLY / USING / INCLUDE / WHERE, MySQL FULLTEXT/SPATIAL & inline KEY/INDEX, MSSQL CLUSTERED/NONCLUSTERED with INCLUDE, SQLite standard indexes.

4.1. Syntax

CREATE [UNIQUE] INDEX [IF NOT EXISTS] [name]
  ON [schema.]table (col1 [ASC|DESC] [NULLS FIRST|LAST], col2, ...)
  [INCLUDE (cols)]      -- PG / MSSQL: covering columns (no badge added)
  [WHERE predicate]     -- PG / MSSQL: partial / filtered index
  [USING method]        -- PG: btree, hash, gin, gist, brin
  [WITH (options)];     -- storage parameters

4.2. Examples

Speed up a frequent FK lookup (single-column IDX):

CREATE INDEX idx_orders_customer ON orders (customer_id);

Composite index (muted IDX pill on every key column):

CREATE INDEX idx_orders_customer_status
  ON orders (customer_id, status);

Promote a column to UNIQUE without altering its definition:

CREATE UNIQUE INDEX uq_shipments_tracking
  ON shipments (tracking_number);

Partial / filtered unique index (PUQ, not a global FK target):

CREATE UNIQUE INDEX ux_users_active_email
  ON users (email)
  WHERE deleted_at IS NULL;

MySQL inline index inside CREATE TABLE:

CREATE TABLE posts (
  id INT PRIMARY KEY,
  slug VARCHAR(160) NOT NULL,
  body TEXT,
  UNIQUE INDEX ux_posts_slug (slug),
  KEY idx_posts_body (body),
  FULLTEXT KEY ft_body (body)
);

MSSQL / SQL Server filtered + INCLUDE index:

CREATE NONCLUSTERED INDEX idx_active_orders
  ON orders (customer_id)
  INCLUDE (total)
  WHERE status = 'active';

4.3. Behavior & Semantics

  • Functional / expression indexes (e.g. CREATE INDEX … ON t (lower(email))) are preserved and produce an explicit warning but no badge — they don't map cleanly to a single column.
  • INCLUDE columns (covering columns) are parsed but never badged — only key columns earn the pill.
  • PK / UQ columns are stronger guarantees than IDX, so any later CREATE INDEX on them is silently skipped — you won't see redundant pills.
  • Partial / filtered unique indexes render as PUQ with the WHERE predicate in column details. They are not accepted as globally unique targets for foreign-key validation.
  • Final badges are order-invariant: a global unique guarantee suppresses a redundant IDX badge whether the unique or non-unique index appears first. Composite index metadata is likewise retained regardless of declaration order.
  • Indexes on unknown tables / columns are silently dropped — useful when porting partial schemas without breaking the diagram.
  • Index lifecycle includes top-level CREATE/DROP INDEX, PostgreSQL DROP INDEX CONCURRENTLY, MySQL ALTER TABLE … ADD/DROP/RENAME INDEX, PostgreSQL ALTER INDEX … RENAME, and SQL Server index renames through sp_rename. Named UQ/PUQ/IDX metadata follows the final surviving name.
Tip: Composite IDX pills render in a muted gray to mirror the existing composite PK / composite UQ visual language. A solid IDX pill always means "single-column index" — easy to scan in dense diagrams.

Multi-Dialect Support

The browser-side parser handles syntax differences across SQLite, PostgreSQL, MySQL, and MSSQL without a database connection. Auto detection uses structural syntax signals and remains neutral for genuinely mixed-dialect input; callers can also provide an explicit dialect. Examples below follow the same standard order: SQLite first, then PostgreSQL, MySQL, and MSSQL.

4.1. SQLite Features

-- SQLite uses type affinities: INTEGER, TEXT, REAL, NUMERIC, BLOB
CREATE TABLE users (
  id       INTEGER PRIMARY KEY,         -- implicit rowid alias
  email    TEXT UNIQUE NOT NULL,
  name     TEXT NOT NULL,
  balance  REAL DEFAULT 0.0,
  metadata BLOB,
  created  TEXT DEFAULT (datetime('now'))
);

-- AUTOINCREMENT prevents rowid reuse after DELETE
CREATE TABLE events (
  id   INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  ts   TEXT DEFAULT CURRENT_TIMESTAMP
);

-- WITHOUT ROWID — clustered on the declared PRIMARY KEY
CREATE TABLE kv_store (
  key   TEXT PRIMARY KEY,
  value TEXT
) WITHOUT ROWID;

-- STRICT mode — enforces declared column types
CREATE TABLE audit_log (
  id      INTEGER PRIMARY KEY,
  action  TEXT NOT NULL,
  payload TEXT
) STRICT;

-- STRICT + WITHOUT ROWID combined
CREATE TABLE config (
  key   TEXT PRIMARY KEY,
  value TEXT NOT NULL
) STRICT, WITHOUT ROWID;

-- Foreign keys with ON DELETE / ON UPDATE actions
CREATE TABLE orders (
  id          INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL
    REFERENCES users(id) ON DELETE CASCADE ON UPDATE SET NULL,
  total       REAL DEFAULT 0 CHECK (total >= 0)
);

-- IF NOT EXISTS works as expected
CREATE TABLE IF NOT EXISTS sessions (
  token   TEXT PRIMARY KEY,
  user_id INTEGER REFERENCES users(id),
  expires TEXT
);
SQLite-Specific: INTEGER PRIMARY KEY (rowid alias), AUTOINCREMENT, WITHOUT ROWID, STRICT tables, typeless non-STRICT columns, type affinities (INTEGER, TEXT, REAL, NUMERIC, BLOB), datetime('now') / CURRENT_TIMESTAMP defaults, and standard REFERENCES with ON DELETE / ON UPDATE actions.
Type correctness: Ordinary SQLite tables accept arbitrary declared type names (such as VARCHAR(255), BOOLEAN, or TIMESTAMP) and may omit a type. STRICT tables follow SQLite's exact allowed set: INT, INTEGER, REAL, TEXT, BLOB, or ANY; every STRICT column must declare one.

SQLite migration replay example:

CREATE TABLE authors (
  id    INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE
) STRICT;

CREATE TABLE posts (
  author_id INTEGER NOT NULL
    REFERENCES authors(id) ON DELETE CASCADE ON UPDATE RESTRICT,
  post_id   TEXT NOT NULL,
  body      TEXT,
  PRIMARY KEY (author_id, post_id)
) STRICT, WITHOUT ROWID;

ALTER TABLE posts ADD COLUMN summary TEXT;
ALTER TABLE posts RENAME COLUMN summary TO excerpt;
ALTER TABLE authors RENAME TO writers;

4.2. PostgreSQL Features

-- Composite type + ENUM + array column types
CREATE TYPE address AS (street TEXT, city VARCHAR(100), zip VARCHAR(20));
CREATE TYPE org_plan AS ENUM ('free','starter','pro','enterprise');

CREATE TABLE organizations (
  id            BIGSERIAL PRIMARY KEY,
  -- Or: id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
  name          VARCHAR(255) NOT NULL,
  plan          org_plan NOT NULL DEFAULT 'free',

  -- PostgreSQL-specific types
  billing_addr  address,                          -- composite type
  data          JSONB DEFAULT '{}',               -- JSON binary
  tags          TEXT[],                           -- 1-D array
  matrix        INT[5],                           -- sized array
  uuid          UUID DEFAULT gen_random_uuid(),
  ip            INET,
  ts            TIMESTAMPTZ DEFAULT NOW(),

  -- Dollar-quoted defaults
  description   TEXT DEFAULT $Hello World$,

  -- Type casting in defaults
  seq_id        INTEGER DEFAULT nextval('org_seq'::regclass)
);

-- Schema-qualified table & type references both work
CREATE TABLE public.users (
  id INTEGER PRIMARY KEY,
  status public.org_plan          -- schema-qualified user-defined type
);

-- Empty body INHERITS (PG-only)
CREATE TABLE child () INHERITS (parent);
PostgreSQL: Supports SERIAL types, identity columns, JSONB, BYTEA, INET, timestamp variants, arrays, composite and ENUM types, UNIQUE NULLS NOT DISTINCT, expression-index preservation, and PostgreSQL's exact case-sensitive identity for quoted names.

4.3. MySQL Features

-- Backtick-quoted identifiers + ENGINE / CHARSET / COLLATE
CREATE TABLE `users` (
  `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(100) NOT NULL,

  -- Inline ENUM and SET (auto-registered as named types)
  `status` ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'active',
  `flags`  SET('verified','staff','beta','muted'),

  -- UNSIGNED / SIGNED modifiers + TINYINT(1)
  `age`     TINYINT UNSIGNED,
  `is_pro`  TINYINT(1) UNSIGNED DEFAULT 0,

  -- COMMENT on columns
  `email` VARCHAR(255) COMMENT 'User email address',

  -- Timestamps with ON UPDATE CURRENT_TIMESTAMP
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  -- MySQL-specific types
  `bio`         MEDIUMTEXT,
  `avatar`      LONGBLOB,
  `birth_year` YEAR(4),
  `location`    POINT,

  -- Inline indexes become ERD badges when they map to columns
  INDEX idx_name (`name`),
  FULLTEXT KEY ft_bio (`bio`),
  UNIQUE INDEX ux_name (`name`),
  UNIQUE KEY uk_email (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
MySQL-Specific: AUTO_INCREMENT, COMMENT, signed modifiers, integer/text/blob/spatial variants, inline ENUM and SET, inline indexes, CHANGE COLUMN, MODIFY COLUMN, FIRST/AFTER physical placement, ON UPDATE CURRENT_TIMESTAMP, and common table options.

4.4. MSSQL / SQL Server Features

-- Bracket-quoted identifiers + IDENTITY + computed columns
CREATE TABLE [dbo].[Users] (
  [UserID]      BIGINT IDENTITY(1,1) NOT NULL CONSTRAINT pk_users PRIMARY KEY CLUSTERED,
  [FirstName]   NVARCHAR(50) NOT NULL,
  [LastName]    NVARCHAR(50) NOT NULL,

  -- Computed column (parenthesized form)
  [FullName]    AS ([FirstName] + ' ' + [LastName]) PERSISTED,

  -- Computed column (bare AS form — also supported)
  [Initials]    AS LEFT([FirstName],1) + LEFT([LastName],1),

  -- MSSQL-specific types
  [IsActive]    BIT DEFAULT 1,
  [CreatedAt]   DATETIME2 DEFAULT GETDATE(),
  [UniqueToken] UNIQUEIDENTIFIER DEFAULT NEWID(),
  [SeqToken]    UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() ROWGUIDCOL,
  [Photo]       VARBINARY(MAX),
  [Notes]       NVARCHAR(MAX),
  [Config]      XML,
  [Location]    GEOGRAPHY,
  [TreePath]    HIERARCHYID
) ON [PRIMARY];
GO

-- ALTER TABLE with WITH CHECK and CONSTRAINT WITH (...)
ALTER TABLE [Users] WITH CHECK
  ADD CONSTRAINT [chk_active] CHECK ([IsActive] IN (0,1));
GO

-- Multi-statement batches separated by GO are honored
CREATE TABLE [dbo].[Orders] (
  [OrderID] INT IDENTITY NOT NULL PRIMARY KEY  -- bare IDENTITY → (1,1)
);
GO
MSSQL-Specific: IDENTITY(seed, increment), NVARCHAR(MAX), date/time, binary, XML, identifier and spatial types, ROWGUIDCOL, clustered indexes, GO batches, ALTER COLUMN, computed columns, named DEFAULT ... FOR column constraints, generated temporal columns, and PERIOD FOR SYSTEM_TIME.
Bare-AS computed columns: col AS expression PERSISTED (no surrounding parens) is fully supported, including function calls (DATEADD, LEFT, …), table-qualified column references (c.col), and CASE expressions.

4.5. Identifier Quoting Styles

All three major quoting conventions are supported:

-- PostgreSQL / Standard SQL: double quotes
CREATE TABLE "My Table" (
  "Column Name" INTEGER
);

-- MySQL: backtick quotes
CREATE TABLE `my_table` (
  `column_name` INT
);

-- MSSQL / SQL Server: square brackets
CREATE TABLE [my_table] (
  [column_name] INT
);

Notes & Tips

Important syntax support details: what contributes to schema structure, what DDL/DML can be replayed with sample rows, what SELECT queries can read, and where the limits are.

✓ Schema / ERD Syntax Support

  • Multi-dialect: SQLite, PostgreSQL, MySQL, and MSSQL syntax can share a script; auto detection remains neutral when strong signals are genuinely mixed
  • Case-insensitive keywords: CREATE, TABLE, ALTER, etc. can be written in any case
  • Quoted identifiers: Double quotes ("name"), backticks (`name`), and brackets ([name])
  • Reserved-word column names: Bare key, index, value, name, order, etc. are accepted when followed by a data type
  • Schema-qualified names: schema.table and schema.type (e.g., public.users, dbo.Orders, app.status_enum) — schema prefixes on user-defined types are preserved for strict lookup and simplified for display only when unambiguous
  • Comments: Single-line (-- comment) and block comments (/* comment */) are automatically skipped
  • BOM & CRLF: UTF-8 byte-order-mark and Windows line endings are handled transparently
  • Custom types: CREATE TYPE … AS ENUM and CREATE TYPE … AS (composite), including schema-qualified names
  • Inline ENUM & SET (MySQL): ENUM('a','b') and SET('a','b','c') inside column types — auto-registered as named types and namespaced by table/column when duplicate column names use different domains
  • Constraints: Column-level and table-level PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK (including composite multi-column variants). FK targets are validated against primary keys, unique constraints, or global unique indexes
  • Identity / auto-increment: SERIAL/BIGSERIAL (PG), GENERATED ALWAYS/BY DEFAULT AS IDENTITY (PG), AUTO_INCREMENT (MySQL), IDENTITY(1,1) and bare IDENTITY (MSSQL) — all unify in the diagram
  • Computed columns: GENERATED ALWAYS AS (expr) STORED/VIRTUAL (PG/MySQL) and MSSQL col AS (expr) [PERSISTED] + bare col AS expr [PERSISTED]
  • PostgreSQL arrays: type[], sized type[5], nested type[][][], and arrays of user-defined types (e.g., webhook_event[])
  • Default values: Literals, dollar-quoted strings, function calls (now(), CURRENT_TIMESTAMP, GETDATE(), NEWID(), NEWSEQUENTIALID(), gen_random_uuid(), nextval()), and ON UPDATE CURRENT_TIMESTAMP (MySQL)
  • FK actions: ON DELETE / ON UPDATE with CASCADE, RESTRICT, SET NULL, SET DEFAULT, NO ACTION, plus MATCH, DEFERRABLE, and INITIALLY metadata. Actions are preserved as relationship metadata.
  • Migration replay: ALTER TABLE ADD COLUMN, add/drop/rename keys and constraints, table/column/index renames, DROP COLUMN, ALTER COLUMN TYPE/SET DATA TYPE, SET/DROP DEFAULT, SET/DROP NOT NULL, MSSQL sp_rename, and MySQL MODIFY/CHANGE COLUMN
  • Indexes: Top-level CREATE/DROP INDEX, PostgreSQL ALTER INDEX … RENAME, MySQL add/drop/rename and inline KEY/INDEX/UNIQUE INDEX/FULLTEXT/SPATIAL definitions. Column-backed indexes render as IDX, UQ, or PUQ badges
  • IF NOT EXISTS, IF EXISTS: handled gracefully on both CREATE and DROP
  • Schema-only transaction wrappers: BEGIN, COMMIT, SET search_path, USE [db] (MSSQL), and SET FOREIGN_KEY_CHECKS (MySQL) are gracefully skipped
  • Gracefully skipped: CREATE VIEW, CREATE MATERIALIZED VIEW, CREATE FUNCTION/TRIGGER/SCHEMA/SEQUENCE, procedural EXEC statements other than sp_rename, DECLARE, PRINT, PARTITION BY, INHERITS (including empty body), WITH OIDS, STORAGE PLAIN, EXCLUDE USING gist, and statement bodies that do not affect ERD structure

✓ DDL/DML Preview Syntax

  • Top-to-bottom execution: The script is replayed in statement order, so sample data should come after the schema it references
  • Supported statements: CREATE TYPE, CREATE TABLE, CREATE VIEW, CREATE INDEX, ALTER TABLE column/constraint changes, DROP INDEX, TRUNCATE TABLE, INSERT, UPDATE, DELETE, DROP TABLE, DROP VIEW, and DROP TYPE
  • WHERE filters: =, !=, <>, >, <, >=, <=, LIKE, IN, NOT IN, NOT LIKE, BETWEEN, NOT BETWEEN, IS NULL, and IS NOT NULL
  • Row materialization: Tables expose row counts, structure metadata, and current row data after DML statements execute
  • Named types: ENUM and composite types created with CREATE TYPE are surfaced separately from tables
  • Execution log: Success, warning, and error states are captured for unknown columns, missing tables, duplicate keys, NULL violations, and broad updates/deletes; invalid mutation bindings fail before rows change
  • Default evaluation: Literal defaults plus functions like now(), CURRENT_TIMESTAMP, CURRENT_DATE, GETDATE(), and NEWID()

✓ SELECT Query Syntax

  • Read-only SELECT: SELECT statements can read the in-memory tables produced from the main SQL file
  • Result metadata: Columns, row counts, elapsed time, NULL/boolean/number formatting, and query errors are preserved with the SELECT result
  • Relational queries: Joins, WHERE filters, GROUP BY, HAVING, ORDER BY, LIMIT/OFFSET, TOP, FETCH FIRST, CTEs, derived tables, subqueries, and set operators
  • Analytical helpers: Aggregates, common scalar functions, JSON helpers, date/string functions, and window functions such as ROW_NUMBER, RANK, NTILE, FIRST_VALUE, LAST_VALUE, LAG, and LEAD; ordered default frames include the current peer group

✗ Still Not Supported / Skipped

  • ALTER TYPE: Cannot modify existing ENUM or composite types
  • LIKE clause: CREATE TABLE foo (LIKE bar INCLUDING …) is gracefully skipped — copied columns are not materialized
  • INHERITS column inheritance: The INHERITS (parent) clause itself parses without errors, but inherited columns are not pulled into the child table for ERD purposes
  • Index execution: Index DDL can create schema badges, but preview execution does not model storage engines, query planners, or index performance
  • SELECT limits: Read-only query syntax does not support recursive CTEs, external database execution, stored procedures, or mutation statements
  • Other objects: Triggers, stored procedures, functions, and sequences
  • External execution: DDL/DML preview does not connect to a real database or persist rows outside the current SQL file
Syntax behavior: DML does not contribute to schema diagram generation. Table, type, and named-index lifecycle statements are replayed in source order, including rename, drop, and later recreation without leaking metadata between object incarnations. Supported DDL/DML statements execute in memory from top to bottom, and read-only SELECT queries read from that in-memory result without mutating it.
Best Practices:
  • End each statement with a semicolon (;)
  • Define CREATE TYPE before tables that reference them
  • Define CREATE TABLE before related INSERT, UPDATE, or DELETE statements
  • Use ALTER TABLE for foreign keys when tables reference each other (circular dependencies)
  • Use DDL for structure first, then DML to validate sample rows and constraint behavior
  • Keep demo data in the SQL file itself because preview rows are derived from the script
Error Recovery: Unsupported statements are skipped, and syntax warnings/errors are surfaced with enough context to keep refining the same file instead of restarting from scratch.

By following these syntax patterns, Erd Go can generate your ERD schema, preview in-memory data, and run read-only queries from the same SQL file.

ErdGo.com - Transform SQL into diagrams and preview data faster