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
39 changes: 39 additions & 0 deletions datastore/dialect/postgres/exec_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package postgres_test

import (
"testing"

"github.com/jackc/pgx/v5"
"github.com/pitabwire/frame/v2/datastore/dialect"
"github.com/pitabwire/frame/v2/datastore/dialect/postgres"
"github.com/stretchr/testify/require"
)

const testDSN = "host=localhost port=5432 user=u password=p dbname=d"

func TestQueryExecModeFollowsConnectionOptions(t *testing.T) {
cases := []struct {
name string
opts dialect.ConnectionOptions
want pgx.QueryExecMode
}{
{
"simple protocol preferred",
dialect.ConnectionOptions{PreferSimpleProtocol: true, PreparedStatements: true},
pgx.QueryExecModeSimpleProtocol,
},
{"no prepared statements", dialect.ConnectionOptions{PreparedStatements: false}, pgx.QueryExecModeExec},
{"prepared statements", dialect.ConnectionOptions{PreparedStatements: true}, pgx.QueryExecModeCacheStatement},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, postgres.QueryExecMode(testDSN, tc.opts))
})
}
}

func TestQueryExecModeDSNOverrideWins(t *testing.T) {
dsn := testDSN + " default_query_exec_mode=cache_describe"
got := postgres.QueryExecMode(dsn, dialect.ConnectionOptions{PreferSimpleProtocol: true, PreparedStatements: true})
require.Equal(t, pgx.QueryExecModeCacheDescribe, got)
}
36 changes: 36 additions & 0 deletions datastore/dialect/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,41 @@ func applyPoolSizing(cfg *pgxpool.Config, opts dialect.ConnectionOptions) {
}
}

// applyQueryExecMode makes PreferSimpleProtocol / PreparedStatements take
// effect at the pgx layer.
//
// gorm's postgres driver only honours Config.PreferSimpleProtocol when it
// parses the DSN itself; this adapter hands it an already-open *sql.DB, so
// the preference was silently dropped and pgx kept its default
// statement-cache mode. Behind a transaction-mode pooler (PgBouncer,
// Supavisor, Neon) that mode fails with "prepared statement ... does not
// exist / already exists" (SQLSTATE 26000 / 42P05) as soon as connections
// are reassigned. An explicit default_query_exec_mode in the DSN always wins.
func applyQueryExecMode(cfg *pgxpool.Config, dsn string, opts dialect.ConnectionOptions) {
cfg.ConnConfig.DefaultQueryExecMode = QueryExecMode(dsn, opts)
}

// QueryExecMode reports the pgx query execution mode the adapter will use for
// the given DSN and connection options.
func QueryExecMode(dsn string, opts dialect.ConnectionOptions) pgx.QueryExecMode {
if strings.Contains(dsn, "default_query_exec_mode") {
cfg, err := pgx.ParseConfig(dsn)
if err == nil {
return cfg.DefaultQueryExecMode
}
}
switch {
case opts.PreferSimpleProtocol:
return pgx.QueryExecModeSimpleProtocol
case !opts.PreparedStatements:
// Extended protocol without server-side prepared statements: safe
// behind transaction pooling, keeps binary parameter encoding.
return pgx.QueryExecModeExec
default:
return pgx.QueryExecModeCacheStatement
}
}

// configureSQLDB applies *sql.DB pool sizing. MaxIdleConns is forced
// to 0 so every release flows through pgxpool, which is the property
// the hook chain relies on for tenancy hook correctness.
Expand Down Expand Up @@ -226,6 +261,7 @@ func (a *Adapter) OpenConnection(
}

applyPoolSizing(cfg, opts)
applyQueryExecMode(cfg, cleanDSN, opts)
cfg.ConnConfig.Tracer = otelpgx.NewTracer()

// Wire PrepareConn / AfterRelease to dispatchers that close over a
Expand Down
Loading