package pgdb import ( "context" "fmt" "os" "time" "github.com/jackc/pgx/v5/pgxpool" "go.ntppool.org/common/database" "gopkg.in/yaml.v3" ) // PoolOptions configures pgxpool connection behavior type PoolOptions struct { // ConfigFiles is a list of config file paths to search for database configuration ConfigFiles []string // MinConns is the minimum number of connections in the pool // Default: 0 (no minimum) MinConns int32 // MaxConns is the maximum number of connections in the pool // Default: 25 MaxConns int32 // MaxConnLifetime is the maximum lifetime of a connection // Default: 1 hour MaxConnLifetime time.Duration // MaxConnIdleTime is the maximum idle time of a connection // Default: 30 minutes MaxConnIdleTime time.Duration // HealthCheckPeriod is how often to check connection health // Default: 1 minute HealthCheckPeriod time.Duration } // DefaultPoolOptions returns sensible defaults for pgxpool func DefaultPoolOptions() PoolOptions { return PoolOptions{ ConfigFiles: getConfigFiles(), MinConns: 0, MaxConns: 25, MaxConnLifetime: time.Hour, MaxConnIdleTime: 30 * time.Minute, HealthCheckPeriod: time.Minute, } } // OpenPool opens a native pgx connection pool with the specified configuration // This is the primary and recommended way to connect to PostgreSQL func OpenPool(ctx context.Context, options PoolOptions) (*pgxpool.Pool, error) { // Find and read config file pgCfg, err := findAndParseConfig(options.ConfigFiles) if err != nil { return nil, err } // Create pool config from PostgreSQL config poolConfig, err := CreatePoolConfig(pgCfg) if err != nil { return nil, err } // Apply pool-specific settings poolConfig.MinConns = options.MinConns poolConfig.MaxConns = options.MaxConns poolConfig.MaxConnLifetime = options.MaxConnLifetime poolConfig.MaxConnIdleTime = options.MaxConnIdleTime poolConfig.HealthCheckPeriod = options.HealthCheckPeriod // Create the pool pool, err := pgxpool.NewWithConfig(ctx, poolConfig) if err != nil { return nil, fmt.Errorf("failed to create connection pool: %w", err) } // Test the connection if err := pool.Ping(ctx); err != nil { pool.Close() return nil, fmt.Errorf("failed to ping database: %w", err) } return pool, nil } // OpenPoolWithConfigFile opens a connection pool using an explicit config file path // This is a convenience function for when you have a specific config file func OpenPoolWithConfigFile(ctx context.Context, configFile string) (*pgxpool.Pool, error) { options := DefaultPoolOptions() options.ConfigFiles = []string{configFile} return OpenPool(ctx, options) } // findAndParseConfig searches for and parses the first existing config file func findAndParseConfig(configFiles []string) (*database.PostgresConfig, error) { var firstErr error for _, configFile := range configFiles { if configFile == "" { continue } // Check if file exists if _, err := os.Stat(configFile); err != nil { if firstErr == nil { firstErr = err } continue } // Try to read and parse the file pgCfg, err := parseConfigFile(configFile) if err != nil { if firstErr == nil { firstErr = err } continue } return pgCfg, nil } if firstErr != nil { return nil, fmt.Errorf("no config file found: %w", firstErr) } return nil, fmt.Errorf("no valid config files provided") } // parseConfigFile reads and parses a YAML config file func parseConfigFile(configFile string) (*database.PostgresConfig, error) { file, err := os.Open(configFile) if err != nil { return nil, fmt.Errorf("failed to open config file: %w", err) } defer file.Close() dec := yaml.NewDecoder(file) cfg := database.Config{} if err := dec.Decode(&cfg); err != nil { return nil, fmt.Errorf("failed to decode config: %w", err) } // Extract PostgreSQL config if cfg.Postgres != nil { return cfg.Postgres, nil } // Check for legacy flat format if cfg.User != "" && cfg.Name != "" { return &database.PostgresConfig{ User: cfg.User, Pass: cfg.Pass, Host: cfg.Host, Port: cfg.Port, Name: cfg.Name, SSLMode: cfg.SSLMode, }, nil } return nil, fmt.Errorf("no PostgreSQL configuration found in %s", configFile) } // getConfigFiles returns the list of config files to search func getConfigFiles() []string { if configFile := os.Getenv("DATABASE_CONFIG_FILE"); configFile != "" { return []string{configFile} } return []string{"database.yaml", "/vault/secrets/database.yaml"} }