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
REST API: create POST /v1/diagrams takes a schema patch or SQL ddl and creates a diagram in the token's team
REST API: proposals POST /v1/diagrams/{uuid}/proposals takes a schema patch for an existing diagram
MCP server: create The create_diagram tool takes a schema patch or SQL ddl (API token with write access)
MCP server: proposals The propose_diagram_changes tool takes a schema patch for an existing diagram (API token with write access)

The URL path and visualize_schema build a new diagram. The editor path applies the patch to the diagram you already have open, and shows a diff before anything changes. A create through the REST API or create_diagram builds a new diagram too. A proposal is applied to an existing diagram only after a person reviews its diff in the editor and applies it.

Positions per surface. create_diagram and POST /v1/diagrams reject left, top, width and height, because the layout is computed. Proposals honour them, and where the review shows a table is where it lands.

Retries. An identical create (same token, name, driver and payload) within 120 seconds returns the diagram it already made; for a DDL import the 120 seconds count from when the import finishes. A proposal is never deduplicated: every request makes a new one.

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, and every stripped key is reported as a warning.
  • 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.

The fields on this page are supported on every surface that accepts a patch. Any other key may be dropped, including fields the editor understands but this page does not list, and each dropped key comes back as a warning so nothing disappears silently. Build against the documented fields.

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.