package db import ( "context" "embed" "fmt" "log" "os" "strconv" "time" "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" "goyomi/internal/db/queries" ) //go:embed migrations/*.sql var migrationFS embed.FS // DB holds the connection pool and sqlc query clients. type DB struct { Pool *pgxpool.Pool Queries *queries.Queries } func Open(ctx context.Context) (*DB, error) { dsn := os.Getenv("DATABASE_URL") if dsn == "" { return nil, fmt.Errorf("DATABASE_URL not set") } cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, fmt.Errorf("parse DATABASE_URL: %w", err) } cfg.MaxConns = int32(envInt("DB_MAX_CONNS", 10)) cfg.MinConns = int32(envInt("DB_MIN_CONNS", 2)) cfg.HealthCheckPeriod = 30 * time.Second pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, fmt.Errorf("open pool: %w", err) } if err := pool.Ping(ctx); err != nil { pool.Close() return nil, fmt.Errorf("ping db: %w", err) } if err := runMigrations(dsn); err != nil { pool.Close() return nil, err } return &DB{ Pool: pool, Queries: queries.New(pool), }, nil } func (d *DB) Close() { d.Pool.Close() } func runMigrations(dsn string) error { src, err := iofs.New(migrationFS, "migrations") if err != nil { return fmt.Errorf("migration source: %w", err) } // golang-migrate pgx/v5 driver expects pgx5:// scheme driverDSN := dsnToMigrateScheme(dsn) m, err := migrate.NewWithSourceInstance("iofs", src, driverDSN) if err != nil { return fmt.Errorf("migrate init: %w", err) } defer m.Close() before, _, _ := m.Version() if err := m.Up(); err != nil && err != migrate.ErrNoChange { return fmt.Errorf("migrate up: %w", err) } after, _, _ := m.Version() if before != after { log.Printf("db: migrated from version %d to %d", before, after) } else { log.Printf("db: schema up to date at version %d", after) } return nil } // dsnToMigrateScheme converts a postgres:// or postgresql:// DSN to pgx5:// // as required by the golang-migrate pgx/v5 driver. func dsnToMigrateScheme(dsn string) string { for _, prefix := range []string{"postgresql://", "postgres://"} { if len(dsn) >= len(prefix) && dsn[:len(prefix)] == prefix { return "pgx5://" + dsn[len(prefix):] } } return dsn } func envInt(key string, def int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def }