#!/usr/bin/env node /** * Docker-friendly migration runner. * Applies SQL files from ./migrations/ in order using pg directly, * without drizzle-kit (which can hang in non-TTY Docker environments). */ import pg from "pg"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const MIGRATIONS_DIR = path.resolve(__dirname, "../migrations"); const url = process.env.DATABASE_URL; if (!url) { console.error("[Migrate] ERROR: DATABASE_URL is not set."); process.exit(1); } const client = new pg.Client({ connectionString: url }); try { await client.connect(); await client.query(` CREATE TABLE IF NOT EXISTS __drizzle_migrations ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() ) `); const { rows: applied } = await client.query( "SELECT name FROM __drizzle_migrations ORDER BY id" ); const appliedSet = new Set(applied.map((r) => r.name)); const files = fs .readdirSync(MIGRATIONS_DIR) .filter((f) => f.endsWith(".sql")) .sort(); let count = 0; for (const file of files) { if (appliedSet.has(file)) continue; const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf8"); console.log(`[Migrate] Applying ${file}…`); await client.query("BEGIN"); try { await client.query(sql); await client.query( "INSERT INTO __drizzle_migrations (name) VALUES ($1)", [file] ); await client.query("COMMIT"); count++; } catch (err) { await client.query("ROLLBACK"); console.error(`[Migrate] ERROR in ${file}:`, err.message); process.exit(1); } } if (count === 0) { console.log("[Migrate] All migrations already applied, nothing to do."); } else { console.log(`[Migrate] Done — applied ${count} migration(s).`); } await client.end(); } catch (err) { console.error("[Migrate] Fatal error:", err.message); process.exit(1); }