You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
4.6 KiB
Go
173 lines
4.6 KiB
Go
// Package postgres provides PostgreSQL database client for the KnowFoolery application.
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"knowfoolery/backend/shared/infra/utils/envutil"
|
|
)
|
|
|
|
// Config holds the configuration for the PostgreSQL client.
|
|
type Config struct {
|
|
Host string
|
|
Port int
|
|
User string
|
|
Password string
|
|
Database string
|
|
SSLMode string
|
|
MaxOpenConns int
|
|
MaxIdleConns int
|
|
ConnMaxLifetime time.Duration
|
|
ConnMaxIdleTime time.Duration
|
|
}
|
|
|
|
// DefaultConfig returns a default configuration for development.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
Host: "localhost",
|
|
Port: 5432,
|
|
User: "postgres",
|
|
Password: "postgres",
|
|
Database: "knowfoolery",
|
|
SSLMode: "disable",
|
|
MaxOpenConns: 25,
|
|
MaxIdleConns: 10,
|
|
ConnMaxLifetime: 5 * time.Minute,
|
|
ConnMaxIdleTime: 1 * time.Minute,
|
|
}
|
|
}
|
|
|
|
// DSN returns the PostgreSQL connection string.
|
|
func (c Config) DSN() string {
|
|
return fmt.Sprintf(
|
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
|
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode,
|
|
)
|
|
}
|
|
|
|
// URL returns the PostgreSQL connection URL.
|
|
func (c Config) URL() string {
|
|
return fmt.Sprintf(
|
|
"postgresql://%s:%s@%s:%d/%s?sslmode=%s",
|
|
c.User, c.Password, c.Host, c.Port, c.Database, c.SSLMode,
|
|
)
|
|
}
|
|
|
|
// HealthCheck performs a stateless health check on PostgreSQL.
|
|
// Services should initialize and own Ent clients directly. This helper is
|
|
// intentionally lightweight and does not expose a long-lived shared DB client.
|
|
func HealthCheck(ctx context.Context, config Config) error {
|
|
if err := validateConfig(config); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
poolConfig, err := pgxpool.ParseConfig(config.URL())
|
|
if err != nil {
|
|
return fmt.Errorf("parse postgres config: %w", err)
|
|
}
|
|
poolConfig.MaxConns = 1
|
|
poolConfig.MinConns = 0
|
|
poolConfig.MaxConnLifetime = config.ConnMaxLifetime
|
|
poolConfig.MaxConnIdleTime = config.ConnMaxIdleTime
|
|
|
|
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("create postgres pool: %w", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
if err := pool.Ping(ctx); err != nil {
|
|
return fmt.Errorf("ping postgres: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConfigFromEnv creates a Config from environment variables with safe fallbacks.
|
|
func ConfigFromEnv() Config {
|
|
cfg := DefaultConfig()
|
|
|
|
cfg.Host = envutil.String("POSTGRES_HOST", cfg.Host)
|
|
cfg.Port = envutil.Int("POSTGRES_PORT", cfg.Port)
|
|
cfg.User = envutil.String("POSTGRES_USER", cfg.User)
|
|
cfg.Password = envutil.String("POSTGRES_PASSWORD", cfg.Password)
|
|
cfg.Database = envutil.String("POSTGRES_DB", cfg.Database)
|
|
cfg.SSLMode = envutil.String("POSTGRES_SSLMODE", cfg.SSLMode)
|
|
|
|
cfg.MaxOpenConns = envutil.Int("POSTGRES_MAX_OPEN_CONNS", cfg.MaxOpenConns)
|
|
cfg.MaxIdleConns = envutil.Int("POSTGRES_MAX_IDLE_CONNS", cfg.MaxIdleConns)
|
|
cfg.ConnMaxLifetime = envutil.Duration("POSTGRES_CONN_MAX_LIFETIME", cfg.ConnMaxLifetime)
|
|
cfg.ConnMaxIdleTime = envutil.Duration("POSTGRES_CONN_MAX_IDLE_TIME", cfg.ConnMaxIdleTime)
|
|
|
|
return cfg
|
|
}
|
|
|
|
func validateConfig(c Config) error {
|
|
switch {
|
|
case c.Host == "":
|
|
return errors.New("postgres host is required")
|
|
case c.Port <= 0:
|
|
return errors.New("postgres port must be greater than 0")
|
|
case c.User == "":
|
|
return errors.New("postgres user is required")
|
|
case c.Database == "":
|
|
return errors.New("postgres database is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Client wraps a pgx connection pool for service repositories.
|
|
type Client struct {
|
|
Pool *pgxpool.Pool
|
|
}
|
|
|
|
// NewClient creates a pooled postgres client and verifies connectivity.
|
|
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
|
|
poolCfg, err := pgxpool.ParseConfig(cfg.URL())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse postgres config: %w", err)
|
|
}
|
|
poolCfg.MaxConns = clampIntToInt32(cfg.MaxOpenConns)
|
|
poolCfg.MinConns = clampIntToInt32(cfg.MaxIdleConns)
|
|
poolCfg.MaxConnLifetime = cfg.ConnMaxLifetime
|
|
poolCfg.MaxConnIdleTime = cfg.ConnMaxIdleTime
|
|
|
|
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create postgres pool: %w", err)
|
|
}
|
|
|
|
if err := pool.Ping(ctx); err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("ping postgres: %w", err)
|
|
}
|
|
|
|
return &Client{Pool: pool}, nil
|
|
}
|
|
|
|
// Close closes the underlying pool.
|
|
func (c *Client) Close() {
|
|
if c != nil && c.Pool != nil {
|
|
c.Pool.Close()
|
|
}
|
|
}
|
|
|
|
func clampIntToInt32(v int) int32 {
|
|
const maxInt32 = int(^uint32(0) >> 1)
|
|
if v <= 0 {
|
|
return 0
|
|
}
|
|
if v > maxInt32 {
|
|
return int32(maxInt32)
|
|
}
|
|
return int32(v)
|
|
}
|