test(prisma): guard against duplicate migration numeric prefixes

Two short-lived branches can each grab the next free migration number and
both merge, leaving the tree with two directories sharing a four-digit
prefix. This structural test fails the build the moment a prefix repeats, so
the collision surfaces at PR CI instead of after merge. One historical
collision (0025) predates the guard and is grandfathered — a migration
directory name is the primary key in the applied-migrations ledger, so it
cannot be renamed without orphaning the record on every deployed database.
This commit is contained in:
Marc-André Bombeck committed 2026-07-23 21:26:54 +02:00
1 parent 9e6453c51d
commit 6e46afc1fc
1 file changed
+69
@@ -0,0 +1,69 @@
/**
* Structural guard — every Prisma migration owns a unique numeric prefix.
*
* Migrations are named `NNNN_description`, and the four-digit prefix is the
* ordering key. When two short-lived branches each grab the next free number
* and both merge, the tree ends up with two `0268_*` directories. Prisma
* applies them in lexical order, but the collision is a silent
* merge-time hazard: a reviewer reading `git log` sees one number twice, and a
* later renumber has to touch history. This test fails the build the moment a
* prefix repeats, so the collision is caught at PR CI rather than after merge.
*/
import { readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { describe, it, expect } from "vitest";
const MIGRATIONS_DIR = resolve(
dirname(fileURLToPath(import.meta.url)),
"../../prisma/migrations",
);
const NUMBERED_MIGRATION = /^(\d+)_/;
/**
* One grandfathered collision that predates this guard. `0025_refresh_tokens`
* and `0025_user_locale_drift_fix` are both long applied on every deployed
* database — a migration directory name is the primary key in the
* `_prisma_migrations` ledger, so renaming either would orphan the applied
* record and force a re-run on every tenant. It is frozen as history; the
* guard grandfathers exactly this prefix and refuses every NEW one.
*/
const GRANDFATHERED_PREFIXES = new Set(["0025"]);
describe("prisma migrations — numeric prefixes are unique", () => {
it("has no two migration directories sharing a numeric prefix", () => {
const prefixes = new Map<string, string[]>();
for (const entry of readdirSync(MIGRATIONS_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const match = NUMBERED_MIGRATION.exec(entry.name);
if (!match) continue;
const prefix = match[1];
const bucket = prefixes.get(prefix) ?? [];
bucket.push(entry.name);
prefixes.set(prefix, bucket);
}
const collisions = [...prefixes.entries()].filter(
([prefix, names]) =>
names.length > 1 && !GRANDFATHERED_PREFIXES.has(prefix),
);
expect(
collisions,
collisions.length > 0
? `Duplicate migration prefixes:\n${collisions
.map(([prefix, names]) => ` ${prefix}: ${names.join(", ")}`)
.join("\n")}`
: undefined,
).toEqual([]);
});
it("finds the migration set (guards against a broken directory path)", () => {
const numbered = readdirSync(MIGRATIONS_DIR, {
withFileTypes: true,
}).filter((e) => e.isDirectory() && NUMBERED_MIGRATION.test(e.name));
expect(numbered.length).toBeGreaterThan(0);
});
});