// Database migration script for Turso // Custom implementation to handle multi-statement SQL files with Turso import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { createClient } from '@libsql/client'; import { drizzle } from 'drizzle-orm/libsql'; import * as schema from './schema'; console.log('🚀 Running database migrations...'); async function runMigrations() { // Get database URL and auth token let databaseUrl = process.env.TURSO_DATABASE_URL || Bun.env?.TURSO_DATABASE_URL; let authToken = process.env.TURSO_AUTH_TOKEN || Bun.env?.TURSO_AUTH_TOKEN; if (!databaseUrl) { console.log('[Migration] TURSO_DATABASE_URL not found. Migrating local SQLite: file:local.db'); databaseUrl = "file:local.db"; } if (!authToken) { console.log('[Migration] TURSO_AUTH_TOKEN not found. Using mock-token.'); authToken = "mock-token"; } // Create Turso client const client = createClient({ url: databaseUrl, authToken: authToken, }); const _db = drizzle(client, { schema }); // Create migrations tracking table if it doesn't exist await client.execute(` CREATE TABLE IF NOT EXISTS __drizzle_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, created_at INTEGER NOT NULL ) `); // Read migration journal const migrationsFolder = './src/db/migrations'; const journalPath = join(migrationsFolder, 'meta/_journal.json'); const journalContent = await readFile(journalPath, 'utf-8'); const journal = JSON.parse(journalContent); // Get already applied migrations const appliedMigrations = await client.execute('SELECT hash FROM __drizzle_migrations'); const appliedHashes = new Set( appliedMigrations.rows.map((row) => (row as unknown as { hash: string }).hash) ); // Process each migration in order for (const entry of journal.entries) { const migrationHash = `${entry.idx}:${entry.tag}`; if (appliedHashes.has(migrationHash)) { console.log(`⏭️ Skipping already applied migration: ${entry.tag}`); continue; } console.log(`📦 Applying migration: ${entry.tag}`); // Read migration SQL file const sqlFilePath = join(migrationsFolder, `${entry.tag}.sql`); const sqlContent = await readFile(sqlFilePath, 'utf-8'); // Split SQL into individual statements let statements: string[]; if (sqlContent.includes('--> statement-breakpoint')) { // Drizzle-generated SQL uses statement-breakpoint statements = sqlContent .split('--> statement-breakpoint') .map((stmt) => { // Remove SQL comments (lines starting with --) return stmt .split('\n') .filter((line) => !line.trim().startsWith('--')) .join('\n') .trim(); }) .filter((stmt) => stmt.length > 0) .map((stmt) => stmt.replace(/;$/, '')); } else { // Hand-written SQL - need to parse carefully // Remove comment lines but preserve block structure for triggers const cleanedSql = sqlContent .split('\n') .filter((line) => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith('--'); }) .join('\n'); // Split by semicolons, but be careful with BEGIN...END blocks statements = []; let current = ''; let inBlock = 0; for (const char of cleanedSql) { current += char; // Track BEGIN...END blocks const upperCurrent = current.toUpperCase(); if (upperCurrent.endsWith('BEGIN')) { inBlock++; } else if (upperCurrent.endsWith('END')) { inBlock--; } // Split on semicolon only if not in a block if (char === ';' && inBlock === 0) { const stmt = current.slice(0, -1).trim(); if (stmt) { statements.push(stmt); } current = ''; } } // Add any remaining statement const lastStmt = current.trim(); if (lastStmt) { statements.push(lastStmt); } } // Execute each statement individually // Turso requires each statement to be executed separately if (statements.length > 0) { try { console.log(` Executing ${statements.length} statements...`); for (let i = 0; i < statements.length; i++) { const stmt = statements[i]; if (stmt.trim()) { try { await client.execute(stmt); console.log(` ✓ Statement ${i + 1}/${statements.length}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(` ✗ Statement ${i + 1}/${statements.length} failed:`, errorMessage); throw error; } } } // Record successful migration await client.execute({ sql: 'INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)', args: [migrationHash, Date.now()], }); console.log(`✅ Applied migration: ${entry.tag}`); } catch (error) { console.error(`❌ Failed to apply migration ${entry.tag}:`, error); throw error; } } } console.log('✅ All migrations completed successfully!'); } try { await runMigrations(); process.exit(0); } catch (error) { console.error('❌ Migration failed:', error); process.exit(1); }