// 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) }