feat(database): add PostgreSQL support with native pgx pool
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
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -58,31 +60,128 @@ func createConnector(configFile string) CreateConnectorFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsn := cfg.MySQL.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")
|
||||
}
|
||||
// Validate configuration
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
dbcfg, err := mysql.ParseDSN(dsn)
|
||||
if err != nil {
|
||||
return nil, 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)
|
||||
}
|
||||
|
||||
if user := cfg.MySQL.User; len(user) > 0 {
|
||||
dbcfg.User = user
|
||||
}
|
||||
|
||||
if pass := cfg.MySQL.Pass; len(pass) > 0 {
|
||||
dbcfg.Passwd = pass
|
||||
}
|
||||
|
||||
if name := cfg.MySQL.DBName; len(name) > 0 {
|
||||
dbcfg.DBName = name
|
||||
}
|
||||
|
||||
return mysql.NewConnector(dbcfg)
|
||||
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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user