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