CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
migrate.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 3ef4c9d | 1 | /** |
| 2 | * Migration runner — executes every `drizzle/*.sql` file in order. | |
| 3 | * Tracks applied migrations in a dedicated table so repeat runs are idempotent. | |
| 6f3ce18 | 4 | * |
| 5 | * Dual-driver: uses Neon HTTP for *.neon.tech URLs, postgres.js TCP for | |
| 6 | * everything else (localhost, RDS, Supabase, plain Postgres). | |
| 3ef4c9d | 7 | */ |
| 8 | ||
| 9 | import { readdir, readFile } from "fs/promises"; | |
| 10 | import { join } from "path"; | |
| fc1817a | 11 | import { neon } from "@neondatabase/serverless"; |
| 6f3ce18 | 12 | import postgres from "postgres"; |
| fc1817a | 13 | import { config } from "../lib/config"; |
| 6f3ce18 | 14 | import { isNeonUrl } from "./index"; |
| 15 | ||
| 16 | type Exec = (query: string, params?: unknown[]) => Promise<unknown>; | |
| fc1817a | 17 | |
| 18 | async function runMigrations() { | |
| 3ef4c9d | 19 | if (!config.databaseUrl) { |
| 20 | throw new Error("DATABASE_URL is not set"); | |
| 21 | } | |
| 6f3ce18 | 22 | |
| 23 | let exec: Exec; | |
| 24 | let cleanup: (() => Promise<void>) | null = null; | |
| 25 | ||
| 26 | if (isNeonUrl(config.databaseUrl)) { | |
| 27 | const neonSql = neon(config.databaseUrl); | |
| 28 | exec = (q, p = []) => neonSql(q, p); | |
| 29 | } else { | |
| 30 | const client = postgres(config.databaseUrl, { max: 1, prepare: false }); | |
| 31 | exec = (q, p) => | |
| 32 | p && p.length > 0 ? client.unsafe(q, p as never[]) : client.unsafe(q); | |
| 33 | cleanup = async () => { | |
| 34 | await client.end({ timeout: 5 }); | |
| 35 | }; | |
| 3ef4c9d | 36 | } |
| 37 | ||
| 6f3ce18 | 38 | try { |
| 39 | // Ensure tracking table exists | |
| 40 | await exec(` | |
| 41 | CREATE TABLE IF NOT EXISTS "_migrations" ( | |
| 42 | "name" text PRIMARY KEY, | |
| 43 | "applied_at" timestamp DEFAULT now() NOT NULL | |
| 44 | ) | |
| 45 | `); | |
| 46 | ||
| 47 | const appliedRows = (await exec(`SELECT name FROM _migrations`)) as Array<{ | |
| 48 | name: string; | |
| 49 | }>; | |
| 50 | const appliedSet = new Set(appliedRows.map((r) => r.name)); | |
| 51 | ||
| 52 | const migrationsDir = join(process.cwd(), "drizzle"); | |
| 53 | const files = (await readdir(migrationsDir)) | |
| 54 | .filter((f) => f.endsWith(".sql")) | |
| 55 | .sort(); | |
| 56 | ||
| 57 | if (files.length === 0) { | |
| 58 | console.log("No migration files found."); | |
| 59 | return; | |
| 3ef4c9d | 60 | } |
| 61 | ||
| 6f3ce18 | 62 | for (const file of files) { |
| 63 | if (appliedSet.has(file)) { | |
| 64 | console.log(`[migrate] ${file} — already applied, skipping`); | |
| 65 | continue; | |
| 66 | } | |
| 67 | ||
| 68 | console.log(`[migrate] applying ${file}...`); | |
| 69 | const content = await readFile(join(migrationsDir, file), "utf8"); | |
| 70 | ||
| 71 | // Split on the drizzle breakpoint marker. Each chunk is one statement. | |
| 72 | const statements = content | |
| 73 | .split(/-->\s*statement-breakpoint/) | |
| 74 | .map((s) => s.trim()) | |
| 75 | .filter((s) => s.length > 0); | |
| 76 | ||
| 77 | for (const raw of statements) { | |
| 78 | const stripped = raw | |
| 79 | .split("\n") | |
| 80 | .filter((line) => !line.trim().startsWith("--")) | |
| 81 | .join("\n") | |
| 82 | .trim(); | |
| 83 | if (!stripped) continue; | |
| 84 | ||
| 85 | try { | |
| 86 | await exec(stripped); | |
| 87 | } catch (err) { | |
| 88 | const msg = err instanceof Error ? err.message : String(err); | |
| 89 | if ( | |
| 90 | msg.includes("already exists") || | |
| 91 | msg.includes("duplicate_column") || | |
| 92 | msg.includes("duplicate_object") | |
| 93 | ) { | |
| 94 | console.warn( | |
| 95 | `[migrate] ${file}: ${msg.slice(0, 120)} (treated as applied)` | |
| 96 | ); | |
| 97 | continue; | |
| 98 | } | |
| 99 | console.error( | |
| 100 | `[migrate] ${file} failed on statement:\n${stripped.slice(0, 500)}\n\n${msg}` | |
| 3ef4c9d | 101 | ); |
| 6f3ce18 | 102 | throw err; |
| 3ef4c9d | 103 | } |
| 104 | } | |
| 6f3ce18 | 105 | |
| 106 | await exec(`INSERT INTO _migrations (name) VALUES ($1)`, [file]); | |
| 107 | console.log(`[migrate] ${file} applied`); | |
| 3ef4c9d | 108 | } |
| 109 | ||
| 6f3ce18 | 110 | console.log("[migrate] all migrations complete"); |
| 111 | } finally { | |
| 112 | if (cleanup) await cleanup(); | |
| 3ef4c9d | 113 | } |
| fc1817a | 114 | } |
| 115 | ||
| 116 | runMigrations().catch((err) => { | |
| 117 | console.error("Migration failed:", err); | |
| 118 | process.exit(1); | |
| 119 | }); |