// Package db opens Postgres and runs SQL migrations. package db import ( "context" "fmt" "os" "path/filepath" "sort" "strings" "github.com/jackc/pgx/v5/pgxpool" ) // Connect opens a pgx pool using DATABASE_URL-style DSN. func Connect(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) { pool, err := pgxpool.New(ctx, databaseURL) if err != nil { return nil, fmt.Errorf("pgxpool: %w", err) } if err := pool.Ping(ctx); err != nil { pool.Close() return nil, fmt.Errorf("ping: %w", err) } return pool, nil } // Migrate applies *.up.sql files under dir that are not yet recorded. func Migrate(ctx context.Context, pool *pgxpool.Pool, dir string) error { if _, err := pool.Exec(ctx, ` CREATE TABLE IF NOT EXISTS schema_migrations ( version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() )`); err != nil { return fmt.Errorf("schema_migrations: %w", err) } entries, err := os.ReadDir(dir) if err != nil { return fmt.Errorf("read migrations: %w", err) } var ups []string for _, e := range entries { name := e.Name() if strings.HasSuffix(name, ".up.sql") { ups = append(ups, name) } } sort.Strings(ups) for _, name := range ups { version := strings.TrimSuffix(name, ".up.sql") var exists bool if err := pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)`, version, ).Scan(&exists); err != nil { return err } if exists { continue } body, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { return err } // DDL (incl. CREATE EXTENSION) may not run inside a transaction. if _, err := pool.Exec(ctx, string(body)); err != nil { return fmt.Errorf("migrate %s: %w", name, err) } if _, err := pool.Exec(ctx, `INSERT INTO schema_migrations(version) VALUES ($1)`, version, ); err != nil { return err } } return nil }