From 0e6d58ff238223555fb4343ca91833b16d9f55bf Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 28 May 2026 16:25:42 -0400 Subject: [PATCH 01/13] fix(ers): lazily register database/sql driver in SQL ERS provider Previously the SQL provider required consumers to add a blank driver import (e.g. _ "github.com/lib/pq") to their own binaries, causing the driver to be registered globally at init time even when the SQL ERS tier was not configured. This change moves driver registration into NewProvider() using pgx/v5/stdlib (already a direct dependency of this module), guarded by a sync.Mutex and a sql.Drivers() pre-check to prevent duplicate-register panics when consumers have already registered the driver themselves. The registration is now lazy: it happens only when a SQL ERS provider is actually instantiated, and only if the driver has not already been registered by the consumer or another code path. Fixes: https://github.com/opentdf/platform/issues/3539 --- .../providers/sql/sql_provider.go | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 47f4328edc..86425a21cb 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -5,15 +5,52 @@ import ( "database/sql" "fmt" "strings" + "sync" - // Database drivers would be imported here: - // _ "github.com/lib/pq" // PostgreSQL driver - // _ "github.com/go-sql-driver/mysql" // MySQL driver - // _ "github.com/mattn/go-sqlite3" // SQLite driver - + "github.com/jackc/pgx/v5/stdlib" "github.com/opentdf/platform/service/entityresolution/multi-strategy/types" ) +var ( + // driverRegMu guards lazy driver registration to prevent duplicate-register panics. + driverRegMu sync.Mutex + registeredDrivers = make(map[string]struct{}) +) + +// ensureDriverRegistered lazily registers the named database/sql driver the first +// time a SQL provider for that driver is created. This avoids the need for +// consumers to add blank driver imports to their own binaries. +// +// Uses pgx/v5/stdlib for postgres (already a platform dependency). Other drivers +// (mysql, sqlite) are not currently auto-registered and must be imported by the +// consumer. Consumers that have already registered the driver themselves are +// handled gracefully via a sql.Drivers() pre-check. +func ensureDriverRegistered(driver string) { + driverRegMu.Lock() + defer driverRegMu.Unlock() + + if _, ok := registeredDrivers[driver]; ok { + return + } + + // Check whether the driver was already registered externally (e.g. via a + // blank import in the consumer binary) before attempting to register it. + for _, d := range sql.Drivers() { + if d == driver { + registeredDrivers[driver] = struct{}{} + return + } + } + + switch strings.ToLower(driver) { + case "postgres": + sql.Register("postgres", stdlib.GetDefaultDriver()) + registeredDrivers[driver] = struct{}{} + } + // mysql and sqlite require imports not present in this module's dependencies. + // Add cases here when those drivers are added to go.mod. +} + // Provider implements the Provider interface for SQL databases type Provider struct { name string @@ -24,6 +61,10 @@ type Provider struct { // NewProvider creates a new SQL provider func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { + // Register the database/sql driver for this provider's configured driver name + // if it has not already been registered. + ensureDriverRegistered(config.Driver) + provider := &Provider{ name: name, config: config, From b0df6b992f3ba22d28c01d69f15b0ec286ed6fbf Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 28 May 2026 17:49:06 -0400 Subject: [PATCH 02/13] fix(ers): normalize driver name to lowercase before registration checks Both the registeredDrivers map lookup and the sql.Drivers() pre-check were case-sensitive, while the switch used strings.ToLower. A consumer passing driver: Postgres (capital P) would bypass the pre-check and trigger a duplicate sql.Register panic. Normalize to strings.ToLower(strings.TrimSpace(driver)) at the top of ensureDriverRegistered and use strings.EqualFold for the sql.Drivers() comparison so all case variants map to a single canonical key. Addresses review feedback from gemini-code-assist and coderabbitai. --- .../multi-strategy/providers/sql/sql_provider.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 86425a21cb..dbba2252b8 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -26,6 +26,10 @@ var ( // consumer. Consumers that have already registered the driver themselves are // handled gracefully via a sql.Drivers() pre-check. func ensureDriverRegistered(driver string) { + // Normalize to lowercase so "Postgres", "POSTGRES", and "postgres" all resolve + // to the same registered driver name. sql.Register is case-sensitive. + driver = strings.ToLower(strings.TrimSpace(driver)) + driverRegMu.Lock() defer driverRegMu.Unlock() @@ -35,14 +39,15 @@ func ensureDriverRegistered(driver string) { // Check whether the driver was already registered externally (e.g. via a // blank import in the consumer binary) before attempting to register it. + // Use strings.EqualFold so the pre-check is also case-insensitive. for _, d := range sql.Drivers() { - if d == driver { + if strings.EqualFold(d, driver) { registeredDrivers[driver] = struct{}{} return } } - switch strings.ToLower(driver) { + switch driver { case "postgres": sql.Register("postgres", stdlib.GetDefaultDriver()) registeredDrivers[driver] = struct{}{} From d8ed893d8f02aebe86876a6e70a2bd700ac57962 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 28 May 2026 18:24:26 -0400 Subject: [PATCH 03/13] fix(ers): normalize config.Driver in NewProvider before sql.Open ensureDriverRegistered already normalizes its input to lowercase, but NewProvider was still passing the raw config.Driver to sql.Open. A mixed-case driver (e.g. 'Postgres') would pass through ensureDriverRegistered correctly but then fail sql.Open with 'unknown driver' since database/sql matches driver names by exact string. Normalize config.Driver at the top of NewProvider so both ensureDriverRegistered and sql.Open receive the canonical lowercase name. Addresses review feedback from coderabbitai and gemini-code-assist. --- .../multi-strategy/providers/sql/sql_provider.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index dbba2252b8..d9c8c4c181 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -66,6 +66,11 @@ type Provider struct { // NewProvider creates a new SQL provider func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { + // Normalize the driver name so "Postgres", "POSTGRES", and "postgres" all + // resolve correctly through ensureDriverRegistered and sql.Open, both of + // which use case-sensitive driver name matching. + config.Driver = strings.ToLower(strings.TrimSpace(config.Driver)) + // Register the database/sql driver for this provider's configured driver name // if it has not already been registered. ensureDriverRegistered(config.Driver) From aa544af7c744344d07abc9fed11159e380024af2 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Tue, 9 Jun 2026 11:18:58 -0400 Subject: [PATCH 04/13] fix(ers): satisfy SQL provider lint Signed-off-by: jp-ayyappan --- .../multi-strategy/providers/sql/sql_config.go | 3 ++- .../multi-strategy/providers/sql/sql_provider.go | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config.go b/service/entityresolution/multi-strategy/providers/sql/sql_config.go index 31e2439588..08c7df4bd4 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config.go @@ -6,6 +6,7 @@ import ( const ( // Default SQL configuration values + defaultPostgreSQLDriver = "postgres" defaultPostgreSQLPort = 5432 defaultMaxOpenConnections = 25 defaultMaxIdleConnections = 5 @@ -45,7 +46,7 @@ type Config struct { // DefaultConfig returns a default SQL configuration func DefaultConfig() Config { return Config{ - Driver: "postgres", + Driver: defaultPostgreSQLDriver, Port: defaultPostgreSQLPort, SSLMode: "require", MaxOpenConnections: defaultMaxOpenConnections, diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index d9c8c4c181..d926dc647c 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -47,9 +47,8 @@ func ensureDriverRegistered(driver string) { } } - switch driver { - case "postgres": - sql.Register("postgres", stdlib.GetDefaultDriver()) + if driver == defaultPostgreSQLDriver { + sql.Register(defaultPostgreSQLDriver, stdlib.GetDefaultDriver()) registeredDrivers[driver] = struct{}{} } // mysql and sqlite require imports not present in this module's dependencies. @@ -305,7 +304,7 @@ func (p *Provider) Close() error { // buildConnectionString creates a connection string based on the driver func (p *Provider) buildConnectionString() (string, error) { switch strings.ToLower(p.config.Driver) { - case "postgres": + case defaultPostgreSQLDriver: return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", p.config.Host, p.config.Port, p.config.Username, p.config.Password, p.config.Database, p.config.SSLMode), nil From c0d984794d168517894a9ef726f9ad3741a8df43 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Tue, 9 Jun 2026 18:09:27 -0400 Subject: [PATCH 05/13] fix(ers): require exact SQL driver precheck Signed-off-by: jp-ayyappan --- .../multi-strategy/providers/sql/sql_provider.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index d926dc647c..1cbd3324f0 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -39,9 +39,10 @@ func ensureDriverRegistered(driver string) { // Check whether the driver was already registered externally (e.g. via a // blank import in the consumer binary) before attempting to register it. - // Use strings.EqualFold so the pre-check is also case-insensitive. + // database/sql driver names are case-sensitive, so only an exact canonical + // match can satisfy sql.Open after the config driver is normalized. for _, d := range sql.Drivers() { - if strings.EqualFold(d, driver) { + if d == driver { registeredDrivers[driver] = struct{}{} return } From c381db925c665ca9fc44b16e894320de3f763b4c Mon Sep 17 00:00:00 2001 From: Jp Ayyappan <108297634+jp-ayyappan@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:32:37 -0400 Subject: [PATCH 06/13] Update service/entityresolution/multi-strategy/providers/sql/sql_provider.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../multi-strategy/providers/sql/sql_provider.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 1cbd3324f0..282e9868e7 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -70,7 +70,8 @@ func NewProvider(ctx context.Context, name string, config Config) (*Provider, er // resolve correctly through ensureDriverRegistered and sql.Open, both of // which use case-sensitive driver name matching. config.Driver = strings.ToLower(strings.TrimSpace(config.Driver)) - +func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { + config.Driver = strings.ToLower(config.Driver) // Register the database/sql driver for this provider's configured driver name // if it has not already been registered. ensureDriverRegistered(config.Driver) From 76973fd481b17cb67598d09efdd49b2fcc2475c0 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Tue, 9 Jun 2026 21:50:52 -0400 Subject: [PATCH 07/13] fix(ers): remove malformed provider declaration Signed-off-by: jp-ayyappan --- .../multi-strategy/providers/sql/sql_provider.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 282e9868e7..1cbd3324f0 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -70,8 +70,7 @@ func NewProvider(ctx context.Context, name string, config Config) (*Provider, er // resolve correctly through ensureDriverRegistered and sql.Open, both of // which use case-sensitive driver name matching. config.Driver = strings.ToLower(strings.TrimSpace(config.Driver)) -func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { - config.Driver = strings.ToLower(config.Driver) + // Register the database/sql driver for this provider's configured driver name // if it has not already been registered. ensureDriverRegistered(config.Driver) From f3d98b2adab19651f6c0bce3acd60b96b5aacf48 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 22 Jun 2026 22:33:46 -0400 Subject: [PATCH 08/13] fix(ers): default sql provider to pgx driver Signed-off-by: jp-ayyappan --- .../entityresolution/multi-strategy/README.md | 6 +- .../multi-strategy/example-config.yaml | 4 +- .../providers/sql/sql_config.go | 4 +- .../providers/sql/sql_config_test.go | 78 +++++++++++++++++++ .../providers/sql/sql_provider.go | 23 ++++-- 5 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 service/entityresolution/multi-strategy/providers/sql/sql_config_test.go diff --git a/service/entityresolution/multi-strategy/README.md b/service/entityresolution/multi-strategy/README.md index 59ee61a019..2740126e10 100644 --- a/service/entityresolution/multi-strategy/README.md +++ b/service/entityresolution/multi-strategy/README.md @@ -46,7 +46,7 @@ services: primary_db: type: sql connection: - driver: postgres + driver: pgx host: localhost port: 5432 database: identity_db @@ -496,7 +496,7 @@ providers: my_db: type: sql connection: - driver: postgres # postgres, mysql, sqlite + driver: pgx # pgx, mysql, sqlite host: localhost port: 5432 database: identity_db @@ -691,4 +691,4 @@ See [`example-config.yaml`](./example-config.yaml) for a comprehensive configura - Cross-backend failover scenarios - Health check configuration -For implementation details and architecture decisions, see the [Multi-Strategy ERS ADR](../../../adr/decisions/2025-07-31-multi-strategy-entity-resolution-service.md). \ No newline at end of file +For implementation details and architecture decisions, see the [Multi-Strategy ERS ADR](../../../adr/decisions/2025-07-31-multi-strategy-entity-resolution-service.md). diff --git a/service/entityresolution/multi-strategy/example-config.yaml b/service/entityresolution/multi-strategy/example-config.yaml index f4be018034..201719370b 100644 --- a/service/entityresolution/multi-strategy/example-config.yaml +++ b/service/entityresolution/multi-strategy/example-config.yaml @@ -22,7 +22,7 @@ services: user_db: type: sql connection: - driver: postgres + driver: pgx host: localhost port: 5432 database: user_directory @@ -276,4 +276,4 @@ services: # # 5. No matching strategy: # - JWT contains: {"aud": ["unknown"]} -# - Result: Error - no matching strategy found (regardless of failure_strategy) \ No newline at end of file +# - Result: Error - no matching strategy found (regardless of failure_strategy) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config.go b/service/entityresolution/multi-strategy/providers/sql/sql_config.go index 08c7df4bd4..79469ac278 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config.go @@ -6,7 +6,7 @@ import ( const ( // Default SQL configuration values - defaultPostgreSQLDriver = "postgres" + defaultPostgreSQLDriver = "pgx" defaultPostgreSQLPort = 5432 defaultMaxOpenConnections = 25 defaultMaxIdleConnections = 5 @@ -18,7 +18,7 @@ const ( // SQLConfig defines configuration for SQL database providers type Config struct { // Database connection configuration - Driver string `mapstructure:"driver"` // "postgres", "mysql", "sqlite" + Driver string `mapstructure:"driver"` // "pgx", "mysql", "sqlite" ("postgres" is accepted as a pgx alias) Host string `mapstructure:"host"` Port int `mapstructure:"port"` Database string `mapstructure:"database"` diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go new file mode 100644 index 0000000000..f51e41b6ff --- /dev/null +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go @@ -0,0 +1,78 @@ +package sql + +import ( + "strings" + "testing" +) + +func TestDefaultConfigUsesPGXDriver(t *testing.T) { + config := DefaultConfig() + if config.Driver != defaultPostgreSQLDriver { + t.Fatalf("expected default driver %q, got %q", defaultPostgreSQLDriver, config.Driver) + } +} + +func TestNormalizeDriverName(t *testing.T) { + tests := []struct { + name string + driver string + want string + }{ + { + name: "pgx", + driver: "pgx", + want: "pgx", + }, + { + name: "postgres alias", + driver: "postgres", + want: "pgx", + }, + { + name: "postgresql alias with whitespace and case", + driver: " PostgreSQL ", + want: "pgx", + }, + { + name: "other driver", + driver: "sqlite3", + want: "sqlite3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := normalizeDriverName(tt.driver); got != tt.want { + t.Fatalf("expected %q, got %q", tt.want, got) + } + }) + } +} + +func TestBuildConnectionStringSupportsPostgresAliases(t *testing.T) { + tests := []string{"pgx", "postgres", "postgresql", "Postgres"} + + for _, driver := range tests { + t.Run(driver, func(t *testing.T) { + provider := &Provider{ + config: Config{ + Driver: driver, + Host: "localhost", + Port: 5432, + Database: "identity_db", + Username: "ers_user", + Password: "ers_password", + SSLMode: "require", + }, + } + + connStr, err := provider.buildConnectionString() + if err != nil { + t.Fatalf("expected postgres alias to build connection string: %v", err) + } + if !strings.Contains(connStr, "dbname=identity_db") { + t.Fatalf("expected database name in connection string, got %q", connStr) + } + }) + } +} diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 1cbd3324f0..8b1ce95deb 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -26,9 +26,7 @@ var ( // consumer. Consumers that have already registered the driver themselves are // handled gracefully via a sql.Drivers() pre-check. func ensureDriverRegistered(driver string) { - // Normalize to lowercase so "Postgres", "POSTGRES", and "postgres" all resolve - // to the same registered driver name. sql.Register is case-sensitive. - driver = strings.ToLower(strings.TrimSpace(driver)) + driver = normalizeDriverName(driver) driverRegMu.Lock() defer driverRegMu.Unlock() @@ -56,6 +54,16 @@ func ensureDriverRegistered(driver string) { // Add cases here when those drivers are added to go.mod. } +func normalizeDriverName(driver string) string { + driver = strings.ToLower(strings.TrimSpace(driver)) + switch driver { + case "postgres", "postgresql": + return defaultPostgreSQLDriver + default: + return driver + } +} + // Provider implements the Provider interface for SQL databases type Provider struct { name string @@ -66,10 +74,9 @@ type Provider struct { // NewProvider creates a new SQL provider func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { - // Normalize the driver name so "Postgres", "POSTGRES", and "postgres" all - // resolve correctly through ensureDriverRegistered and sql.Open, both of - // which use case-sensitive driver name matching. - config.Driver = strings.ToLower(strings.TrimSpace(config.Driver)) + // Normalize aliases so "postgres" and "postgresql" use the registered pgx + // database/sql driver name. + config.Driver = normalizeDriverName(config.Driver) // Register the database/sql driver for this provider's configured driver name // if it has not already been registered. @@ -304,7 +311,7 @@ func (p *Provider) Close() error { // buildConnectionString creates a connection string based on the driver func (p *Provider) buildConnectionString() (string, error) { - switch strings.ToLower(p.config.Driver) { + switch normalizeDriverName(p.config.Driver) { case defaultPostgreSQLDriver: return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", p.config.Host, p.config.Port, p.config.Username, p.config.Password, From 647473ac60e3cf70f9ba7e1ee7e8c8c00505347f Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 22 Jun 2026 22:45:01 -0400 Subject: [PATCH 09/13] fix(ers): extract postgres driver aliases Signed-off-by: jp-ayyappan --- .../multi-strategy/providers/sql/sql_config.go | 2 ++ .../multi-strategy/providers/sql/sql_config_test.go | 4 ++-- .../multi-strategy/providers/sql/sql_provider.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config.go b/service/entityresolution/multi-strategy/providers/sql/sql_config.go index 79469ac278..daadd7b9e8 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config.go @@ -7,6 +7,8 @@ import ( const ( // Default SQL configuration values defaultPostgreSQLDriver = "pgx" + postgresDriverAlias = "postgres" + postgresQLDriverAlias = "postgresql" defaultPostgreSQLPort = 5432 defaultMaxOpenConnections = 25 defaultMaxIdleConnections = 5 diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go index f51e41b6ff..c040cb7709 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go @@ -25,7 +25,7 @@ func TestNormalizeDriverName(t *testing.T) { }, { name: "postgres alias", - driver: "postgres", + driver: postgresDriverAlias, want: "pgx", }, { @@ -50,7 +50,7 @@ func TestNormalizeDriverName(t *testing.T) { } func TestBuildConnectionStringSupportsPostgresAliases(t *testing.T) { - tests := []string{"pgx", "postgres", "postgresql", "Postgres"} + tests := []string{"pgx", postgresDriverAlias, postgresQLDriverAlias, "Postgres"} for _, driver := range tests { t.Run(driver, func(t *testing.T) { diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 8b1ce95deb..df85c4e700 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -57,7 +57,7 @@ func ensureDriverRegistered(driver string) { func normalizeDriverName(driver string) string { driver = strings.ToLower(strings.TrimSpace(driver)) switch driver { - case "postgres", "postgresql": + case postgresDriverAlias, postgresQLDriverAlias: return defaultPostgreSQLDriver default: return driver From 8a9faeb50bfe8263fb592e95c11a078bc42fa92a Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 22 Jun 2026 23:07:06 -0400 Subject: [PATCH 10/13] fix(ers): use versioned pgx sql driver Signed-off-by: jp-ayyappan --- service/entityresolution/multi-strategy/README.md | 4 ++-- .../multi-strategy/example-config.yaml | 2 +- .../multi-strategy/providers/sql/sql_config.go | 5 +++-- .../multi-strategy/providers/sql/sql_config_test.go | 10 +++++----- .../multi-strategy/providers/sql/sql_provider.go | 6 +++--- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/service/entityresolution/multi-strategy/README.md b/service/entityresolution/multi-strategy/README.md index 2740126e10..34b70bc96c 100644 --- a/service/entityresolution/multi-strategy/README.md +++ b/service/entityresolution/multi-strategy/README.md @@ -46,7 +46,7 @@ services: primary_db: type: sql connection: - driver: pgx + driver: pgx/v5 host: localhost port: 5432 database: identity_db @@ -496,7 +496,7 @@ providers: my_db: type: sql connection: - driver: pgx # pgx, mysql, sqlite + driver: pgx/v5 # pgx/v5, mysql, sqlite host: localhost port: 5432 database: identity_db diff --git a/service/entityresolution/multi-strategy/example-config.yaml b/service/entityresolution/multi-strategy/example-config.yaml index 201719370b..9cdd42dc4a 100644 --- a/service/entityresolution/multi-strategy/example-config.yaml +++ b/service/entityresolution/multi-strategy/example-config.yaml @@ -22,7 +22,7 @@ services: user_db: type: sql connection: - driver: pgx + driver: pgx/v5 host: localhost port: 5432 database: user_directory diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config.go b/service/entityresolution/multi-strategy/providers/sql/sql_config.go index daadd7b9e8..5a0011b340 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config.go @@ -6,7 +6,8 @@ import ( const ( // Default SQL configuration values - defaultPostgreSQLDriver = "pgx" + defaultPostgreSQLDriver = "pgx/v5" + pgxDriverAlias = "pgx" postgresDriverAlias = "postgres" postgresQLDriverAlias = "postgresql" defaultPostgreSQLPort = 5432 @@ -20,7 +21,7 @@ const ( // SQLConfig defines configuration for SQL database providers type Config struct { // Database connection configuration - Driver string `mapstructure:"driver"` // "pgx", "mysql", "sqlite" ("postgres" is accepted as a pgx alias) + Driver string `mapstructure:"driver"` // "pgx/v5", "mysql", "sqlite" ("pgx", "postgres", and "postgresql" are accepted as aliases) Host string `mapstructure:"host"` Port int `mapstructure:"port"` Database string `mapstructure:"database"` diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go index c040cb7709..0d2db5c59a 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go @@ -20,18 +20,18 @@ func TestNormalizeDriverName(t *testing.T) { }{ { name: "pgx", - driver: "pgx", - want: "pgx", + driver: pgxDriverAlias, + want: defaultPostgreSQLDriver, }, { name: "postgres alias", driver: postgresDriverAlias, - want: "pgx", + want: defaultPostgreSQLDriver, }, { name: "postgresql alias with whitespace and case", driver: " PostgreSQL ", - want: "pgx", + want: defaultPostgreSQLDriver, }, { name: "other driver", @@ -50,7 +50,7 @@ func TestNormalizeDriverName(t *testing.T) { } func TestBuildConnectionStringSupportsPostgresAliases(t *testing.T) { - tests := []string{"pgx", postgresDriverAlias, postgresQLDriverAlias, "Postgres"} + tests := []string{defaultPostgreSQLDriver, pgxDriverAlias, postgresDriverAlias, postgresQLDriverAlias, "Postgres"} for _, driver := range tests { t.Run(driver, func(t *testing.T) { diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index df85c4e700..7be013b43e 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -57,7 +57,7 @@ func ensureDriverRegistered(driver string) { func normalizeDriverName(driver string) string { driver = strings.ToLower(strings.TrimSpace(driver)) switch driver { - case postgresDriverAlias, postgresQLDriverAlias: + case pgxDriverAlias, postgresDriverAlias, postgresQLDriverAlias: return defaultPostgreSQLDriver default: return driver @@ -74,8 +74,8 @@ type Provider struct { // NewProvider creates a new SQL provider func NewProvider(ctx context.Context, name string, config Config) (*Provider, error) { - // Normalize aliases so "postgres" and "postgresql" use the registered pgx - // database/sql driver name. + // Normalize aliases so "pgx", "postgres", and "postgresql" use the + // registered pgx/v5 database/sql driver name. config.Driver = normalizeDriverName(config.Driver) // Register the database/sql driver for this provider's configured driver name From 8386557157757987d69978266d322c657c16a333 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 22 Jun 2026 23:16:35 -0400 Subject: [PATCH 11/13] fix(ers): rely on pgx stdlib driver registration Signed-off-by: jp-ayyappan --- .../providers/sql/sql_provider.go | 51 +------------------ 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 7be013b43e..0b28f79069 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -5,55 +5,12 @@ import ( "database/sql" "fmt" "strings" - "sync" - "github.com/jackc/pgx/v5/stdlib" + // Register the pgx/v5 database/sql driver for SQL providers. + _ "github.com/jackc/pgx/v5/stdlib" "github.com/opentdf/platform/service/entityresolution/multi-strategy/types" ) -var ( - // driverRegMu guards lazy driver registration to prevent duplicate-register panics. - driverRegMu sync.Mutex - registeredDrivers = make(map[string]struct{}) -) - -// ensureDriverRegistered lazily registers the named database/sql driver the first -// time a SQL provider for that driver is created. This avoids the need for -// consumers to add blank driver imports to their own binaries. -// -// Uses pgx/v5/stdlib for postgres (already a platform dependency). Other drivers -// (mysql, sqlite) are not currently auto-registered and must be imported by the -// consumer. Consumers that have already registered the driver themselves are -// handled gracefully via a sql.Drivers() pre-check. -func ensureDriverRegistered(driver string) { - driver = normalizeDriverName(driver) - - driverRegMu.Lock() - defer driverRegMu.Unlock() - - if _, ok := registeredDrivers[driver]; ok { - return - } - - // Check whether the driver was already registered externally (e.g. via a - // blank import in the consumer binary) before attempting to register it. - // database/sql driver names are case-sensitive, so only an exact canonical - // match can satisfy sql.Open after the config driver is normalized. - for _, d := range sql.Drivers() { - if d == driver { - registeredDrivers[driver] = struct{}{} - return - } - } - - if driver == defaultPostgreSQLDriver { - sql.Register(defaultPostgreSQLDriver, stdlib.GetDefaultDriver()) - registeredDrivers[driver] = struct{}{} - } - // mysql and sqlite require imports not present in this module's dependencies. - // Add cases here when those drivers are added to go.mod. -} - func normalizeDriverName(driver string) string { driver = strings.ToLower(strings.TrimSpace(driver)) switch driver { @@ -78,10 +35,6 @@ func NewProvider(ctx context.Context, name string, config Config) (*Provider, er // registered pgx/v5 database/sql driver name. config.Driver = normalizeDriverName(config.Driver) - // Register the database/sql driver for this provider's configured driver name - // if it has not already been registered. - ensureDriverRegistered(config.Driver) - provider := &Provider{ name: name, config: config, From f72b3c44597aa01f87dd30788763356e4b0e9a4e Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 22 Jun 2026 23:20:08 -0400 Subject: [PATCH 12/13] test(ers): cover mysql driver passthrough Signed-off-by: jp-ayyappan --- .../multi-strategy/providers/sql/sql_config_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go index 0d2db5c59a..15ce11baf3 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go @@ -35,6 +35,11 @@ func TestNormalizeDriverName(t *testing.T) { }, { name: "other driver", + driver: "mysql", + want: "mysql", + }, + { + name: "sqlite driver", driver: "sqlite3", want: "sqlite3", }, From 0d90cdb4e0b950c74726228db1eb53d486c92dba Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 25 Jun 2026 07:53:24 -0400 Subject: [PATCH 13/13] fix(ers): keep postgres as sql config default Signed-off-by: jp-ayyappan --- .../entityresolution/multi-strategy/README.md | 4 +-- .../multi-strategy/example-config.yaml | 2 +- .../providers/sql/sql_config.go | 6 ++-- .../providers/sql/sql_config_test.go | 33 ++++++++----------- .../providers/sql/sql_provider.go | 6 ++-- 5 files changed, 22 insertions(+), 29 deletions(-) diff --git a/service/entityresolution/multi-strategy/README.md b/service/entityresolution/multi-strategy/README.md index 34b70bc96c..76c838f110 100644 --- a/service/entityresolution/multi-strategy/README.md +++ b/service/entityresolution/multi-strategy/README.md @@ -46,7 +46,7 @@ services: primary_db: type: sql connection: - driver: pgx/v5 + driver: postgres host: localhost port: 5432 database: identity_db @@ -496,7 +496,7 @@ providers: my_db: type: sql connection: - driver: pgx/v5 # pgx/v5, mysql, sqlite + driver: postgres # postgres, mysql, sqlite host: localhost port: 5432 database: identity_db diff --git a/service/entityresolution/multi-strategy/example-config.yaml b/service/entityresolution/multi-strategy/example-config.yaml index 9cdd42dc4a..8d64dbe9fb 100644 --- a/service/entityresolution/multi-strategy/example-config.yaml +++ b/service/entityresolution/multi-strategy/example-config.yaml @@ -22,7 +22,7 @@ services: user_db: type: sql connection: - driver: pgx/v5 + driver: postgres host: localhost port: 5432 database: user_directory diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config.go b/service/entityresolution/multi-strategy/providers/sql/sql_config.go index 5a0011b340..8e305f484f 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config.go @@ -6,9 +6,9 @@ import ( const ( // Default SQL configuration values - defaultPostgreSQLDriver = "pgx/v5" + defaultPostgreSQLDriver = "postgres" + canonicalPGXDriver = "pgx/v5" pgxDriverAlias = "pgx" - postgresDriverAlias = "postgres" postgresQLDriverAlias = "postgresql" defaultPostgreSQLPort = 5432 defaultMaxOpenConnections = 25 @@ -21,7 +21,7 @@ const ( // SQLConfig defines configuration for SQL database providers type Config struct { // Database connection configuration - Driver string `mapstructure:"driver"` // "pgx/v5", "mysql", "sqlite" ("pgx", "postgres", and "postgresql" are accepted as aliases) + Driver string `mapstructure:"driver"` // "postgres", "mysql", "sqlite" ("pgx", "pgx/v5", and "postgresql" are accepted as aliases) Host string `mapstructure:"host"` Port int `mapstructure:"port"` Database string `mapstructure:"database"` diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go index 15ce11baf3..3c49729321 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_config_test.go @@ -1,15 +1,14 @@ package sql import ( - "strings" "testing" + + "github.com/stretchr/testify/require" ) -func TestDefaultConfigUsesPGXDriver(t *testing.T) { +func TestDefaultConfigUsesPostgreSQLDriver(t *testing.T) { config := DefaultConfig() - if config.Driver != defaultPostgreSQLDriver { - t.Fatalf("expected default driver %q, got %q", defaultPostgreSQLDriver, config.Driver) - } + require.Equal(t, defaultPostgreSQLDriver, config.Driver) } func TestNormalizeDriverName(t *testing.T) { @@ -21,17 +20,17 @@ func TestNormalizeDriverName(t *testing.T) { { name: "pgx", driver: pgxDriverAlias, - want: defaultPostgreSQLDriver, + want: canonicalPGXDriver, }, { - name: "postgres alias", - driver: postgresDriverAlias, - want: defaultPostgreSQLDriver, + name: "postgres default", + driver: defaultPostgreSQLDriver, + want: canonicalPGXDriver, }, { name: "postgresql alias with whitespace and case", driver: " PostgreSQL ", - want: defaultPostgreSQLDriver, + want: canonicalPGXDriver, }, { name: "other driver", @@ -47,15 +46,13 @@ func TestNormalizeDriverName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := normalizeDriverName(tt.driver); got != tt.want { - t.Fatalf("expected %q, got %q", tt.want, got) - } + require.Equal(t, tt.want, normalizeDriverName(tt.driver)) }) } } func TestBuildConnectionStringSupportsPostgresAliases(t *testing.T) { - tests := []string{defaultPostgreSQLDriver, pgxDriverAlias, postgresDriverAlias, postgresQLDriverAlias, "Postgres"} + tests := []string{canonicalPGXDriver, defaultPostgreSQLDriver, pgxDriverAlias, postgresQLDriverAlias, "Postgres"} for _, driver := range tests { t.Run(driver, func(t *testing.T) { @@ -72,12 +69,8 @@ func TestBuildConnectionStringSupportsPostgresAliases(t *testing.T) { } connStr, err := provider.buildConnectionString() - if err != nil { - t.Fatalf("expected postgres alias to build connection string: %v", err) - } - if !strings.Contains(connStr, "dbname=identity_db") { - t.Fatalf("expected database name in connection string, got %q", connStr) - } + require.NoError(t, err) + require.Contains(t, connStr, "dbname=identity_db") }) } } diff --git a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go index 0b28f79069..f4e139ac2a 100644 --- a/service/entityresolution/multi-strategy/providers/sql/sql_provider.go +++ b/service/entityresolution/multi-strategy/providers/sql/sql_provider.go @@ -14,8 +14,8 @@ import ( func normalizeDriverName(driver string) string { driver = strings.ToLower(strings.TrimSpace(driver)) switch driver { - case pgxDriverAlias, postgresDriverAlias, postgresQLDriverAlias: - return defaultPostgreSQLDriver + case defaultPostgreSQLDriver, pgxDriverAlias, postgresQLDriverAlias: + return canonicalPGXDriver default: return driver } @@ -265,7 +265,7 @@ func (p *Provider) Close() error { // buildConnectionString creates a connection string based on the driver func (p *Provider) buildConnectionString() (string, error) { switch normalizeDriverName(p.config.Driver) { - case defaultPostgreSQLDriver: + case canonicalPGXDriver: return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", p.config.Host, p.config.Port, p.config.Username, p.config.Password, p.config.Database, p.config.SSLMode), nil