cufflink_pgcodec/
pooler.rs1use crate::statement::{changes_session, requires_autocommit};
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub enum Pooler {
10 #[default]
11 Direct,
12 PgBouncerTransaction,
16}
17
18impl FromStr for Pooler {
19 type Err = String;
20
21 fn from_str(s: &str) -> Result<Self, Self::Err> {
22 match s.trim().to_ascii_lowercase().as_str() {
23 "direct" => Ok(Pooler::Direct),
24 "pgbouncer_transaction" => Ok(Pooler::PgBouncerTransaction),
25 other => Err(format!(
26 "'{other}' is not one of direct, pgbouncer_transaction"
27 )),
28 }
29 }
30}
31
32impl Pooler {
33 pub fn is_transaction_pooler(self) -> bool {
36 self == Pooler::PgBouncerTransaction
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum PoolerRefusal {
43 Autocommit,
47 SessionState,
51}
52
53impl PoolerRefusal {
54 pub fn message(self) -> &'static str {
55 match self {
56 PoolerRefusal::Autocommit => {
57 "a statement that cannot run inside a transaction is refused under \
58 TENANT_DB_POOLER=pgbouncer_transaction; run it in a deploy-time migration"
59 }
60 PoolerRefusal::SessionState => {
61 "SET and RESET are refused under TENANT_DB_POOLER=pgbouncer_transaction; \
62 use SET LOCAL inside a transaction"
63 }
64 }
65 }
66}
67
68pub fn pooler_refusal(pooler: Pooler, sql: &str) -> Option<PoolerRefusal> {
70 if !pooler.is_transaction_pooler() {
71 None
72 } else if requires_autocommit(sql) {
73 Some(PoolerRefusal::Autocommit)
74 } else if changes_session(sql) {
75 Some(PoolerRefusal::SessionState)
76 } else {
77 None
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn a_pooler_is_named_in_any_case() {
87 assert_eq!(
88 "PgBouncer_Transaction".parse::<Pooler>(),
89 Ok(Pooler::PgBouncerTransaction)
90 );
91 assert_eq!(" direct ".parse::<Pooler>(), Ok(Pooler::Direct));
92 assert!("session".parse::<Pooler>().is_err());
93 assert!(Pooler::PgBouncerTransaction.is_transaction_pooler());
94 assert!(!Pooler::Direct.is_transaction_pooler());
95 assert_eq!(Pooler::default(), Pooler::Direct);
96 }
97
98 #[test]
99 fn a_transaction_pooler_refuses_autocommit_and_session_statements_only() {
100 let pooled = |sql| pooler_refusal(Pooler::PgBouncerTransaction, sql);
101 let concurrently = "CREATE INDEX CONCURRENTLY i ON t (c)";
102 assert_eq!(pooled(concurrently), Some(PoolerRefusal::Autocommit));
103 assert_eq!(pooled("SET ROLE other"), Some(PoolerRefusal::SessionState));
104 assert_eq!(pooled("SELECT 1"), None);
105 assert_eq!(pooled("SET LOCAL app.x = '1'"), None);
106 for sql in [concurrently, "SET ROLE other", "RESET ALL"] {
107 assert_eq!(pooler_refusal(Pooler::Direct, sql), None, "{sql}");
108 }
109 }
110}