Skip to content

Store postgres snapshots as deltas in format 9 and check migration branches for conflicts - #6503

Open
dankochetov wants to merge 55 commits into
rc5from
snapshots
Open

dankochetov wants to merge 55 commits into
rc5from
snapshots

Conversation

@dankochetov

@dankochetov dankochetov commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Postgres migrations get snapshot format 9 and a conflict check for migrations from two git branches. The work builds on the reworked DDL engine and the commutativity engine of #6046. This PR replaces #6046.

Snapshot format 9 (postgres only)

  • The first snapshot of a folder is the base snapshot. It holds the whole schema state. Every later snapshot holds only the deltas of its own migration (one create, drop, alter or rename for each changed entity) and the statements of that migration.
  • A rename is one delta. The DDL engine applies it through its edges, so the entities that reference the renamed entity get the new name. The diff records each rename with the same ddl.rename call.
  • reconstruct builds the state of a migration from the base snapshot and the deltas. A merge snapshot stores its own deltas, relative to the combined state of its parents.
  • up converts a folder of format 8 or older to format 9.
  • A snapshot stores no hash of its state. A later change to the edges or the key functions can change the state that an existing folder reconstructs to. This risk is accepted, so that such a change needs no new format.

Conflicts between branches

  • check compares the stored statements of the migrations on two branches with the commutativity engine. It reports each pair of statements that does not commute.
  • generate --fix-conflicts deletes the conflicting migrations and their children, then generates one new migration. The MCP server has the same fix as the fix_conflicts tool.

Breaking change

  • Postgres rejects --ignore-conflicts in generate, migrate and check. Format 9 checks conflicts from the stored statements, so a conflict can no longer be ignored. Use generate --fix-conflicts, or merge the branches by hand.

Diff fixes that format 9 needs

The merge tests of format 9 (merge-pairs.test.ts) found postgres diff cases where generate wrote no migration or wrote SQL that fails: a change of array dimensions, a column that switches to an enum with the same name in another schema, a change between two serial types, and a sequence that moves to another schema with a new name. These fixes stay in this PR. Review comments on the lines explain each one.

Other changes

  • Drizzle Studio: after a rename, the diff updates the new state too, as rc5 did.
  • The bundled skills are one drizzle skill.

Follow-up pull requests

These target this branch and move to rc5 after this PR merges:

The rename-order cases that the new tests found are tracked in #6509.

AlexBlokh and others added 30 commits July 21, 2026 11:32
# Conflicts:
#	drizzle-kit/tests/postgres/pg-columns.test.ts
#	drizzle-kit/tests/postgres/pg-enums.test.ts
#	drizzle-kit/tests/postgres/pg-tables.test.ts
#	drizzle-kit/tests/postgres/pull.test.ts
#	drizzle-orm/tests/sql-builder.test.ts
#	integration-tests/tests/bun/effect-sqlite-bun.test.ts
#	integration-tests/tests/cockroach/common.ts
#	integration-tests/tests/mssql/mssql.test.ts
#	integration-tests/tests/mysql/mysql-common-8.ts
#	integration-tests/tests/pg/common-cache.ts
#	integration-tests/tests/pg/common-pt1.ts
#	integration-tests/tests/pg/common-pt2.ts
#	integration-tests/tests/pg/common-rqb.ts
#	integration-tests/tests/sqlite/sqlite-common.ts
Postgres snapshots move to format 9. The first snapshot in a migrations
folder is a base snapshot with the full state. Every later snapshot is
a diff snapshot that holds only the deltas of its own migration: keyed
creates, drops, alters, renames and relocates. Commands rebuild a state
from the base and the deltas. A merge snapshot starts from the combined
state of its parents, which every command computes the same way.

- ddlDiff returns the deltas next to its statements, and `update`
  returns a change log that the deltas come from.
- generate writes base and diff snapshots, lists every leaf of a merge
  with the newest folder first, and names a new folder after its
  newest parent when the local clock is behind. The folder prefix now
  uses the UTC year for every dialect, like the rest of the prefix.
- check compares every pair of leaf branches statement by statement
  with the facet rules, so a change and its later reversal no longer
  hide a conflict.
- The folder loader stops postgres commands on a missing parent, a
  missing base, an unreadable snapshot or a folder that sorts before
  its parent, and the message says how to fix it.
- `drizzle-kit up` converts format 8 and older postgres folders to
  format 9 and writes no snapshot if any rebuilt state differs from
  the original.
- The programmatic API returns format 9 from `generateDrizzleJson` and
  `up`, accepts older snapshots in `generateMigration`, and exports
  `reconstruct`. `up` returned format 8 before.
- The commutativity matrix is committed as data with a contract test,
  and a PGlite test checks every matrix cell with the SQL that
  drizzle-kit emits. Privilege statements now need their grantee role.
- ddlDiff finds a view that moves to another schema and changes its
  definition, and it drops a recreated view after its rename or move.
- Property tests cover delta replay, histories with forks and merges,
  and up.
- The mysql and sqlite commutativity tests are back.
- The JSON contract, the skills and the utils readme describe format 9.
  The pull skill also corrects what `--init` does.
- `migrateToFoldersV3` compared the length of a file name instead of
  the number of matching SQL files, so any single prefix match threw a
  conflict error. It now moves the one matching file and throws only
  when several files match.
- Postgres pull checks the migrations folder before it connects or
  writes a file, so a broken folder leaves no schema.ts or
  relations.ts behind.
- MCP.md, SDK.md and the MCP tool description say what `pull --init`
  does: it records the pulled migration as applied in the migrations
  table and does not run its SQL.
Postgres generate no longer writes a merge over conflicting branches.
`drizzle-kit generate --fix-conflicts` deletes the migrations that hold
the conflicting statements that check reports, with all their children,
and checks again until no conflict is left. It then generates one
migration from the rest of the history to the schema.

- On a conflict, check, generate and migrate explain the fix: the
  folders that it deletes, the custom migrations among them, and that
  every database that ran a deleted migration must be reset. In
  interactive mode, generate offers to apply the fix. The JSON error
  lists the folders in `fix`, and the generate result lists
  `deleted_migrations`.
- The fix deletes nothing while generate still needs hints, and
  nothing in explain mode.
- Postgres rejects `--ignore-conflicts`, and the other dialects reject
  `--fix-conflicts`. This is a breaking change for postgres users of
  `--ignore-conflicts`. mysql, sqlite and the other dialects keep
  `--ignore-conflicts` as it was.
- The MCP generate tool takes `fixConflicts` and signals the deletion
  in `_meta`.
- A merge that would replay a checkpoint stops with an error. The
  replay added the rows of a checkpoint as creates, which brought back
  rows that the first parent had dropped.
- The conflict report names the snapshot of each conflicting statement.
- `upToV9` moves to `src/dialects/postgres/versions-v9.ts`.
- The folder flow harness and the fuzz tests resolve conflicts with
  the fix, and the docs and skills describe it.
Deltas must describe the same change as the SQL of their migration,
because check verifies that the SQL of two branches does not conflict.

- `up` no longer puts back the name of a primary key that has no
  explicit name and has the Postgres default name `<table>_pkey`.
  Postgres keeps that name when a table is renamed, and drizzle-kit
  0.x wrote no SQL for the key, but its files, converted to format 8,
  name the key after the new table. The state now keeps the name that
  the database has, so a later change of the key migrates. `up`
  ignores such key names when it compares a rebuilt state with the
  old file.
- Check derives statements without keeping generated names. The drop
  and create deltas that `up` writes for SQL from drizzle-kit 0.x are
  now a drop and a create statement. So check finds the conflict when
  two open branches rename the two ends of a foreign key.
- The MCP server gets a `fix_conflicts` tool with `destructiveHint:
  true`. The `generate` tool loses `fixConflicts` and its `_meta`
  signal, so the static annotation of each tool holds for every call.
- `checkSnapshots` returns a typed report and fix plan, and only
  `checkHandler` throws the conflict error. `planConflictFix` reuses
  the first report, so one `generate --fix-conflicts` runs detection
  twice instead of four times. generate no longer catches the error to
  read the plan.
- Postgres `prepareSnapshot` computes the leaves from the history that
  it loads and no longer takes the check result.
- The DDL records deltas only after `recordDeltas`, so the shared
  `update` tracks no changes for the other dialects. This removes
  `ChangeLogEntry` and the `addToLog` wrappers in postgres ddlDiff.
- `AbstractCommutativity` is split into `FacetCommutativity` for
  postgres and `ConflictListCommutativity` for mysql and sqlite. The
  defaults that threw, `getCommutativityModel` and the postgres
  overrides that nothing called are removed. `getReasonsFromStatements`
  loses a parameter that had no effect.
- The fuzz harness keeps the rename-hint fallback only for renames whose
  entity is missing from the state that generate starts from. A fuzz
  property calls `snapshotStatements` directly instead of a spy, and the
  `up` property compares default primary key names as `up` does.
- Test helpers lose duplicated code, and comments match the code.
- JSON_CONTRACT.md, MCP.md, OUTPUT_MODES.md, README.md and the skills
  describe the `up` folder layout, the SQL lookup, the scope of
  `malformed`, `deleted_migrations`, the postgres-only clock fallback
  and the conflict prompt as the code does.
- `generate` rejects `--explain` together with `--custom` before it
  loads the config. A custom migration has no SQL to explain, and every
  dialect wrote the file anyway. The error is
  `unsupported_command_dialect_error` with the meta `option: 'explain'`
  and `conflictsWith: 'custom'`.
- `generate --explain --fix-conflicts` shows the migrations that the
  fix would delete: the fix text in text mode, and `fix` in the JSON
  `ok` and `no_changes` results. When the rest of the history already
  matches the schema, the text says that the fix writes no migration.
- tsconfig.json excludes `tests/**/tmp`, so tsc does not fail on temp
  files that a running test deletes.
- The MCP stdio smoke test fails with a clear message when the local
  `dist` build is older than `src`.
- Two check-handler tests assert the leaves and conflicts that
  postgres reads.
- OUTPUT_MODES.md names the prompts that hints do not answer, SDK.md
  documents `fixConflicts` and `ignoreConflicts`, and the umbrella
  skill names the MCP `fix_conflicts` tool.
- The check header says "Conflicting migrations detected", and the
  `--ignore-conflicts` help text says "conflict checks".
- The fix text says that drizzle-kit deletes the conflicting migration
  branches, gives the number of migrations in total, and says that it
  generates one new migration from the schema.
- The fix text drops the sentence about the migrator skipping a deleted
  migration.
- The `drizzle-kit generate --fix-conflicts` command is on its own line
  in the fix text and in the errors that suggest it.
- The docs, the migrations skill and the MCP tool description use the
  same wording.
The function that builds the claims read every statement through
a cast to `any`, so the compiler did not see which fields a rule
reads. Each case now reads the statement through its narrowed type,
and the helpers take typed arguments. The rules give the same claims
as before.
…s SDK

A postgres migrations folder now holds base and diff snapshots only.
A snapshot with the type `checkpoint` is malformed, and so is a base
snapshot that lists a parent. The merge rule that refused a
checkpoint is gone with the type.

`generateDrizzleJson`, `up` and `reconstruct` return a schema state:
a full state with an id and parents, and no `type` field. It is not
a snapshot, and a migrations folder never holds one.
`generateMigration` reads only the rows of its arguments, so it takes
schema states and states in formats 5 to 8 in either position.

The check fixtures build their snapshots from a real diff, because a
full state with a parent is no longer valid.
…history

`up` now converts every snapshot of an older postgres folder in
memory, checks the result, and only then moves and writes files. A
failure leaves every file where it was. The move out of a journal
folder is split into a plan and an apply step for this.

`up` refuses a folder whose history it cannot convert, with the new
error kind `history`: a parent is missing, or a journal entry has no
SQL file. The message names the migration and the fix, and links to
the troubleshooting page.

A folder that sorts before its parent now gives `check_error` with
the kind `order`. The message is the same as before.

`up` no longer validates a folder that holds only format 9
snapshots. `check` does that.
`rename_constraint` now holds the constraint kind in `entityType`.
A primary key rename claims the key of its table, so it conflicts
with an add, a drop and a change of that key on another branch.
Before, check passed such a pair, and the migration failed in the
database. A unique, check or foreign key rename claims only names of
its own kind.

The statement matrix has one row for each constraint kind, and a
test checks that every rename that the diff writes has a matrix row
with claims on the same kinds of nodes.
`create_table` now reads the enum types of its columns, so a new
table with a column of an enum type conflicts with a rename, a move
and a drop of that enum. Before, check passed such a pair, and the
SQL failed in one order.

`recreate_enum` now writes the existence of the enum, and it reads
the table and the name of every column that it converts. Its SQL
drops and creates the type and alters each of these columns.

The statement kind `alter_type_drop_value` is deleted. The diff
never wrote it, and no SQL writer handled it.
`alter_column` had one claim for the whole column, so two alters of
different properties conflicted, and `generate --fix-conflicts`
deleted migrations whose SQL runs in either order. The statement now
reads the name of the column and writes one node for each property
that its SQL changes or needs. NOT NULL commutes with a change of
the type, the default or the generation expression. A type change
also claims the default, and an identity change also claims the
default and NOT NULL, because postgres fails or gives another result
for these pairs.

`alter_column` reads the enum name only when it changes the type or
the default, because only SET DATA TYPE and SET DEFAULT name the
type. A privilege statement claims its table, its grantee and its
privilege type, and a grant reads the grantor that GRANTED BY names.

The tests that used NOT NULL against a type change as their conflict
now use two type changes of one column.
…names

Check missed three kinds of conflicts, and the SQL then failed in
one order.

A create, alter or recreate of a policy now reads its roles, except
the built-in role names, so it conflicts with a rename or a drop of
such a role.

A new or recreated foreign key now reads the unique key on the
columns that it references. A primary key, a unique constraint and a
unique index provide that key. The engine has a new claim list,
`provides`, for this: it conflicts with a statement that needs the
node, and not with another statement that provides it, so a primary
key and a unique constraint on the same columns still commute.

Names are now also claimed in the namespaces that postgres uses:
relations (tables, views, sequences, indexes) and types (tables,
views, enums) in a schema, and constraints in a table. A primary key
or a unique constraint also takes the relation name of its index.
Every format 9 snapshot now has a required `statements` field with
the statements of its own migration, in the order of the SQL.
`generate` writes them for base and diff snapshots, `pull` writes
the create statements of the state that it read, and `up` computes
them for every snapshot that it converts. A custom migration stores
an empty list. A snapshot without the field, or with a statement of
an unknown kind, is malformed.

`check` now takes its conflicts only from the stored statements. It
no longer rebuilds the history and runs the diff for each snapshot,
so its result for an old migration does not change with a newer
drizzle-kit. Two independent first migrations are compared through
the statements of their base snapshots.

In this step the stored form is the full statement as the diff
builds it. The diff helper of the postgres tests now asserts that
the SQL from the stored statements equals the SQL of the diff.
…nerate`

The code that rebuilds a state from snapshots is part of the format.
A change to it could give another state for an existing folder with
no error. Two guards now catch this.

Every format 9 snapshot stores a required `stateHash`: the SHA-256
hash of the state after the snapshot. The hash reads a canonical
form: rows in the order of their keys, fields in the order of their
names, both by code unit, and no null fields. `generate`, `pull` and
`up` write it. A rebuild compares it for the requested snapshot and
for the first parent of every merge on the way. `check` rebuilds
every leaf once for this comparison and reports a mismatch as
`check_error` with the kind `state_hash`. Its conflicts still come
only from the stored statements.

Before `generate` writes a diff snapshot, it applies the new deltas
to the starting state and compares the result with the state that
the diff used. When they differ, it writes nothing.
A rename changes other rows too, such as the columns of a renamed
table or the column list of a key after a column rename. A snapshot
stored each changed row as its own keyed delta, so a merge of two
branches could lose such a change: the delta of one branch named a
row that the other branch had renamed, and the merge replay skipped
it. The combined state then kept an old name.

A rename now stores instructions. An instruction selects the rows of
one entity type by field values and writes the new name into them.
The three ops are `update`, `replace_in_list` and
`update_list_items`. `generate` derives them from the reference
rules: one for every rule that points at the renamed entity type,
and for every rule that points at a type whose keys change as a
result, whether a row matches or not. The replay runs each
instruction on the rows that the state has at that moment and reads
no reference rules.

While the diff records deltas, it now changes its starting state
only through the recorded deltas, so the diff continues with the
state that a replay gives.

The relocate op is removed. A rename or an instruction that moves a
row onto a taken key throws an error that names both rows. `up`
writes instructions for converted folders and refuses an old merge
whose branches gave one name to two entities.
A snapshot stored each statement as the diff builds it, with fields
that only the diff needs. A stored statement now holds what the SQL
writer and the claim rule of its kind read, and nothing else.

Every statement kind has one function that makes its stored form. It
sits next to the SQL writer of the kind, and the SQL is written from
the stored form only. The claim rules are typed on the stored form
too, so a writer or a rule that reads a field which is not stored
does not compile. A schema for each stored form gives its type and
its strict validator: a missing field or an unknown field makes the
snapshot malformed, and a row inside a statement is checked like a
row of a snapshot.

This step covers `create_table`, `drop_table`, `rename_table`,
`move_table`, `add_column`, `drop_column`, `rename_column`,
`alter_column` and `recreate_column`. The other kinds keep the full
statement for now. Three statement interfaces that nothing used are
removed.
The stored forms of `create_index`, `drop_index`, `rename_index`,
`recreate_index`, `add_pk`, `drop_pk`, `alter_pk`, `create_fk`,
`drop_fk`, `recreate_fk`, `add_unique`, `drop_unique`,
`alter_unique`, `add_check`, `drop_check`, `alter_check` and
`rename_constraint` now hold what their SQL writer and their claim
rule read, and nothing else.

A statement that creates an index or a constraint stores its full
row. A drop stores its names and columns, and a dropped index also
stores what decides whether it gave a unique key. A statement that
replaces an index or a key stores the old columns next to the new
row.
… form

A snapshot now stores, for each of these 19 statement kinds, only the
fields that the SQL writer and the claim rule of the kind read. Each
kind has a strict schema, so a missing field or an unknown field makes
the snapshot malformed.

An `alter_enum` statement stores the added values with the value that
each one goes in front of. A `recreate_enum` statement stores the full
enum and, for each column of the type, the names, the type and both
defaults.
A snapshot now stores, for each of these 13 statement kinds, only the
fields that the SQL writer and the claim rule of the kind read. Each
kind has a strict schema, so a missing field or an unknown field makes
the snapshot malformed.

A `revoke_privilege` statement stores no grantor, because REVOKE does
not name it. An `alter_policy` statement stores the names, the roles
and both expressions of the policy.

Every statement kind now has a stored form of its own, so the function
that stored a statement as the diff builds it is removed.
Every statement kind has a strict schema now, so the fallbacks are
removed. A statement kind without a schema does not compile. The
validator checks every stored statement against the schema of its
kind, so a statement with an unknown kind, a missing field or an
unknown field makes the snapshot malformed.

The JSON contract says that a stored statement holds the data of its
SQL statement, and that the SQL of a migration can be rebuilt from the
statements of its snapshot.
Each row of the table is a base schema and two or three branches,
where at least one branch renames or moves an entity. The test runs
every row with each branch as the first parent of the merge, through
real commands on a temporary folder and a PGlite database.

For branches whose SQL runs in either order, the test asserts that
`check` passes, that the merge migration holds only its own change,
and that the database equals the schema after `migrate`. For the
other branches, it asserts that `check` reports a conflict.

The table holds 52 rows of the first kind and 15 rows of the second
kind. The rename merge cases of the merge flow test move into it.
The replay is part of the snapshot format: a change to it can give
another state for a folder that already exists. It lived in code that
every dialect and `generate` share.

`replay-v9.ts` now holds the delta types, the code that runs keyed
deltas and instructions, the rebuild of a state from a history and the
merge replay. The module imports no other module. It gets the rows and
the key functions as data, and the caller gives it the entity
definitions and the state hash check. A test fails if the module gets
an import.

The engine applies the deltas that it records through the same code.
The behavior does not change. The developer notes say that a change to
the replay needs a new snapshot format with its own module.
No reference rule pointed from the roles of a policy, or from the
grantee and the grantor of a privilege, to roles. So after a role
rename, the migration also revoked the privilege from the old role
name, which no longer exists at that point, granted it again and
altered the policy.

Three rules now point at roles. A role rename changes those rows in
the state, the migration holds only `ALTER ROLE ... RENAME TO`, and
the snapshot stores one instruction for each rule. A drop of a role
does not drop the rows that name it.

The roles of a policy are a sorted list, and a rename can leave them
in another order. The diff no longer writes `ALTER POLICY` for a
change of the order only.
The key of an index held its table. After a table rename on one
branch, a keyed index delta of another branch named the old table, and
the merge replay skipped it. `check` hid this with a conflict that
postgres does not have: `DROP INDEX` and `ALTER INDEX` do not name
the table.

An index is now keyed by schema and name, as postgres keeps index
names unique in a schema. A table rename changes the `table` field of
its indexes and leaves their keys unchanged. The claim rules of
`drop_index` and `rename_index` no longer read the name of the
table. They read that the table exists in its schema, which a drop of
the table and a move to another schema both end.

The merge table has rows for a table rename and for a table move
against a drop and a rename of an index. The two known exceptions of
the ground truth test are removed.
A claim says which object a statement changes, and not what it
changes the object to. So two branches that make the same change were
reported as a conflict, also when the SQL can run a second time with
the same result.

`check` now treats two equal stored statements on two branches as one
change when the kind is repeatable: the alter and recreate kinds, RLS,
and the privilege kinds. Two different changes of one object still
conflict, and so do two equal statements of any other kind.

Some statements of a repeatable kind hold SQL that fails on the second
run, and they stay conflicts: an `alter_column` that adds or drops an
identity, drops a generated expression or changes a serial column, a
`recreate_index` without its drop, and an `alter_pk` without its
drop. The ground truth test confirms the column cases on PGlite.

The 23 known exceptions of the ground truth test for the same change
on both branches are removed. The merge table has a row where both
branches set NOT NULL on one column.

The policy role comparison in the diff no longer uses `toSorted`.
… renames by table in postgres

A migration that moved a table to another schema wrote the move after
the statements that already named the table in its new schema: a
policy drop, a foreign key drop and the row level security statement.
That SQL failed. The move now runs right after the table renames.

A migration that dropped a role wrote `DROP ROLE` before the policy
changes that take the role out of a policy, and postgres refuses such
a drop. A role drop now runs after the policy statements.

A rename decision for a check, a foreign key, a policy, a primary key
or a unique constraint selected rows of the old state by schema and
name only. So it also renamed an object with the same name on another
table, and a change of that other object got no SQL while the snapshot
recorded it. The rename now selects by the table too.
`check` validated only the migrations folder. After a git merge of
two branches, the database can differ from the merged schema until
`generate` writes a merge migration, and nothing said so.

For postgresql, `check` now compares the schema with the folder when
it has a schema path, from the config file or from the new `--schema`
option. The schema equals the folder when `generate` would write no
SQL. With one leaf, a difference means that the schema has a change
that no migration holds. With several leaves, it means that a merge
migration is needed. Each case is a warning: text mode prints it in
place of the success line, the `ok` envelope has it in `warnings`,
and the exit code stays 0.

A schema that cannot be compared is a warning too: no file matches the
path, the schema fails to load, or it has errors. The comparison
writes nothing.
…k` warnings a kind

A table move takes the indexes and constraints of the table into the
new schema. When the same migration drops a table that still holds one
of those names there, the move must run after the drop. The table
drops run before the moves again, and only the statements about a
moved table run after them.

A warning of `check` is now `{ kind, message }`. The kinds
`schema_changed` and `merge_needed` mean that `generate` would
write a migration, and `not_compared` means that the schema could not
be loaded or compared. The messages no longer say that the schema is
ahead of the migrations, because the difference can go in either
direction, and they name `generate --explain` first.

An exception anywhere in the comparison is a `not_compared` warning
now. Before, only a failed schema load was one, and another exception
failed `check` on a valid folder.

generate, export and check share one conversion of a schema into a
state.
…and show `check` warnings in the GitHub action

Postgres diff:
- A sequence or a view that moves to another schema and gets a new
  name in one rename decision gets a move and a rename statement.
  Before, the rename of a sequence was lost, and the move of a view
  named a view that did not exist.
- A view that moves and changes between plain and materialized is
  moved with its old kind.
- A column whose enum is created again in another schema gets
  `SET DATA TYPE`. A change of only the schema of the type counts when
  both states name a schema, because a folder of an older format can
  store none.
- The recreate of a generated column creates the foreign keys from
  that column again.
- Renames of unique constraints, and renames of primary keys, run in
  an order that frees a name before another rename takes it.
- A table that moves and gets a new name moves first when the old
  schema has a table with the new name and the old name is free in the
  new schema.

Cockroach diff:
- `DROP ROLE` runs after the policy statements.
- Table moves run directly after table renames, so a statement about a
  moved or renamed table finds the table.
- Renames of checks, primary keys, foreign keys and policies select
  their row by table too.

`check` prepares the migrations folder one time for the conflict check
and the schema comparison. The GitHub action lists the warnings of
`check` in its comment.
…es and cockroach diffs, and two `check` verdicts

Postgres diff:
- A sequence, a view or a table that moves to another schema and gets
  a new name takes the other order of its two statements when a
  relation holds the name on the way.
- A rename of an index, a primary key or a unique constraint runs
  after the rename of another kind that frees its name.
- The recreate of a generated column handles a new, a renamed and a
  shared foreign key, and creates an index or a unique constraint of
  the column one time.
- A change between two serial types writes the integer type and keeps
  the sequence.
- A type change of a generated column has no USING clause, and the
  expression is dropped before a type change.
- The recreate of an enum finds its columns with the schema file when
  the old state stores no type schema.

Array dimensions, postgres and cockroach:
- A change of a column between a scalar type and an array type writes
  SET DATA TYPE with a plain cast. Most types have no such cast, so
  the user writes the conversion into the USING clause.
- In postgres, generate writes the same statement for a change between
  two array dimension counts, and push writes nothing for it.
- The schema reader counts the brackets of a custom type, and the
  postgres database reader reports a dimension only for a type whose
  text ends with brackets.

Postgres `check`:
- A removed enum value conflicts with a type change of a column of
  that enum.
- The same change between two serial types on two branches is one
  change. The same change between a scalar type and an array type is
  reported.

Cockroach diff:
- The postgres fixes for sequences, views, enum type schemas and
  generated columns are ported.
- A check rename decision writes RENAME CONSTRAINT, after the
  statement that frees its new name.
- An index rename or drop names its table when another table of the
  schema holds the same index name.
- A view that reads a renamed or moved table is dropped before the
  table statements and created after them.
- The data loss queries of push name a moved or renamed table as the
  database has it.
…ine, and remove the state hash

Snapshot format 9 changes its stored shape before its first release,
so the format number stays 9.

Snapshots:
- A diff snapshot stores one delta for each changed entity, and a
  rename is one `rename` delta. The entities that reference the
  renamed entity get no delta.
- The operations `update`, `replace_in_list` and `update_list_items`
  leave the delta type and the validator, so a snapshot that holds one
  of them is malformed.
- No snapshot stores `stateHash`. The validator ignores the field in a
  snapshot that still holds it.

Reconstruct:
- The code that applies deltas is in
  `src/dialects/postgres/reconstruct.ts`, and it calls the DDL engine
  for each delta. A rename goes through `update`, which follows the
  edges and writes the new name into the referencing entities.
- One set of rules covers a delta that does not fit the state, in a
  merge and outside a merge. A drop, an alter or a rename of a missing
  entity is skipped. A create onto a taken key does nothing when the
  entity there is equal, and it throws when a field differs. A rename
  that moves an entity onto a taken key throws.

DDL engine:
- While the postgres diff records deltas, `update` follows the edges
  on the old state and records one delta for each changed entity.
- `update` copies the entities only when a key changes.
- `update` and `delete` accept the key of one entity in `where`.

Commands:
- `check` and `migrate` reconstruct no state. `generate` reconstructs
  every leaf before it writes. A leaf or a combined state that does
  not reconstruct stops it with a `check_error` of kind `malformed`,
  and `check` with a schema path reports it as a warning.
- A migrations folder with a second base snapshot is refused. The
  error carries both paths in `snapshot` and `baseSnapshot`. `up`
  refuses an older folder with two first migrations.
- `up` refuses an old merge whose branches created different entities
  under one key.
- The error kind `state_hash` is removed, and `generate` no longer
  applies its new deltas to the parent state before it writes.

Tests:
- The committed format 9 history is deleted.
- New folder flow tests cover a rename of each entity type, the rules
  for a delta that does not fit, and a merge in which one branch
  stores a delta with no statement.
- Eight date-gated postgres tests move their date from 2026-10-03 to
  2026-11-08.
…ix to the API `reconstruct` error

Skills:
- The eight installable skills become one `drizzle` skill. The seven
  per-command bodies are topic files beside `SKILL.md`. They have no
  frontmatter, and they reference each other with links inside the
  folder.
- `SKILL.md` gets a description that covers every topic, and a routing
  table over the topic files. Both also carry the triggers of the
  migrations folder: migrations from several git branches, a broken
  migrations folder, branch conflicts and `drizzle-kit up`.
- `drizzle-kit skills` removes the seven superseded skills after a
  successful add. It removes them in project scope only and without a
  prompt. It prints nothing when no superseded skill is installed, and
  it prints the manual command when the removal fails.
- `metadata.revision` stays 14.

API:
- `reconstruct` from `drizzle-kit/api-postgres` reports a state that
  does not reconstruct with the same message as `generate`. The message
  names the migration and the fix, and one function builds it for both.

Tests:
- `skills-bundle.test.ts` covers the shape of the bundle, the links of
  the routing table, and the arguments and the scope of the removal.
…and the fix of `generate`

`reconstruct` from `drizzle-kit/api-postgres` added the folder name and
the fix only to a key collision. A base snapshot that lists one entity
twice gave an error with no folder name and no fix. The API now wraps
every error of a snapshot that the folder holds, as `generate` does for
a leaf. The error for an id that the folder does not hold keeps its own
text, which already says what to do.
…t lists of `check`, and shrink its tests

Source:
- `src/dialects/postgres/folder.ts` is the one loaded model of a
  postgres migrations folder. It reads each `snapshot.json` one time,
  runs the history rules and the status check, and gives the snapshots
  by id, their files and the ids of the leaves. `generate`, `check`,
  `pull` and the API `reconstruct` use it. One `generate` or `check`
  run now parses each file one time, where it parsed each file three
  times.
- `prepareOutFolder` only lists the snapshot files, as on `rc5`. A
  command of another dialect no longer runs the postgres history rules
  on a folder that holds postgres snapshots.
- The API `reconstruct` follows the rule of `generate` for a folder in
  which two snapshot files hold one id: an error names the last folder
  with that id, and the leaf list counts the parents of the last file of
  each id.
- Postgres `check` no longer holds the conflict lists of the older
  model, which it did not read. One function gives the target of a
  statement for the report. The report text and the JSON output are
  unchanged.
- The claim rule of `alter_column` reads the enum name only when the
  default, the type, the type schema or the dimensions change. A
  `DROP EXPRESSION` on a generated column of an enum type no longer
  conflicts with a rename or a move of that enum.
- The MCP description of `up` says that postgres converts and checks
  every snapshot before it moves or writes a file.

Docs:
- The hints topic file and `HINTS.md` no longer list the `privilege`
  hint kind, which the code rejects.
- The developer note describes the folder module and the new layout of
  the matrix file.

Tests:
- `commutativity-statements.ts` is the one catalog of statement
  fixtures, and the invariants suite covers all 74 rows.
- `commutativity-matrix.json` holds only `types`, `matrix` and
  `impossible`, with one string for each row. It has 230 lines, down
  from 15,492, and every cell is unchanged.
- `forkFlow` in `folder-flow.ts` sets up a fork of two branches for the
  flow tests. Five tests of `check-branches.test.ts` are rows of the
  merge pairs table now, so each of them runs with each branch as the
  first parent.
- `cli-skills.test.ts` replaces `skills-bundle.test.ts` and tests only
  the arguments of the removal of the superseded skills. The tests of
  the skill markdown are removed.
- Every test date gate moves to 2026-11-08.
…statement names, and simplify `up` and `check`

Diff:
- The diffs of postgres, mysql and sqlite apply a table rename and a
  column rename to the new state again. Drizzle Studio builds the new
  state from the form of one table and copies the other tables from the
  old state, so its new state can still hold old names in constraints,
  indexes, policies and the foreign keys of other tables. Without the
  update, the diff wrote statements that name a table or a column that
  the rename removed. A new state from schema files never holds the old
  name, so `generate` and `push` write the same SQL as before.
- The postgres diff returns the stored statements, and its three
  callers no longer build them a second time.
- `renameResolvers` accepts only an entity type of the postgres DDL.
- Cockroach writes a view that it must drop and create again as
  `drop_view` with a `cause` plus `create_view`, as postgres does. The
  SQL is unchanged. `--explain` shows such a view one time, and the
  statement `recreate_view` is removed.

Stored statements of postgres snapshot format 9:
- Four stored fields get new names: `indexNames` of `move_table`,
  `becomesSerial` of `alter_column`, `from` and `to` in the column
  defaults of `recreate_enum`, and `shouldDrop` of `alter_pk`, which
  holds the opposite value of the field `deleted` that it replaces.
- A stored `move_enum` or `move_sequence` requires a schema name.

`check`:
- The claim rule of `recreate_column` reads the enum type of its
  column, so a new expression of a generated enum column conflicts with
  a rename, a move or a drop of that enum.
- The node kinds of the claim rules are a union type, the privilege
  types come from the DDL definition, the statements of a branch are
  read without a promise cache, and postgres has no dry snapshot.
- The warnings about a schema that differs from the migrations move to
  `src/cli/commands/check-postgres.ts`.

`up` and the API:
- `up` parses each file one time. For a folder with a journal, its
  errors name the journal entries, because the migration folders do not
  exist yet, and a wrong order has its own message with the fix.
- The refusal of an old merge that cannot be converted says which
  migrations to delete.
- Messages and docs say "reconstruct" where they said "rebuild".

Engine:
- `splitDeltaKey` throws for a key that has no entity type.
- Ten short private names in `dialect.ts` are full words, `hasDiff`
  uses `isChangePair`, and the comments that named removed code are
  corrected.

Skills command and MCP:
- The removal of the superseded skills uses `spawnSync`, one constant
  for its arguments, and the words "remove" and "superseded".
- The MCP tools `generate` and `fix_conflicts` share one handler.

Docs:
- The changelog names the removed postgres flag `--ignore-conflicts`
  and the single `drizzle` skill with the removal of the older skills.

Tests:
- `tests/other/studio-diff.test.ts` calls the Studio entry points of
  the three dialects with new states that hold old names.
- The matrix has three new rows, for `recreate_column` of an enum
  column and for a foreign key against a dropped unique constraint. The
  cells of the 74 older rows are unchanged.
- `flow.generate` fails when it writes no migration, and
  `flow.tryGenerate` returns the raw result. Helpers build the error
  envelope of `up` and a format 8 folder.
- The tests of the state hash, the instruction operations and the
  checkpoints are deleted, together with a repeated rename test, the
  squash test and `commutativity-renames.pglite.test.ts`.
…r and remove leftovers of earlier snapshot designs

The DDL engine recorded every `update` call so that the postgres diff
could read rename deltas back. The diff now makes each rename or move
decision through `renameEntity`, which applies the rename to the old
state and returns its delta. The recorder is gone from the engine.

Removed, because nothing reads it any more:

- `renames` in the result of the postgres `ddlDiff` and `ddlDiffDry`.
  The fuzz test of `up` builds the format 8 rename strings itself.
- `renames.ts`. Its function is `resolversOfRenameDeltas` in
  `versions-v9.ts`, next to `format8RenameResolver`.
- `listSnapshots`. `prepareOutFolder` lists the snapshots for the
  postgres folder and for `up`.
- `isPK` in the stored `add_column` statement. The primary key of a
  new column has its own statement.
- The copy of the previous state in `generate`, the `prevIds`
  parameter and the `async` of `prepareSnapshot`, and the list of
  snapshot paths.
- The second loop for an enum that moves to another schema, in
  postgres and cockroach. One loop applies the rename and the move.

The test helpers read `snapshotStatements` from the diff result.
Conflicts:

- The date gates of skipped tests take the values of `rc5`.
- `push-cockroach.ts` takes the `identifier` closure of `rc5` for
  the data loss checks. It replaces the `tableInDatabase` helper of
  this branch, which did the same lookup.
- `mysql/diff.ts` and `sqlite/diff.ts` keep the rename block of this
  branch. It already builds a new object for a renamed index column,
  which is the only change of `rc5` in that block.
- `ext/api-postgres.ts` has the `hints` option and `runWithApiHints`
  of `rc5`, with the snapshot types, `up` and `reconstruct` of this
  branch. The `ddlDiff` of this branch has no privilege resolver.
- The changelog has the lines of both branches.
- The skills revision stays 14.
- `cockroach/tables.test.ts` and `pg-tables.test.ts` keep the tests
  of both branches. Four CockroachDB tests of this branch expect the
  warning text that `rc5` uses now.
const result = snapshots.reduce(
(accum, it) => {
const raw = JSON.parse(readFileSync(`./${it}`).toString());
const raw = JSON.parse(readFileSync(it).toString());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old line read readFileSync(`./${it}`). That breaks for an absolute migrations folder.

prepareOutFolder builds each path with join(out, subdir, 'snapshot.json'). When out is absolute, the old line made .//abs/drizzle/0001/snapshot.json, and Node resolves that against the working directory: <cwd>/abs/drizzle/0001/snapshot.json. The file is then not found. Without the ./ prefix, a relative path and an absolute path both work.

The change came with 25ff4f9e2 ("Store postgres snapshots as deltas in format 9"). At that commit the postgres up read its folder through validateWithReport, and the new up-format9.test.ts runs up on absolute temporary folders. The postgres up no longer calls this function, but the up of mysql, sqlite, mssql and singlestore still does, so they keep the fix for an absolute out.

…branches, and remove code for unreported cases

The `check` comparison of the schema with the migrations, the
CockroachDB diff fixes and the postgres diff fixes that format 9 does
not need move to their own pull requests on top of this branch.

- Remove `check-postgres.ts`, the `--schema` option of `check`, its
  `warnings` and their docs, tests and GitHub action output.
- Restore the CockroachDB diff, convertor, statements and their tests to
  rc5. Restore the postgres statement order around table moves, the drop
  of recreated views, `DROP ROLE`, `DROP EXPRESSION`, the recreate of
  generated columns and the dimensions of custom types to rc5. Keep the
  array, enum schema, serial and sequence move fixes, because the merge
  tests of format 9 need them.
- Remove `inOtherOrder` and the ordering of renames that take a freed
  name, with their tests.
- Restore `lowestCommonAncestor`, the member order of
  `ConflictListCommutativity` and the merge block of the commutativity
  engine.
- Remove guards and tests for states that no caller produces: repeated
  history checks, a delta key without an entity type, an unknown delta
  operation, a cycle of first parents, the interactive prompt of the
  conflict fix and several parents in `generateDrizzleJson`.
- Delete tests that repeat other tests or cannot fail, and the committed
  page of the commutativity matrix.
- Store `toSerial` and `default` of `alter_column` with their statement
  names, and name the indexes of a moved table `indexNames` in both
  forms.
- Add `rename` and `clone` to every DDL object and `parseEntityKey` to the
  engine, give `Field` and `Store` named types, let `validate` return a
  type predicate so that the snapshot validators need no explicit type
  arguments, and rename `toJsonSnapshot` to `toJsonBaseSnapshot`.
- Reject an entity type or a field that only an inherited property name
  matches, such as `constructor` or `__proto__`.
- Rewrite the comments of the branch so that each one starts with the
  purpose of its code, and fix wrong or unclear comments and docs.
return composed;
}

export abstract class AbstractCommutativity<

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postgres snapshots store the statements of their migration, so postgres check needs no conflict lists, no diffSnapshots and no dry snapshot. With one class and the getCommutativityModel switch of 55db3bc, postgres still had to implement those abstract members, and nothing called them. AbstractCommutativity now holds what both models use: the footprint match, branchOf and buildSnapshotGraph. FacetCommutativity adds the facet footprints and detectBranchConflicts. ConflictListCommutativity holds the rc5 code, so mysql and sqlite change only the name of their base class and give the same results.


// Tells whether two statements of two branches are one change, so that they do not conflict. A footprint
// names only the object that a statement changes, so two equal statements always match.
private isSameChange(a: TStatement, b: TStatement): boolean {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A footprint names the object that a statement changes and not the new value, so two equal statements on two branches always conflict. When the SQL gives the same result on a second run, for example SET NOT NULL on one column, this conflict is a false positive, and generate --fix-conflicts deletes a migration without need. isRepeatable is false in the base class, so mysql and sqlite keep their results. findFootprintIntersections tries the last statement of a hash first, which is the statement that the map of rc5 kept. The diagonal cells of rules agree with postgres on every possible cell run each catalog statement twice on PGlite, and the same change on two branches, against a real database covers the alter_column cases whose SQL fails on a second run.


// Returns the target that the report and the JSON output of `check` show for a statement. A constraint, a
// policy and a privilege show their table. A role and a renamed index have the kind 'table' with their own name.
function targetOf(statement: SnapshotStatement): ConflictTarget {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 55db3bc the conflict lists served only the report text, because matching used only buildFacetPaths. The lists also read JsonStatement, and check now gets the stored form SnapshotStatement. targetOf and describe give the same text and the same JSON target as check on rc5, so a role and a renamed index keep the kind 'table'. check-handler.test.ts expects drop_column: text on groups table, and check-branches.test.ts expects rename_constraint on users table.

// kinds with one name conflict. In a schema, `rel` holds tables, views, sequences and indexes, and `type`
// holds tables, views and enums. In a table, `con` holds constraints. A primary key or a unique constraint
// also has an index with its name.
const sharedNames = (k: string, p: string[]): string[] => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An index tags on one branch and a table tags on the other branch fail in one order, because Postgres keeps both names in the relation namespace of the schema. In 55db3bc each kind had its own name node, so check passed this pair. The test an index and a table that take one name conflict in check-branches.test.ts runs that migration and gets the error on CREATE TABLE. A primary key and a unique constraint also give their name to an index, so pkNames and create_table write that name too.

// Returns what DROP INDEX and ALTER INDEX read of the table of the index. Their SQL names the index only by
// its schema and name, so a rename of the table does not break them. A drop or a move of the table also
// drops or moves the index, so they read that the table exists in its schema.
const indexHome = (schema: string, table: string) => [eref('tbl', schema, table), nref('sch', schema)];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 55db3bc, drop_index and rename_index read the table name with underTable, so a table rename on another branch conflicted with them, although both orders run. indexHome reads only that the table exists in its schema. A table move takes the indexes to the other schema, so move_table now writes that existence and the index names in both schemas. In merge-pairs.test.ts, a table rename against a drop of an index of that table merges, and a table move to another schema against a drop of an index of that table is reported.

db?: Awaited<ReturnType<typeof preparePostgresDB>>,
) => {
const entries = readEntries(out);
assertHistory(entries);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pull now checks the history of the folder before it connects, like generate, check and migrate, so a broken postgres folder stops it with the same check_error. pull reads only the number of snapshots and the newest path, so a broken history does not change what it writes. Without the check, it would still write schema.ts and relations.ts into a folder that the next generate refuses. The rule is in skills/drizzle/migrations.md, and two tests in folder-integrity.test.ts assert that pull throws.


In a postgres folder that it converts, `up` also returns `check_error` for a `snapshot.json` that is not valid JSON (`kind: "malformed"`) and for a folder that sorts before one of its parents (`kind: "order"`). See [`check` integrity errors](#check-integrity-errors). In a folder with `meta/_journal.json`, the migration folders do not exist yet. The `malformed` message names the file under `meta/`. The `order` message and the messages of the conversion errors name the journal entries, for example `0001_posts`. The `order` error then means that the time in `when` of a journal entry is not at least one second after the time of its parent, and the fix is to correct `when` in `meta/_journal.json`.

The move out of `meta/_journal.json` can also fail. It returns `migration_snapshot_not_found_error` when the journal lists a migration whose `meta/<n>_snapshot.json` is missing. If `<tag>.sql` is missing, `up` moves the one SQL file whose name starts with the `<NNNN>_` prefix of the tag. It returns `migration_sql_files_conflict_error` (meta `prefix`) when several SQL files have that prefix. If no SQL file has that prefix, postgres returns the `history` error above. The other dialects skip the migration, and its snapshot is deleted with the `meta/` folder.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence describes the move for mysql, sqlite, turso and singlestore, which this PR does not change. applyFoldersV3 gives an entry without an SQL file no folder, so its snapshot is deleted with meta/. Postgres refuses such an entry with a history error before it moves a file, because the next snapshot would lose its parent. The test up refuses a journal entry that has no SQL file and changes no file in up-format9.test.ts covers the postgres side.

if (existsSync(sqlPath)) return { tag: entry.tag, folder, snapshotPath, sqlPath };
// When `entry.tag` names no SQL file, for example after a rename by hand, the SQL file is found by
// `snapshotPrefix`.
const sqlFileNames = sqlFiles.filter((file) => file.startsWith(`${snapshotPrefix}_`) && file.endsWith('.sql'));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On rc5 the lookup by prefix could not succeed: sqlFileName?.length > 1 compared the length of a file name with 1, so a single match threw MigrationSqlFilesConflictCliError. After a hand rename of an SQL file, postgres up would report a conflict for one file instead of moving it. The lookup now matches <prefix>_*.sql and throws only for several matches. The fix reaches every dialect, because they all run planFoldersV3, and no test covers the prefix lookup.

return lines.join('\n');
};

export const assertConflictOptions = (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postgres rejects --ignore-conflicts in generate, check and migrate, because a format 9 merge over conflicting branches can fail to reconstruct. When two branches create different entities under one key, the combined state throws, and every later generate on the folder stops, which a create of a different entity onto a taken key stops generate, and the error names the migration of that delta in delta-rules.test.ts shows. generate --fix-conflicts replaces the flag, and the changelog names this breaking change for postgres. postgresql rejects ignore-conflicts in cli-json.test.ts replaces the rc5 tests ignore-conflicts emits an ok envelope despite conflicts in cli-json.test.ts and ignoreConflicts keeps all open leaf ids for conflicting branches in check-handler.test.ts. The other dialects keep the flag, and ignoreConflicts keeps both leaf ids for conflicting branches in tests/sqlite/generate-flow.test.ts covers it for sqlite.

key: (type: string, row: Row) => string;
// Renames the entity `from` and returns the delta of that change. The result is null when the state does
// not change: no field of `set` differs, the state does not hold `from`, or the new key is taken.
rename: (from: Row, set: Row) => Delta | null;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are readability changes to the engine after 55db3bc. None of them changes behavior.

  • rename and clone are methods of every DDL object. ddlDiff records each rename with ddl1.rename(from, set), and up copies a state with start.clone().
  • splitQual is parseEntityKey, because it splits the key of a delta into the entity type and the identity.
  • compileEdge uses full names (ListEdge, identityOf, source, elements), and readList is inline.
  • Field and Store have named types (EnumField, ObjectField, ObjectArrayField, EntityType, Identity) in place of trailing comments.
  • validate returns a type predicate, and array has an overload that takes one. So the snapshot validators of all dialects need no explicit type arguments.
  • The postgres toJsonSnapshot is toJsonBaseSnapshot, because it writes only the base snapshot.

… moves, renames, roles and generated columns

Postgres:
- Count the brackets of a custom type's SQL type as array dimensions.
- Drop and create the foreign keys of a recreated generated column one
  time each, and create its indexes and unique constraints one time.
- Rename a view in its old schema before it moves, and move it with its
  old kind.
- Drop a recreated view after its rename or move.
- Run the policy drops, foreign key drops and row level security
  statements of a moved table after `SET SCHEMA`.
- Run `DROP ROLE` after the policy statements.
- Run `DROP EXPRESSION` first and write no `USING` clause for a
  generated column.
- Find the enum columns that a format 7 snapshot stores without a type
  schema when the enum is recreated, with the s06 tests and fixtures.

CockroachDB:
- Drop a view that reads a renamed or moved table before the table
  statement and create it after it. Replace `recreate_view` with a
  `drop_view` that has a `cause`, in the diff, in push and in
  `cockroachExplain`.
- Write both statements for a view or a sequence that moves and gets a
  new name, and look up the old view of a moved view that is recreated.
- Name the table in an index rename or drop when two tables hold the
  index name.
- Write `RENAME CONSTRAINT` for a check rename.
- Select the row of the renamed check, index, primary key, foreign key
  or policy by its table.
- Change the column type when only the type schema or the array
  dimensions change, and count the brackets of a custom type's SQL type.
- Run table renames and moves before the statements that name the table
  in its new place.
- Create the foreign keys of a recreated generated column again.
- Run `DROP ROLE` after the policy statements.
…rifies

The helper files of the tests get a header that says what they provide
and which test files use them. Where a file already had a comment that
described the whole file, that comment is part of the header now.
@dankochetov
dankochetov marked this pull request as ready for review October 15, 2026 15:01
@AlexBlokh
AlexBlokh added this pull request to stack #6512 October 15, 2026 15:05
…reating an add of a unique guarantee as a conflict

`owns` and `destroys` behaved the same in the engine, so they are one
list, `writes`. `needs` is `reads`, and `provides` is `sharedWrites`.

The node for the uniqueness of a set of columns is `uniqueness`. Only a
drop of a primary key, a unique constraint or a unique index writes it,
because an add cannot break a foreign key that references the columns.
So an add on one branch and a new foreign key on the other no longer
conflict. A drop still conflicts with a new foreign key, also when
another guarantee of the same columns remains, because the rules cannot
count the guarantees.
*
* Keys are opaque strings minted by the dialect; the engine only compares them.
*/
export type FacetPaths = {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The facet paths now say what the engine does with them. owns and destroys behaved the same: both put key#w into both lists. So they are one list, writes. needs is reads, and provides is sharedWrites.

sharedWrites is for a fact that several objects guarantee. Its only node is the uniqueness of a set of columns (e|uniqueness|schema.table.columns), which a new foreign key reads. Only a drop of a primary key, a unique constraint or a unique index writes it, because an add cannot break a foreign key. Two drops share the write, because each one alone leaves the uniqueness to the other guarantees.

The rules cannot count the guarantees. So a drop still conflicts with a new foreign key on the other branch when another guarantee of the same columns remains, although both orders run then. The curated cases in commutativity.facet.test.ts cover an add against a new foreign key (no conflict) and two drops (no conflict).

// `ddl2` holds no column with the old name in this table, so every such name is stale.
const newNameOf = (column: string) => column === rename.from.name ? rename.to.name : column;
const inTable = { table: rename.to.table };
ddl2.indexes.update({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is supposedly redundant, we need to check
cc: @RomanNabukhotnyi @dankochetov

table: rename.from.name,
schema: rename.from.schema,
},
// Applies the rename to the rows of `ddl2` that still name the old table. Drizzle Studio builds the new

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Applies the rename to the indexes and constraints of `ddl2` that still name the old column. The form
// of Drizzle Studio renames a column only in the column list. A new state from schema files has no such
// rows.
const newNameOf = (column: string) => column === rename.from.name ? rename.to.name : column;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Defines how a snapshot stores one statement kind and how that kind becomes SQL. `store` picks the fields
// that the snapshot keeps. `convert` gets only these fields, so the SQL of a migration can be built again
// from its snapshot, and a field that `store` does not keep is a type error in `convert`.
export const convertor = <TType extends StatementType>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shall just have a:

const storedFor = (statement: JsonStatement) => {
  // long if else or switch with exhaustive check

  // return row type derived from ddl entry update type too with all fields required, so that whenever we change ddl entries - it's an exhaustive compile error here
}

that way we will not overload json statement to sql convertor with anything related to storing metadata for commutativity

cc: @dankochetov

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RomanNabukhotnyi please check if that's still relevant

AlexBlokh and others added 2 commits October 18, 2026 12:21
Fix postgres and CockroachDB diff cases that write failing SQL around moves, renames, roles and generated columns
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants