// 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 { db, err := sql.Open("pgx", dsn) if err != nil { return fmt.Errorf("pgstore: open migration handle: %w", err) } defer db.Close() db.SetMaxOpenConns(1) locker, err := lock.NewPostgresSessionLocker() if err != nil { return fmt.Errorf("pgstore: locker: %w", err) } p, err := goose.NewProvider(goose.DialectPostgres, db, Migrations(), goose.WithSessionLocker(locker)) if err != nil { return fmt.Errorf("pgstore: goose provider: %w", err) } if _, err := p.Up(ctx); err != nil { return fmt.Errorf("pgstore: migrate: %w", err) } return nil }