Skip to content
Open

Beta.16 #5376

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a871303
Add new migrations versioning
AndriiSherman Feb 13, 2026
45147e2
added up logic for migration tables for mssql, cockroach, sqlite, mysql
AleksandrSherman Feb 13, 2026
f3ec80d
updated all migrators for rest dbs
AleksandrSherman Feb 13, 2026
b9865b8
moved CURRENT_MIGRATION_TABLE_VERSION to common folder
AleksandrSherman Feb 14, 2026
79e9742
fixed up migration table for sqlite
AleksandrSherman Feb 14, 2026
e2fee3e
updates in up migrators (name fix + sqlite fixes)
AleksandrSherman Feb 14, 2026
00112b6
cut millis to sec + 000
AleksandrSherman Feb 15, 2026
2458948
Fix migrator utils
AndriiSherman Feb 15, 2026
87f38f6
updates + fixed up migrations
AleksandrSherman Feb 16, 2026
c8afbc8
Merge branch 'beta.16' of github.com:drizzle-team/drizzle-orm into be…
AleksandrSherman Feb 16, 2026
e1dd99e
updates in up migrators
AleksandrSherman Feb 16, 2026
0961126
Fix mysql commutative
AndriiSherman Feb 16, 2026
38cf350
updates in pg migrator - check on table shape and remove "version" co…
AleksandrSherman Feb 17, 2026
e54f2c2
removed "version" column for all migrators + updated up-migrator for …
AleksandrSherman Feb 17, 2026
af7a979
check if drizzle_migrations table exists by validating result length …
AleksandrSherman Feb 17, 2026
74dcc66
added pg-effect up migrator
AleksandrSherman Feb 17, 2026
57cfec2
up-migrators
AleksandrSherman Feb 17, 2026
536c9b3
d1 migrator fix
AleksandrSherman Feb 18, 2026
6b2b488
migrator-fixes
AleksandrSherman Feb 18, 2026
fdab9e6
more fixes
AleksandrSherman Feb 18, 2026
63ec30a
fixes in pg
AleksandrSherman Feb 18, 2026
2f5ee0e
custom wrapper for pg up migration
AleksandrSherman Feb 18, 2026
ea709e0
wrappers on sqlite and mysql up migrators
AleksandrSherman Feb 18, 2026
e933a2c
singlestore fix: looks like final fix
AleksandrSherman Feb 19, 2026
a2ee747
cockroach removed access to crdb_internal after v25. Fixed introspect
AleksandrSherman Feb 20, 2026
ce6c5e0
Add last check for commutative
AndriiSherman Feb 23, 2026
0894418
Merge branch 'beta.16' of github.com:drizzle-team/drizzle-orm into be…
AndriiSherman Feb 23, 2026
7ef627b
Update brocli
AndriiSherman Feb 23, 2026
0db7527
Fixed migration upgrade function
Sukairo-02 Feb 24, 2026
9482d20
Import fix
Sukairo-02 Feb 24, 2026
d104e94
update schema
AndriiSherman Feb 24, 2026
5da0c94
Update utils
AndriiSherman Feb 24, 2026
afb06e9
cockroach introspect query fix
AleksandrSherman Feb 24, 2026
1761d78
Merge branch 'beta.16' of github.com:drizzle-team/drizzle-orm into be…
AleksandrSherman Feb 24, 2026
901bfe4
fix cockroach introspect query
AleksandrSherman Feb 24, 2026
9485290
add static cockroach version for docker since problems in v26
AleksandrSherman Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions changelogs/drizzle-kit/1.0.0-beta.16.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Drizzle ORM beta.16 updates

We've fixed a regression in migrations introduced in `beta.13` that persisted through `beta.15`, and used the opportunity to significantly improve the entire migration infrastructure going forward.

After receiving github issue, we focused on not just fixing the symptoms but rethinking how migrations are tracked, validated, and applied. As we always do with Drizzle we went deeper and redesigned the underlying architecture so it can be stable for all future changes, additions, and edge cases

We went through how we check migrations, how we store them, and how we help developers keep their migration flow reliable. After several iterations of rewriting the `migrate` function in both ORM and Kit, adding migration table versioning, and building a new commutativity check system: here's what changed and why.

## What happened in beta.12–beta.15 and what caused the issue

On `latest` (pre-beta) versions, the migrations folder used a journal-based structure. A `meta/_journal.json` file stored a timestamp for each migration in **milliseconds**. That same millis value was stored in the database's `created_at` column and used to determine which migrations had been applied. Because the journal enforced ordering, we could simply fetch the last applied migration from the database and apply everything after it.

With the new v3 folder structure (introduced in beta), each migration lives in its own folder named `<YYYYMMDDHHmmss>_<name>`. This format only has **second** precision. The new structure also intentionally allows out-of-order migrations (as it should for team workflows), so we switched to reading *all* migrations from the database and comparing against all local migrations.

The problem appeared after `drizzle-kit up` converted the old journal structure to v3 folders. It mapped millis timestamps to the `YYYYMMDDHHmmss_name` format, stripping away the millisecond precision. So when the new migration checker compared what was in the database (millis) to what was on disk (seconds), nothing matched causing migrations to be re-applied on every run.

**This only affected beta users who upgraded from the journal-based format. Users on `latest` were not impacted.**

## How we fixed it

### 1. Versioned migration table

We introduced an internal version number for the migrations table schema. On `beta.16`, the table will automatically upgrade itself from `version_0` to `version_1` the first time you run `migrate`.

**Version 0** (old schema):
| Column | Type |
|---|---|
| `id` | serial |
| `hash` | text |
| `created_at` | bigint (millis) |

**Version 1** (new schema):
| Column | Type | |
|---|---| ---|
| `id` | serial |
| `hash` | text |
| `created_at` | bigint | (legacy) |
| `name` | text |
| `applied_at` | timestamp |
| `version` | integer |

The `name` column stores the full folder name of the migration (e.g. `20250220153045_brave_wolverine`). The `applied_at` column records when the migration was actually executed. For pre-existing migrations that were backfilled during upgrade, `applied_at` is set to `NULL` to distinguish them from newly applied ones.

This versioning system means we can add more fields in the future (like migration state for rollbacks) without any disruption for developers

### 2. Matching by folder name instead of timestamps

All migrations are now checked against the **full folder name**: the combination of a 14-digit UTC timestamp and a name suffix (random or custom). Even if two migrations are generated within the same second, the name suffix guarantees uniqueness.

The detection logic is simple: build a set of `name` values from the database, filter local migrations whose name isn't in that set, and apply those. No more timestamp arithmetic, no more precision mismatches.

### 3. Automatic upgrade with smart backfilling

When `beta.16` detects a `version_0` table, it adds the new columns and backfills the `name` for each existing row using a multi-step matching strategy:

1. **Millis match**: truncate the stored millis to seconds and match against local migration folder timestamps
2. **Hash tiebreaker**: if multiple migrations share the same second, use the SQL hash to pick the right one
3. **Hash-only fallback**: if millis matching fails entirely, fall back to matching by hash alone

This means the upgrade handles all edge cases: normal single-developer projects, teams with closely-timed migrations, and even cases where the old journal data doesn't perfectly align with the new folder names.

### 4. New commutativity checks (`drizzle-kit check`)

When working in teams, multiple developers may generate migrations from the same base schema on different branches. These migrations can conflict in non-obvious ways, for example, two branches both adding a column to the same table, or one renaming a table that another is altering.

We built a new `drizzle-kit check` command that detects these non-commutative migrations. It works by:

- Building a DAG (directed acyclic graph) from snapshot `prevIds` to understand the branch structure
- Finding fork points where branches diverged
- Computing the DDL diff from the parent snapshot to each branch leaf
- Checking for conflicting operations using a comprehensive footprint map that knows which DDL statement types can interfere with each other

This is available for PostgreSQL and MySQL today. If conflicts are found, the report tells you exactly which migrations on which branches are incompatible and what statements are conflicting.

The new folder structure also changed snapshot metadata from `prevId: string` to `prevIds: string[]`, enabling proper DAG representation of migration history across branches.

## Upgrading to beta.16

The upgrade is automatic. When you run `migrate` for the first time on `beta.16`:

1. Drizzle detects your migration table version
2. If it's `version_0`, it adds the new columns and backfills `name` from your local migration files
3. All future migrations are tracked by name
4. No manual steps required

If you're also upgrading your migration **folder structure** from the old journal format, run `drizzle-kit up` first to convert to the v3 folder layout, then `migrate` will handle the rest.

## Upgrading to beta.16 is not fixing my problem

If you're still hitting migration issues after upgrading, please reach out to us directly - drop a message in [Discord](https://discord.gg/7NKUQWP9c8) or open a [GitHub issue](https://github.com/drizzle-team/drizzle-orm/issues/new/choose) with your migration folder structure and the contents of your migrations table. We'll help you sort it out.
90 changes: 90 additions & 0 deletions changelogs/drizzle-orm/1.0.0-beta.16.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Drizzle ORM beta.16 updates

We've fixed a regression in migrations introduced in `beta.13` that persisted through `beta.15`, and used the opportunity to significantly improve the entire migration infrastructure going forward.

After receiving github issue, we focused on not just fixing the symptoms but rethinking how migrations are tracked, validated, and applied. As we always do with Drizzle we went deeper and redesigned the underlying architecture so it can be stable for all future changes, additions, and edge cases

We went through how we check migrations, how we store them, and how we help developers keep their migration flow reliable. After several iterations of rewriting the `migrate` function in both ORM and Kit, adding migration table versioning, and building a new commutativity check system: here's what changed and why.

## What happened in beta.12–beta.15 and what caused the issue

On `latest` (pre-beta) versions, the migrations folder used a journal-based structure. A `meta/_journal.json` file stored a timestamp for each migration in **milliseconds**. That same millis value was stored in the database's `created_at` column and used to determine which migrations had been applied. Because the journal enforced ordering, we could simply fetch the last applied migration from the database and apply everything after it.

With the new v3 folder structure (introduced in beta), each migration lives in its own folder named `<YYYYMMDDHHmmss>_<name>`. This format only has **second** precision. The new structure also intentionally allows out-of-order migrations (as it should for team workflows), so we switched to reading *all* migrations from the database and comparing against all local migrations.

The problem appeared after `drizzle-kit up` converted the old journal structure to v3 folders. It mapped millis timestamps to the `YYYYMMDDHHmmss_name` format, stripping away the millisecond precision. So when the new migration checker compared what was in the database (millis) to what was on disk (seconds), nothing matched causing migrations to be re-applied on every run.

**This only affected beta users who upgraded from the journal-based format. Users on `latest` were not impacted.**

## How we fixed it

### 1. Versioned migration table

We introduced an internal version number for the migrations table schema. On `beta.16`, the table will automatically upgrade itself from `version_0` to `version_1` the first time you run `migrate`.

**Version 0** (old schema):
| Column | Type |
|---|---|
| `id` | serial |
| `hash` | text |
| `created_at` | bigint (millis) |

**Version 1** (new schema):
| Column | Type | |
|---|---| ---|
| `id` | serial |
| `hash` | text |
| `created_at` | bigint | (legacy) |
| `name` | text |
| `applied_at` | timestamp |
| `version` | integer |

The `name` column stores the full folder name of the migration (e.g. `20250220153045_brave_wolverine`). The `applied_at` column records when the migration was actually executed. For pre-existing migrations that were backfilled during upgrade, `applied_at` is set to `NULL` to distinguish them from newly applied ones.

This versioning system means we can add more fields in the future (like migration state for rollbacks) without any disruption for developers

### 2. Matching by folder name instead of timestamps

All migrations are now checked against the **full folder name**: the combination of a 14-digit UTC timestamp and a name suffix (random or custom). Even if two migrations are generated within the same second, the name suffix guarantees uniqueness.

The detection logic is simple: build a set of `name` values from the database, filter local migrations whose name isn't in that set, and apply those. No more timestamp arithmetic, no more precision mismatches.

### 3. Automatic upgrade with smart backfilling

When `beta.16` detects a `version_0` table, it adds the new columns and backfills the `name` for each existing row using a multi-step matching strategy:

1. **Millis match**: truncate the stored millis to seconds and match against local migration folder timestamps
2. **Hash tiebreaker**: if multiple migrations share the same second, use the SQL hash to pick the right one
3. **Hash-only fallback**: if millis matching fails entirely, fall back to matching by hash alone

This means the upgrade handles all edge cases: normal single-developer projects, teams with closely-timed migrations, and even cases where the old journal data doesn't perfectly align with the new folder names.

### 4. New commutativity checks (`drizzle-kit check`)

When working in teams, multiple developers may generate migrations from the same base schema on different branches. These migrations can conflict in non-obvious ways, for example, two branches both adding a column to the same table, or one renaming a table that another is altering.

We built a new `drizzle-kit check` command that detects these non-commutative migrations. It works by:

- Building a DAG (directed acyclic graph) from snapshot `prevIds` to understand the branch structure
- Finding fork points where branches diverged
- Computing the DDL diff from the parent snapshot to each branch leaf
- Checking for conflicting operations using a comprehensive footprint map that knows which DDL statement types can interfere with each other

This is available for PostgreSQL and MySQL today. If conflicts are found, the report tells you exactly which migrations on which branches are incompatible and what statements are conflicting.

The new folder structure also changed snapshot metadata from `prevId: string` to `prevIds: string[]`, enabling proper DAG representation of migration history across branches.

## Upgrading to beta.16

The upgrade is automatic. When you run `migrate` for the first time on `beta.16`:

1. Drizzle detects your migration table version
2. If it's `version_0`, it adds the new columns and backfills `name` from your local migration files
3. All future migrations are tracked by name
4. No manual steps required

If you're also upgrading your migration **folder structure** from the old journal format, run `drizzle-kit up` first to convert to the v3 folder layout, then `migrate` will handle the rest.

## Upgrading to beta.16 is not fixing my problem

If you're still hitting migration issues after upgrading, please reach out to us directly - drop a message in [Discord](https://discord.gg/7NKUQWP9c8) or open a [GitHub issue](https://github.com/drizzle-team/drizzle-orm/issues/new/choose) with your migration folder structure and the contents of your migrations table. We'll help you sort it out.
6 changes: 3 additions & 3 deletions compose/cockroach-many.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
cockroach0:
image: cockroachdb/cockroach:latest
image: cockroachdb/cockroach:v25.2.0
command: start-single-node --insecure --store=type=mem,size=2GiB
ports:
- "26260:26257"
Expand All @@ -12,7 +12,7 @@ services:
retries: 60

cockroach1:
image: cockroachdb/cockroach:latest
image: cockroachdb/cockroach:v25.2.0
command: start-single-node --insecure --store=type=mem,size=2GiB
ports:
- "26261:26257"
Expand All @@ -24,7 +24,7 @@ services:
retries: 60

cockroach2:
image: cockroachdb/cockroach:latest
image: cockroachdb/cockroach:v25.2.0
command: start-single-node --insecure --store=type=mem,size=2GiB
ports:
- "26262:26257"
Expand Down
2 changes: 1 addition & 1 deletion compose/cockroach.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
cockroach:
image: cockroachdb/cockroach:latest
image: cockroachdb/cockroach:v25.2.0
command: start-single-node --insecure --store=type=mem,size=2GiB
ports:
- "26257:26257"
Expand Down
147 changes: 126 additions & 21 deletions drizzle-kit/src/cli/commands/check.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,122 @@
import chalk from 'chalk';
import { readFileSync } from 'fs';
import { render } from 'hanji';
import type { MigrationNode, NonCommutativityReport, UnifiedBranchConflict } from 'src/utils/commutativity';
import { detectNonCommutative } from 'src/utils/commutativity';
import type { Dialect } from '../../utils/schemaValidator';
import { prepareOutFolder, validatorForDialect } from '../../utils/utils-node';
import { info } from '../views';

export const checkHandler = async (out: string, dialect: Dialect) => {
// ─── Shared helpers ──────────────────────────────────────────────────────────

const countLeafs = (conflicts: UnifiedBranchConflict[]): number => {
const ids = new Set<string>();
for (const c of conflicts) {
const lastA = c.branchA.chain[c.branchA.chain.length - 1];
const lastB = c.branchB.chain[c.branchB.chain.length - 1];
if (lastA) ids.add(lastA.id);
if (lastB) ids.add(lastB.id);
}
return ids.size;
};

const headerBadge = (conflicts: UnifiedBranchConflict[]): string => {
const leafCount = countLeafs(conflicts);
return (
`${chalk.white.bgRed(' Non-commutative migrations detected ')} `
+ `Found ${chalk.bold(String(conflicts.length))} conflict${conflicts.length === 1 ? '' : 's'} `
+ `across ${chalk.bold(String(leafCount))} migration${leafCount === 1 ? '' : 's'}`
);
};

export const renderReportDirectory = (
report: NonCommutativityReport,
): string => {
const { conflicts } = report;
const lines: string[] = ['', headerBadge(conflicts), ''];

// Group conflicts by parentId
const byParent: Record<string, UnifiedBranchConflict[]> = {};
for (const c of conflicts) {
(byParent[c.parentId] ??= []).push(c);
}

const parentEntries = Object.entries(byParent);

for (let p = 0; p < parentEntries.length; p++) {
const [parentId, parentConflicts] = parentEntries[p];
const parentLabel = parentConflicts[0].parentPath ?? parentId;

// Collect unique branches, dedupe by leaf id
const branches: { chain: MigrationNode[]; descriptions: string[] }[] = [];
const seenLeafs: Record<string, number> = {};

for (const c of parentConflicts) {
for (const branch of [c.branchA, c.branchB]) {
const leafId = branch.chain[branch.chain.length - 1]?.id ?? '';
if (leafId in seenLeafs) {
const descs = branches[seenLeafs[leafId]].descriptions;
if (!descs.includes(branch.statementDescription)) {
descs.push(branch.statementDescription);
}
} else {
seenLeafs[leafId] = branches.length;
branches.push({
chain: branch.chain,
descriptions: [branch.statementDescription],
});
}
}
}

lines.push(` ${chalk.white(parentLabel)}`);

for (let b = 0; b < branches.length; b++) {
const { chain, descriptions } = branches[b];
const isLast = b === branches.length - 1;
const prefix = isLast ? ' ' : '│ ';

for (let m = 0; m < chain.length; m++) {
const label = m === chain.length - 1
? chalk.red.bold(chain[m].path)
: chalk.green(chain[m].path);

lines.push(
m === 0
? ` ${chalk.gray(isLast ? '└──' : '├──')} ${label}`
: ` ${chalk.gray(prefix)}${label}`,
);
}

for (let d = 0; d < descriptions.length; d++) {
const connector = d === descriptions.length - 1 ? '└─' : '├─';
lines.push(
` ${chalk.gray(prefix)}${chalk.gray(connector)} ${chalk.yellow('⚠')} ${chalk.yellow(descriptions[d])}`,
);
}
}

if (p < parentEntries.length - 1) lines.push('');
}

lines.push(
chalk.gray('\nPlease refer to our guide on how to resolve such conflicts:'),
chalk.bold.underline.blue('https://orm.drizzle.team/docs/migrations'),
);
return lines.join('\n');
};

export const checkHandler = async (
out: string,
dialect: Dialect,
ignoreConflicts?: boolean,
) => {
const { snapshots } = prepareOutFolder(out);
const validator = validatorForDialect(dialect);

// const snapshotsData: PostgresSnapshot[] = [];

for (const snapshot of snapshots) {
const raw = JSON.parse(readFileSync(`./${snapshot}`).toString());

// snapshotsData.push(raw);

const res = validator(raw);
if (res.status === 'unsupported') {
console.log(
Expand All @@ -24,29 +127,31 @@ export const checkHandler = async (out: string, dialect: Dialect) => {
process.exit(0);
}
if (res.status === 'malformed') {
// more explanation
console.log(`${snapshot} data is malformed`);
process.exit(1);
}

if (res.status === 'nonLatest') {
console.log(`${snapshot} is not of the latest version, please run "drizzle-kit up"`);
console.log(
`${snapshot} is not of the latest version, please run "drizzle-kit up"`,
);
process.exit(1);
}
}

// try {
// const response = await detectNonCommutative(snapshots, dialect);
// if (response!.conflicts.length > 0) {
// console.log('\nNon-commutative migration branches detected:');
// for (const c of response!.conflicts) {
// console.log(`- Parent ${c.parentId}${c.parentPath ? ` (${c.parentPath})` : ''}`);
// console.log(` A: ${c.branchA.headId} (${c.branchA.path})`);
// console.log(` B: ${c.branchB.headId} (${c.branchB.path})`);
// // for (const r of c.reasons) console.log(` • ${r}`);
// }
// }
// } catch (e) {
// console.error(e);
// }
if (ignoreConflicts) {
return;
}

try {
const response = await detectNonCommutative(snapshots, dialect);
if (response.conflicts.length === 0) {
return;
}

render(renderReportDirectory(response));
process.exit(1);
} catch (e) {
console.error(e);
}
};
Loading