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