fix(db): tolerate missing constraints during clone upgrades

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 21:21:18 -05:00
1 parent d001430ce4
commit 5ae7368656
2 files changed
+39 -1

No files matched your search

+31
View File
@@ -168,6 +168,37 @@ describeEmbeddedPostgres("resetPostgresDatabase", () => {
});
describeEmbeddedPostgres("applyPendingMigrations", () => {
it("upgrades an older clone whose replaced connection constraint is already absent", async () => {
const clusterUrl = new URL(await createTempDatabase());
const databaseName = "missing_connection_constraint";
clusterUrl.pathname = "/postgres";
await ensurePostgresDatabase(clusterUrl.toString(), databaseName);
clusterUrl.pathname = "/" + databaseName;
const connectionString = clusterUrl.toString();
const prefixFolder = await fs.promises.mkdtemp(join(tmpdir(), "paperclip-migration-prefix-"));
cleanups.push(() => fs.promises.rm(prefixFolder, { recursive: true, force: true }));
await fs.promises.mkdir(join(prefixFolder, "meta"));
const journal = JSON.parse(await fs.promises.readFile(new URL("./migrations/meta/_journal.json", import.meta.url), "utf8"));
journal.entries = journal.entries.filter((entry: { idx: number }) => entry.idx < 255);
await fs.promises.writeFile(join(prefixFolder, "meta/_journal.json"), JSON.stringify(journal));
for (const entry of journal.entries) {
await fs.promises.copyFile(new URL(`./migrations/${entry.tag}.sql`, import.meta.url), join(prefixFolder, `${entry.tag}.sql`));
}
const sql = postgres(connectionString, { max: 1, onnotice: () => {} });
try {
await migrate(drizzle(sql), { migrationsFolder: prefixFolder });
await sql`ALTER TABLE "tool_connections" DROP CONSTRAINT "tool_connections_transport_check"`;
await applyPendingMigrations(connectionString);
expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
const [constraint] = await sql<{ definition: string }[]>`
SELECT pg_get_constraintdef(oid) AS definition FROM pg_constraint
WHERE conrelid = 'tool_connections'::regclass AND conname = 'tool_connections_transport_check'
`;
expect(constraint?.definition).toContain("chat_sdk");
expect(constraint?.definition).toContain("runtime_auth");
} finally { await sql.end(); }
}, 30_000);
it("upgrades renumbered recovery migrations and replays their schema idempotently", async () => {
const connectionString = await createTempDatabase();
await applyPendingMigrations(connectionString);
+8 -1
View File
@@ -494,7 +494,14 @@ async function applyPendingMigrationsManually(
await runInTransaction(sql, async () => {
for (const statement of splitMigrationStatements(migrationContent)) {
await sql.unsafe(statement);
// Older dev schemas can already lack a constraint that the next
// migration replaces. Keep the published SQL/hash intact and let
// that harmless drop proceed; the replacement still runs normally.
const executable = statement.replace(
/^(\s*ALTER TABLE "[^"]+" DROP CONSTRAINT) (?!IF EXISTS\b)/i,
"$1 IF EXISTS ",
);
await sql.unsafe(executable);
}
await recordMigrationHistoryEntry(