From ce5106524333b9bc7ee214b699d76e4b52371519 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Sat, 28 Mar 2026 11:45:54 +0800 Subject: [PATCH] fix: handle migration race condition in concurrent test workers Drizzle's migrate() throws when multiple vitest workers race to apply migrations on the same temp database. The DrizzleError wraps a SqliteError ("table already exists") in its cause chain. Add a same-process guard and a catch that checks both the outer message and cause for "already exists" so the second worker continues safely. --- apps/api/src/db/migrate.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/api/src/db/migrate.ts b/apps/api/src/db/migrate.ts index 4f9379f2..d049f337 100644 --- a/apps/api/src/db/migrate.ts +++ b/apps/api/src/db/migrate.ts @@ -9,6 +9,34 @@ const __dirname = dirname(__filename); // Resolve migrations folder relative to this file, not the working directory const migrationsFolder = join(__dirname, "../../drizzle"); +function isAlreadyExistsError(err: unknown): boolean { + if (err instanceof Error) { + if (err.message.includes("already exists")) return true; + // DrizzleError wraps the real SqliteError in .cause + if ("cause" in err && err.cause instanceof Error) { + return err.cause.message.includes("already exists"); + } + } + return false; +} + +let migrated = false; + export function runMigrations() { - migrate(db, { migrationsFolder }); + if (migrated) return; + try { + migrate(db, { migrationsFolder }); + } catch (err: unknown) { + // In test / multi-process environments, concurrent workers may race to + // apply migrations on the same database file. If a table already exists, + // the schema is in place and we can safely continue. + // Drizzle wraps the SqliteError in a DrizzleError, so check both the + // outer message and the cause chain. + if (isAlreadyExistsError(err)) { + // Tables created by another process — DB is ready + } else { + throw err; + } + } + migrated = true; }