Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.

migrate.tsBlame122 lines · 2 contributors
3ef4c9dClaude1/**
2 * Migration runner — executes every `drizzle/*.sql` file in order.
3 * Tracks applied migrations in a dedicated table so repeat runs are idempotent.
6f3ce18Claude4 *
5 * Dual-driver: uses Neon HTTP for *.neon.tech URLs, postgres.js TCP for
6 * everything else (localhost, RDS, Supabase, plain Postgres).
3ef4c9dClaude7 */
8
9import { readdir, readFile } from "fs/promises";
10import { join } from "path";
fc1817aClaude11import { neon } from "@neondatabase/serverless";
6f3ce18Claude12import postgres from "postgres";
fc1817aClaude13import { config } from "../lib/config";
6f3ce18Claude14import { isNeonUrl } from "./index";
15
16type Exec = (query: string, params?: unknown[]) => Promise<unknown>;
fc1817aClaude17
18async function runMigrations() {
3ef4c9dClaude19 if (!config.databaseUrl) {
20 throw new Error("DATABASE_URL is not set");
21 }
6f3ce18Claude22
23 let exec: Exec;
24 let cleanup: (() => Promise<void>) | null = null;
25
26 if (isNeonUrl(config.databaseUrl)) {
cab3dc9Test User27 // @neondatabase/serverless 1.x: the tagged-template factory only accepts
28 // a TemplateStringsArray. Use the .query(string, params) helper for the
29 // dynamic-string flow this migration runner needs.
6f3ce18Claude30 const neonSql = neon(config.databaseUrl);
cab3dc9Test User31 exec = (q, p = []) => neonSql.query(q, p);
6f3ce18Claude32 } else {
33 const client = postgres(config.databaseUrl, { max: 1, prepare: false });
34 exec = (q, p) =>
35 p && p.length > 0 ? client.unsafe(q, p as never[]) : client.unsafe(q);
36 cleanup = async () => {
37 await client.end({ timeout: 5 });
38 };
3ef4c9dClaude39 }
40
6f3ce18Claude41 try {
42 // Ensure tracking table exists
43 await exec(`
44 CREATE TABLE IF NOT EXISTS "_migrations" (
45 "name" text PRIMARY KEY,
46 "applied_at" timestamp DEFAULT now() NOT NULL
47 )
48 `);
49
50 const appliedRows = (await exec(`SELECT name FROM _migrations`)) as Array<{
51 name: string;
52 }>;
53 const appliedSet = new Set(appliedRows.map((r) => r.name));
54
55 const migrationsDir = join(process.cwd(), "drizzle");
56 const files = (await readdir(migrationsDir))
57 .filter((f) => f.endsWith(".sql"))
58 .sort();
59
60 if (files.length === 0) {
61 console.log("No migration files found.");
62 return;
3ef4c9dClaude63 }
64
6f3ce18Claude65 for (const file of files) {
66 if (appliedSet.has(file)) {
67 console.log(`[migrate] ${file} — already applied, skipping`);
68 continue;
69 }
70
71 console.log(`[migrate] applying ${file}...`);
72 const content = await readFile(join(migrationsDir, file), "utf8");
73
74 // Split on the drizzle breakpoint marker. Each chunk is one statement.
75 const statements = content
76 .split(/-->\s*statement-breakpoint/)
77 .map((s) => s.trim())
78 .filter((s) => s.length > 0);
79
80 for (const raw of statements) {
81 const stripped = raw
82 .split("\n")
83 .filter((line) => !line.trim().startsWith("--"))
84 .join("\n")
85 .trim();
86 if (!stripped) continue;
87
88 try {
89 await exec(stripped);
90 } catch (err) {
91 const msg = err instanceof Error ? err.message : String(err);
92 if (
93 msg.includes("already exists") ||
94 msg.includes("duplicate_column") ||
95 msg.includes("duplicate_object")
96 ) {
97 console.warn(
98 `[migrate] ${file}: ${msg.slice(0, 120)} (treated as applied)`
99 );
100 continue;
101 }
102 console.error(
103 `[migrate] ${file} failed on statement:\n${stripped.slice(0, 500)}\n\n${msg}`
3ef4c9dClaude104 );
6f3ce18Claude105 throw err;
3ef4c9dClaude106 }
107 }
6f3ce18Claude108
109 await exec(`INSERT INTO _migrations (name) VALUES ($1)`, [file]);
110 console.log(`[migrate] ${file} applied`);
3ef4c9dClaude111 }
112
6f3ce18Claude113 console.log("[migrate] all migrations complete");
114 } finally {
115 if (cleanup) await cleanup();
3ef4c9dClaude116 }
fc1817aClaude117}
118
119runMigrations().catch((err) => {
120 console.error("Migration failed:", err);
121 process.exit(1);
122});