package db import ( "database/sql" "fmt" "os" "path/filepath" "sort" "strings" ) type Migration struct { Version string Name string SQL string } func RunMigrations(db *sql.DB, migrationsDir string) error { // Ensure migrations table exists if _, err := db.Exec(` CREATE TABLE IF NOT EXISTS boc_schema_migrations ( version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); err != nil { return fmt.Errorf("create migrations table: %w", err) } // Read migration files files, err := os.ReadDir(migrationsDir) if err != nil { return fmt.Errorf("read migrations dir: %w", err) } var migrations []Migration for _, f := range files { if f.IsDir() || !strings.HasSuffix(f.Name(), ".sql") { continue } content, err := os.ReadFile(filepath.Join(migrationsDir, f.Name())) if err != nil { return fmt.Errorf("read migration %s: %w", f.Name(), err) } version := strings.Split(f.Name(), "_")[0] migrations = append(migrations, Migration{ Version: version, Name: f.Name(), SQL: string(content), }) } // Sort by version sort.Slice(migrations, func(i, j int) bool { return migrations[i].Version < migrations[j].Version }) // Apply migrations in transaction for _, m := range migrations { var applied bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM boc_schema_migrations WHERE version = $1)", m.Version).Scan(&applied) if err != nil { return fmt.Errorf("check migration %s: %w", m.Version, err) } if applied { continue } tx, err := db.Begin() if err != nil { return fmt.Errorf("begin transaction: %w", err) } if _, err := tx.Exec(m.SQL); err != nil { tx.Rollback() return fmt.Errorf("apply migration %s: %w", m.Name, err) } if _, err := tx.Exec("INSERT INTO boc_schema_migrations (version) VALUES ($1)", m.Version); err != nil { tx.Rollback() return fmt.Errorf("record migration %s: %w", m.Name, err) } if err := tx.Commit(); err != nil { return fmt.Errorf("commit migration %s: %w", m.Name, err) } fmt.Printf("✅ Applied migration: %s\n", m.Name) } return nil }