textmachine/platform/internal/pgstore/migrate.go

66 lines
2.1 KiB
Go

// Package pgstore is the platform's Postgres access: migrations, the pool, and the queries the
// HTTP layer needs. It is the only package that speaks SQL.
package pgstore
import (
"context"
"database/sql"
"embed"
"fmt"
"io/fs"
_ "github.com/jackc/pgx/v5/stdlib" // database/sql driver "pgx", used by goose only
"github.com/pressly/goose/v3"
"github.com/pressly/goose/v3/lock"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// Migrations exposes the embedded SQL for tests that check the set without a database.
func Migrations() fs.FS {
sub, err := fs.Sub(migrationsFS, "migrations")
if err != nil {
panic(err) // the path is a compile-time constant of this package
}
return sub
}
// Migrate applies every pending migration and returns when the schema is current.
//
// It opens its OWN database/sql handle rather than borrowing the pgx pool: goose speaks
// database/sql, and a one-connection handle is what the session locker needs anyway. The lock is a
// Postgres advisory lock, so two instances rolling out at once serialize instead of racing.
func Migrate(ctx context.Context, dsn string) error {
p, closeDB, err := newProvider(dsn)
if err != nil {
return err
}
defer closeDB()
if _, err := p.Up(ctx); err != nil {
return fmt.Errorf("pgstore: migrate: %w", err)
}
return nil
}
// newProvider builds the goose provider. Shared with the down-path test so that the rollback the
// test proves is the rollback the deployment would run, not a second implementation of it.
func newProvider(dsn string) (*goose.Provider, func(), error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, nil, fmt.Errorf("pgstore: open migration handle: %w", err)
}
db.SetMaxOpenConns(1)
locker, err := lock.NewPostgresSessionLocker()
if err != nil {
db.Close()
return nil, nil, fmt.Errorf("pgstore: locker: %w", err)
}
p, err := goose.NewProvider(goose.DialectPostgres, db, Migrations(), goose.WithSessionLocker(locker))
if err != nil {
db.Close()
return nil, nil, fmt.Errorf("pgstore: goose provider: %w", err)
}
return p, func() { db.Close() }, nil
}