91 lines
3.1 KiB
Go
91 lines
3.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/pgxpool"
|
|
_ "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"
|
|
"github.com/riverqueue/river/riverdriver/riverpgxv5"
|
|
"github.com/riverqueue/river/rivermigrate"
|
|
)
|
|
|
|
//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 migrateQueue(ctx, dsn)
|
|
}
|
|
|
|
// migrateQueue applies River's own migrations.
|
|
//
|
|
// They are not goose migrations and must not be copied into the manifest: River owns that SQL, it
|
|
// versions it independently, and a hand-copied snapshot of someone else's schema is a snapshot that
|
|
// silently stops matching the library reading it. Two ledgers in one database is the honest form —
|
|
// the append-only rule of this zone applies to the migrations THIS zone writes.
|
|
func migrateQueue(ctx context.Context, dsn string) error {
|
|
pool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
return fmt.Errorf("pgstore: queue migration pool: %w", err)
|
|
}
|
|
defer pool.Close()
|
|
m, err := rivermigrate.New(riverpgxv5.New(pool), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("pgstore: queue migrator: %w", err)
|
|
}
|
|
if _, err := m.Migrate(ctx, rivermigrate.DirectionUp, nil); err != nil {
|
|
return fmt.Errorf("pgstore: migrate queue: %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
|
|
}
|