/** * Prisma Schema Safety Check * ─────────────────────────── * Runs before `prisma db push` in the build pipeline. * Aborts the build if the pending schema diff contains any destructive * operations: DROP COLUMN, DROP TABLE, DROP CONSTRAINT, DROP INDEX, * or ALTER COLUMN ... DROP. * * DROP INDEX is included because db push silently drops any index that * is not declared in schema.prisma, even if it is a large production * index (e.g. SiteEvent_sessionId_timestamp_idx on the ~22GB SiteEvent * table). Losing such an index causes full table scans on hot queries. * * Override: set ALLOW_SCHEMA_DROPS=true in the build environment to skip. */ import { execSync } from "child_process"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.log("[prisma-safety] DATABASE_URL not set — skipping safety check"); process.exit(0); } if (process.env.ALLOW_SCHEMA_DROPS === "true") { console.log("[prisma-safety] ALLOW_SCHEMA_DROPS=true — skipping drop check"); process.exit(0); } console.log("[prisma-safety] Computing schema diff against live database..."); // Retry up to 3 times to handle transient P1001 connection errors on cold starts. const MAX_RETRIES = 3; let diffSql; let lastErr; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { diffSql = execSync( "npx prisma migrate diff --from-url $DATABASE_URL --to-schema-datamodel prisma/schema.prisma --script", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }, ); lastErr = null; break; } catch (err) { lastErr = err; const stderr = err.stderr ?? ""; const isTransient = stderr.includes("P1001") || stderr.includes("Could not connect") || stderr.includes("connection"); if (isTransient && attempt < MAX_RETRIES) { console.warn(`[prisma-safety] Connection attempt ${attempt} failed — retrying...`); // brief pause before retry (synchronous busy-wait, safe in build context) const wait = attempt * 2000; const end = Date.now() + wait; while (Date.now() < end) { /* spin */ } continue; } if (isTransient) { console.warn("[prisma-safety] Could not connect to database after retries — skipping safety check"); console.warn("[prisma-safety]", stderr.split("\n")[0]); process.exit(0); } console.error("[prisma-safety] Failed to compute schema diff:"); console.error(stderr || err.message); process.exit(1); } } if (lastErr) { // Should not reach here, but guard anyway. console.warn("[prisma-safety] Exhausted retries — skipping safety check"); process.exit(0); } // Patterns that signal data-destructive or performance-destructive operations. // DROP INDEX is included: db push silently drops undeclared indexes, turning // hot queries into full table scans (root cause of the SiteEvent incident). const DROP_PATTERNS = [ /^\s*ALTER TABLE\s+\S+\s+DROP COLUMN\s+/im, /^\s*DROP TABLE\s+/im, /^\s*DROP CONSTRAINT\s+/im, /^\s*ALTER TABLE\s+\S+\s+DROP CONSTRAINT\s+/im, /^\s*ALTER COLUMN\s+\S+\s+DROP\s+/im, /^\s*DROP INDEX\s+/im, ]; const matchedStatements = DROP_PATTERNS .flatMap((pattern) => { const lines = diffSql.split("\n"); return lines.filter((line) => pattern.test(line)); }) .filter((line, i, arr) => arr.indexOf(line) === i); // deduplicate if (matchedStatements.length > 0) { console.error(""); console.error("╔══════════════════════════════════════════════════════════════╗"); console.error("║ PRISMA SAFETY CHECK FAILED — BUILD ABORTED ║"); console.error("╠══════════════════════════════════════════════════════════════╣"); console.error("║ The schema diff would execute the following destructive ║"); console.error("║ SQL statements that could permanently delete production ║"); console.error("║ data or drop production indexes (causing full table scans): ║"); console.error("╚══════════════════════════════════════════════════════════════╝"); console.error(""); matchedStatements.forEach((stmt) => console.error(" ⚠", stmt.trim())); console.error(""); console.error(" Full diff SQL:"); console.error(" " + diffSql.split("\n").join("\n ")); console.error(""); console.error(" If this drop is intentional, set ALLOW_SCHEMA_DROPS=true"); console.error(" in the Vercel build environment and redeploy."); console.error(""); process.exit(1); } console.log("[prisma-safety] No destructive operations found. Build can proceed."); process.exit(0);