package migrate import ( "context" "errors" "fmt" "log/slog" "time" "github.com/pressly/goose/v3" ) // ApplyPendingMigrations applies pending migrations and returns the number successfully applied. // A zero pendingCount is a no-op, a negative value applies all pending migrations, // and a positive value caps successful applications. Exhausting pending migrations // is successful even when fewer than the requested cap remain. Versions are identifiers, // not counts, and may contain gaps or timestamps. func ApplyPendingMigrations(ctx context.Context, pendingCount int64) (int64, error) { migrationsModule.RequireLoaded() p, err := Provider() if err != nil { return 0, err } count, _, err := applyPendingMigrations(ctx, p, migrationsModule.Logger(), pendingCount) return count, err } type migrationStepper interface { UpByOne(context.Context) (*goose.MigrationResult, error) } // The last successful version is used for summaries; it cannot be inferred from count. func applyPendingMigrations(ctx context.Context, p migrationStepper, logger *slog.Logger, pendingCount int64) (count, lastVersion int64, err error) { for pendingCount < 0 || count < pendingCount { res, err := p.UpByOne(ctx) // A migration itself can fail with ErrNoNextVersion. Goose reports that // through PartialError, which must not be mistaken for normal exhaustion. var partial *goose.PartialError if res == nil && errors.Is(err, goose.ErrNoNextVersion) && !errors.As(err, &partial) { return count, lastVersion, nil } if err := handleMigrationResults(logger, res, err); err != nil { return count, lastVersion, err } if res == nil { return count, lastVersion, nil } count++ lastVersion = res.Source.Version } return count, lastVersion, 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, lastVersion, err := applyPendingMigrations(ctx, p, migrationsModule.Logger(), -1) if count > 0 { current = lastVersion } if err != nil { migrationsModule.Logger().Error("Couldn't apply pending migrations", "current", current, "target", target, "appliedCount", count, "err", err) return err } migrationsModule.Logger().Info("Applied pending migrations", "current", current, "appliedCount", count, "duration", time.Since(now)) return nil } func handleMigrationResults(logger *slog.Logger, res *goose.MigrationResult, err error) error { if err != nil { var partial *goose.PartialError if errors.As(err, &partial) && partial.Failed != nil { _ = handleMigrationResults(logger, partial.Failed, 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 }