Skip to content

Commit 7c37da2

Browse files
authored
Support unnamed prepared statements (#635)
* Add golang test suite to reproduce issue with unnamed parameterized prepared statements * Allow caching of unnamed prepared statements * Passthrough describe on portals * Remove unneeded kill * Update Dockerfile.ci with golang * Move out update of Dockerfiles to separate PR
1 parent b45c6b1 commit 7c37da2

File tree

8 files changed

+327
-19
lines changed

8 files changed

+327
-19
lines changed

.circleci/run_tests.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ cd ../..
108108
pip3 install -r tests/python/requirements.txt
109109
python3 tests/python/tests.py || exit 1
110110

111+
112+
#
113+
# Go tests
114+
# Starts its own pgcat server
115+
#
116+
pushd tests/go
117+
/usr/local/go/bin/go test || exit 1
118+
popd
119+
111120
start_pgcat "info"
112121

113122
# Admin tests

src/client.rs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,18 +1704,14 @@ where
17041704
/// and also the pool's statement cache. Add it to extended protocol data.
17051705
fn buffer_parse(&mut self, message: BytesMut, pool: &ConnectionPool) -> Result<(), Error> {
17061706
// Avoid parsing if prepared statements not enabled
1707-
let client_given_name = match self.prepared_statements_enabled {
1708-
true => Parse::get_name(&message)?,
1709-
false => "".to_string(),
1710-
};
1711-
1712-
if client_given_name.is_empty() {
1707+
if !self.prepared_statements_enabled {
17131708
debug!("Anonymous parse message");
17141709
self.extended_protocol_data_buffer
17151710
.push_back(ExtendedProtocolData::create_new_parse(message, None));
17161711
return Ok(());
17171712
}
17181713

1714+
let client_given_name = Parse::get_name(&message)?;
17191715
let parse: Parse = (&message).try_into()?;
17201716

17211717
// Compute the hash of the parse statement
@@ -1753,18 +1749,15 @@ where
17531749
/// saved in the client cache.
17541750
async fn buffer_bind(&mut self, message: BytesMut) -> Result<(), Error> {
17551751
// Avoid parsing if prepared statements not enabled
1756-
let client_given_name = match self.prepared_statements_enabled {
1757-
true => Bind::get_name(&message)?,
1758-
false => "".to_string(),
1759-
};
1760-
1761-
if client_given_name.is_empty() {
1752+
if !self.prepared_statements_enabled {
17621753
debug!("Anonymous bind message");
17631754
self.extended_protocol_data_buffer
17641755
.push_back(ExtendedProtocolData::create_new_bind(message, None));
17651756
return Ok(());
17661757
}
17671758

1759+
let client_given_name = Bind::get_name(&message)?;
1760+
17681761
match self.prepared_statements.get(&client_given_name) {
17691762
Some((rewritten_parse, _)) => {
17701763
let message = Bind::rename(message, &rewritten_parse.name)?;
@@ -1807,19 +1800,23 @@ where
18071800
/// saved in the client cache.
18081801
async fn buffer_describe(&mut self, message: BytesMut) -> Result<(), Error> {
18091802
// Avoid parsing if prepared statements not enabled
1810-
let describe: Describe = match self.prepared_statements_enabled {
1811-
true => (&message).try_into()?,
1812-
false => Describe::empty_new(),
1813-
};
1814-
1815-
if describe.anonymous() {
1803+
if !self.prepared_statements_enabled {
18161804
debug!("Anonymous describe message");
18171805
self.extended_protocol_data_buffer
18181806
.push_back(ExtendedProtocolData::create_new_describe(message, None));
18191807

18201808
return Ok(());
18211809
}
18221810

1811+
let describe: Describe = (&message).try_into()?;
1812+
if describe.target == 'P' {
1813+
debug!("Portal describe message");
1814+
self.extended_protocol_data_buffer
1815+
.push_back(ExtendedProtocolData::create_new_describe(message, None));
1816+
1817+
return Ok(());
1818+
}
1819+
18231820
let client_given_name = describe.statement_name.clone();
18241821

18251822
match self.prepared_statements.get(&client_given_name) {

src/messages.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,7 @@ pub struct Describe {
11091109

11101110
#[allow(dead_code)]
11111111
len: i32,
1112-
target: char,
1112+
pub target: char,
11131113
pub statement_name: String,
11141114
}
11151115

tests/go/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module pgcat
2+
3+
go 1.21
4+
5+
require github.com/lib/pq v1.10.9

tests/go/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
2+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=

tests/go/pgcat.toml

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#
2+
# PgCat config example.
3+
#
4+
5+
#
6+
# General pooler settings
7+
[general]
8+
# What IP to run on, 0.0.0.0 means accessible from everywhere.
9+
host = "0.0.0.0"
10+
11+
# Port to run on, same as PgBouncer used in this example.
12+
port = "${PORT}"
13+
14+
# Whether to enable prometheus exporter or not.
15+
enable_prometheus_exporter = true
16+
17+
# Port at which prometheus exporter listens on.
18+
prometheus_exporter_port = 9930
19+
20+
# How long to wait before aborting a server connection (ms).
21+
connect_timeout = 1000
22+
23+
# How much time to give the health check query to return with a result (ms).
24+
healthcheck_timeout = 1000
25+
26+
# How long to keep connection available for immediate re-use, without running a healthcheck query on it
27+
healthcheck_delay = 30000
28+
29+
# How much time to give clients during shutdown before forcibly killing client connections (ms).
30+
shutdown_timeout = 5000
31+
32+
# For how long to ban a server if it fails a health check (seconds).
33+
ban_time = 60 # Seconds
34+
35+
# If we should log client connections
36+
log_client_connections = false
37+
38+
# If we should log client disconnections
39+
log_client_disconnections = false
40+
41+
# Reload config automatically if it changes.
42+
autoreload = 15000
43+
44+
server_round_robin = false
45+
46+
# TLS
47+
tls_certificate = "../../.circleci/server.cert"
48+
tls_private_key = "../../.circleci/server.key"
49+
50+
# Credentials to access the virtual administrative database (pgbouncer or pgcat)
51+
# Connecting to that database allows running commands like `SHOW POOLS`, `SHOW DATABASES`, etc..
52+
admin_username = "admin_user"
53+
admin_password = "admin_pass"
54+
55+
# pool
56+
# configs are structured as pool.<pool_name>
57+
# the pool_name is what clients use as database name when connecting
58+
# For the example below a client can connect using "postgres://sharding_user:sharding_user@pgcat_host:pgcat_port/sharded_db"
59+
[pools.sharded_db]
60+
# Pool mode (see PgBouncer docs for more).
61+
# session: one server connection per connected client
62+
# transaction: one server connection per client transaction
63+
pool_mode = "transaction"
64+
65+
# If the client doesn't specify, route traffic to
66+
# this role by default.
67+
#
68+
# any: round-robin between primary and replicas,
69+
# replica: round-robin between replicas only without touching the primary,
70+
# primary: all queries go to the primary unless otherwise specified.
71+
default_role = "any"
72+
73+
# Query parser. If enabled, we'll attempt to parse
74+
# every incoming query to determine if it's a read or a write.
75+
# If it's a read query, we'll direct it to a replica. Otherwise, if it's a write,
76+
# we'll direct it to the primary.
77+
query_parser_enabled = true
78+
79+
# If the query parser is enabled and this setting is enabled, we'll attempt to
80+
# infer the role from the query itself.
81+
query_parser_read_write_splitting = true
82+
83+
# If the query parser is enabled and this setting is enabled, the primary will be part of the pool of databases used for
84+
# load balancing of read queries. Otherwise, the primary will only be used for write
85+
# queries. The primary can always be explicitely selected with our custom protocol.
86+
primary_reads_enabled = true
87+
88+
# So what if you wanted to implement a different hashing function,
89+
# or you've already built one and you want this pooler to use it?
90+
#
91+
# Current options:
92+
#
93+
# pg_bigint_hash: PARTITION BY HASH (Postgres hashing function)
94+
# sha1: A hashing function based on SHA1
95+
#
96+
sharding_function = "pg_bigint_hash"
97+
98+
# Prepared statements cache size.
99+
prepared_statements_cache_size = 500
100+
101+
# Credentials for users that may connect to this cluster
102+
[pools.sharded_db.users.0]
103+
username = "sharding_user"
104+
password = "sharding_user"
105+
# Maximum number of server connections that can be established for this user
106+
# The maximum number of connection from a single Pgcat process to any database in the cluster
107+
# is the sum of pool_size across all users.
108+
pool_size = 5
109+
statement_timeout = 0
110+
111+
112+
[pools.sharded_db.users.1]
113+
username = "other_user"
114+
password = "other_user"
115+
pool_size = 21
116+
statement_timeout = 30000
117+
118+
# Shard 0
119+
[pools.sharded_db.shards.0]
120+
# [ host, port, role ]
121+
servers = [
122+
[ "127.0.0.1", 5432, "primary" ],
123+
[ "localhost", 5432, "replica" ]
124+
]
125+
# Database name (e.g. "postgres")
126+
database = "shard0"
127+
128+
[pools.sharded_db.shards.1]
129+
servers = [
130+
[ "127.0.0.1", 5432, "primary" ],
131+
[ "localhost", 5432, "replica" ],
132+
]
133+
database = "shard1"
134+
135+
[pools.sharded_db.shards.2]
136+
servers = [
137+
[ "127.0.0.1", 5432, "primary" ],
138+
[ "localhost", 5432, "replica" ],
139+
]
140+
database = "shard2"
141+
142+
143+
[pools.simple_db]
144+
pool_mode = "session"
145+
default_role = "primary"
146+
query_parser_enabled = true
147+
query_parser_read_write_splitting = true
148+
primary_reads_enabled = true
149+
sharding_function = "pg_bigint_hash"
150+
151+
[pools.simple_db.users.0]
152+
username = "simple_user"
153+
password = "simple_user"
154+
pool_size = 5
155+
statement_timeout = 30000
156+
157+
[pools.simple_db.shards.0]
158+
servers = [
159+
[ "127.0.0.1", 5432, "primary" ],
160+
[ "localhost", 5432, "replica" ]
161+
]
162+
database = "some_db"

tests/go/prepared_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package pgcat
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
_ "github.com/lib/pq"
8+
"testing"
9+
)
10+
11+
func Test(t *testing.T) {
12+
t.Cleanup(setup(t))
13+
t.Run("Named parameterized prepared statement works", namedParameterizedPreparedStatement)
14+
t.Run("Unnamed parameterized prepared statement works", unnamedParameterizedPreparedStatement)
15+
}
16+
17+
func namedParameterizedPreparedStatement(t *testing.T) {
18+
db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=sharded_db user=sharding_user password=sharding_user sslmode=disable", port))
19+
if err != nil {
20+
t.Fatalf("could not open connection: %+v", err)
21+
}
22+
23+
stmt, err := db.Prepare("SELECT $1")
24+
25+
if err != nil {
26+
t.Fatalf("could not prepare: %+v", err)
27+
}
28+
29+
for i := 0; i < 100; i++ {
30+
rows, err := stmt.Query(1)
31+
if err != nil {
32+
t.Fatalf("could not query: %+v", err)
33+
}
34+
_ = rows.Close()
35+
}
36+
}
37+
38+
func unnamedParameterizedPreparedStatement(t *testing.T) {
39+
db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d database=sharded_db user=sharding_user password=sharding_user sslmode=disable", port))
40+
if err != nil {
41+
t.Fatalf("could not open connection: %+v", err)
42+
}
43+
44+
for i := 0; i < 100; i++ {
45+
// Under the hood QueryContext generates an unnamed parameterized prepared statement
46+
rows, err := db.QueryContext(context.Background(), "SELECT $1", 1)
47+
if err != nil {
48+
t.Fatalf("could not query: %+v", err)
49+
}
50+
_ = rows.Close()
51+
}
52+
}

0 commit comments

Comments
 (0)