Skip to main content

cynos_query/
plan_cache.rs

1//! Query plan cache for avoiding repeated optimization.
2//!
3//! This module provides a simple LRU cache for compiled physical plans.
4//! When the same logical plan is executed multiple times, the cached
5//! physical plan can be reused, skipping the optimization phase.
6
7use crate::ast::Expr;
8use crate::planner::{LogicalPlan, PhysicalPlan};
9use alloc::collections::BTreeMap;
10use core::hash::Hasher;
11
12/// A simple hasher for computing plan fingerprints.
13/// Uses FNV-1a algorithm which is fast and has good distribution.
14#[derive(Default)]
15struct FnvHasher {
16    state: u64,
17}
18
19impl FnvHasher {
20    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
21    const FNV_PRIME: u64 = 0x100000001b3;
22
23    fn new() -> Self {
24        Self {
25            state: Self::FNV_OFFSET,
26        }
27    }
28}
29
30impl Hasher for FnvHasher {
31    fn finish(&self) -> u64 {
32        self.state
33    }
34
35    fn write(&mut self, bytes: &[u8]) {
36        for byte in bytes {
37            self.state ^= *byte as u64;
38            self.state = self.state.wrapping_mul(Self::FNV_PRIME);
39        }
40    }
41}
42
43/// Computes a fingerprint (hash) for a logical plan.
44/// Plans with the same structure will have the same fingerprint.
45pub fn compute_plan_fingerprint(plan: &LogicalPlan) -> u64 {
46    let mut hasher = FnvHasher::new();
47    hash_logical_plan(plan, &mut hasher);
48    hasher.finish()
49}
50
51fn hash_logical_plan<H: Hasher>(plan: &LogicalPlan, hasher: &mut H) {
52    match plan {
53        LogicalPlan::Scan { table } => {
54            hasher.write(b"scan");
55            hasher.write(table.as_bytes());
56        }
57        LogicalPlan::IndexScan {
58            table,
59            index,
60            range_start,
61            range_end,
62            include_start,
63            include_end,
64        } => {
65            hasher.write(b"index_scan");
66            hasher.write(table.as_bytes());
67            hasher.write(index.as_bytes());
68            if let Some(v) = range_start {
69                hash_value(v, hasher);
70            }
71            if let Some(v) = range_end {
72                hash_value(v, hasher);
73            }
74            hasher.write(&[*include_start as u8, *include_end as u8]);
75        }
76        LogicalPlan::IndexGet { table, index, key } => {
77            hasher.write(b"index_get");
78            hasher.write(table.as_bytes());
79            hasher.write(index.as_bytes());
80            hash_value(key, hasher);
81        }
82        LogicalPlan::IndexInGet { table, index, keys } => {
83            hasher.write(b"index_in_get");
84            hasher.write(table.as_bytes());
85            hasher.write(index.as_bytes());
86            for key in keys {
87                hash_value(key, hasher);
88            }
89        }
90        LogicalPlan::GinIndexScan {
91            table,
92            index,
93            column,
94            column_index,
95            path,
96            value,
97            query_type,
98        } => {
99            hasher.write(b"gin_index_scan");
100            hasher.write(table.as_bytes());
101            hasher.write(index.as_bytes());
102            hasher.write(column.as_bytes());
103            hasher.write(&column_index.to_le_bytes());
104            hasher.write(path.as_bytes());
105            if let Some(v) = value {
106                hash_value(v, hasher);
107            }
108            hasher.write(query_type.as_bytes());
109        }
110        LogicalPlan::GinIndexScanMulti {
111            table,
112            index,
113            column,
114            pairs,
115        } => {
116            hasher.write(b"gin_index_scan_multi");
117            hasher.write(table.as_bytes());
118            hasher.write(index.as_bytes());
119            hasher.write(column.as_bytes());
120            for (path, value) in pairs {
121                hasher.write(path.as_bytes());
122                hash_value(value, hasher);
123            }
124        }
125        LogicalPlan::Filter { input, predicate } => {
126            hasher.write(b"filter");
127            hash_logical_plan(input, hasher);
128            hash_expr(predicate, hasher);
129        }
130        LogicalPlan::Project { input, columns } => {
131            hasher.write(b"project");
132            hash_logical_plan(input, hasher);
133            for col in columns {
134                hash_expr(col, hasher);
135            }
136        }
137        LogicalPlan::Join {
138            left,
139            right,
140            condition,
141            join_type,
142        } => {
143            hasher.write(b"join");
144            hash_logical_plan(left, hasher);
145            hash_logical_plan(right, hasher);
146            hash_expr(condition, hasher);
147            hasher.write(&[*join_type as u8]);
148        }
149        LogicalPlan::Aggregate {
150            input,
151            group_by,
152            aggregates,
153        } => {
154            hasher.write(b"aggregate");
155            hash_logical_plan(input, hasher);
156            for col in group_by {
157                hash_expr(col, hasher);
158            }
159            for (func, expr) in aggregates {
160                hasher.write(&[*func as u8]);
161                hash_expr(expr, hasher);
162            }
163        }
164        LogicalPlan::Sort { input, order_by } => {
165            hasher.write(b"sort");
166            hash_logical_plan(input, hasher);
167            for (expr, order) in order_by {
168                hash_expr(expr, hasher);
169                hasher.write(&[*order as u8]);
170            }
171        }
172        LogicalPlan::Limit {
173            input,
174            limit,
175            offset,
176        } => {
177            hasher.write(b"limit");
178            hash_logical_plan(input, hasher);
179            hasher.write(&limit.to_le_bytes());
180            hasher.write(&offset.to_le_bytes());
181        }
182        LogicalPlan::CrossProduct { left, right } => {
183            hasher.write(b"cross_product");
184            hash_logical_plan(left, hasher);
185            hash_logical_plan(right, hasher);
186        }
187        LogicalPlan::Union { left, right, .. } => {
188            hasher.write(b"union");
189            hash_logical_plan(left, hasher);
190            hash_logical_plan(right, hasher);
191        }
192        LogicalPlan::Empty => {
193            hasher.write(b"empty");
194        }
195    }
196}
197
198fn hash_expr<H: Hasher>(expr: &Expr, hasher: &mut H) {
199    match expr {
200        Expr::Column(col_ref) => {
201            hasher.write(b"col");
202            hasher.write(col_ref.table.as_bytes());
203            hasher.write(col_ref.column.as_bytes());
204            hasher.write(&col_ref.index.to_le_bytes());
205        }
206        Expr::Literal(v) => {
207            hasher.write(b"lit");
208            hash_value(v, hasher);
209        }
210        Expr::BinaryOp { left, op, right } => {
211            hasher.write(b"binop");
212            hasher.write(&[*op as u8]);
213            hash_expr(left, hasher);
214            hash_expr(right, hasher);
215        }
216        Expr::UnaryOp { op, expr } => {
217            hasher.write(b"unop");
218            hasher.write(&[*op as u8]);
219            hash_expr(expr, hasher);
220        }
221        Expr::Function { name, args } => {
222            hasher.write(b"func");
223            hasher.write(name.as_bytes());
224            for arg in args {
225                hash_expr(arg, hasher);
226            }
227        }
228        Expr::Aggregate {
229            func,
230            expr,
231            distinct,
232        } => {
233            hasher.write(b"agg");
234            hasher.write(&[*func as u8]);
235            if let Some(e) = expr {
236                hash_expr(e, hasher);
237            }
238            hasher.write(&[*distinct as u8]);
239        }
240        Expr::Between { expr, low, high } => {
241            hasher.write(b"between");
242            hash_expr(expr, hasher);
243            hash_expr(low, hasher);
244            hash_expr(high, hasher);
245        }
246        Expr::NotBetween { expr, low, high } => {
247            hasher.write(b"not_between");
248            hash_expr(expr, hasher);
249            hash_expr(low, hasher);
250            hash_expr(high, hasher);
251        }
252        Expr::In { expr, list } => {
253            hasher.write(b"in");
254            hash_expr(expr, hasher);
255            for item in list {
256                hash_expr(item, hasher);
257            }
258        }
259        Expr::NotIn { expr, list } => {
260            hasher.write(b"not_in");
261            hash_expr(expr, hasher);
262            for item in list {
263                hash_expr(item, hasher);
264            }
265        }
266        Expr::Like { expr, pattern } => {
267            hasher.write(b"like");
268            hash_expr(expr, hasher);
269            hasher.write(pattern.as_bytes());
270        }
271        Expr::NotLike { expr, pattern } => {
272            hasher.write(b"not_like");
273            hash_expr(expr, hasher);
274            hasher.write(pattern.as_bytes());
275        }
276        Expr::Match { expr, pattern } => {
277            hasher.write(b"match");
278            hash_expr(expr, hasher);
279            hasher.write(pattern.as_bytes());
280        }
281        Expr::NotMatch { expr, pattern } => {
282            hasher.write(b"not_match");
283            hash_expr(expr, hasher);
284            hasher.write(pattern.as_bytes());
285        }
286    }
287}
288
289fn hash_value<H: Hasher>(value: &cynos_core::Value, hasher: &mut H) {
290    use cynos_core::Value;
291    match value {
292        Value::Null => hasher.write(b"null"),
293        Value::Boolean(b) => {
294            hasher.write(b"bool");
295            hasher.write(&[*b as u8]);
296        }
297        Value::Int32(i) => {
298            hasher.write(b"i32");
299            hasher.write(&i.to_le_bytes());
300        }
301        Value::Int64(i) => {
302            hasher.write(b"i64");
303            hasher.write(&i.to_le_bytes());
304        }
305        Value::Float64(f) => {
306            hasher.write(b"f64");
307            hasher.write(&f.to_le_bytes());
308        }
309        Value::String(s) => {
310            hasher.write(b"str");
311            hasher.write(s.as_bytes());
312        }
313        Value::DateTime(dt) => {
314            hasher.write(b"dt");
315            hasher.write(&dt.to_le_bytes());
316        }
317        Value::Bytes(b) => {
318            hasher.write(b"bytes");
319            hasher.write(b);
320        }
321        Value::Jsonb(j) => {
322            hasher.write(b"jsonb");
323            // Hash the debug representation for simplicity
324            use alloc::format;
325            let s = format!("{:?}", j);
326            hasher.write(s.as_bytes());
327        }
328    }
329}
330
331/// Cache entry with access tracking for LRU eviction.
332struct CacheEntry {
333    plan: PhysicalPlan,
334    last_access: u64,
335}
336
337/// LRU cache for compiled physical plans.
338///
339/// The cache stores physical plans keyed by their logical plan fingerprint.
340/// When the cache is full, the least recently used entry is evicted.
341pub struct PlanCache {
342    /// Cached plans indexed by fingerprint.
343    cache: BTreeMap<u64, CacheEntry>,
344    /// Maximum number of entries.
345    max_size: usize,
346    /// Global access counter for LRU tracking.
347    access_counter: u64,
348    /// Cache statistics.
349    hits: u64,
350    misses: u64,
351}
352
353impl PlanCache {
354    /// Creates a new plan cache with the given maximum size.
355    pub fn new(max_size: usize) -> Self {
356        Self {
357            cache: BTreeMap::new(),
358            max_size,
359            access_counter: 0,
360            hits: 0,
361            misses: 0,
362        }
363    }
364
365    /// Creates a plan cache with default size (64 entries).
366    pub fn default_size() -> Self {
367        Self::new(64)
368    }
369
370    /// Gets a cached plan by fingerprint, or returns None if not cached.
371    pub fn get(&mut self, fingerprint: u64) -> Option<&PhysicalPlan> {
372        self.access_counter += 1;
373        if let Some(entry) = self.cache.get_mut(&fingerprint) {
374            entry.last_access = self.access_counter;
375            self.hits += 1;
376            Some(&entry.plan)
377        } else {
378            self.misses += 1;
379            None
380        }
381    }
382
383    /// Inserts a plan into the cache.
384    /// If the cache is full, evicts the least recently used entry.
385    pub fn insert(&mut self, fingerprint: u64, plan: PhysicalPlan) {
386        // Evict if necessary
387        if self.cache.len() >= self.max_size {
388            self.evict_lru();
389        }
390
391        self.access_counter += 1;
392        self.cache.insert(
393            fingerprint,
394            CacheEntry {
395                plan,
396                last_access: self.access_counter,
397            },
398        );
399    }
400
401    /// Gets a cached plan or compiles and caches a new one.
402    pub fn get_or_insert_with<F>(&mut self, fingerprint: u64, compile: F) -> &PhysicalPlan
403    where
404        F: FnOnce() -> PhysicalPlan,
405    {
406        self.access_counter += 1;
407
408        if self.cache.contains_key(&fingerprint) {
409            let entry = self.cache.get_mut(&fingerprint).unwrap();
410            entry.last_access = self.access_counter;
411            self.hits += 1;
412            &self.cache.get(&fingerprint).unwrap().plan
413        } else {
414            self.misses += 1;
415
416            // Evict if necessary
417            if self.cache.len() >= self.max_size {
418                self.evict_lru();
419            }
420
421            let plan = compile();
422            self.cache.insert(
423                fingerprint,
424                CacheEntry {
425                    plan,
426                    last_access: self.access_counter,
427                },
428            );
429            &self.cache.get(&fingerprint).unwrap().plan
430        }
431    }
432
433    /// Evicts the least recently used entry.
434    fn evict_lru(&mut self) {
435        if self.cache.is_empty() {
436            return;
437        }
438
439        // Find the entry with the smallest last_access
440        let lru_key = self
441            .cache
442            .iter()
443            .min_by_key(|(_, entry)| entry.last_access)
444            .map(|(k, _)| *k);
445
446        if let Some(key) = lru_key {
447            self.cache.remove(&key);
448        }
449    }
450
451    /// Clears the cache.
452    pub fn clear(&mut self) {
453        self.cache.clear();
454        self.hits = 0;
455        self.misses = 0;
456    }
457
458    /// Returns the number of cached plans.
459    pub fn len(&self) -> usize {
460        self.cache.len()
461    }
462
463    /// Returns true if the cache is empty.
464    pub fn is_empty(&self) -> bool {
465        self.cache.is_empty()
466    }
467
468    /// Returns cache hit count.
469    pub fn hits(&self) -> u64 {
470        self.hits
471    }
472
473    /// Returns cache miss count.
474    pub fn misses(&self) -> u64 {
475        self.misses
476    }
477
478    /// Returns cache hit rate (0.0 to 1.0).
479    pub fn hit_rate(&self) -> f64 {
480        let total = self.hits + self.misses;
481        if total == 0 {
482            0.0
483        } else {
484            self.hits as f64 / total as f64
485        }
486    }
487
488    /// Invalidates all cached plans for a specific table.
489    /// Call this when table schema or data changes significantly.
490    pub fn invalidate_table(&mut self, _table: &str) {
491        // For simplicity, clear the entire cache.
492        // A more sophisticated implementation could track which plans
493        // reference which tables and only invalidate those.
494        self.cache.clear();
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use alloc::boxed::Box;
502    use alloc::string::String;
503
504    #[test]
505    fn test_plan_fingerprint_same_plan() {
506        let plan1 = LogicalPlan::Scan {
507            table: "users".into(),
508        };
509        let plan2 = LogicalPlan::Scan {
510            table: "users".into(),
511        };
512
513        assert_eq!(
514            compute_plan_fingerprint(&plan1),
515            compute_plan_fingerprint(&plan2)
516        );
517    }
518
519    #[test]
520    fn test_plan_fingerprint_different_plans() {
521        let plan1 = LogicalPlan::Scan {
522            table: "users".into(),
523        };
524        let plan2 = LogicalPlan::Scan {
525            table: "orders".into(),
526        };
527
528        assert_ne!(
529            compute_plan_fingerprint(&plan1),
530            compute_plan_fingerprint(&plan2)
531        );
532    }
533
534    #[test]
535    fn test_plan_fingerprint_with_filter() {
536        let plan1 = LogicalPlan::Filter {
537            input: Box::new(LogicalPlan::Scan {
538                table: "users".into(),
539            }),
540            predicate: Expr::eq(
541                Expr::column("users", "id", 0),
542                Expr::literal(cynos_core::Value::Int64(42)),
543            ),
544        };
545        let plan2 = LogicalPlan::Filter {
546            input: Box::new(LogicalPlan::Scan {
547                table: "users".into(),
548            }),
549            predicate: Expr::eq(
550                Expr::column("users", "id", 0),
551                Expr::literal(cynos_core::Value::Int64(42)),
552            ),
553        };
554
555        assert_eq!(
556            compute_plan_fingerprint(&plan1),
557            compute_plan_fingerprint(&plan2)
558        );
559    }
560
561    #[test]
562    fn test_plan_fingerprint_different_values() {
563        let plan1 = LogicalPlan::Filter {
564            input: Box::new(LogicalPlan::Scan {
565                table: "users".into(),
566            }),
567            predicate: Expr::eq(
568                Expr::column("users", "id", 0),
569                Expr::literal(cynos_core::Value::Int64(42)),
570            ),
571        };
572        let plan2 = LogicalPlan::Filter {
573            input: Box::new(LogicalPlan::Scan {
574                table: "users".into(),
575            }),
576            predicate: Expr::eq(
577                Expr::column("users", "id", 0),
578                Expr::literal(cynos_core::Value::Int64(43)),
579            ),
580        };
581
582        assert_ne!(
583            compute_plan_fingerprint(&plan1),
584            compute_plan_fingerprint(&plan2)
585        );
586    }
587
588    #[test]
589    fn test_cache_basic() {
590        let mut cache = PlanCache::new(10);
591
592        let table: String = "users".into();
593        let plan = PhysicalPlan::table_scan(table);
594        let fingerprint = 12345u64;
595
596        cache.insert(fingerprint, plan);
597
598        assert!(cache.get(fingerprint).is_some());
599        assert!(cache.get(99999).is_none());
600    }
601
602    #[test]
603    fn test_cache_lru_eviction() {
604        let mut cache = PlanCache::new(2);
605
606        let t1: String = "t1".into();
607        let t2: String = "t2".into();
608        let t3: String = "t3".into();
609
610        cache.insert(1, PhysicalPlan::table_scan(t1));
611        cache.insert(2, PhysicalPlan::table_scan(t2));
612
613        // Access entry 1 to make it more recently used
614        cache.get(1);
615
616        // Insert entry 3, should evict entry 2 (LRU)
617        cache.insert(3, PhysicalPlan::table_scan(t3));
618
619        assert!(cache.get(1).is_some());
620        assert!(cache.get(2).is_none()); // Evicted
621        assert!(cache.get(3).is_some());
622    }
623
624    #[test]
625    fn test_cache_stats() {
626        let mut cache = PlanCache::new(10);
627
628        let t1: String = "t1".into();
629        cache.insert(1, PhysicalPlan::table_scan(t1));
630
631        cache.get(1); // Hit
632        cache.get(1); // Hit
633        cache.get(2); // Miss
634
635        assert_eq!(cache.hits(), 2);
636        assert_eq!(cache.misses(), 1);
637    }
638
639    #[test]
640    fn test_cache_get_or_insert() {
641        let mut cache = PlanCache::new(10);
642
643        let fingerprint = 12345u64;
644        let mut compile_count = 0;
645
646        // First call should compile
647        let _ = cache.get_or_insert_with(fingerprint, || {
648            compile_count += 1;
649            let table: String = "users".into();
650            PhysicalPlan::table_scan(table)
651        });
652
653        // Second call should use cache
654        let _ = cache.get_or_insert_with(fingerprint, || {
655            compile_count += 1;
656            let table: String = "users".into();
657            PhysicalPlan::table_scan(table)
658        });
659
660        assert_eq!(compile_count, 1);
661        assert_eq!(cache.hits(), 1);
662        assert_eq!(cache.misses(), 1);
663    }
664}