1use std::collections::HashMap;
16use std::hash::Hash;
17
18mod template;
19
20pub use template::{
21 bind_physical_plan_parameters, parameter_scalar_types, parameterize_logical_plan,
22 parameterize_value_list, resolve_plan_template_value, ParameterizedLogicalPlan,
23 PlanParameterCacheKey,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PlanCacheLookup {
28 Hit,
29 Miss,
30 Bypass(PlanCacheBypassReason),
31}
32
33impl PlanCacheLookup {
34 pub const fn as_str(self) -> &'static str {
35 match self {
36 Self::Hit => "hit",
37 Self::Miss => "miss",
38 Self::Bypass(_) => "bypass",
39 }
40 }
41
42 pub const fn bypass_reason(self) -> Option<PlanCacheBypassReason> {
43 match self {
44 Self::Bypass(reason) => Some(reason),
45 Self::Hit | Self::Miss => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum PlanCacheBypassReason {
52 MutationPlanning,
53 OptimizerDirective,
54 StatementNotCacheable,
55}
56
57impl PlanCacheBypassReason {
58 pub const fn as_str(self) -> &'static str {
59 match self {
60 Self::MutationPlanning => "mutation_planning",
61 Self::OptimizerDirective => "optimizer_directive",
62 Self::StatementNotCacheable => "statement_not_cacheable",
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct PlanCacheStats {
69 pub max_entries: Option<usize>,
70 pub entries: usize,
71 pub hits: u64,
72 pub misses: u64,
73 pub admissions: u64,
74 pub disabled_misses: u64,
75 pub bypasses: u64,
76 pub evictions: u64,
77 pub memory_pressure_events: u64,
78}
79
80#[derive(Debug, Clone, PartialEq)]
81struct LfuCacheEntry<V> {
82 value: V,
83 frequency: u64,
84 last_access_tick: u64,
85}
86
87#[derive(Debug, Clone)]
88pub struct LfuCache<K, V> {
89 max_entries: Option<usize>,
90 entries: HashMap<K, LfuCacheEntry<V>>,
91 access_tick: u64,
92 hits: u64,
93 misses: u64,
94 admissions: u64,
95 disabled_misses: u64,
96 bypasses: u64,
97 evictions: u64,
98 memory_pressure_events: u64,
99}
100
101impl<K, V> LfuCache<K, V>
102where
103 K: Eq + Hash + Ord + Clone,
104 V: Clone,
105{
106 pub fn new(max_entries: Option<usize>) -> Self {
107 Self {
108 max_entries,
109 entries: HashMap::new(),
110 access_tick: 0,
111 hits: 0,
112 misses: 0,
113 admissions: 0,
114 disabled_misses: 0,
115 bypasses: 0,
116 evictions: 0,
117 memory_pressure_events: 0,
118 }
119 }
120
121 pub fn get(&mut self, key: &K) -> Option<V> {
122 if self.max_entries == Some(0) {
123 self.misses += 1;
124 self.disabled_misses += 1;
125 return None;
126 }
127 self.access_tick = self.access_tick.saturating_add(1);
128 let Some(entry) = self.entries.get_mut(key) else {
129 self.misses += 1;
130 return None;
131 };
132 self.hits += 1;
133 entry.frequency = entry.frequency.saturating_add(1);
134 entry.last_access_tick = self.access_tick;
135 Some(entry.value.clone())
136 }
137
138 pub fn record_bypass(&mut self) {
139 self.bypasses = self.bypasses.saturating_add(1);
140 }
141
142 pub fn insert(&mut self, key: K, value: V) {
143 let Some(max_entries) = self.max_entries else {
144 self.insert_entry(key, value);
145 self.admissions = self.admissions.saturating_add(1);
146 return;
147 };
148 if max_entries == 0 {
149 return;
150 }
151 let new_key = !self.entries.contains_key(&key);
152 if new_key && self.entries.len() >= max_entries {
153 self.memory_pressure_events = self.memory_pressure_events.saturating_add(1);
154 }
155 self.insert_entry(key, value);
156 self.admissions = self.admissions.saturating_add(1);
157 while self.entries.len() > max_entries {
158 let Some(evicted) = self.lfu_victim_key() else {
159 break;
160 };
161 if self.entries.remove(&evicted).is_some() {
162 self.evictions += 1;
163 }
164 }
165 }
166
167 fn insert_entry(&mut self, key: K, value: V) {
168 self.access_tick = self.access_tick.saturating_add(1);
169 if let Some(entry) = self.entries.get_mut(&key) {
170 entry.value = value;
171 entry.frequency = entry.frequency.saturating_add(1);
172 entry.last_access_tick = self.access_tick;
173 return;
174 }
175 self.entries.insert(
176 key,
177 LfuCacheEntry {
178 value,
179 frequency: 1,
180 last_access_tick: self.access_tick,
181 },
182 );
183 }
184
185 fn lfu_victim_key(&self) -> Option<K> {
186 self.entries
187 .iter()
188 .min_by(|(left_key, left), (right_key, right)| {
189 (left.frequency, left.last_access_tick, *left_key).cmp(&(
190 right.frequency,
191 right.last_access_tick,
192 *right_key,
193 ))
194 })
195 .map(|(key, _)| key.clone())
196 }
197
198 pub fn stats(&self) -> PlanCacheStats {
199 PlanCacheStats {
200 max_entries: self.max_entries,
201 entries: self.entries.len(),
202 hits: self.hits,
203 misses: self.misses,
204 admissions: self.admissions,
205 disabled_misses: self.disabled_misses,
206 bypasses: self.bypasses,
207 evictions: self.evictions,
208 memory_pressure_events: self.memory_pressure_events,
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::{LfuCache, PlanCacheBypassReason, PlanCacheLookup};
216 use std::sync::{Arc, Barrier, Mutex};
217 use std::thread;
218
219 #[test]
220 fn lfu_cache_evicts_least_frequently_used_entry() {
221 let mut cache = LfuCache::new(Some(2));
222 cache.insert("hot", 1);
223 cache.insert("cold", 2);
224
225 assert_eq!(cache.get(&"hot"), Some(1));
226 cache.insert("new", 3);
227
228 assert_eq!(cache.get(&"hot"), Some(1));
229 assert_eq!(cache.get(&"new"), Some(3));
230 assert_eq!(cache.get(&"cold"), None);
231 assert_eq!(cache.stats().admissions, 3);
232 assert_eq!(cache.stats().evictions, 1);
233 assert_eq!(cache.stats().memory_pressure_events, 1);
234 }
235
236 #[test]
237 fn lfu_cache_uses_oldest_access_as_tie_breaker() {
238 let mut cache = LfuCache::new(Some(2));
239 cache.insert("older", 1);
240 cache.insert("newer", 2);
241 cache.insert("third", 3);
242
243 assert_eq!(cache.get(&"older"), None);
244 assert_eq!(cache.get(&"newer"), Some(2));
245 assert_eq!(cache.get(&"third"), Some(3));
246 }
247
248 #[test]
249 fn zero_capacity_cache_records_misses_without_entries() {
250 let mut cache = LfuCache::new(Some(0));
251 cache.insert("ignored", 1);
252
253 assert_eq!(cache.get(&"ignored"), None);
254 assert_eq!(cache.stats().entries, 0);
255 assert_eq!(cache.stats().misses, 1);
256 assert_eq!(cache.stats().disabled_misses, 1);
257 assert_eq!(cache.stats().admissions, 0);
258 assert_eq!(cache.stats().memory_pressure_events, 0);
259 }
260
261 #[test]
262 fn lookup_reports_cacheability_and_bypass_reason() {
263 let bypass = PlanCacheLookup::Bypass(PlanCacheBypassReason::OptimizerDirective);
264
265 assert_eq!(PlanCacheLookup::Hit.as_str(), "hit");
266 assert_eq!(bypass.as_str(), "bypass");
267 assert_eq!(
268 bypass.bypass_reason(),
269 Some(PlanCacheBypassReason::OptimizerDirective)
270 );
271 assert_eq!(
272 PlanCacheBypassReason::StatementNotCacheable.as_str(),
273 "statement_not_cacheable"
274 );
275 }
276
277 #[test]
278 fn hash_lookup_updates_lfu_accounting_once() {
279 let mut cache = LfuCache::new(Some(2));
280 cache.insert("query-a", 1);
281 cache.insert("query-b", 2);
282
283 assert_eq!(cache.get(&"query-b"), Some(2));
284 assert_eq!(cache.get(&"query-c"), None);
285 assert_eq!(cache.stats().hits, 1);
286 assert_eq!(cache.stats().misses, 1);
287 }
288
289 #[test]
290 fn duplicate_hash_keys_update_one_entry() {
291 let mut cache = LfuCache::new(Some(2));
292 cache.insert("query", 1);
293 cache.insert("query", 2);
294
295 assert_eq!(cache.stats().entries, 1);
296 assert_eq!(cache.get(&"query"), Some(2));
297 assert_eq!(cache.stats().entries, 1);
298 }
299
300 #[test]
301 fn cache_records_bypassed_plans_separately_from_misses() {
302 let mut cache: LfuCache<&str, i32> = LfuCache::new(Some(2));
303
304 cache.record_bypass();
305
306 assert_eq!(cache.stats().bypasses, 1);
307 assert_eq!(cache.stats().misses, 0);
308 assert_eq!(cache.stats().hits, 0);
309 assert_eq!(cache.stats().admissions, 0);
310 }
311
312 #[test]
313 fn unbounded_cache_records_admissions_without_pressure() {
314 let mut cache = LfuCache::new(None);
315
316 cache.insert("first", 1);
317 cache.insert("second", 2);
318
319 assert_eq!(cache.stats().entries, 2);
320 assert_eq!(cache.stats().admissions, 2);
321 assert_eq!(cache.stats().memory_pressure_events, 0);
322 assert_eq!(cache.stats().evictions, 0);
323 }
324
325 #[test]
326 fn cache_remains_bounded_under_mutex_protected_concurrent_access() {
327 let cache = Arc::new(Mutex::new(LfuCache::new(Some(8))));
328 let start = Arc::new(Barrier::new(4));
329
330 let handles = (0..4)
331 .map(|worker| {
332 let cache = Arc::clone(&cache);
333 let start = Arc::clone(&start);
334 thread::spawn(move || {
335 start.wait();
336 for offset in 0..64 {
337 let key = format!("key-{}", (worker + offset) % 16);
338 let mut cache = cache.lock().unwrap();
339 cache.insert(key.clone(), worker * 100 + offset);
340 let _ = cache.get(&key);
341 }
342 })
343 })
344 .collect::<Vec<_>>();
345
346 for handle in handles {
347 handle.join().unwrap();
348 }
349
350 let stats = cache.lock().unwrap().stats();
351 assert_eq!(stats.entries, 8);
352 assert_eq!(stats.admissions, 256);
353 assert!(stats.hits > 0);
354 assert!(stats.evictions > 0);
355 assert!(stats.memory_pressure_events > 0);
356 }
357}
358
359#[cfg(all(test, feature = "loom-tests"))]
360mod loom_tests {
361 use super::LfuCache;
362 use loom::sync::{Arc, Mutex};
363 use loom::thread;
364
365 #[test]
366 fn lfu_cache_preserves_bounds_under_modeled_concurrent_access() {
367 loom::model(|| {
368 let cache = Arc::new(Mutex::new(LfuCache::new(Some(2))));
369
370 let writer = {
371 let cache = Arc::clone(&cache);
372 thread::spawn(move || {
373 let mut cache = cache.lock().unwrap();
374 cache.insert("first", 1);
375 cache.insert("second", 2);
376 })
377 };
378
379 let reader_writer = {
380 let cache = Arc::clone(&cache);
381 thread::spawn(move || {
382 let mut cache = cache.lock().unwrap();
383 let _ = cache.get(&"first");
384 cache.insert("third", 3);
385 })
386 };
387
388 writer.join().unwrap();
389 reader_writer.join().unwrap();
390
391 let stats = cache.lock().unwrap().stats();
392 assert!(stats.entries <= 2);
393 assert!(stats.admissions <= 3);
394 assert!(stats.evictions <= 1);
395 assert_eq!(stats.max_entries, Some(2));
396 });
397 }
398}