1use parking_lot::RwLock;
10use rustc_hash::FxHashMap;
11use std::collections::VecDeque;
12use std::hash::{BuildHasher, Hasher};
13use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
14use std::sync::Arc;
15
16use super::ast::Query;
17use super::error::ParseError;
18use super::Parser;
19
20#[derive(Debug, Clone, Copy, Default)]
22pub struct CacheStats {
23 pub hits: u64,
25 pub misses: u64,
27 pub evictions: u64,
29}
30
31impl CacheStats {
32 #[must_use]
34 #[allow(clippy::cast_precision_loss)]
35 pub fn hit_rate(&self) -> f64 {
36 let total = self.hits + self.misses;
37 if total == 0 {
38 return 0.0;
39 }
40 (self.hits as f64 / total as f64) * 100.0
41 }
42}
43
44pub struct QueryCache {
73 inner: RwLock<CacheInner>,
76 size: AtomicUsize,
79 max_size: usize,
81 hash_fn: fn(&str) -> u64,
83 stats: AtomicCacheStats,
85}
86
87struct CacheInner {
89 map: FxHashMap<u64, Vec<CacheEntry>>,
91 order: VecDeque<CacheKey>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97struct CacheKey {
98 hash: u64,
99 original_query: String,
100}
101
102#[derive(Debug)]
103struct CacheEntry {
104 original_query: String,
105 canonical_query: String,
106 parsed: Arc<Query>,
108 referenced: AtomicBool,
111}
112
113#[derive(Debug, Default)]
114struct AtomicCacheStats {
115 hits: AtomicU64,
116 misses: AtomicU64,
117 evictions: AtomicU64,
118}
119
120impl AtomicCacheStats {
121 fn snapshot(&self) -> CacheStats {
122 CacheStats {
123 hits: self.hits.load(Ordering::Relaxed),
124 misses: self.misses.load(Ordering::Relaxed),
125 evictions: self.evictions.load(Ordering::Relaxed),
126 }
127 }
128
129 fn clear(&self) {
130 self.hits.store(0, Ordering::Relaxed);
131 self.misses.store(0, Ordering::Relaxed);
132 self.evictions.store(0, Ordering::Relaxed);
133 }
134}
135
136impl QueryCache {
137 #[must_use]
143 pub fn new(max_size: usize) -> Self {
144 Self::new_with_hasher(max_size, default_query_hash)
145 }
146
147 fn new_with_hasher(max_size: usize, hash_fn: fn(&str) -> u64) -> Self {
148 let max_size = max_size.max(1);
149 Self {
150 inner: RwLock::new(CacheInner {
151 map: FxHashMap::default(),
152 order: VecDeque::with_capacity(max_size),
153 }),
154 size: AtomicUsize::new(0),
155 max_size,
156 hash_fn,
157 stats: AtomicCacheStats::default(),
158 }
159 }
160
161 pub fn parse(&self, query: &str) -> Result<Arc<Query>, ParseError> {
167 self.parse_impl(query, true)
168 }
169
170 #[cfg(feature = "internal-bench")]
171 pub(crate) fn parse_without_stats(&self, query: &str) -> Result<Arc<Query>, ParseError> {
172 self.parse_impl(query, false)
173 }
174
175 fn parse_impl(&self, query: &str, record_stats: bool) -> Result<Arc<Query>, ParseError> {
176 let canonical_query = canonicalize_query(query);
177 let hash = (self.hash_fn)(&canonical_query);
178
179 if let Some(cached) = self.try_cache_hit(hash, query, &canonical_query, record_stats) {
180 return Ok(cached);
181 }
182
183 let parsed = Arc::new(Parser::parse(query)?);
184 self.insert_into_cache(hash, canonical_query, query, &parsed, record_stats);
185 Ok(parsed)
186 }
187
188 fn try_cache_hit(
192 &self,
193 hash: u64,
194 original_query: &str,
195 canonical_query: &str,
196 record_stats: bool,
197 ) -> Option<Arc<Query>> {
198 let inner = self.inner.read();
199 let entry = inner.map.get(&hash).and_then(|entries| {
200 entries.iter().find(|entry| {
201 entry.original_query == original_query && entry.canonical_query == canonical_query
202 })
203 })?;
204
205 entry.referenced.store(true, Ordering::Relaxed);
208 let parsed = Arc::clone(&entry.parsed);
209 drop(inner);
210
211 if record_stats {
212 self.stats.hits.fetch_add(1, Ordering::Relaxed);
213 }
214 Some(parsed)
215 }
216
217 fn insert_into_cache(
219 &self,
220 hash: u64,
221 canonical_query: String,
222 raw_query: &str,
223 parsed: &Arc<Query>,
224 record_stats: bool,
225 ) {
226 let mut inner = self.inner.write();
227
228 if record_stats {
229 self.stats.misses.fetch_add(1, Ordering::Relaxed);
230 }
231
232 let key = CacheKey {
233 hash,
234 original_query: raw_query.to_string(),
235 };
236
237 let is_new_key = !Self::bucket_contains(&inner.map, hash, raw_query);
240 if is_new_key {
241 self.evict_until_below_bound(&mut inner, record_stats);
242 }
243
244 let new_entry = CacheEntry {
245 original_query: raw_query.to_string(),
246 canonical_query,
247 parsed: Arc::clone(parsed),
248 referenced: AtomicBool::new(false),
249 };
250
251 let bucket = inner.map.entry(hash).or_default();
252 bucket.retain(|entry| entry.original_query != raw_query);
253 bucket.push(new_entry);
254
255 if is_new_key {
256 inner.order.push_back(key);
257 self.size.fetch_add(1, Ordering::Relaxed);
258 }
259 debug_assert_eq!(self.size.load(Ordering::Relaxed), inner.order.len());
260 }
261
262 fn evict_until_below_bound(&self, inner: &mut CacheInner, record_stats: bool) {
267 while self.size.load(Ordering::Relaxed) >= self.max_size {
268 let Some(candidate) = inner.order.pop_front() else {
269 break;
270 };
271 if Self::take_second_chance(&inner.map, &candidate) {
272 inner.order.push_back(candidate);
273 continue;
274 }
275 Self::remove_entry(&mut inner.map, &candidate);
276 self.size.fetch_sub(1, Ordering::Relaxed);
277 if record_stats {
278 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
279 }
280 }
281 }
282
283 fn take_second_chance(map: &FxHashMap<u64, Vec<CacheEntry>>, key: &CacheKey) -> bool {
286 map.get(&key.hash)
287 .and_then(|bucket| {
288 bucket
289 .iter()
290 .find(|entry| entry.original_query == key.original_query)
291 })
292 .is_some_and(|entry| entry.referenced.swap(false, Ordering::Relaxed))
293 }
294
295 fn remove_entry(map: &mut FxHashMap<u64, Vec<CacheEntry>>, key: &CacheKey) {
298 if let Some(bucket) = map.get_mut(&key.hash) {
299 bucket.retain(|entry| entry.original_query != key.original_query);
300 if bucket.is_empty() {
301 map.remove(&key.hash);
302 }
303 }
304 }
305
306 fn bucket_contains(map: &FxHashMap<u64, Vec<CacheEntry>>, hash: u64, raw_query: &str) -> bool {
308 map.get(&hash)
309 .is_some_and(|bucket| bucket.iter().any(|entry| entry.original_query == raw_query))
310 }
311
312 #[must_use]
314 pub fn stats(&self) -> CacheStats {
315 self.stats.snapshot()
316 }
317
318 #[must_use]
320 pub fn len(&self) -> usize {
321 self.size.load(Ordering::Relaxed)
322 }
323
324 #[must_use]
326 pub fn is_empty(&self) -> bool {
327 self.len() == 0
328 }
329
330 pub fn clear(&self) {
332 let mut inner = self.inner.write();
333 inner.map.clear();
334 inner.order.clear();
335 self.size.store(0, Ordering::Relaxed);
336 self.stats.clear();
337 }
338}
339
340impl Default for QueryCache {
341 fn default() -> Self {
342 Self::new(1000)
343 }
344}
345
346fn default_query_hash(query: &str) -> u64 {
347 let mut hasher = rustc_hash::FxBuildHasher.build_hasher();
348 hasher.write(query.as_bytes());
349 hasher.finish()
350}
351
352fn canonicalize_query(query: &str) -> String {
353 query.split_whitespace().collect::<Vec<_>>().join(" ")
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_cache_stats_hit_rate_empty() {
362 let stats = CacheStats::default();
363 assert!((stats.hit_rate() - 0.0).abs() < 1e-5);
364 }
365
366 #[test]
367 fn test_cache_stats_hit_rate_all_hits() {
368 let stats = CacheStats {
369 hits: 10,
370 misses: 0,
371 evictions: 0,
372 };
373 assert!((stats.hit_rate() - 100.0).abs() < 1e-5);
374 }
375
376 #[test]
377 fn test_cache_stats_hit_rate_half() {
378 let stats = CacheStats {
379 hits: 5,
380 misses: 5,
381 evictions: 0,
382 };
383 assert!((stats.hit_rate() - 50.0).abs() < 1e-5);
384 }
385
386 #[test]
387 fn test_query_cache_new() {
388 let cache = QueryCache::new(100);
389 assert!(cache.is_empty());
390 assert_eq!(cache.len(), 0);
391 }
392
393 #[test]
394 fn test_query_cache_default() {
395 let cache = QueryCache::default();
396 assert!(cache.is_empty());
397 }
398
399 #[test]
400 fn test_query_cache_parse_and_hit() {
401 let cache = QueryCache::new(10);
402 let query = "SELECT * FROM docs LIMIT 5";
403
404 let result1 = cache.parse(query);
405 assert!(result1.is_ok());
406 assert_eq!(cache.stats().misses, 1);
407 assert_eq!(cache.stats().hits, 0);
408
409 let result2 = cache.parse(query);
410 assert!(result2.is_ok());
411 assert_eq!(cache.stats().hits, 1);
412 }
413
414 #[test]
415 fn test_query_cache_clear() {
416 let cache = QueryCache::new(10);
417 let _ = cache.parse("SELECT * FROM docs LIMIT 1");
418 assert!(!cache.is_empty());
419
420 cache.clear();
421 assert!(cache.is_empty());
422 assert_eq!(cache.stats().hits, 0);
423 assert_eq!(cache.stats().misses, 0);
424 }
425
426 #[test]
427 fn test_query_cache_eviction() {
428 let cache = QueryCache::new(2);
429
430 let _ = cache.parse("SELECT * FROM docs LIMIT 1");
431 let _ = cache.parse("SELECT * FROM docs LIMIT 2");
432 assert_eq!(cache.len(), 2);
433
434 let _ = cache.parse("SELECT * FROM docs LIMIT 3");
435 assert_eq!(cache.len(), 2);
436 assert!(cache.stats().evictions >= 1);
437 }
438
439 #[test]
440 fn test_query_cache_hit_keeps_clock_ring_unique() {
441 let cache = QueryCache::new(3);
445 let q1 = "SELECT * FROM docs LIMIT 1";
446 let q2 = "SELECT * FROM docs LIMIT 2";
447 let q3 = "SELECT * FROM docs LIMIT 3";
448
449 let _ = cache.parse(q1);
450 let _ = cache.parse(q2);
451 let _ = cache.parse(q3);
452 let _ = cache.parse(q1); let inner = cache.inner.read();
455 assert_eq!(inner.order.len(), cache.len());
456 assert_eq!(
457 inner
458 .order
459 .iter()
460 .filter(|v| v.original_query.as_str() == q1)
461 .count(),
462 1,
463 "no duplicate ring entries on hit"
464 );
465 }
466
467 #[test]
468 fn test_query_cache_clock_referenced_entry_survives_eviction() {
469 let cache = QueryCache::new(2);
472 let q1 = "SELECT * FROM docs LIMIT 1";
473 let q2 = "SELECT * FROM docs LIMIT 2";
474 let q3 = "SELECT * FROM docs LIMIT 3";
475
476 let _ = cache.parse(q1);
477 let _ = cache.parse(q2);
478 let _ = cache.parse(q1); let _ = cache.parse(q3); assert_eq!(cache.len(), 2);
482 let hits_before = cache.stats().hits;
484 let _ = cache.parse(q1);
485 assert_eq!(cache.stats().hits, hits_before + 1, "q1 must survive");
486 }
487
488 #[test]
489 fn test_query_cache_hit_path_takes_no_write_lock() {
490 use std::sync::Arc;
494 use std::thread;
495
496 let cache = Arc::new(QueryCache::new(10));
497 let q = "SELECT * FROM docs LIMIT 1";
498 let _ = cache.parse(q); let held = cache.inner.read(); let cache2 = Arc::clone(&cache);
503 let handle = thread::spawn(move || cache2.parse(q).map(|_| ()));
504
505 let res = handle
508 .join()
509 .expect("hit thread must finish without deadlock");
510 assert!(res.is_ok());
511 drop(held);
512 }
513
514 #[test]
515 fn test_query_cache_hit_returns_shared_arc() {
516 let cache = QueryCache::new(10);
518 let q = "SELECT * FROM docs LIMIT 1";
519
520 let first = cache.parse(q).expect("parse");
521 let second = cache.parse(q).expect("hit");
522
523 assert!(
524 Arc::ptr_eq(&first, &second),
525 "hit must return the same Arc allocation (no deep clone)"
526 );
527 assert!(Arc::strong_count(&first) >= 3);
529 }
530
531 #[test]
532 fn test_query_cache_concurrent_invariant_no_order_duplicates() {
533 use std::sync::Arc;
534 use std::thread;
535
536 let cache = Arc::new(QueryCache::new(32));
537 let queries = [
538 "SELECT * FROM docs LIMIT 1",
539 "SELECT * FROM docs LIMIT 2",
540 "SELECT * FROM docs LIMIT 3",
541 "SELECT * FROM docs LIMIT 4",
542 "SELECT * FROM docs LIMIT 5",
543 ];
544
545 let mut handles = Vec::new();
546 for _ in 0..8 {
547 let cache = Arc::clone(&cache);
548 handles.push(thread::spawn(move || {
549 for i in 0..200 {
550 let q = queries[i % queries.len()];
551 let _ = cache.parse(q);
552 }
553 }));
554 }
555
556 for h in handles {
557 h.join().expect("thread must complete");
558 }
559
560 let inner = cache.inner.read();
561 let mut uniq = std::collections::HashSet::new();
562 for key in &inner.order {
563 assert!(uniq.insert(key.clone()), "duplicate query in CLOCK ring");
564 }
565 assert_eq!(inner.order.len(), cache.len());
566 }
567
568 #[test]
569 fn test_query_cache_collision_safe_with_forced_hash_collision() {
570 let cache = QueryCache::new_with_hasher(10, |_| 42);
571 let q1 = "SELECT * FROM docs LIMIT 1";
572 let q2 = "SELECT id FROM docs LIMIT 2";
573
574 let r1 = cache.parse(q1).expect("q1 should parse");
575 let r2 = cache.parse(q2).expect("q2 should parse");
576 let r1_again = cache.parse(q1).expect("q1 should be cache hit");
577
578 assert_eq!(r1, r1_again);
579 assert_ne!(r1, r2);
580 assert_eq!(cache.len(), 2);
581 }
582
583 #[test]
584 fn test_query_cache_min_size() {
585 let cache = QueryCache::new(0);
586 let _ = cache.parse("SELECT * FROM docs LIMIT 1");
587 assert!(!cache.is_empty());
588 }
589
590 #[test]
591 fn test_query_cache_invalid_query() {
592 let cache = QueryCache::new(10);
593 let result = cache.parse("INVALID QUERY SYNTAX!!!");
594 assert!(result.is_err());
595 }
596}