// 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 }