elasticube_core/
cache.rs

1//! Query result caching for improved performance
2//!
3//! Implements an LRU (Least Recently Used) cache for query results to avoid
4//! re-executing identical queries.
5
6use crate::query::QueryResult;
7use lru::LruCache;
8use std::hash::Hash;
9use std::num::NonZeroUsize;
10use std::sync::{Arc, Mutex};
11
12/// A query cache key based on the SQL query string
13#[derive(Debug, Clone, Eq, PartialEq, Hash)]
14pub struct QueryCacheKey {
15    /// The SQL query string (normalized)
16    query: String,
17}
18
19impl QueryCacheKey {
20    /// Create a new cache key from a query string
21    pub fn new(query: impl Into<String>) -> Self {
22        let query = query.into();
23        // Normalize the query (trim whitespace, convert to lowercase)
24        let normalized = query.trim().to_lowercase();
25        Self {
26            query: normalized,
27        }
28    }
29}
30
31/// Query result cache with LRU eviction policy
32pub struct QueryCache {
33    /// LRU cache storing query results
34    cache: Arc<Mutex<LruCache<QueryCacheKey, QueryResult>>>,
35
36    /// Number of cache hits
37    hits: Arc<Mutex<usize>>,
38
39    /// Number of cache misses
40    misses: Arc<Mutex<usize>>,
41}
42
43impl QueryCache {
44    /// Create a new query cache with the specified capacity
45    ///
46    /// # Arguments
47    /// * `capacity` - Maximum number of cached query results
48    pub fn new(capacity: usize) -> Self {
49        let capacity = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap());
50
51        Self {
52            cache: Arc::new(Mutex::new(LruCache::new(capacity))),
53            hits: Arc::new(Mutex::new(0)),
54            misses: Arc::new(Mutex::new(0)),
55        }
56    }
57
58    /// Get a cached query result if it exists
59    ///
60    /// # Arguments
61    /// * `key` - The query cache key
62    ///
63    /// # Returns
64    /// Some(QueryResult) if the query is cached, None otherwise
65    pub fn get(&self, key: &QueryCacheKey) -> Option<QueryResult> {
66        let mut cache = self.cache.lock().unwrap();
67        if let Some(result) = cache.get(key) {
68            // Cache hit
69            *self.hits.lock().unwrap() += 1;
70            Some(result.clone())
71        } else {
72            // Cache miss
73            *self.misses.lock().unwrap() += 1;
74            None
75        }
76    }
77
78    /// Insert a query result into the cache
79    ///
80    /// # Arguments
81    /// * `key` - The query cache key
82    /// * `result` - The query result to cache
83    pub fn put(&self, key: QueryCacheKey, result: QueryResult) {
84        let mut cache = self.cache.lock().unwrap();
85        cache.put(key, result);
86    }
87
88    /// Clear all cached results
89    pub fn clear(&self) {
90        let mut cache = self.cache.lock().unwrap();
91        cache.clear();
92        *self.hits.lock().unwrap() = 0;
93        *self.misses.lock().unwrap() = 0;
94    }
95
96    /// Get the current cache size (number of entries)
97    pub fn len(&self) -> usize {
98        let cache = self.cache.lock().unwrap();
99        cache.len()
100    }
101
102    /// Check if the cache is empty
103    pub fn is_empty(&self) -> bool {
104        self.len() == 0
105    }
106
107    /// Get cache statistics
108    pub fn stats(&self) -> CacheStats {
109        let hits = *self.hits.lock().unwrap();
110        let misses = *self.misses.lock().unwrap();
111        let total = hits + misses;
112        let hit_rate = if total > 0 {
113            (hits as f64 / total as f64) * 100.0
114        } else {
115            0.0
116        };
117
118        CacheStats {
119            hits,
120            misses,
121            total_requests: total,
122            hit_rate,
123            entries: self.len(),
124        }
125    }
126}
127
128/// Cache statistics
129#[derive(Debug, Clone, PartialEq)]
130pub struct CacheStats {
131    /// Number of cache hits
132    pub hits: usize,
133
134    /// Number of cache misses
135    pub misses: usize,
136
137    /// Total number of requests
138    pub total_requests: usize,
139
140    /// Cache hit rate (percentage)
141    pub hit_rate: f64,
142
143    /// Current number of cached entries
144    pub entries: usize,
145}
146
147impl std::fmt::Display for CacheStats {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(
150            f,
151            "Cache Stats: {} hits, {} misses, {:.2}% hit rate, {} entries",
152            self.hits, self.misses, self.hit_rate, self.entries
153        )
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn create_dummy_result() -> QueryResult {
162        QueryResult::new_for_testing(Vec::new(), 0)
163    }
164
165    #[test]
166    fn test_cache_key_normalization() {
167        let key1 = QueryCacheKey::new("SELECT * FROM cube");
168        let key2 = QueryCacheKey::new("  select * from cube  ");
169        assert_eq!(key1, key2);
170    }
171
172    #[test]
173    fn test_cache_put_get() {
174        let cache = QueryCache::new(10);
175        let key = QueryCacheKey::new("SELECT * FROM cube");
176        let result = create_dummy_result();
177
178        cache.put(key.clone(), result.clone());
179
180        let cached = cache.get(&key);
181        assert!(cached.is_some());
182        assert_eq!(cached.unwrap().row_count(), result.row_count());
183    }
184
185    #[test]
186    fn test_cache_miss() {
187        let cache = QueryCache::new(10);
188        let key = QueryCacheKey::new("SELECT * FROM cube");
189
190        let cached = cache.get(&key);
191        assert!(cached.is_none());
192    }
193
194    #[test]
195    fn test_cache_eviction() {
196        let cache = QueryCache::new(2);
197
198        cache.put(QueryCacheKey::new("query1"), create_dummy_result());
199        cache.put(QueryCacheKey::new("query2"), create_dummy_result());
200        cache.put(QueryCacheKey::new("query3"), create_dummy_result());
201
202        // query1 should have been evicted
203        assert_eq!(cache.len(), 2);
204    }
205
206    #[test]
207    fn test_cache_clear() {
208        let cache = QueryCache::new(10);
209        cache.put(QueryCacheKey::new("query1"), create_dummy_result());
210        cache.put(QueryCacheKey::new("query2"), create_dummy_result());
211
212        assert_eq!(cache.len(), 2);
213
214        cache.clear();
215        assert_eq!(cache.len(), 0);
216        assert!(cache.is_empty());
217    }
218
219    #[test]
220    fn test_cache_stats() {
221        let cache = QueryCache::new(10);
222        let key = QueryCacheKey::new("SELECT * FROM cube");
223
224        cache.put(key.clone(), create_dummy_result());
225
226        cache.get(&key); // Hit
227        cache.get(&QueryCacheKey::new("nonexistent")); // Miss
228
229        let stats = cache.stats();
230        assert_eq!(stats.hits, 1);
231        assert_eq!(stats.misses, 1);
232        assert_eq!(stats.total_requests, 2);
233        assert_eq!(stats.hit_rate, 50.0);
234    }
235}