Skip to content

[BUG]: migrations.sort() in migrator.js does nothing - migrations run in wrong order on Linux #5123

@nervetattoo

Description

@nervetattoo

Report hasn't been filed before.

  • I have verified that the bug I'm about to report hasn't been filed before.

What version of drizzle-orm are you using?

^1.0.0-beta.2-0f52822

What version of drizzle-kit are you using?

^1.0.0-beta.2-0f52822

Other packages

No response

Describe the Bug

The v3 migration format has a bug in migrator.js where migrations.sort() is called on an array of objects without a comparator function. This causes migrations to run in filesystem order rather than chronological order.

Problem

In migrator.js line 47-48:

const migrations = readdirSync(migrationFolderTo)
  .map((subdir) => ({ path: join(migrationFolderTo, subdir, "migration.sql"), name: subdir }))
  .filter((it) => existsSync(it.path));
migrations.sort();  // <-- BUG: does nothing

Since .sort() without a comparator converts elements to strings for comparison, and all objects stringify to "[object Object]", the sort is effectively a no-op:

const arr = [
  { name: '20251210095918_second' },
  { name: '20251210095732_first' }
];
arr.sort();
// Result: unchanged - still second, first

Impact

  • macOS/Windows: readdirSync typically returns entries in alphabetical order, so this bug is hidden
  • Linux: readdirSync returns entries in inode order (non-deterministic), causing migrations to run out of order

This leads to failures like:

Error: Failed query: ALTER TABLE "some_table" ADD COLUMN "new_col" ...

...because the table creation migration ran after the alter migration.

Possible test case to trigger failure on some platforms

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, expect, test } from 'vitest';
import { readMigrationFiles } from '~/migrator.ts';

let tempDir: string;

beforeEach(() => {
	tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'drizzle-migrator-test-'));
});

afterEach(() => {
	fs.rmSync(tempDir, { recursive: true, force: true });
});

test('readMigrationFiles should return migrations sorted by folder name', () => {
	// Create migrations in non-alphabetical order on disk
	// On Linux, readdirSync may return these in creation/inode order
	const secondMigration = '20251210095918_add_column';
	const firstMigration = '20251210095732_create_table';

	// Create "second" migration first to increase chance of wrong order on Linux
	fs.mkdirSync(path.join(tempDir, secondMigration));
	fs.writeFileSync(
		path.join(tempDir, secondMigration, 'migration.sql'),
		'ALTER TABLE users ADD COLUMN email TEXT;',
	);

	fs.mkdirSync(path.join(tempDir, firstMigration));
	fs.writeFileSync(
		path.join(tempDir, firstMigration, 'migration.sql'),
		'CREATE TABLE users (id INT);',
	);

	const migrations = readMigrationFiles({ migrationsFolder: tempDir });

	// Migrations must be sorted chronologically by folder name
	// 20251210095732 should come before 20251210095918
	expect(migrations).toHaveLength(2);
	expect(migrations[0]!.sql[0]).toBe('CREATE TABLE users (id INT);');
	expect(migrations[1]!.sql[0]).toBe('ALTER TABLE users ADD COLUMN email TEXT;');
});

Fix

We use this as a local patch to fix it.

migrations.sort((a, b) => a.name.localeCompare(b.name));

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions