package migrations import ( "context" "database/sql" "embed" "fmt" "log/slog" "os" _ "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" ) //go:embed sql/*.sql var FS embed.FS const DefaultURL = "postgres://postgres:postgres@localhost:5432/lookbook?sslmode=disable" // Up applies all available migrations using the provided database URL. func Up(ctx context.Context, dbURL string, logger *slog.Logger) error { url := dbURL if url == "" { url = DefaultURL } db, err := openDB(url, logger) if err != nil { return err } defer db.Close() if err := goose.UpContext(ctx, db, "sql"); err != nil { return fmt.Errorf("apply migrations: %w", err) } logger.Info("database migrated") return nil } // Down rolls back migrations. If targetVersion < 0, it steps back one migration; otherwise it migrates down to the target version. func Down(ctx context.Context, dbURL string, targetVersion int64, logger *slog.Logger) error { url := dbURL if url == "" { url = DefaultURL } db, err := openDB(url, logger) if err != nil { return err } defer db.Close() if targetVersion < 0 { if err := goose.DownContext(ctx, db, "sql"); err != nil { return fmt.Errorf("rollback one: %w", err) } logger.Info("rolled back one migration") return nil } if err := goose.DownToContext(ctx, db, "sql", targetVersion); err != nil { return fmt.Errorf("rollback to version %d: %w", targetVersion, err) } logger.Info("rolled back to version", slog.Int64("version", targetVersion)) return nil } func CheckPending(ctx context.Context, dbURL string, logger *slog.Logger) (int, error) { url := dbURL if url == "" { url = DefaultURL } db, err := openDB(url, logger) if err != nil { return 0, err } defer db.Close() current, err := goose.GetDBVersionContext(ctx, db) if err != nil { return 0, fmt.Errorf("get db version: %w", err) } migrations, err := goose.CollectMigrations("sql", 0, goose.MaxVersion) if err != nil { return 0, fmt.Errorf("collect migrations: %w", err) } pending := 0 for _, m := range migrations { if m.Version > current { pending++ } } return pending, nil } // slogLogger adapts slog to goose's minimal logging interface. type slogLogger struct { logger *slog.Logger } func (l slogLogger) Printf(format string, v ...any) { l.logger.Info(fmt.Sprintf(format, v...)) } func (l slogLogger) Fatalf(format string, v ...any) { l.logger.Error(fmt.Sprintf(format, v...)) os.Exit(1) } func openDB(url string, logger *slog.Logger) (*sql.DB, error) { goose.SetBaseFS(FS) goose.SetLogger(slogLogger{logger: logger}) db, err := sql.Open("pgx", url) if err != nil { return nil, fmt.Errorf("open db: %w", err) } if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("ping db: %w", err) } return db, nil }