package db import ( "context" "embed" "fmt" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jackc/pgx/v5/pgxpool" ) //go:embed migrations/*.sql var migrationsFS embed.FS func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error) { pool, err := pgxpool.New(ctx, dsn) if err != nil { return nil, fmt.Errorf("create pool: %w", err) } if err := pool.Ping(ctx); err != nil { return nil, fmt.Errorf("ping db: %w", err) } return pool, nil } func Migrate(dsn string) error { src, err := iofs.New(migrationsFS, "migrations") if err != nil { return fmt.Errorf("load migrations: %w", err) } // convert pgx DSN → lib/pq DSN format for migrate m, err := migrate.NewWithSourceInstance("iofs", src, "postgres://"+dsnToURL(dsn)) if err != nil { return fmt.Errorf("init migrate: %w", err) } if err := m.Up(); err != nil && err != migrate.ErrNoChange { return fmt.Errorf("run migrations: %w", err) } return nil } // dsnToURL converts "host=... port=... dbname=... user=... password=... sslmode=..." // to a postgres:// URL for golang-migrate. func dsnToURL(dsn string) string { // pgxpool DSN is already accepted by migrate if we prefix with postgres:// // Actually golang-migrate accepts the postgres:// URL format. // Since our DSN is key=value, we need to convert it. // Simplest approach: parse key=value pairs. params := map[string]string{} for _, part := range splitDSN(dsn) { if len(part) == 0 { continue } idx := indexByte(part, '=') if idx < 0 { continue } params[part[:idx]] = part[idx+1:] } host := params["host"] port := params["port"] if port == "" { port = "5432" } return fmt.Sprintf("%s:%s@%s:%s/%s?sslmode=%s", params["user"], params["password"], host, port, params["dbname"], orDefault(params["sslmode"], "disable"), ) } func splitDSN(dsn string) []string { var parts []string cur := "" for _, r := range dsn { if r == ' ' { if cur != "" { parts = append(parts, cur) cur = "" } } else { cur += string(r) } } if cur != "" { parts = append(parts, cur) } return parts } func indexByte(s string, b byte) int { for i := 0; i < len(s); i++ { if s[i] == b { return i } } return -1 } func orDefault(s, def string) string { if s == "" { return def } return s }