// Package migrate configures Goose providers for embedded application migrations. // Applications own their database, migration files, and startup ordering. package migrate import ( "database/sql" "embed" "fmt" "io/fs" "log/slog" "github.com/pressly/goose/v3" "github.com/pressly/goose/v3/lock" ) // New returns a provider for SQL files in dir and optional provider-local Go // migrations. Pass "." for files at the embedded root and a nonnil logger. // Construction does not connect to db; the caller owns and closes db. // // PostgreSQL uses Goose's session advisory locker with its default lock ID and // retry limits. Other dialects require the caller to serialize migration runs // across providers/processes. PostgreSQL connections must preserve session // affinity (transaction-pooling proxies cannot safely hold session locks). // // Use the returned Provider's Up, UpTo, DownTo, Status, GetDBVersion, GetVersions, // and HasPending methods directly. Versions may be sequential or timestamps; // Goose orders and tracks them. Global Go migration registration is ignored. func New(db *sql.DB, dialect goose.Dialect, files embed.FS, dir string, logger *slog.Logger, migrations ...*goose.Migration) (*goose.Provider, error) { for i, migration := range migrations { if migration == nil { return nil, fmt.Errorf("migrate: Go migration %d must not be nil", i) } } root, err := fs.Sub(files, dir) if err != nil { return nil, fmt.Errorf("migrate: directory %q: %w", dir, err) } opts := []goose.ProviderOption{ goose.WithDisableGlobalRegistry(true), goose.WithGoMigrations(migrations...), goose.WithSlog(logger), goose.WithVerbose(true), } if dialect == goose.DialectPostgres { locker, err := lock.NewPostgresSessionLocker() if err != nil { return nil, fmt.Errorf("migrate: postgres locker: %w", err) } opts = append(opts, goose.WithSessionLocker(locker)) } provider, err := goose.NewProvider(dialect, db, root, opts...) if err != nil { return nil, fmt.Errorf("migrate: new provider: %w", err) } return provider, nil }