package migrate import ( "context" "fmt" "log/slog" "time" "github.com/pressly/goose/v3" ) // ApplyPendingMigrations applies pending migrations. pendingCount: 0 none, <0 auto-detect, >0 cap. func ApplyPendingMigrations(ctx context.Context, pendingCount int64) (int64, error) { migrationsModule.RequireLoaded() p, err := Provider() if err != nil { return 0, err } if pendingCount == 0 { return 0, nil } if pendingCount < 0 { curr, target, err := p.GetVersions(ctx) if err != nil { return 0, err } pendingCount = target - curr } var count int64 for range pendingCount { res, err := p.UpByOne(ctx) if err := handleMigrationResults(migrationsModule.Logger(), res, err); err != nil { return count, err } if res != nil && res.Error == nil { count++ } } return count, nil } // MigrateToBlank rolls back all applied migrations. func MigrateToBlank(ctx context.Context) (int64, error) { migrationsModule.RequireLoaded() p, err := Provider() if err != nil { return 0, err } current, target, err := p.GetVersions(ctx) if err != nil { return 0, err } moduleMigrateBlank.Logger().Info("Database versions", "current", current, "target", target) if current == 0 { return 0, nil } results, err := p.DownTo(ctx, 0) if err != nil { return 0, err } for _, res := range results { if err := handleMigrationResults(moduleMigrateBlank.Logger(), res, nil); err != nil { return int64(len(results)), err } } return int64(len(results)), nil } // AutoMigrate applies pending migrations when auto-migration is enabled. func AutoMigrate(ctx context.Context) error { migrationsModule.RequireLoaded() p, err := Provider() if err != nil { return err } current, target, err := p.GetVersions(ctx) if err != nil { migrationsModule.Logger().Error("Couldn't check for pending migrations", "err", err) return err } fields := []any{"current", current, "target", target} if current >= target { migrationsModule.Logger().Info("No pending migrations", "version", current) return nil } if !autoMigrateEnabled { migrationsModule.Logger().Error( "Pending migrations detected, but auto-migration is disabled. Run `migrate up` to apply them.", fields..., ) return ErrPendingMigrations } migrationsModule.Logger().Info("Pending migrations detected, applying them...", fields...) now := time.Now() count, err := ApplyPendingMigrations(ctx, target-current) if err != nil { migrationsModule.Logger().Error("Couldn't apply pending migrations", "current", current+count, "target", target, "err", err) return err } migrationsModule.Logger().Info("Applied pending migrations", "current", target, "appliedCount", count, "duration", time.Since(now)) return nil } func handleMigrationResults(logger *slog.Logger, res *goose.MigrationResult, err error) error { if err != nil { return err } if res == nil { return nil } fields := []any{ "dir", res.Direction, "version", res.Source.Version, "source", res.Source.Path, "duration", res.Duration, } if res.Error != nil { fields = append(fields, "err", res.Error) logger.Error("Couldn't apply migration", fields...) return res.Error } if res.Empty { logger.Warn("Applied empty migration", fields...) } else { logger.Info("Applied migration", fields...) } return nil } // PrintMigrationStatus prints migration status to stdout via the provider. func PrintMigrationStatus(ctx context.Context) error { p, err := Provider() if err != nil { return err } statuses, err := p.Status(ctx) if err != nil { return fmt.Errorf("migration status: %w", err) } for _, s := range statuses { fmt.Printf("%s\t%s\n", s.State, s.Source.Path) } return nil }