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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>;
|
||||
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,33 +441,18 @@ 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());
|
||||
// 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 });
|
||||
});
|
||||
migrate.on("error", (err) => {
|
||||
console.error("Failed to run prisma migrate deploy:", err.message);
|
||||
res.json({ success: true }); // still report success — data is restored
|
||||
});
|
||||
});
|
||||
|
||||
if (isZip && tmpZipPath) {
|
||||
// Pipe the first .sql entry from the zip directly into psql stdin
|
||||
|
||||
Reference in New Issue
Block a user