Add PostgreSQL support to database package alongside existing MySQL support. Both databases share common infrastructure (pool management, metrics, transactions) while using database-specific connectors. database/ changes: - Add PostgresConfig struct and PostgreSQL connector using pgx/stdlib - Change MySQL config from DBConfig to *MySQLConfig (pointer) - Add Config.Validate() to prevent multiple database configs - Add PostgreSQL connector with secure config building (no password in DSN) - Add field validation and secure defaults (SSLMode="prefer") - Support legacy flat PostgreSQL config format for backward compatibility - Add tests for PostgreSQL configs and validation New database/pgdb/ package: - Native pgx connection pool support (*pgxpool.Pool) - OpenPool() and OpenPoolWithConfigFile() APIs - CreatePoolConfig() for secure config conversion - PoolOptions for fine-grained pool control - Full test coverage and documentation Security: - Passwords never exposed in DSN strings - Set passwords separately in pgx config objects - Validate all configuration before connection Architecture: - Shared code in database/ for both MySQL and PostgreSQL (sql.DB) - database/pgdb/ for PostgreSQL-specific native pool support
188 lines
5.0 KiB
Go
188 lines
5.0 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/stdlib"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// from https://github.com/Boostport/dynamic-database-config
|
|
|
|
// CreateConnectorFunc is a function that creates a database connector
|
|
type CreateConnectorFunc func() (driver.Connector, error)
|
|
|
|
// Driver implements the sql/driver interface with dynamic configuration
|
|
type Driver struct {
|
|
CreateConnectorFunc CreateConnectorFunc
|
|
}
|
|
|
|
// Driver returns the driver instance
|
|
func (d Driver) Driver() driver.Driver {
|
|
return d
|
|
}
|
|
|
|
// Connect creates a new database connection using the dynamic connector
|
|
func (d Driver) Connect(ctx context.Context) (driver.Conn, error) {
|
|
connector, err := d.CreateConnectorFunc()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating connector from function: %w", err)
|
|
}
|
|
|
|
return connector.Connect(ctx)
|
|
}
|
|
|
|
// Open is not supported for dynamic configuration
|
|
func (d Driver) Open(name string) (driver.Conn, error) {
|
|
return nil, errors.New("open is not supported")
|
|
}
|
|
|
|
// createConnector creates a connector function that reads configuration from a file
|
|
func createConnector(configFile string) CreateConnectorFunc {
|
|
return func() (driver.Connector, error) {
|
|
dbFile, err := os.Open(configFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer dbFile.Close()
|
|
|
|
dec := yaml.NewDecoder(dbFile)
|
|
cfg := Config{}
|
|
|
|
err = dec.Decode(&cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate configuration
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
|
}
|
|
|
|
// Determine database type and create appropriate connector
|
|
if cfg.MySQL != nil {
|
|
return createMySQLConnector(cfg.MySQL)
|
|
} else if cfg.Postgres != nil {
|
|
return createPostgresConnector(cfg.Postgres)
|
|
} else if cfg.User != "" && cfg.Name != "" {
|
|
// Legacy flat PostgreSQL format (requires at minimum user and dbname)
|
|
return createPostgresConnectorFromFlat(&cfg)
|
|
}
|
|
|
|
return nil, fmt.Errorf("no valid database configuration found (mysql or postgres section required)")
|
|
}
|
|
}
|
|
|
|
// createMySQLConnector creates a MySQL connector from configuration
|
|
func createMySQLConnector(cfg *MySQLConfig) (driver.Connector, error) {
|
|
dsn := cfg.DSN
|
|
if len(dsn) == 0 {
|
|
dsn = os.Getenv("DATABASE_DSN")
|
|
if len(dsn) == 0 {
|
|
return nil, fmt.Errorf("dsn config in database.yaml or DATABASE_DSN environment variable required")
|
|
}
|
|
}
|
|
|
|
dbcfg, err := mysql.ParseDSN(dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user := cfg.User; len(user) > 0 {
|
|
dbcfg.User = user
|
|
}
|
|
|
|
if pass := cfg.Pass; len(pass) > 0 {
|
|
dbcfg.Passwd = pass
|
|
}
|
|
|
|
if name := cfg.DBName; len(name) > 0 {
|
|
dbcfg.DBName = name
|
|
}
|
|
|
|
return mysql.NewConnector(dbcfg)
|
|
}
|
|
|
|
// createPostgresConnector creates a PostgreSQL connector from configuration
|
|
func createPostgresConnector(cfg *PostgresConfig) (driver.Connector, error) {
|
|
// Validate required fields
|
|
if cfg.Host == "" {
|
|
return nil, fmt.Errorf("postgres: host is required")
|
|
}
|
|
if cfg.User == "" {
|
|
return nil, fmt.Errorf("postgres: user is required")
|
|
}
|
|
if cfg.Name == "" {
|
|
return nil, fmt.Errorf("postgres: database name is required")
|
|
}
|
|
|
|
// Validate SSLMode
|
|
validSSLModes := map[string]bool{
|
|
"disable": true, "allow": true, "prefer": true,
|
|
"require": true, "verify-ca": true, "verify-full": true,
|
|
}
|
|
if cfg.SSLMode != "" && !validSSLModes[cfg.SSLMode] {
|
|
return nil, fmt.Errorf("postgres: invalid sslmode: %s", cfg.SSLMode)
|
|
}
|
|
|
|
// Build config directly (security: no DSN string with password)
|
|
connConfig, err := pgx.ParseConfig("")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("postgres: failed to create pgx config: %w", err)
|
|
}
|
|
|
|
connConfig.Host = cfg.Host
|
|
connConfig.Port = cfg.Port
|
|
connConfig.User = cfg.User
|
|
connConfig.Password = cfg.Pass
|
|
connConfig.Database = cfg.Name
|
|
|
|
// Map SSLMode to pgx configuration
|
|
// Note: pgx uses different SSL handling than libpq
|
|
// For now, we'll construct a minimal DSN with sslmode for ParseConfig
|
|
if cfg.SSLMode != "" {
|
|
// Reconstruct with sslmode only (no password in DSN)
|
|
dsnWithoutPassword := fmt.Sprintf("host=%s port=%d user=%s dbname=%s sslmode=%s",
|
|
cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.SSLMode)
|
|
connConfig, err = pgx.ParseConfig(dsnWithoutPassword)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("postgres: failed to parse config with sslmode: %w", err)
|
|
}
|
|
// Set password separately after parsing
|
|
connConfig.Password = cfg.Pass
|
|
}
|
|
|
|
return stdlib.GetConnector(*connConfig), nil
|
|
}
|
|
|
|
// createPostgresConnectorFromFlat creates a PostgreSQL connector from flat config format
|
|
func createPostgresConnectorFromFlat(cfg *Config) (driver.Connector, error) {
|
|
pgCfg := &PostgresConfig{
|
|
User: cfg.User,
|
|
Pass: cfg.Pass,
|
|
Host: cfg.Host,
|
|
Port: cfg.Port,
|
|
Name: cfg.Name,
|
|
SSLMode: cfg.SSLMode,
|
|
}
|
|
|
|
// Set defaults for PostgreSQL
|
|
if pgCfg.Host == "" {
|
|
pgCfg.Host = "localhost"
|
|
}
|
|
if pgCfg.Port == 0 {
|
|
pgCfg.Port = 5432
|
|
}
|
|
if pgCfg.SSLMode == "" {
|
|
pgCfg.SSLMode = "prefer"
|
|
}
|
|
|
|
return createPostgresConnector(pgCfg)
|
|
}
|