package database import ( "context" "fmt" "log/slog" "os" "path/filepath" "sort" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" ) type DB struct { Pool *pgxpool.Pool } func Connect(ctx context.Context, databaseURL string) (*DB, error) { cfg, err := pgxpool.ParseConfig(databaseURL) if err != nil { return nil, fmt.Errorf("parse database url: %w", err) } cfg.MaxConns = 10 cfg.MinConns = 1 cfg.MaxConnLifetime = time.Hour cfg.MaxConnIdleTime = 30 * time.Minute pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, fmt.Errorf("create database pool: %w", err) } if err := pool.Ping(ctx); err != nil { pool.Close() return nil, fmt.Errorf("ping database: %w", err) } return &DB{Pool: pool}, nil } func (db *DB) Close() { if db != nil && db.Pool != nil { db.Pool.Close() } } func (db *DB) Migrate(ctx context.Context, logger *slog.Logger, migrationsDir string) error { files, err := filepath.Glob(filepath.Join(migrationsDir, "*.sql")) if err != nil { return fmt.Errorf("list migrations: %w", err) } sort.Strings(files) for _, file := range files { sql, err := os.ReadFile(file) if err != nil { return fmt.Errorf("read migration %s: %w", file, err) } statement := strings.TrimSpace(string(sql)) if statement == "" { continue } if _, err := db.Pool.Exec(ctx, statement); err != nil { return fmt.Errorf("run migration %s: %w", file, err) } logger.Info("migration applied", "file", filepath.Base(file)) } return nil }