This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user