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. | |
| 4 | */ | |
| 5 | ||
| 6 | import { readdir, readFile } from "fs/promises"; | |
| 7 | import { join } from "path"; | |
| fc1817a | 8 | import { neon } from "@neondatabase/serverless"; |
| 9 | import { config } from "../lib/config"; | |
| 10 | ||
| 11 | async function runMigrations() { | |
| 3ef4c9d | 12 | if (!config.databaseUrl) { |
| 13 | throw new Error("DATABASE_URL is not set"); | |
| 14 | } | |
| fc1817a | 15 | const sql = neon(config.databaseUrl); |
| 3ef4c9d | 16 | |
| 17 | // Ensure tracking table exists | |
| 18 | await sql` | |
| 19 | CREATE TABLE IF NOT EXISTS "_migrations" ( | |
| 20 | "name" text PRIMARY KEY, | |
| 21 | "applied_at" timestamp DEFAULT now() NOT NULL | |
| 22 | ) | |
| 23 | `; | |
| 24 | ||
| 25 | const applied = await sql`SELECT name FROM _migrations`; | |
| 26 | const appliedSet = new Set( | |
| 27 | (applied as Array<{ name: string }>).map((r) => r.name) | |
| 28 | ); | |
| 29 | ||
| 30 | const migrationsDir = join(process.cwd(), "drizzle"); | |
| 31 | const files = (await readdir(migrationsDir)) | |
| 32 | .filter((f) => f.endsWith(".sql")) | |
| 33 | .sort(); | |
| 34 | ||
| 35 | if (files.length === 0) { | |
| 36 | console.log("No migration files found."); | |
| 37 | return; | |
| 38 | } | |
| 39 | ||
| 40 | for (const file of files) { | |
| 41 | if (appliedSet.has(file)) { | |
| 42 | console.log(`[migrate] ${file} — already applied, skipping`); | |
| 43 | continue; | |
| 44 | } | |
| 45 | ||
| 46 | console.log(`[migrate] applying ${file}...`); | |
| 47 | const content = await readFile(join(migrationsDir, file), "utf8"); | |
| 48 | ||
| 49 | // Split on the drizzle breakpoint marker. Each chunk is one statement. | |
| 50 | const statements = content | |
| 51 | .split(/-->\s*statement-breakpoint/) | |
| 52 | .map((s) => s.trim()) | |
| 53 | .filter((s) => s.length > 0); | |
| 54 | ||
| 55 | for (const raw of statements) { | |
| 56 | const stripped = raw | |
| 57 | .split("\n") | |
| 58 | .filter((line) => !line.trim().startsWith("--")) | |
| 59 | .join("\n") | |
| 60 | .trim(); | |
| 61 | if (!stripped) continue; | |
| 62 | ||
| 63 | try { | |
| 64 | await sql(stripped); | |
| 65 | } catch (err) { | |
| 66 | const msg = err instanceof Error ? err.message : String(err); | |
| 67 | if ( | |
| 68 | msg.includes("already exists") || | |
| 69 | msg.includes("duplicate_column") || | |
| 70 | msg.includes("duplicate_object") | |
| 71 | ) { | |
| 72 | console.warn( | |
| 73 | `[migrate] ${file}: ${msg.slice(0, 120)} (treated as applied)` | |
| 74 | ); | |
| 75 | continue; | |
| 76 | } | |
| 77 | console.error( | |
| 78 | `[migrate] ${file} failed on statement:\n${stripped.slice(0, 500)}\n\n${msg}` | |
| 79 | ); | |
| 80 | throw err; | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | await sql`INSERT INTO _migrations (name) VALUES (${file})`; | |
| 85 | console.log(`[migrate] ${file} applied`); | |
| 86 | } | |
| 87 | ||
| 88 | console.log("[migrate] all migrations complete"); | |
| fc1817a | 89 | } |
| 90 | ||
| 91 | runMigrations().catch((err) => { | |
| 92 | console.error("Migration failed:", err); | |
| 93 | process.exit(1); | |
| 94 | }); |