diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts index be8f478..6876189 100644 --- a/src/server/db/migrate.ts +++ b/src/server/db/migrate.ts @@ -6,6 +6,10 @@ * This applies any pending migrations from the drizzle/ directory to the * database specified by DATABASE_URL. It is safe to run multiple times — * Drizzle tracks applied migrations in the __drizzle_migrations table. + * + * If the database was previously set up via `db:push` (no migration history), + * this script will baseline it: seed the migration history without re-running + * the SQL, so only future migrations are applied. */ import * as dotenv from "dotenv"; @@ -17,6 +21,8 @@ import { Pool } from "pg"; import { drizzle } from "drizzle-orm/node-postgres"; import { migrate } from "drizzle-orm/node-postgres/migrator"; import path from "path"; +import fs from "fs"; +import crypto from "crypto"; import { fileURLToPath } from "url"; const databaseUrl = process.env.DATABASE_URL; @@ -36,9 +42,80 @@ const pool = new Pool({ const db = drizzle(pool); +/** + * Baseline: if the DB has existing tables but no migration history, seed the + * __drizzle_migrations table so Drizzle won't try to re-run already-applied SQL. + */ +async function baselineIfNeeded(client: Pool) { + // Check if migration tracking table exists and has entries + const { rows: migRows } = await client.query<{ count: string }>(` + SELECT COUNT(*)::text AS count + FROM information_schema.tables + WHERE table_schema = 'drizzle' + AND table_name = '__drizzle_migrations' + `); + const hasMigrationsTable = parseInt(migRows[0]?.count ?? "0") > 0; + + if (hasMigrationsTable) { + const { rows: entryRows } = await client.query<{ count: string }>( + `SELECT COUNT(*)::text AS count FROM drizzle.__drizzle_migrations` + ); + if (parseInt(entryRows[0]?.count ?? "0") > 0) { + // Migration history exists — normal flow + return; + } + } + + // No migration history. Check if the DB already has our tables (was db:push'd). + const { rows: tableRows } = await client.query<{ count: string }>(` + SELECT COUNT(*)::text AS count + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'beenvoice_account' + `); + const dbAlreadyExists = parseInt(tableRows[0]?.count ?? "0") > 0; + + if (!dbAlreadyExists) { + // Fresh database — let migrate() run normally + return; + } + + console.log("[migrate] Existing database detected without migration history — baselining..."); + + // Create the drizzle schema + migrations table if needed + await client.query(`CREATE SCHEMA IF NOT EXISTS drizzle`); + await client.query(` + CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations ( + id SERIAL PRIMARY KEY, + hash text NOT NULL, + created_at bigint + ) + `); + + // Read the journal and seed a record for every migration file + const journal = JSON.parse( + fs.readFileSync(path.join(migrationsFolder, "meta/_journal.json"), "utf8") + ) as { entries: { idx: number; tag: string; when: number }[] }; + + for (const entry of journal.entries) { + const sqlPath = path.join(migrationsFolder, `${entry.tag}.sql`); + const sql = fs.readFileSync(sqlPath, "utf8"); + const hash = crypto.createHash("sha256").update(sql).digest("hex"); + + await client.query( + `INSERT INTO drizzle.__drizzle_migrations (hash, created_at) VALUES ($1, $2)`, + [hash, entry.when] + ); + console.log(`[migrate] Baselined: ${entry.tag}`); + } + + console.log("[migrate] Baseline complete — future migrations will apply normally"); +} + console.log("[migrate] Running migrations from", migrationsFolder); try { + await baselineIfNeeded(pool); await migrate(db, { migrationsFolder }); console.log("[migrate] All migrations applied successfully"); } catch (err) {