Developers

Schema patch format

.md

A schema patch is a single JSON object that describes a change to a diagram. The same format is accepted by #patch= URLs, the MCP server, and the paste-back box in the editor.

Where patches are accepted

Surface How
Diagram URL https://drawsql.app/draw#patch=<url-encoded JSON> — renders instantly, no account
MCP server The visualize_schema tool takes a schema patch or SQL ddl
Editor Edit → Take to your AI copies your schema, and accepts a patch pasted back

The URL and MCP paths build a new diagram. The editor path applies the patch to the diagram you already have open, and shows a diff before anything changes.

Strategy

Every patch carries a strategy:

  • merge — add, update and delete. Required if the patch has deletions.
  • additive — add only. Existing entities are never touched.

Reference tables and columns by name. Include only what changes; omit unchanged columns and unused optional fields.

Minimal example

{
  "strategy": "merge",
  "tables": [
    {
      "name": "posts",
      "comment": "Blog posts",
      "columns": [
        { "name": "id", "type": "bigint", "is_primary_key": true, "is_auto_increment": true },
        { "name": "user_id", "type": "bigint", "is_index": true },
        { "name": "title", "type": "varchar", "length": 255 },
        { "name": "body", "type": "text", "is_nullable": true }
      ]
    }
  ]
}

Tables

Property Type Notes
name string Table name (required)
old_name string Previous name. Set this only when renaming
comment string Table comment
parent_name string Name of a group to nest this table in. The group may be in the same patch
parent_uuid string UUID of an existing group
columns array See below
indexes array See below
left, top integer Canvas coordinates in pixels. Omit them and DrawSQL lays the diagram out for you

Columns

Property Type Notes
name string Column name (required)
type string Data type name, resolved for the diagram's driver (required)
length integer Type length, e.g. 255 for varchar(255)
is_primary_key boolean Primary key
is_auto_increment boolean Auto-increment or serial
is_nullable boolean Allows NULL
is_unique_key boolean Unique constraint
is_index boolean Single-column index
is_unsigned boolean Unsigned numeric (MySQL)
default string Default value
enum_values array Allowed values for enum and set columns

Use enum_values for enums. Do not describe the allowed values in a sticky note.

{
  "strategy": "merge",
  "tables": [
    {
      "name": "users",
      "columns": [
        { "name": "role", "type": "enum", "enum_values": ["admin", "instructor", "student"] }
      ]
    }
  ]
}

Accepted aliases

These older names still work. The engine rewrites them before applying the patch.

Alias Becomes
is_primary is_primary_key
is_not_null is_nullable, inverted
is_unique is_unique_key

Indexes

{ "name": "idx_users_email", "type": "unique", "columns": ["email"] }

type is one of primary, unique, index. All three fields are required.

Relationships

{
  "type": "one-to-many",
  "source_table": "users",
  "source_column": "id",
  "target_table": "posts",
  "target_column": "user_id"
}

type is one of one-to-one, one-to-many, many-to-one.

For one-to-many the source is the "one" side. For many-to-one it is the "many" side, so the same link can be written either way round.

Groups

Create the group, then point tables at it with parent_name. Both can live in one patch.

{
  "strategy": "merge",
  "groups": [{ "name": "Auth" }, { "name": "Content" }],
  "tables": [
    { "name": "users", "parent_name": "Auth", "columns": [{ "name": "id", "type": "bigint", "is_primary_key": true }] },
    { "name": "posts", "parent_name": "Content", "columns": [{ "name": "id", "type": "bigint", "is_primary_key": true }] }
  ]
}

Every table that belongs to a group must set parent_name itself. Groups do not list their members.

Sticky notes

{ "content": "All timestamps are UTC", "parent_name": "Auth" }

parent_name and parent_uuid are optional.

Renames

Set old_name alongside the new name. This works on tables and on columns.

{
  "strategy": "merge",
  "tables": [
    { "name": "articles", "old_name": "posts", "columns": [{ "name": "content", "old_name": "body" }] }
  ]
}

Deletions

deletions requires "strategy": "merge".

{
  "strategy": "merge",
  "deletions": {
    "tables": ["tmp"],
    "columns": [{ "table": "users", "columns": ["legacy"] }],
    "relationships": [
      { "source_table": "users", "source_column": "id", "target_table": "posts", "target_column": "user_id" }
    ]
  }
}
  • tables — table names
  • columns, indexes — grouped by table. table takes a name or a UUID. The object shorthand {"users": ["legacy"]} is accepted too
  • relationships — the four endpoint fields
  • groups{ "name": "Auth", "delete_children": false }. With delete_children false the members are ungrouped rather than removed
  • sticky_notes — sticky note UUIDs

Tolerance

The patch engine expects imperfect input and repairs it:

  • Unknown keys are stripped rather than rejected.
  • String booleans ("true", "false") are coerced.
  • String numbers ("255") are coerced.
  • Whitespace around names is trimmed.
  • Duplicate entries for one table are merged.

A patch that conflicts with itself is refused with an error naming the entity, for example a table listed in both tables and deletions.tables.

Canonical contract

The machine-readable contract is published at /schema-patch-contract.json. It is the same file the applier and the AI tool schemas are built from, so it never disagrees with what the engine accepts. Read it if you are generating patches programmatically.