// 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. // The selected directory must exist, including when using only Go migrations. // 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. Create migration files with Goose's create // command; lexical filename order must match numeric version order. Go migrations // are registered per provider; global Go 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) } info, err := fs.Stat(root, ".") if err != nil { return nil, fmt.Errorf("migrate: directory %q: %w", dir, err) } if !info.IsDir() { return nil, fmt.Errorf("migrate: directory %q: not a directory", dir) } 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 }