Skip to content

Commit a34d2b5

Browse files
committed
Update PgBouncer guidance and expand integration coverage
Document prepared statement support in PgBouncer 1.21+ and test all compatible query execution modes, including parameterized queries, transactions, Exec, and batches. Also enable pgbouncer tests in CI.
1 parent 140be86 commit a34d2b5

8 files changed

Lines changed: 229 additions & 51 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,36 @@ jobs:
128128
PGX_SSL_PASSWORD: ${{ matrix.pgx-ssl-password }}
129129
PGX_TEST_TLS_CLIENT_CONN_STRING: ${{ matrix.pgx-test-tls-client-conn-string }}
130130

131+
test-pgbouncer:
132+
name: Test PgBouncer with PostgreSQL 18
133+
runs-on: ubuntu-22.04
134+
env:
135+
PGBOUNCER_VERSION: "1.25.2"
136+
PGBOUNCER_SHA256: "924ad35113fd0a71c8e2dbe85b5d03445532e2b7b37a9f8a48983beea238b332"
137+
138+
steps:
139+
- name: Check out code into the Go module directory
140+
uses: actions/checkout@v5
141+
142+
- name: Set up Go 1.26
143+
uses: actions/setup-go@v6
144+
with:
145+
go-version: "1.26"
146+
147+
- name: Set up PostgreSQL 18
148+
run: ci/setup_test.bash
149+
env:
150+
PGVERSION: "18"
151+
152+
- name: Set up PgBouncer
153+
run: ci/setup_pgbouncer.bash
154+
155+
- name: Test PgBouncer
156+
run: go test -parallel=1 -race -count=1 -v ./...
157+
env:
158+
PGX_TEST_DATABASE: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
159+
PGX_TEST_PGBOUNCER_CONN_STRING: "host=127.0.0.1 port=6432 user=pgx_md5 password=secret dbname=pgx_test"
160+
131161
test-windows:
132162
name: Test Windows
133163
runs-on: windows-latest

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ psql --no-psqlrc -f testsetup/postgresql_setup.sql
130130
### PgBouncer
131131

132132
There are tests specific for PgBouncer that will be executed if `PGX_TEST_PGBOUNCER_CONN_STRING` is set.
133+
The test PgBouncer must be version 1.21.0 or newer, use transaction pooling, and have `max_prepared_statements` set to a
134+
non-zero value. This ensures the tests cover PgBouncer's protocol-level named prepared statement support in addition to
135+
the pgx query modes that do not use named prepared statements.
133136

134137
### Optional Tests
135138

ci/setup_pgbouncer.bash

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
set -euxo pipefail
3+
4+
: "${PGBOUNCER_VERSION:?PGBOUNCER_VERSION must be set}"
5+
: "${PGBOUNCER_SHA256:?PGBOUNCER_SHA256 must be set}"
6+
7+
sudo apt-get install -y build-essential curl libevent-dev libssl-dev pkg-config
8+
9+
pgbouncer_build_dir="$(mktemp -d)"
10+
pgbouncer_archive="$pgbouncer_build_dir/pgbouncer-$PGBOUNCER_VERSION.tar.gz"
11+
curl --fail --location --silent --show-error \
12+
"https://www.pgbouncer.org/downloads/files/$PGBOUNCER_VERSION/pgbouncer-$PGBOUNCER_VERSION.tar.gz" \
13+
--output "$pgbouncer_archive"
14+
[[ "$(sha256sum "$pgbouncer_archive" | cut -d ' ' -f 1)" == "$PGBOUNCER_SHA256" ]]
15+
tar -xzf "$pgbouncer_archive" -C "$pgbouncer_build_dir"
16+
17+
(
18+
cd "$pgbouncer_build_dir/pgbouncer-$PGBOUNCER_VERSION"
19+
./configure --prefix=/usr/local
20+
make -j2 pgbouncer
21+
sudo install -m 0755 pgbouncer /usr/local/bin/pgbouncer
22+
)
23+
24+
/usr/local/bin/pgbouncer --version | grep -Fqx "PgBouncer $PGBOUNCER_VERSION"
25+
26+
sudo install -o postgres -g postgres -m 0750 -d /etc/pgbouncer
27+
sudo install -o postgres -g postgres -m 0640 testsetup/pgbouncer.ini /etc/pgbouncer/pgbouncer.ini
28+
sudo install -o postgres -g postgres -m 0640 testsetup/pgbouncer-userlist.txt /etc/pgbouncer/userlist.txt
29+
sudo -u postgres /usr/local/bin/pgbouncer -d /etc/pgbouncer/pgbouncer.ini
30+
31+
for _ in {1..30}; do
32+
if PGPASSWORD=secret psql "host=127.0.0.1 port=6432 user=pgx_md5 dbname=pgx_test" -c "select 1"; then
33+
exit 0
34+
fi
35+
sleep 1
36+
done
37+
38+
sudo cat /var/log/postgresql/pgbouncer.log || true
39+
exit 1

conn.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ type ConnConfig struct {
3636
DescriptionCacheCapacity int
3737

3838
// DefaultQueryExecMode controls the default mode for executing queries. By default pgx uses the extended protocol
39-
// and automatically prepares and caches prepared statements. However, this may be incompatible with proxies such as
40-
// PGBouncer. In this case it may be preferable to use [QueryExecModeExec] or [QueryExecModeSimpleProtocol]. The same
41-
// functionality can be controlled on a per query basis by passing a [QueryExecMode] as the first query argument.
39+
// and automatically prepares and caches prepared statements. This may be incompatible with proxies such as PgBouncer
40+
// unless they are configured to support protocol-level prepared statements. In an incompatible configuration it may
41+
// be preferable to use [QueryExecModeExec] or [QueryExecModeSimpleProtocol]. The same functionality can be controlled
42+
// on a per query basis by passing a [QueryExecMode] as the first query argument.
4243
DefaultQueryExecMode QueryExecMode
4344

4445
createdByParseConfig bool // Used to enforce created by ParseConfig rule.

doc.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,18 @@ implemented on top of [pgconn.PgConn]. The [Conn.PgConn] method can be used to a
210210
211211
PgBouncer
212212
213-
By default pgx automatically uses prepared statements. Prepared statements are incompatible with PgBouncer. This can be
214-
disabled by setting a different [QueryExecMode] in [ConnConfig.DefaultQueryExecMode].
213+
By default pgx automatically uses protocol-level named prepared statements. PgBouncer 1.21.0 and newer can use these
214+
prepared statements in transaction and statement pooling modes when its max_prepared_statements setting is greater than
215+
zero. In this configuration the default [QueryExecModeCacheStatement] mode may be used. See the PgBouncer documentation
216+
for limitations and configuration details: https://www.pgbouncer.org/config.html#max_prepared_statements.
217+
218+
When using an older PgBouncer version or when prepared statement support is disabled, set
219+
[ConnConfig.DefaultQueryExecMode] to [QueryExecModeExec]. [QueryExecModeCacheDescribe] and
220+
[QueryExecModeSimpleProtocol] are also compatible with PgBouncer. Prefer [QueryExecModeExec] over
221+
[QueryExecModeSimpleProtocol] whenever possible. Do not use [QueryExecModeDescribeExec] with transaction pooling because
222+
PgBouncer may assign a different server connection between the describe and execute steps.
223+
224+
SQL-level prepared statements created with PREPARE are not supported by PgBouncer in transaction pooling mode.
215225
*/
216226
package pgx
217227

pgbouncer_test.go

Lines changed: 118 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,146 @@ package pgx_test
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"os"
68
"testing"
9+
"time"
710

811
"github.com/jackc/pgx/v5"
9-
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
13+
"golang.org/x/sync/errgroup"
1114
)
1215

13-
func TestPgbouncerStatementCacheDescribe(t *testing.T) {
16+
func TestPgbouncerQueryExecModes(t *testing.T) {
1417
connString := os.Getenv("PGX_TEST_PGBOUNCER_CONN_STRING")
1518
if connString == "" {
1619
t.Skipf("Skipping due to missing environment variable %v", "PGX_TEST_PGBOUNCER_CONN_STRING")
1720
}
1821

19-
config := mustParseConfig(t, connString)
20-
config.DefaultQueryExecMode = pgx.QueryExecModeCacheDescribe
21-
config.DescriptionCacheCapacity = 1024
22-
23-
testPgbouncer(t, config, 10, 100)
24-
}
25-
26-
func TestPgbouncerSimpleProtocol(t *testing.T) {
27-
connString := os.Getenv("PGX_TEST_PGBOUNCER_CONN_STRING")
28-
if connString == "" {
29-
t.Skipf("Skipping due to missing environment variable %v", "PGX_TEST_PGBOUNCER_CONN_STRING")
22+
tests := []struct {
23+
name string
24+
mode pgx.QueryExecMode
25+
statementCacheCapacity int
26+
descriptionCacheCapacity int
27+
}{
28+
{
29+
name: "cache statement",
30+
mode: pgx.QueryExecModeCacheStatement,
31+
statementCacheCapacity: 32,
32+
},
33+
{
34+
name: "cache describe",
35+
mode: pgx.QueryExecModeCacheDescribe,
36+
descriptionCacheCapacity: 32,
37+
},
38+
{
39+
name: "exec",
40+
mode: pgx.QueryExecModeExec,
41+
},
42+
{
43+
name: "simple protocol",
44+
mode: pgx.QueryExecModeSimpleProtocol,
45+
},
3046
}
3147

32-
config := mustParseConfig(t, connString)
33-
config.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
config := mustParseConfig(t, connString)
51+
config.DefaultQueryExecMode = tt.mode
52+
config.StatementCacheCapacity = tt.statementCacheCapacity
53+
config.DescriptionCacheCapacity = tt.descriptionCacheCapacity
3454

35-
testPgbouncer(t, config, 10, 100)
55+
testPgbouncer(t, config, 10, 100)
56+
})
57+
}
3658
}
3759

3860
func testPgbouncer(t *testing.T, config *pgx.ConnConfig, workers, iterations int) {
39-
doneChan := make(chan struct{})
61+
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
62+
defer cancel()
63+
64+
eg, ctx := errgroup.WithContext(ctx)
4065

4166
for range workers {
42-
go func() {
43-
defer func() { doneChan <- struct{}{} }()
44-
conn, err := pgx.ConnectConfig(context.Background(), config)
45-
require.Nil(t, err)
46-
defer closeConn(t, conn)
47-
48-
for range iterations {
49-
var i32 int32
50-
var i64 int64
51-
var f32 float32
52-
var s string
53-
var s2 string
54-
err = conn.QueryRow(context.Background(), "select 1::int4, 2::int8, 3::float4, 'hi'::text").Scan(&i32, &i64, &f32, &s)
55-
require.NoError(t, err)
56-
assert.Equal(t, int32(1), i32)
57-
assert.Equal(t, int64(2), i64)
58-
assert.Equal(t, float32(3), f32)
59-
assert.Equal(t, "hi", s)
60-
61-
err = conn.QueryRow(context.Background(), "select 1::int8, 2::float4, 'bye'::text, 4::int4, 'whatever'::text").Scan(&i64, &f32, &s, &i32, &s2)
62-
require.NoError(t, err)
63-
assert.Equal(t, int64(1), i64)
64-
assert.Equal(t, float32(2), f32)
65-
assert.Equal(t, "bye", s)
66-
assert.Equal(t, int32(4), i32)
67-
assert.Equal(t, "whatever", s2)
67+
eg.Go(func() (err error) {
68+
conn, err := pgx.ConnectConfig(ctx, config)
69+
if err != nil {
70+
return err
6871
}
69-
}()
72+
defer func() {
73+
err = errors.Join(err, conn.Close(ctx))
74+
}()
75+
76+
return exercisePgbouncerConn(ctx, conn, iterations)
77+
})
7078
}
7179

72-
for range workers {
73-
<-doneChan
80+
require.NoError(t, eg.Wait())
81+
}
82+
83+
func exercisePgbouncerConn(ctx context.Context, conn *pgx.Conn, iterations int) error {
84+
for i := range iterations {
85+
var i32 int32
86+
var i64 int64
87+
var f32 float32
88+
var s string
89+
90+
err := conn.QueryRow(ctx, "select $1::int4, $2::int8, $3::float4, $4::text", int32(i), int64(i+1), float32(i+2), "hi").Scan(&i32, &i64, &f32, &s)
91+
if err != nil {
92+
return err
93+
}
94+
if i32 != int32(i) || i64 != int64(i+1) || f32 != float32(i+2) || s != "hi" {
95+
return fmt.Errorf("unexpected query result: %d, %d, %f, %q", i32, i64, f32, s)
96+
}
97+
}
98+
99+
commandTag, err := conn.Exec(ctx, "select $1::int4", int32(42))
100+
if err != nil {
101+
return err
102+
}
103+
if commandTag.String() != "SELECT 1" {
104+
return fmt.Errorf("unexpected command tag: %s", commandTag)
105+
}
106+
107+
tx, err := conn.Begin(ctx)
108+
if err != nil {
109+
return err
110+
}
111+
var txValue int32
112+
if err := tx.QueryRow(ctx, "select $1::int4", int32(43)).Scan(&txValue); err != nil {
113+
_ = tx.Rollback(ctx)
114+
return err
115+
}
116+
if txValue != 43 {
117+
_ = tx.Rollback(ctx)
118+
return fmt.Errorf("unexpected transaction query result: %d", txValue)
119+
}
120+
if err := tx.Commit(ctx); err != nil {
121+
return err
74122
}
123+
124+
batch := &pgx.Batch{}
125+
batch.Queue("select $1::int4", int32(44))
126+
batch.Queue("select $1::text", "batch")
127+
batchResults := conn.SendBatch(ctx, batch)
128+
129+
var batchInt int32
130+
if err := batchResults.QueryRow().Scan(&batchInt); err != nil {
131+
_ = batchResults.Close()
132+
return err
133+
}
134+
var batchText string
135+
if err := batchResults.QueryRow().Scan(&batchText); err != nil {
136+
_ = batchResults.Close()
137+
return err
138+
}
139+
if err := batchResults.Close(); err != nil {
140+
return err
141+
}
142+
if batchInt != 44 || batchText != "batch" {
143+
return fmt.Errorf("unexpected batch query results: %d, %q", batchInt, batchText)
144+
}
145+
146+
return nil
75147
}

testsetup/pgbouncer-userlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"pgx_md5" "secret"

testsetup/pgbouncer.ini

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[databases]
2+
pgx_test = host=127.0.0.1 port=5432 dbname=pgx_test user=pgx_md5 password=secret
3+
4+
[pgbouncer]
5+
listen_addr = 127.0.0.1
6+
listen_port = 6432
7+
unix_socket_dir = /var/run/postgresql
8+
9+
auth_type = md5
10+
auth_file = /etc/pgbouncer/userlist.txt
11+
admin_users = pgx_md5
12+
13+
pool_mode = transaction
14+
default_pool_size = 2
15+
max_client_conn = 100
16+
max_prepared_statements = 200
17+
18+
server_tls_sslmode = disable
19+
client_tls_sslmode = disable
20+
21+
logfile = /var/log/postgresql/pgbouncer.log
22+
pidfile = /var/run/postgresql/pgbouncer.pid

0 commit comments

Comments
 (0)