Skip to main content

oxirs_arq/advanced_optimizer/
optimization_cache.rs

1//! Optimization Cache for Query Plans
2//!
3//! This module provides caching of optimization decisions and query plans
4//! to improve performance of repeated optimization operations.
5
6use std::collections::HashMap;
7use std::time::{Duration, Instant};
8
9use crate::algebra::Algebra;
10
11/// Cache for optimization decisions and plans
12#[derive(Clone)]
13pub struct OptimizationCache {
14    plan_cache: HashMap<u64, CachedPlan>,
15    decision_cache: HashMap<u64, CachedDecision>,
16    config: CacheConfig,
17    statistics: CacheStatistics,
18}
19
20/// Configuration for optimization cache
21#[derive(Debug, Clone)]
22pub struct CacheConfig {
23    pub max_plan_entries: usize,
24    pub max_decision_entries: usize,
25    pub ttl_seconds: u64,
26    pub enable_statistics: bool,
27}
28
29/// Cached query plan
30#[derive(Debug, Clone)]
31pub struct CachedPlan {
32    pub optimized_plan: Algebra,
33    pub estimated_cost: f64,
34    pub creation_time: Instant,
35    pub access_count: usize,
36    pub last_access: Instant,
37}
38
39/// Cached optimization decision
40#[derive(Debug, Clone)]
41pub struct CachedDecision {
42    pub decision_type: DecisionType,
43    pub confidence: f64,
44    pub estimated_benefit: f64,
45    pub context_hash: u64,
46    pub creation_time: Instant,
47}
48
49/// Types of optimization decisions
50#[derive(Debug, Clone)]
51pub enum DecisionType {
52    IndexSelection(String),
53    JoinAlgorithm(String),
54    StreamingStrategy(String),
55    ParallelismDegree(usize),
56    MaterializeView(String),
57}
58
59/// Cache performance statistics
60#[derive(Debug, Default, Clone)]
61pub struct CacheStatistics {
62    pub plan_hits: usize,
63    pub plan_misses: usize,
64    pub decision_hits: usize,
65    pub decision_misses: usize,
66    pub evictions: usize,
67    pub total_lookups: usize,
68}
69
70impl OptimizationCache {
71    /// Create a new optimization cache
72    pub fn new(config: CacheConfig) -> Self {
73        Self {
74            plan_cache: HashMap::new(),
75            decision_cache: HashMap::new(),
76            config,
77            statistics: CacheStatistics::default(),
78        }
79    }
80
81    /// Cache an optimized plan
82    pub fn cache_plan(&mut self, query_hash: u64, plan: Algebra, cost: f64) {
83        if self.plan_cache.len() >= self.config.max_plan_entries {
84            self.evict_least_recently_used_plan();
85        }
86
87        let cached_plan = CachedPlan {
88            optimized_plan: plan,
89            estimated_cost: cost,
90            creation_time: Instant::now(),
91            access_count: 0,
92            last_access: Instant::now(),
93        };
94
95        self.plan_cache.insert(query_hash, cached_plan);
96    }
97
98    /// Get cached plan if available and not expired
99    pub fn get_cached_plan(&mut self, query_hash: u64) -> Option<Algebra> {
100        self.statistics.total_lookups += 1;
101
102        // Check if entry exists and is not expired
103        let should_remove = if let Some(cached) = self.plan_cache.get(&query_hash) {
104            self.is_expired(cached.creation_time)
105        } else {
106            false
107        };
108
109        if should_remove {
110            self.plan_cache.remove(&query_hash);
111            self.statistics.plan_misses += 1;
112            return None;
113        }
114
115        if let Some(cached) = self.plan_cache.get_mut(&query_hash) {
116            cached.access_count += 1;
117            cached.last_access = Instant::now();
118            self.statistics.plan_hits += 1;
119            Some(cached.optimized_plan.clone())
120        } else {
121            self.statistics.plan_misses += 1;
122            None
123        }
124    }
125
126    /// Cache an optimization decision
127    pub fn cache_decision(&mut self, context_hash: u64, decision: CachedDecision) {
128        if self.decision_cache.len() >= self.config.max_decision_entries {
129            self.evict_oldest_decision();
130        }
131
132        self.decision_cache.insert(context_hash, decision);
133    }
134
135    /// Get cached decision if available
136    pub fn get_cached_decision(&mut self, context_hash: u64) -> Option<CachedDecision> {
137        self.statistics.total_lookups += 1;
138
139        // Check if entry exists and is not expired
140        let should_remove = if let Some(decision) = self.decision_cache.get(&context_hash) {
141            self.is_expired(decision.creation_time)
142        } else {
143            false
144        };
145
146        if should_remove {
147            self.decision_cache.remove(&context_hash);
148            self.statistics.decision_misses += 1;
149            return None;
150        }
151
152        if let Some(decision) = self.decision_cache.get(&context_hash) {
153            self.statistics.decision_hits += 1;
154            Some(decision.clone())
155        } else {
156            self.statistics.decision_misses += 1;
157            None
158        }
159    }
160
161    /// Get cache statistics
162    pub fn statistics(&self) -> &CacheStatistics {
163        &self.statistics
164    }
165
166    /// Clear all cached entries
167    pub fn clear(&mut self) {
168        self.plan_cache.clear();
169        self.decision_cache.clear();
170        self.statistics = CacheStatistics::default();
171    }
172
173    /// Get cache hit ratio for plans
174    pub fn plan_hit_ratio(&self) -> f64 {
175        let total = self.statistics.plan_hits + self.statistics.plan_misses;
176        if total == 0 {
177            0.0
178        } else {
179            self.statistics.plan_hits as f64 / total as f64
180        }
181    }
182
183    /// Get cache hit ratio for decisions
184    pub fn decision_hit_ratio(&self) -> f64 {
185        let total = self.statistics.decision_hits + self.statistics.decision_misses;
186        if total == 0 {
187            0.0
188        } else {
189            self.statistics.decision_hits as f64 / total as f64
190        }
191    }
192
193    /// Get overall cache hit ratio (combining plans and decisions)
194    pub fn hit_ratio(&self) -> f64 {
195        let total_hits = self.statistics.plan_hits + self.statistics.decision_hits;
196        let total_misses = self.statistics.plan_misses + self.statistics.decision_misses;
197        let total = total_hits + total_misses;
198        if total == 0 {
199            0.0
200        } else {
201            total_hits as f64 / total as f64
202        }
203    }
204
205    /// Get total number of requests made to the cache
206    pub fn total_requests(&self) -> usize {
207        self.statistics.total_lookups
208    }
209
210    fn is_expired(&self, creation_time: Instant) -> bool {
211        creation_time.elapsed() > Duration::from_secs(self.config.ttl_seconds)
212    }
213
214    fn evict_least_recently_used_plan(&mut self) {
215        if let Some((&key, _)) = self
216            .plan_cache
217            .iter()
218            .min_by_key(|(_, cached)| cached.last_access)
219        {
220            self.plan_cache.remove(&key);
221            self.statistics.evictions += 1;
222        }
223    }
224
225    fn evict_oldest_decision(&mut self) {
226        if let Some((&key, _)) = self
227            .decision_cache
228            .iter()
229            .min_by_key(|(_, decision)| decision.creation_time)
230        {
231            self.decision_cache.remove(&key);
232            self.statistics.evictions += 1;
233        }
234    }
235}
236
237impl Default for CacheConfig {
238    fn default() -> Self {
239        Self {
240            max_plan_entries: 1000,
241            max_decision_entries: 5000,
242            ttl_seconds: 3600, // 1 hour
243            enable_statistics: true,
244        }
245    }
246}