1use std::sync::Arc;
4
5use deadpool_postgres::Object;
6use tokio_postgres::Row;
7use tracing::{debug, trace};
8
9use prax_query::sql::is_valid_sql_identifier;
10
11use crate::error::{PgError, PgResult};
12use crate::statement::PreparedStatementCache;
13
14pub struct PgConnection {
16 client: Object,
17 statement_cache: Arc<PreparedStatementCache>,
18}
19
20fn is_stale_cached_plan(err: &tokio_postgres::Error) -> bool {
31 match err.as_db_error() {
35 Some(db) => {
36 db.code() == &tokio_postgres::error::SqlState::FEATURE_NOT_SUPPORTED
37 && is_stale_cached_plan_message(db.message())
38 }
39 None => false,
40 }
41}
42
43fn is_stale_cached_plan_message(msg: &str) -> bool {
47 msg.contains("cached plan must not change result type")
48}
49
50impl PgConnection {
51 pub(crate) fn new(client: Object, statement_cache: Arc<PreparedStatementCache>) -> Self {
53 Self {
54 client,
55 statement_cache,
56 }
57 }
58
59 pub async fn query(
61 &self,
62 sql: &str,
63 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
64 ) -> PgResult<Vec<Row>> {
65 trace!(sql = %sql, "Executing query");
66
67 let stmt = self
69 .statement_cache
70 .get_or_prepare(&self.client, sql)
71 .await?;
72
73 match self.client.query(&stmt, params).await {
74 Ok(rows) => Ok(rows),
75 Err(e) if is_stale_cached_plan(&e) => {
76 let stmt = self.reprepare_after_stale_plan(sql).await?;
77 let rows = self.client.query(&stmt, params).await?;
78 Ok(rows)
79 }
80 Err(e) => Err(e.into()),
81 }
82 }
83
84 pub async fn query_one(
86 &self,
87 sql: &str,
88 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
89 ) -> PgResult<Row> {
90 trace!(sql = %sql, "Executing query_one");
91
92 let stmt = self
93 .statement_cache
94 .get_or_prepare(&self.client, sql)
95 .await?;
96
97 match self.client.query_one(&stmt, params).await {
98 Ok(row) => Ok(row),
99 Err(e) if is_stale_cached_plan(&e) => {
100 let stmt = self.reprepare_after_stale_plan(sql).await?;
101 let row = self.client.query_one(&stmt, params).await?;
102 Ok(row)
103 }
104 Err(e) => Err(e.into()),
105 }
106 }
107
108 pub async fn query_opt(
110 &self,
111 sql: &str,
112 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
113 ) -> PgResult<Option<Row>> {
114 trace!(sql = %sql, "Executing query_opt");
115
116 let stmt = self
117 .statement_cache
118 .get_or_prepare(&self.client, sql)
119 .await?;
120
121 match self.client.query_opt(&stmt, params).await {
122 Ok(row) => Ok(row),
123 Err(e) if is_stale_cached_plan(&e) => {
124 let stmt = self.reprepare_after_stale_plan(sql).await?;
125 let row = self.client.query_opt(&stmt, params).await?;
126 Ok(row)
127 }
128 Err(e) => Err(e.into()),
129 }
130 }
131
132 pub async fn execute(
134 &self,
135 sql: &str,
136 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
137 ) -> PgResult<u64> {
138 trace!(sql = %sql, "Executing statement");
139
140 let stmt = self
141 .statement_cache
142 .get_or_prepare(&self.client, sql)
143 .await?;
144
145 match self.client.execute(&stmt, params).await {
146 Ok(count) => Ok(count),
147 Err(e) if is_stale_cached_plan(&e) => {
148 let stmt = self.reprepare_after_stale_plan(sql).await?;
149 let count = self.client.execute(&stmt, params).await?;
150 Ok(count)
151 }
152 Err(e) => Err(e.into()),
153 }
154 }
155
156 async fn reprepare_after_stale_plan(&self, sql: &str) -> PgResult<tokio_postgres::Statement> {
165 debug!(
166 sql = %sql,
167 "Recovering from stale cached plan (0A000): re-preparing statement"
168 );
169 self.statement_cache.evict(sql);
170 let stmt = self.client.prepare(sql).await?;
171 Ok(stmt)
172 }
173
174 pub async fn batch_execute(&self, sql: &str) -> PgResult<()> {
176 trace!(sql = %sql, "Executing batch");
177 self.client.batch_execute(sql).await?;
178 Ok(())
179 }
180
181 pub async fn transaction(&mut self) -> PgResult<PgTransaction<'_>> {
183 debug!("Beginning transaction");
184 let txn = self.client.transaction().await?;
185 Ok(PgTransaction {
186 txn,
187 statement_cache: self.statement_cache.clone(),
188 })
189 }
190
191 pub fn inner(&self) -> &Object {
195 &self.client
196 }
197
198 #[inline]
204 pub async fn query_cached(
205 &self,
206 sql: &str,
207 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
208 ) -> PgResult<Vec<Row>> {
209 self.query(sql, params).await
210 }
211
212 pub async fn query_raw(
217 &self,
218 sql: &str,
219 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
220 ) -> PgResult<Vec<Row>> {
221 trace!(sql = %sql, "Executing raw query (no statement cache)");
222 let rows = self.client.query(sql, params).await?;
223 Ok(rows)
224 }
225
226 pub async fn query_opt_raw(
228 &self,
229 sql: &str,
230 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
231 ) -> PgResult<Option<Row>> {
232 trace!(sql = %sql, "Executing raw query_opt (no statement cache)");
233 let row = self.client.query_opt(sql, params).await?;
234 Ok(row)
235 }
236}
237
238const MAX_SAVEPOINT_NAME_LEN: usize = 63;
240
241fn validate_savepoint_name(name: &str) -> PgResult<()> {
246 let valid = name.len() <= MAX_SAVEPOINT_NAME_LEN && is_valid_sql_identifier(name);
247 if !valid {
248 return Err(PgError::query(format!("invalid savepoint name: {name:?}")));
249 }
250 Ok(())
251}
252
253pub struct PgTransaction<'a> {
255 txn: deadpool_postgres::Transaction<'a>,
256 statement_cache: Arc<PreparedStatementCache>,
257}
258
259impl<'a> PgTransaction<'a> {
260 pub async fn query(
270 &self,
271 sql: &str,
272 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
273 ) -> PgResult<Vec<Row>> {
274 trace!(sql = %sql, "Executing query in transaction");
275
276 let stmt = self
277 .statement_cache
278 .get_or_prepare_in_txn(&self.txn, sql)
279 .await?;
280
281 let rows = self.txn.query(&stmt, params).await?;
282 Ok(rows)
283 }
284
285 pub async fn query_one(
287 &self,
288 sql: &str,
289 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
290 ) -> PgResult<Row> {
291 let stmt = self
292 .statement_cache
293 .get_or_prepare_in_txn(&self.txn, sql)
294 .await?;
295
296 let row = self.txn.query_one(&stmt, params).await?;
297 Ok(row)
298 }
299
300 pub async fn query_opt(
302 &self,
303 sql: &str,
304 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
305 ) -> PgResult<Option<Row>> {
306 let stmt = self
307 .statement_cache
308 .get_or_prepare_in_txn(&self.txn, sql)
309 .await?;
310
311 let row = self.txn.query_opt(&stmt, params).await?;
312 Ok(row)
313 }
314
315 pub async fn execute(
317 &self,
318 sql: &str,
319 params: &[&(dyn tokio_postgres::types::ToSql + Sync)],
320 ) -> PgResult<u64> {
321 let stmt = self
322 .statement_cache
323 .get_or_prepare_in_txn(&self.txn, sql)
324 .await?;
325
326 let count = self.txn.execute(&stmt, params).await?;
327 Ok(count)
328 }
329
330 pub async fn savepoint(&mut self, name: &str) -> PgResult<()> {
332 validate_savepoint_name(name)?;
333 debug!(name = %name, "Creating savepoint");
334 self.txn
335 .batch_execute(&format!("SAVEPOINT {}", name))
336 .await?;
337 Ok(())
338 }
339
340 pub async fn rollback_to(&mut self, name: &str) -> PgResult<()> {
342 validate_savepoint_name(name)?;
343 debug!(name = %name, "Rolling back to savepoint");
344 self.txn
345 .batch_execute(&format!("ROLLBACK TO SAVEPOINT {}", name))
346 .await?;
347 Ok(())
348 }
349
350 pub async fn release_savepoint(&mut self, name: &str) -> PgResult<()> {
352 validate_savepoint_name(name)?;
353 debug!(name = %name, "Releasing savepoint");
354 self.txn
355 .batch_execute(&format!("RELEASE SAVEPOINT {}", name))
356 .await?;
357 Ok(())
358 }
359
360 pub async fn commit(self) -> PgResult<()> {
362 debug!("Committing transaction");
363 self.txn.commit().await?;
364 Ok(())
365 }
366
367 pub async fn rollback(self) -> PgResult<()> {
369 debug!("Rolling back transaction");
370 self.txn.rollback().await?;
371 Ok(())
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
383 fn test_stale_cached_plan_message_gate() {
384 assert!(is_stale_cached_plan_message(
386 "db error: ERROR: cached plan must not change result type"
387 ));
388 assert!(!is_stale_cached_plan_message(
391 "ERROR: cannot insert into view \"v\""
392 ));
393 assert!(!is_stale_cached_plan_message("some unrelated error"));
394 }
395
396 #[test]
397 fn test_validate_savepoint_name_accepts_valid_names() {
398 assert!(validate_savepoint_name("sp1").is_ok());
399 assert!(validate_savepoint_name("my_savepoint").is_ok());
400 assert!(validate_savepoint_name("_private").is_ok());
401 assert!(validate_savepoint_name("SP_2").is_ok());
402 assert!(validate_savepoint_name("a").is_ok());
403 let max_name = "a".repeat(MAX_SAVEPOINT_NAME_LEN);
405 assert!(validate_savepoint_name(&max_name).is_ok());
406 }
407
408 #[test]
409 fn test_validate_savepoint_name_rejects_invalid_names() {
410 assert!(validate_savepoint_name("sp1; DROP TABLE").is_err());
411 assert!(validate_savepoint_name("my savepoint").is_err());
412 assert!(validate_savepoint_name("\"quoted\"").is_err());
413 assert!(validate_savepoint_name("").is_err());
414 assert!(validate_savepoint_name("1leading_digit").is_err());
415 assert!(validate_savepoint_name("has-dash").is_err());
416 assert!(validate_savepoint_name("has.dot").is_err());
417 let too_long = "a".repeat(MAX_SAVEPOINT_NAME_LEN + 1);
419 assert!(validate_savepoint_name(&too_long).is_err());
420 }
421}