1use parking_lot::RwLock;
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11use std::collections::{HashMap, VecDeque};
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tracing::{debug, info};
15
16#[derive(Debug, Clone)]
18pub struct StatementCacheConfig {
19 pub max_size: usize,
21 pub ttl_secs: u64,
23 pub enable_fingerprinting: bool,
25}
26
27impl Default for StatementCacheConfig {
28 fn default() -> Self {
29 Self {
30 max_size: 1000,
31 ttl_secs: 3600, enable_fingerprinting: true,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39struct CachedStatement {
40 sql: String,
42 #[allow(dead_code)]
44 fingerprint: String,
45 cached_at: Instant,
47 reuse_count: u64,
49 last_accessed: Instant,
51}
52
53impl CachedStatement {
54 fn new(sql: String, fingerprint: String) -> Self {
55 let now = Instant::now();
56 Self {
57 sql,
58 fingerprint,
59 cached_at: now,
60 reuse_count: 0,
61 last_accessed: now,
62 }
63 }
64
65 fn is_expired(&self, ttl: Duration) -> bool {
66 self.cached_at.elapsed() > ttl
67 }
68
69 fn touch(&mut self) {
70 self.reuse_count += 1;
71 self.last_accessed = Instant::now();
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct StatementCache {
78 config: StatementCacheConfig,
79 cache: Arc<RwLock<HashMap<String, CachedStatement>>>,
80 lru_queue: Arc<RwLock<VecDeque<String>>>,
81 stats: Arc<RwLock<CacheStats>>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct CacheStats {
87 pub hits: u64,
89 pub misses: u64,
91 pub evictions: u64,
93 pub expirations: u64,
95 pub cached_statements: usize,
97}
98
99impl CacheStats {
100 pub fn hit_rate(&self) -> f64 {
102 let total = self.hits + self.misses;
103 if total == 0 {
104 0.0
105 } else {
106 self.hits as f64 / total as f64
107 }
108 }
109
110 pub fn reset(&mut self) {
112 self.hits = 0;
113 self.misses = 0;
114 self.evictions = 0;
115 self.expirations = 0;
116 }
117}
118
119impl Default for StatementCache {
120 fn default() -> Self {
121 Self::new(StatementCacheConfig::default())
122 }
123}
124
125impl StatementCache {
126 pub fn new(config: StatementCacheConfig) -> Self {
128 Self {
129 config,
130 cache: Arc::new(RwLock::new(HashMap::new())),
131 lru_queue: Arc::new(RwLock::new(VecDeque::new())),
132 stats: Arc::new(RwLock::new(CacheStats::default())),
133 }
134 }
135
136 pub fn fingerprint(&self, sql: &str) -> String {
138 if !self.config.enable_fingerprinting {
139 return sql.to_string();
140 }
141
142 let normalized = normalize_sql(sql);
144
145 let mut hasher = Sha256::new();
147 hasher.update(normalized.as_bytes());
148 let result = hasher.finalize();
149
150 hex::encode(result.as_slice())
151 }
152
153 pub fn get(&self, sql: &str) -> Option<String> {
155 let fingerprint = self.fingerprint(sql);
156
157 let mut cache = self.cache.write();
158 let mut stats = self.stats.write();
159
160 if let Some(stmt) = cache.get_mut(&fingerprint) {
161 if stmt.is_expired(Duration::from_secs(self.config.ttl_secs)) {
163 cache.remove(&fingerprint);
164 self.remove_from_lru(&fingerprint);
165 stats.expirations += 1;
166 stats.misses += 1;
167 debug!(fingerprint = %fingerprint, "Statement expired");
168 return None;
169 }
170
171 stmt.touch();
172 stats.hits += 1;
173
174 self.update_lru(&fingerprint);
176
177 debug!(
178 fingerprint = %fingerprint,
179 reuse_count = stmt.reuse_count,
180 "Statement cache hit"
181 );
182
183 Some(stmt.sql.clone())
184 } else {
185 stats.misses += 1;
186 debug!(fingerprint = %fingerprint, "Statement cache miss");
187 None
188 }
189 }
190
191 pub fn put(&self, sql: String) {
193 let fingerprint = self.fingerprint(&sql);
194
195 let mut cache = self.cache.write();
196 let mut stats = self.stats.write();
197
198 while cache.len() >= self.config.max_size {
200 if let Some(oldest) = self.lru_queue.write().pop_back() {
201 cache.remove(&oldest);
202 stats.evictions += 1;
203 debug!(fingerprint = %oldest, "Statement evicted from cache");
204 } else {
205 break;
206 }
207 }
208
209 let stmt = CachedStatement::new(sql, fingerprint.clone());
211 cache.insert(fingerprint.clone(), stmt);
212
213 self.lru_queue.write().push_front(fingerprint.clone());
215
216 stats.cached_statements = cache.len();
217
218 debug!(
219 fingerprint = %fingerprint,
220 cache_size = cache.len(),
221 "Statement cached"
222 );
223 }
224
225 pub fn clear(&self) {
227 self.cache.write().clear();
228 self.lru_queue.write().clear();
229 self.stats.write().cached_statements = 0;
230
231 info!("Statement cache cleared");
232 }
233
234 pub fn stats(&self) -> CacheStats {
236 self.stats.read().clone()
237 }
238
239 pub fn remove_expired(&self) {
241 let ttl = Duration::from_secs(self.config.ttl_secs);
242 let mut cache = self.cache.write();
243 let mut stats = self.stats.write();
244
245 let expired: Vec<String> = cache
246 .iter()
247 .filter(|(_, stmt)| stmt.is_expired(ttl))
248 .map(|(fp, _)| fp.clone())
249 .collect();
250
251 for fp in &expired {
252 cache.remove(fp);
253 self.remove_from_lru(fp);
254 stats.expirations += 1;
255 }
256
257 stats.cached_statements = cache.len();
258
259 if !expired.is_empty() {
260 info!(count = expired.len(), "Removed expired statements");
261 }
262 }
263
264 fn update_lru(&self, fingerprint: &str) {
266 let mut queue = self.lru_queue.write();
267
268 if let Some(pos) = queue.iter().position(|fp| fp == fingerprint) {
270 queue.remove(pos);
271 }
272
273 queue.push_front(fingerprint.to_string());
275 }
276
277 fn remove_from_lru(&self, fingerprint: &str) {
279 let mut queue = self.lru_queue.write();
280 if let Some(pos) = queue.iter().position(|fp| fp == fingerprint) {
281 queue.remove(pos);
282 }
283 }
284}
285
286fn normalize_sql(sql: &str) -> String {
288 let normalized = sql
290 .split_whitespace()
291 .collect::<Vec<_>>()
292 .join(" ")
293 .to_lowercase();
294
295 replace_literals(&normalized)
299}
300
301static RE_LITERAL_STRING: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
302 regex::Regex::new(r"'[^']*'")
303 .expect("static regex pattern for SQL string literals is always valid")
304});
305
306static RE_LITERAL_NUMBER: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
307 regex::Regex::new(r"\b\d+\b")
308 .expect("static regex pattern for SQL numeric literals is always valid")
309});
310
311fn replace_literals(sql: &str) -> String {
313 let sql = RE_LITERAL_STRING.replace_all(sql, "?");
315
316 let sql = RE_LITERAL_NUMBER.replace_all(&sql, "?");
318
319 sql.to_string()
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_statement_cache_config_default() {
328 let config = StatementCacheConfig::default();
329 assert_eq!(config.max_size, 1000);
330 assert_eq!(config.ttl_secs, 3600);
331 assert!(config.enable_fingerprinting);
332 }
333
334 #[test]
335 fn test_fingerprint_generation() {
336 let cache = StatementCache::default();
337
338 let sql1 = "SELECT * FROM users WHERE id = 1";
339 let sql2 = "SELECT * FROM users WHERE id = 2";
340
341 let fp1 = cache.fingerprint(sql1);
342 let fp2 = cache.fingerprint(sql2);
343
344 assert_eq!(fp1, fp2);
346 }
347
348 #[test]
349 fn test_cache_put_and_get() {
350 let cache = StatementCache::default();
351 let sql = "SELECT * FROM users WHERE id = 1".to_string();
352
353 cache.put(sql.clone());
354
355 let result = cache.get(&sql);
356 assert!(result.is_some());
357 assert_eq!(result.unwrap(), sql);
358 }
359
360 #[test]
361 fn test_cache_miss() {
362 let cache = StatementCache::default();
363 let sql = "SELECT * FROM users WHERE id = 1";
364
365 let result = cache.get(sql);
366 assert!(result.is_none());
367 }
368
369 #[test]
370 fn test_cache_stats() {
371 let cache = StatementCache::default();
372 let sql = "SELECT * FROM users WHERE id = 1".to_string();
373
374 cache.get(&sql);
376
377 cache.put(sql.clone());
379
380 cache.get(&sql);
382 cache.get(&sql);
383
384 let stats = cache.stats();
385 assert_eq!(stats.hits, 2);
386 assert_eq!(stats.misses, 1);
387 assert_eq!(stats.cached_statements, 1);
388 }
389
390 #[test]
391 fn test_cache_hit_rate() {
392 let stats = CacheStats {
393 hits: 80,
394 misses: 20,
395 evictions: 0,
396 expirations: 0,
397 cached_statements: 100,
398 };
399
400 assert!((stats.hit_rate() - 0.8).abs() < 0.001);
401 }
402
403 #[test]
404 fn test_cache_eviction() {
405 let config = StatementCacheConfig {
406 max_size: 3,
407 enable_fingerprinting: false, ..Default::default()
409 };
410
411 let cache = StatementCache::new(config);
412
413 cache.put("SELECT 1".to_string());
414 cache.put("SELECT 2".to_string());
415 cache.put("SELECT 3".to_string());
416 cache.put("SELECT 4".to_string()); let stats = cache.stats();
419 assert_eq!(stats.cached_statements, 3);
420 assert_eq!(stats.evictions, 1);
421 }
422
423 #[test]
424 fn test_cache_clear() {
425 let cache = StatementCache::default();
426
427 cache.put("SELECT 1".to_string());
428 cache.put("SELECT 2".to_string());
429
430 cache.clear();
431
432 let stats = cache.stats();
433 assert_eq!(stats.cached_statements, 0);
434 }
435
436 #[test]
437 fn test_normalize_sql() {
438 let sql = "SELECT * FROM users WHERE id = 1";
439 let normalized = normalize_sql(sql);
440
441 assert_eq!(normalized, "select * from users where id = ?");
442 }
443
444 #[test]
445 fn test_replace_literals() {
446 let sql = "SELECT * FROM users WHERE id = 123 AND name = 'john'";
447 let replaced = replace_literals(sql);
448
449 assert!(replaced.contains("?"));
450 assert!(!replaced.contains("123"));
451 assert!(!replaced.contains("john"));
452 }
453
454 #[test]
455 fn test_statement_reuse_count() {
456 let cache = StatementCache::default();
457 let sql = "SELECT * FROM users WHERE id = 1".to_string();
458
459 cache.put(sql.clone());
460
461 for _ in 0..5 {
463 cache.get(&sql);
464 }
465
466 let fingerprint = cache.fingerprint(&sql);
467 let cached_cache = cache.cache.read();
468 let stmt = cached_cache.get(&fingerprint).unwrap();
469
470 assert_eq!(stmt.reuse_count, 5);
471 }
472
473 #[test]
474 fn test_stats_reset() {
475 let mut stats = CacheStats {
476 hits: 100,
477 misses: 50,
478 evictions: 10,
479 expirations: 5,
480 cached_statements: 200,
481 };
482
483 stats.reset();
484
485 assert_eq!(stats.hits, 0);
486 assert_eq!(stats.misses, 0);
487 assert_eq!(stats.evictions, 0);
488 assert_eq!(stats.expirations, 0);
489 }
490}