prax_postgres/
statement.rs1use std::num::NonZeroUsize;
4use std::sync::Mutex;
5
6use deadpool_postgres::{Object, Transaction};
7use lru::LruCache;
8use tokio_postgres::Statement;
9use tracing::{debug, trace};
10
11use crate::error::PgResult;
12
13pub struct PreparedStatementCache {
24 max_size: usize,
25 prepared_queries: Mutex<LruCache<String, ()>>,
32}
33
34impl PreparedStatementCache {
35 pub fn new(max_size: usize) -> Self {
39 let cap = NonZeroUsize::new(max_size.max(1)).expect("max(1) ensures non-zero");
40 Self {
41 max_size,
42 prepared_queries: Mutex::new(LruCache::new(cap)),
43 }
44 }
45
46 pub async fn get_or_prepare(&self, client: &Object, sql: &str) -> PgResult<Statement> {
48 let is_cached = {
49 let mut cache = self
50 .prepared_queries
51 .lock()
52 .unwrap_or_else(|e| e.into_inner());
53 if cache.get(sql).is_some() {
54 true
55 } else {
56 cache.put(sql.to_string(), ());
57 false
58 }
59 };
60
61 if is_cached {
62 trace!(sql = %sql, "Using cached prepared statement");
63 } else {
64 trace!(sql = %sql, "Preparing new statement");
65 }
66
67 let stmt = client.prepare_cached(sql).await?;
69 Ok(stmt)
70 }
71
72 pub async fn get_or_prepare_in_txn<'a>(
74 &self,
75 txn: &Transaction<'a>,
76 sql: &str,
77 ) -> PgResult<Statement> {
78 let is_cached = {
79 let mut cache = self
80 .prepared_queries
81 .lock()
82 .unwrap_or_else(|e| e.into_inner());
83 if cache.get(sql).is_some() {
84 true
85 } else {
86 cache.put(sql.to_string(), ());
87 false
88 }
89 };
90
91 if is_cached {
92 trace!(sql = %sql, "Using cached prepared statement (txn)");
93 } else {
94 trace!(sql = %sql, "Preparing new statement (txn)");
95 }
96
97 let stmt = txn.prepare_cached(sql).await?;
98 Ok(stmt)
99 }
100
101 pub fn clear(&self) {
103 let mut cache = self
104 .prepared_queries
105 .lock()
106 .unwrap_or_else(|e| e.into_inner());
107 cache.clear();
108 debug!("Statement cache cleared");
109 }
110
111 pub fn evict(&self, sql: &str) -> bool {
121 let mut cache = self
122 .prepared_queries
123 .lock()
124 .unwrap_or_else(|e| e.into_inner());
125 let existed = cache.pop(sql).is_some();
126 if existed {
127 debug!(sql = %sql, "Evicted stale prepared statement from cache");
128 }
129 existed
130 }
131
132 pub fn len(&self) -> usize {
134 let cache = self
135 .prepared_queries
136 .lock()
137 .unwrap_or_else(|e| e.into_inner());
138 cache.len()
139 }
140
141 pub fn is_empty(&self) -> bool {
143 self.len() == 0
144 }
145
146 pub fn max_size(&self) -> usize {
148 self.max_size
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_cache_creation() {
158 let cache = PreparedStatementCache::new(100);
159 assert_eq!(cache.max_size(), 100);
160 assert!(cache.is_empty());
161 }
162
163 #[test]
164 fn test_cache_clear() {
165 let cache = PreparedStatementCache::new(100);
166
167 {
169 let mut inner = cache.prepared_queries.lock().unwrap();
170 inner.put("SELECT 1".to_string(), ());
171 inner.put("SELECT 2".to_string(), ());
172 }
173
174 assert_eq!(cache.len(), 2);
175 cache.clear();
176 assert!(cache.is_empty());
177 }
178
179 #[test]
180 fn test_cache_lru_eviction() {
181 let cache = PreparedStatementCache::new(2);
182 {
183 let mut inner = cache.prepared_queries.lock().unwrap();
184 inner.put("A".to_string(), ());
185 inner.put("B".to_string(), ());
186 let _ = inner.get("A");
188 inner.put("C".to_string(), ());
189 }
190 let inner = cache.prepared_queries.lock().unwrap();
191 assert_eq!(inner.len(), 2);
192 assert!(inner.peek("A").is_some());
193 assert!(inner.peek("B").is_none(), "B should have been evicted");
194 assert!(inner.peek("C").is_some());
195 }
196
197 #[test]
198 fn test_evict_removes_only_the_named_sql() {
199 let cache = PreparedStatementCache::new(10);
200 {
201 let mut inner = cache.prepared_queries.lock().unwrap();
202 inner.put("SELECT 1".to_string(), ());
203 inner.put("SELECT 2".to_string(), ());
204 }
205 assert!(cache.evict("SELECT 1"));
207 {
208 let inner = cache.prepared_queries.lock().unwrap();
209 assert!(inner.peek("SELECT 1").is_none());
210 assert!(inner.peek("SELECT 2").is_some());
211 }
212 assert!(!cache.evict("SELECT 1"));
214 assert!(!cache.evict("never cached"));
215 }
216}