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:
2025-09-27 16:55:54 -07:00
parent 4767caf7b8
commit 45308cd4bf
9 changed files with 832 additions and 43 deletions

View File

@@ -56,9 +56,9 @@ func TestMonitorConfigOptions(t *testing.T) {
}
func TestConfigStructures(t *testing.T) {
// Test that configuration structures can be created and populated
// Test that MySQL configuration structures can be created and populated
config := Config{
MySQL: DBConfig{
MySQL: &MySQLConfig{
DSN: "user:pass@tcp(localhost:3306)/dbname",
User: "testuser",
Pass: "testpass",
@@ -79,3 +79,118 @@ func TestConfigStructures(t *testing.T) {
t.Errorf("Expected DBName='testdb', got '%s'", config.MySQL.DBName)
}
}
func TestPostgresConfigStructures(t *testing.T) {
// Test that PostgreSQL configuration structures can be created and populated
config := Config{
Postgres: &PostgresConfig{
Host: "localhost",
Port: 5432,
User: "testuser",
Pass: "testpass",
Name: "testdb",
SSLMode: "require",
},
}
if config.Postgres.Host != "localhost" {
t.Errorf("Expected Host='localhost', got '%s'", config.Postgres.Host)
}
if config.Postgres.Port != 5432 {
t.Errorf("Expected Port=5432, got %d", config.Postgres.Port)
}
if config.Postgres.User != "testuser" {
t.Errorf("Expected User='testuser', got '%s'", config.Postgres.User)
}
if config.Postgres.Pass != "testpass" {
t.Errorf("Expected Pass='testpass', got '%s'", config.Postgres.Pass)
}
if config.Postgres.Name != "testdb" {
t.Errorf("Expected Name='testdb', got '%s'", config.Postgres.Name)
}
if config.Postgres.SSLMode != "require" {
t.Errorf("Expected SSLMode='require', got '%s'", config.Postgres.SSLMode)
}
}
func TestLegacyPostgresConfig(t *testing.T) {
// Test that legacy flat PostgreSQL format can be created
config := Config{
User: "testuser",
Pass: "testpass",
Host: "localhost",
Port: 5432,
Name: "testdb",
SSLMode: "require",
}
if config.User != "testuser" {
t.Errorf("Expected User='testuser', got '%s'", config.User)
}
if config.Name != "testdb" {
t.Errorf("Expected Name='testdb', got '%s'", config.Name)
}
}
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
config Config
wantErr bool
}{
{
name: "valid mysql config",
config: Config{
MySQL: &MySQLConfig{DSN: "test"},
},
wantErr: false,
},
{
name: "valid postgres config",
config: Config{
Postgres: &PostgresConfig{User: "test", Host: "localhost", Name: "test"},
},
wantErr: false,
},
{
name: "valid legacy postgres config",
config: Config{
User: "test",
Host: "localhost",
Name: "testdb",
},
wantErr: false,
},
{
name: "both mysql and postgres set",
config: Config{
MySQL: &MySQLConfig{DSN: "test"},
Postgres: &PostgresConfig{User: "test"},
},
wantErr: true,
},
{
name: "mysql and legacy postgres set",
config: Config{
MySQL: &MySQLConfig{DSN: "test"},
User: "test",
Name: "testdb",
},
wantErr: true,
},
{
name: "no config set",
config: Config{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}