diff --git a/README.md b/README.md index 3b016088..92a32f40 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,21 @@ The connector is configured using a YAML file that defines: -- **Database Connection**: Connection details via DSN (Data Source Name) +- **Database Connection**: Connection details via DSN (Data Source Name) and optional `connect.params` - **Resource Types**: Map database tables/queries to resources (users, roles, etc.) - **Account Provisioning**: Define schemas and credential options for user creation - **Entitlements**: Permissions and roles that can be granted to resources - **Provisioning Actions**: SQL queries for granting/revoking entitlements +For Postgres behind a transaction-mode pooler (PgBouncer, Supabase pooler on port 6543, etc.), set `default_query_exec_mode` to `simple_protocol` via the DSN query string or `connect.params` to avoid prepared-statement conflicts (SQLSTATE 42P05). When unset, baton-sql leaves the URL unchanged and pgx uses its default (`cache_statement`). + +```yaml +connect: + dsn: "postgres://${HOST}:6543/${DB}" + params: + default_query_exec_mode: simple_protocol +``` + See examples in the [examples](https://github.com/ConductorOne/baton-sql/tree/main/examples) directory. ### Query placeholders diff --git a/examples/redshift-test.yml b/examples/redshift-test.yml index f8c1a422..7a7b9718 100644 --- a/examples/redshift-test.yml +++ b/examples/redshift-test.yml @@ -38,7 +38,7 @@ # # Driver caveat: baton-sql uses jackc/pgx/v5 for the postgres scheme. If prepared- # statement quirks surface, pin to simple-query protocol via the DSN parameter -# default_query_exec_mode=simple_protocol. +# default_query_exec_mode=simple_protocol (or connect.params). app_name: Amazon Redshift app_description: Sync Redshift cluster identities, roles, groups, schemas, and tables. diff --git a/pkg/database/database_test.go b/pkg/database/database_test.go index 2e2fab9b..6ee8b3ee 100644 --- a/pkg/database/database_test.go +++ b/pkg/database/database_test.go @@ -804,3 +804,115 @@ func Test_buildConnectionURL(t *testing.T) { }) } } + +func Test_buildConnectionURL_PostgresQueryExecMode(t *testing.T) { + const modeKey = "default_query_exec_mode" + + tests := []struct { + name string + opts ConnectOptions + wantMode string // empty means key must be absent + wantOther map[string]string + }{ + { + name: "unset postgres DSN does not inject default_query_exec_mode", + opts: ConnectOptions{ + DSN: "postgres://user:pass@localhost:5432/db", + }, + wantMode: "", + }, + { + name: "structured postgres with sslmode only has no mode key", + opts: ConnectOptions{ + Scheme: "postgres", + Host: "db.example", + Port: "5432", + Database: "app", + User: "u", + Password: "p", + Params: map[string]string{ + "sslmode": "require", + }, + }, + wantMode: "", + wantOther: map[string]string{ + "sslmode": "require", + }, + }, + { + name: "DSN already sets cache_statement is preserved", + opts: ConnectOptions{ + DSN: "postgres://user:pass@localhost:5432/db?default_query_exec_mode=cache_statement", + }, + wantMode: "cache_statement", + }, + { + name: "Params set exec is preserved", + opts: ConnectOptions{ + Scheme: "postgres", + Host: "db.example", + Database: "app", + Params: map[string]string{ + "default_query_exec_mode": "exec", + }, + }, + wantMode: "exec", + }, + { + name: "Params set simple_protocol is preserved", + opts: ConnectOptions{ + Scheme: "postgres", + Host: "db.example", + Database: "app", + Params: map[string]string{ + "default_query_exec_mode": "simple_protocol", + }, + }, + wantMode: "simple_protocol", + }, + { + name: "mysql scheme does not inject mode", + opts: ConnectOptions{ + Scheme: "mysql", + Host: "db.example", + Port: "3306", + Database: "app", + User: "u", + Password: "p", + }, + wantMode: "", + }, + { + name: "mysql DSN does not inject mode", + opts: ConnectOptions{ + DSN: "mysql://user:pass@localhost:3306/db", + }, + wantMode: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildConnectionURL(tt.opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + q := got.Query() + gotMode := q.Get(modeKey) + if gotMode != tt.wantMode { + t.Fatalf("%s = %q, want %q (url=%s)", modeKey, gotMode, tt.wantMode, got.String()) + } + if tt.wantMode == "" { + if _, ok := q[modeKey]; ok { + t.Fatalf("%s present when unset: %s", modeKey, got.String()) + } + } + for k, want := range tt.wantOther { + if got := q.Get(k); got != want { + t.Fatalf("query %s = %q, want %q", k, got, want) + } + } + }) + } +}