// Package main is the migration runner. It applies embedded SQL migrations to the // configured database. Intended to run as a one-shot container/job before the API. // // Usage: migrate [up|down|version|force ] package main import ( "context" "errors" "fmt" "log/slog" "os" "time" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/mitbringsl/backend/internal/config" "github.com/mitbringsl/backend/internal/logging" "github.com/mitbringsl/backend/migrations" ) func main() { cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "config: %v\n", err) os.Exit(1) } logging.Init(cfg.LogLevel) cmd := "up" if len(os.Args) > 1 { cmd = os.Args[1] } if err := run(cfg, cmd); err != nil { slog.Error("migration failed", "cmd", cmd, "error", err) os.Exit(1) } } func run(cfg *config.Config, cmd string) error { src, err := iofs.New(migrations.FS, ".") if err != nil { return fmt.Errorf("create source: %w", err) } // Use the configured DATABASE_URL; golang-migrate accepts the same DSN // form pgx uses (postgres://...). Allow a short startup grace period. dbURL := cfg.DatabaseURL ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var m *migrate.Migrate var lastErr error for attempt := 0; attempt < 30; attempt++ { m, lastErr = migrate.NewWithSourceInstance("iofs", src, dbURL) if lastErr == nil { break } select { case <-ctx.Done(): return fmt.Errorf("db not ready: %w", lastErr) case <-time.After(time.Second): } } if lastErr != nil { return fmt.Errorf("create migrate instance: %w", lastErr) } defer m.Close() switch cmd { case "up": if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return err } slog.Info("migrations applied (up)") case "down": if err := m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return err } slog.Info("migrations applied (down)") case "version": v, dirty, err := m.Version() if err != nil { return err } fmt.Printf("version=%d dirty=%v\n", v, dirty) case "force": if len(os.Args) < 3 { return errors.New("force requires a version argument") } var v int fmt.Sscanf(os.Args[2], "%d", &v) if err := m.Force(v); err != nil { return err } slog.Info("forced migration version", "version", v) default: return fmt.Errorf("unknown command %q (use up|down|version|force)", cmd) } return nil }