Schema patch format
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 hasdeletions.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 namescolumns,indexes— grouped by table.tabletakes a name or a UUID. The object shorthand{"users": ["legacy"]}is accepted toorelationships— the four endpoint fieldsgroups—{ "name": "Auth", "delete_children": false }. Withdelete_childrenfalse the members are ungrouped rather than removedsticky_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.