From 6e46afc1fc45aa41d255af9149e93788bc961082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 23 Jul 2026 21:26:54 +0200 Subject: [PATCH] test(prisma): guard against duplicate migration numeric prefixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../migration-number-uniqueness.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/__tests__/migration-number-uniqueness.test.ts diff --git a/src/__tests__/migration-number-uniqueness.test.ts b/src/__tests__/migration-number-uniqueness.test.ts new file mode 100644 index 000000000..de7ea3c53 --- /dev/null +++ b/src/__tests__/migration-number-uniqueness.test.ts @@ -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(); + 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); + }); +});