fix(api): skip baseline replay when schema already exists
Dokploy boots crash-looped on CREATE TYPE AdminRole against an existing Postgres with an empty _goldbank_migrations table. Adopt Prisma CLI history when present, record the baseline if AdminRole exists, and treat duplicate object errors as already-applied so the API can start without DB shell access. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+75
-11
@@ -164102,6 +164102,7 @@ var require_migrate = __commonJS({
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.splitSqlStatements = splitSqlStatements;
|
||||
exports2.sortMigrations = sortMigrations;
|
||||
exports2.isAlreadyExistsPgError = isAlreadyExistsPgError;
|
||||
exports2.runMigrations = runMigrations;
|
||||
exports2.runOnce = runOnce;
|
||||
var fs_1 = require("fs");
|
||||
@@ -164212,6 +164213,50 @@ var require_migrate = __commonJS({
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
function isAlreadyExistsPgError(err) {
|
||||
const meta = err && typeof err === "object" && "meta" in err ? err.meta : void 0;
|
||||
const code = String(meta?.code ?? "");
|
||||
if (code === "42710" || code === "42P07" || code === "42701")
|
||||
return true;
|
||||
const msg = [
|
||||
err instanceof Error ? err.message : String(err),
|
||||
meta?.message ?? ""
|
||||
].join(" ");
|
||||
return /already exists/i.test(msg) || /duplicate_(object|table|column|relation)/i.test(msg);
|
||||
}
|
||||
async function typeExists(prisma, typname) {
|
||||
const rows = await prisma.$queryRawUnsafe(`SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_type t
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE t.typname = $1 AND n.nspname = current_schema()
|
||||
) AS "exists"`, typname);
|
||||
return Boolean(rows[0]?.exists);
|
||||
}
|
||||
async function recordApplied(prisma, name) {
|
||||
await prisma.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)
|
||||
ON CONFLICT (name) DO NOTHING`, name);
|
||||
}
|
||||
async function adoptPrismaMigrationLedger(prisma, appliedNames) {
|
||||
const table = await prisma.$queryRawUnsafe(`SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = current_schema() AND table_name = '_prisma_migrations'
|
||||
) AS "exists"`);
|
||||
if (!table[0]?.exists)
|
||||
return [];
|
||||
const rows = await prisma.$queryRawUnsafe(`SELECT migration_name FROM "_prisma_migrations"
|
||||
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL`);
|
||||
const adopted = [];
|
||||
for (const row of rows) {
|
||||
const name = row.migration_name;
|
||||
if (!name || appliedNames.has(name))
|
||||
continue;
|
||||
await recordApplied(prisma, name);
|
||||
appliedNames.add(name);
|
||||
adopted.push(name);
|
||||
}
|
||||
return adopted;
|
||||
}
|
||||
async function runMigrations(prisma) {
|
||||
const dir = resolveMigrationsDir();
|
||||
const files = discoverMigrationFiles(dir);
|
||||
@@ -164226,26 +164271,45 @@ var require_migrate = __commonJS({
|
||||
)`);
|
||||
const alreadyApplied = await prisma.$queryRawUnsafe(`SELECT name FROM "${TRACKING_TABLE}"`);
|
||||
const appliedNames = new Set(alreadyApplied.map((row) => row.name));
|
||||
const adopted = await adoptPrismaMigrationLedger(prisma, appliedNames);
|
||||
if (adopted.length > 0) {
|
||||
console.warn(`[migrate] Adopted ${adopted.length} migration(s) from _prisma_migrations: ${adopted.join(", ")}`);
|
||||
}
|
||||
const applied = [];
|
||||
for (const file2 of files) {
|
||||
const legacyName = LEGACY_NAME_ALIASES[file2.name];
|
||||
if (appliedNames.has(file2.name) || legacyName && appliedNames.has(legacyName))
|
||||
continue;
|
||||
if (file2.name === BASELINE_MIGRATION && await typeExists(prisma, "AdminRole")) {
|
||||
await recordApplied(prisma, file2.name);
|
||||
appliedNames.add(file2.name);
|
||||
applied.push(file2.name);
|
||||
console.warn(`[migrate] Baseline ${file2.name} already present in schema (AdminRole); recorded without re-running SQL`);
|
||||
continue;
|
||||
}
|
||||
const statements = splitSqlStatements((0, fs_1.readFileSync)(file2.path, "utf8")).filter((statement) => {
|
||||
const normalized = statement.replace(/\s+/g, " ").trim().toLowerCase();
|
||||
return !normalized.startsWith(`drop table "${TRACKING_TABLE.toLowerCase()}"`) && !normalized.startsWith(`drop table ${TRACKING_TABLE.toLowerCase()}`);
|
||||
});
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
for (const statement of statements) {
|
||||
await tx.$executeRawUnsafe(statement);
|
||||
}
|
||||
await tx.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`, file2.name);
|
||||
},
|
||||
// The baseline migration is hundreds of DDL statements; the 5s default is
|
||||
// not enough, especially against a remote database.
|
||||
{ maxWait: 6e4, timeout: 6e5 }
|
||||
);
|
||||
try {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
for (const statement of statements) {
|
||||
await tx.$executeRawUnsafe(statement);
|
||||
}
|
||||
await tx.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`, file2.name);
|
||||
},
|
||||
// The baseline migration is hundreds of DDL statements; the 5s default is
|
||||
// not enough, especially against a remote database.
|
||||
{ maxWait: 6e4, timeout: 6e5 }
|
||||
);
|
||||
} catch (err) {
|
||||
if (!isAlreadyExistsPgError(err))
|
||||
throw err;
|
||||
await recordApplied(prisma, file2.name);
|
||||
console.warn(`[migrate] ${file2.name}: objects already exist \u2014 recorded as applied (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
|
||||
}
|
||||
appliedNames.add(file2.name);
|
||||
applied.push(file2.name);
|
||||
}
|
||||
return { freshDatabase, applied };
|
||||
|
||||
Reference in New Issue
Block a user