From b148c0de307afa0f26c1a7d069dd3a518b7413b4 Mon Sep 17 00:00:00 2001 From: ff Date: Sun, 17 May 2026 22:36:35 -0400 Subject: [PATCH] fix: apply missing migrations directly after DB restore Replace the prisma migrate deploy subprocess with a custom applyMissingMigrations() that reads each migration SQL file and runs it via prisma.$executeRawUnsafe. This avoids the Prisma CLI aborting when the restored _prisma_migrations table contains orphaned entries (migration names that no longer have a corresponding file in the migrations folder). Co-Authored-By: Claude Sonnet 4.6 --- .../Backend/src/routes/database-management.ts | 84 +++++++++++++------ 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/apps/Backend/src/routes/database-management.ts b/apps/Backend/src/routes/database-management.ts index ba7c4c4f..5d0ba183 100755 --- a/apps/Backend/src/routes/database-management.ts +++ b/apps/Backend/src/routes/database-management.ts @@ -8,6 +8,53 @@ import { prisma } from "@repo/db/client"; import { storage } from "../storage"; import { backupDatabaseToPath } from "../services/databaseBackupService"; +const MIGRATIONS_DIR = path.resolve(__dirname, "../../../../packages/db/prisma/migrations"); + +// Applies migration SQL files that are missing from the database. +// Reads each folder in the migrations directory in sorted order, checks +// whether the migration is already recorded as successfully applied in +// _prisma_migrations, and runs the SQL if not. Safe to call after any +// restore because it uses IF NOT EXISTS semantics or checks first. +async function applyMissingMigrations() { + let folders: string[]; + try { + folders = fs.readdirSync(MIGRATIONS_DIR) + .filter((name) => fs.statSync(path.join(MIGRATIONS_DIR, name)).isDirectory()) + .sort(); + } catch { + console.warn("Could not read migrations directory, skipping post-restore migration."); + return; + } + + // Fetch the set of successfully-applied migration names from the DB. + let applied: Set; + try { + const rows = await prisma.$queryRaw<{ migration_name: string }[]>` + SELECT migration_name FROM "_prisma_migrations" WHERE finished_at IS NOT NULL + `; + applied = new Set(rows.map((r) => r.migration_name)); + } catch { + // _prisma_migrations may not exist in very old backups; proceed anyway. + applied = new Set(); + } + + for (const folder of folders) { + if (applied.has(folder)) continue; + const sqlFile = path.join(MIGRATIONS_DIR, folder, "migration.sql"); + if (!fs.existsSync(sqlFile)) continue; + + const sql = fs.readFileSync(sqlFile, "utf8"); + try { + await prisma.$executeRawUnsafe(sql); + console.log(`Applied migration: ${folder}`); + } catch (err: any) { + // Log but continue — some statements may already exist (e.g. after a + // partial restore) and that is acceptable. + console.warn(`Migration ${folder} had errors (may already be applied):`, err.message); + } + } +} + const restoreUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 500 * 1024 * 1024 }, // 500 MB @@ -394,32 +441,17 @@ router.post("/restore", restoreUpload.single("file"), async (req: Request, res: await prisma.$connect(); } catch (_) {} - // Apply any migrations the backup may be missing so the schema matches - // the current codebase. Uses prisma migrate deploy which is safe to run - // repeatedly — it skips already-applied migrations. - const migrate = spawn( - "npx", - ["prisma", "migrate", "deploy", "--schema", path.resolve(__dirname, "../../../../packages/db/prisma/schema.prisma")], - { - env: { ...process.env, DATABASE_URL: process.env.DATABASE_URL || "" }, - cwd: path.resolve(__dirname, "../../../../packages/db"), - } - ); - let migrateOut = ""; - migrate.stdout.on("data", (d) => (migrateOut += d.toString())); - migrate.stderr.on("data", (d) => (migrateOut += d.toString())); - migrate.on("close", (migrateCode) => { - if (migrateCode !== 0) { - console.error("prisma migrate deploy failed after restore:", migrateOut); - } else { - console.log("prisma migrate deploy completed after restore:", migrateOut.trim()); - } - res.json({ success: true }); - }); - migrate.on("error", (err) => { - console.error("Failed to run prisma migrate deploy:", err.message); - res.json({ success: true }); // still report success — data is restored - }); + // Apply any migrations the backup may be missing. We run each migration + // SQL file directly instead of using `prisma migrate deploy` because the + // restored _prisma_migrations table may contain orphaned entries (migration + // names that have no matching file) which cause Prisma CLI to abort. + try { + await applyMissingMigrations(); + } catch (err) { + console.error("applyMissingMigrations failed after restore:", err); + } + + res.json({ success: true }); }); if (isZip && tmpZipPath) {