M1/#291: wire pgxpool + golang-migrate runner with embedded migrations
This commit is contained in:
+77
-20
@@ -1,33 +1,90 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// MigrationsFS exposes the embedded migration files. The concrete migration
|
||||
// runner (goose) will be wired in M2 when Postgres work begins; this package
|
||||
// intentionally keeps the embed + a placeholder surface so the file layout
|
||||
// and `go:embed` directive are established from day one.
|
||||
func MigrationsFS() embed.FS {
|
||||
return migrationsFS
|
||||
}
|
||||
|
||||
// ErrNotConfigured is returned by Migrate until the real runner is wired.
|
||||
var ErrNotConfigured = errors.New("db: migrations not yet configured (wired in M2)")
|
||||
|
||||
// Migrate is a placeholder. It validates that the embedded migrations load
|
||||
// and then returns ErrNotConfigured so callers can detect the stub.
|
||||
func Migrate() error {
|
||||
entries, err := migrationsFS.ReadDir("migrations")
|
||||
// MigrationsFS exposes the embedded migration set for callers that need
|
||||
// raw access (e.g. tests that construct their own migration runner).
|
||||
func MigrationsFS() fs.FS {
|
||||
sub, err := fs.Sub(migrationsFS, "migrations")
|
||||
if err != nil {
|
||||
return err
|
||||
panic(fmt.Sprintf("db: sub fs: %v", err))
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return errors.New("db: no migration files found")
|
||||
}
|
||||
return ErrNotConfigured
|
||||
return sub
|
||||
}
|
||||
|
||||
// Open establishes a pgx connection pool. The caller owns the returned pool
|
||||
// and must Close() it on shutdown.
|
||||
func Open(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
|
||||
if dsn == "" {
|
||||
return nil, errors.New("db: database URL is empty")
|
||||
}
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db: create pool: %w", err)
|
||||
}
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("db: ping: %w", err)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// Migrate applies any pending migrations embedded in this package. It is safe
|
||||
// to call concurrently with application traffic only if the database is idle;
|
||||
// in practice this runs once at startup before the HTTP server binds.
|
||||
func Migrate(dsn string, logger *slog.Logger) error {
|
||||
if dsn == "" {
|
||||
return errors.New("db: database URL is empty")
|
||||
}
|
||||
|
||||
src, err := iofs.New(migrationsFS, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("db: iofs source: %w", err)
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithSourceInstance("iofs", src, "pgx5://"+trimScheme(dsn))
|
||||
if err != nil {
|
||||
return fmt.Errorf("db: migrate init: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if srcErr, dbErr := m.Close(); srcErr != nil || dbErr != nil {
|
||||
logger.Warn("migrate close", "src_err", srcErr, "db_err", dbErr)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("db: migrate up: %w", err)
|
||||
}
|
||||
version, dirty, verr := m.Version()
|
||||
if verr != nil && !errors.Is(verr, migrate.ErrNilVersion) {
|
||||
return fmt.Errorf("db: migrate version: %w", verr)
|
||||
}
|
||||
logger.Info("migrations applied", "version", version, "dirty", dirty)
|
||||
return nil
|
||||
}
|
||||
|
||||
// trimScheme rewrites a postgres://... or postgresql://... DSN to the bare
|
||||
// form expected by the migrate pgx driver (it registers its own scheme).
|
||||
func trimScheme(dsn string) string {
|
||||
for _, prefix := range []string{"postgres://", "postgresql://"} {
|
||||
if len(dsn) > len(prefix) && dsn[:len(prefix)] == prefix {
|
||||
return dsn[len(prefix):]
|
||||
}
|
||||
}
|
||||
return dsn
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user