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