Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/redshift-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
112 changes: 112 additions & 0 deletions pkg/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
})
}
}
Loading