114 lines
2.5 KiB
Go
Executable File
114 lines
2.5 KiB
Go
Executable File
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
|
|
}
|