Skip to main content

oxirs_samm/
query_cache.rs

1//! Query Result Caching
2//!
3//! This module provides caching for expensive query operations on SAMM models.
4//! It wraps `ModelQuery` and caches results to avoid repeated computations.
5//!
6//! # Features
7//!
8//! - **Automatic Caching**: Transparently caches query results
9//! - **LRU Eviction**: Least recently used results are evicted when cache is full
10//! - **Cache Statistics**: Track hit rates and performance
11//! - **Configurable Size**: Control memory usage
12//!
13//! # Examples
14//!
15//! ```rust
16//! use oxirs_samm::query_cache::CachedModelQuery;
17//! use oxirs_samm::metamodel::Aspect;
18//!
19//! # fn example(aspect: &Aspect) {
20//! let mut query = CachedModelQuery::new(aspect, 100); // Cache up to 100 results
21//!
22//! // First call computes result
23//! let metrics1 = query.complexity_metrics();
24//!
25//! // Second call returns cached result (much faster)
26//! let metrics2 = query.complexity_metrics();
27//!
28//! // Check cache performance
29//! let stats = query.cache_statistics();
30//! println!("Hit rate: {:.2}%", stats.hit_rate * 100.0);
31//! # }
32//! ```
33
34use crate::metamodel::{Aspect, ModelElement, Property};
35use crate::query::{ComplexityMetrics, Dependency, ModelQuery};
36use std::collections::HashMap;
37use std::sync::{Arc, RwLock};
38
39/// Cache key for query results
40#[derive(Debug, Clone, Hash, Eq, PartialEq)]
41enum CacheKey {
42    ComplexityMetrics,
43    OptionalProperties,
44    RequiredProperties,
45    CollectionProperties,
46    DependencyGraph,
47    CircularDependencies,
48    FindByType(String),
49    FindByNamespace(String),
50}
51
52/// Cached model query wrapper
53pub struct CachedModelQuery<'a> {
54    query: ModelQuery<'a>,
55    cache: Arc<RwLock<HashMap<CacheKey, CachedValue>>>,
56    hits: Arc<RwLock<usize>>,
57    misses: Arc<RwLock<usize>>,
58    max_cache_size: usize,
59}
60
61/// Cached value with access tracking
62#[derive(Clone)]
63struct CachedValue {
64    value: CachedResult,
65    access_count: usize,
66    last_accessed: std::time::Instant,
67}
68
69/// Union type for different cached results
70#[derive(Clone)]
71enum CachedResult {
72    ComplexityMetrics(ComplexityMetrics),
73    Properties(Vec<String>), // Store URNs instead of references
74    Dependencies(Vec<Dependency>),
75}
76
77impl<'a> CachedModelQuery<'a> {
78    /// Create a new cached query wrapper
79    ///
80    /// # Arguments
81    ///
82    /// * `aspect` - The aspect to query
83    /// * `max_cache_size` - Maximum number of cached results
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// use oxirs_samm::query_cache::CachedModelQuery;
89    /// # use oxirs_samm::metamodel::Aspect;
90    /// # fn example(aspect: &Aspect) {
91    /// let query = CachedModelQuery::new(aspect, 50);
92    /// # }
93    /// ```
94    pub fn new(aspect: &'a Aspect, max_cache_size: usize) -> Self {
95        Self {
96            query: ModelQuery::new(aspect),
97            cache: Arc::new(RwLock::new(HashMap::new())),
98            hits: Arc::new(RwLock::new(0)),
99            misses: Arc::new(RwLock::new(0)),
100            max_cache_size: max_cache_size.max(1),
101        }
102    }
103
104    /// Get complexity metrics (cached)
105    pub fn complexity_metrics(&mut self) -> ComplexityMetrics {
106        let key = CacheKey::ComplexityMetrics;
107
108        if let Some(CachedResult::ComplexityMetrics(metrics)) = self.get_cached(&key) {
109            return metrics;
110        }
111
112        // Compute and cache
113        let metrics = self.query.complexity_metrics();
114        self.cache_result(key, CachedResult::ComplexityMetrics(metrics.clone()));
115        metrics
116    }
117
118    /// Find optional properties (cached)
119    pub fn find_optional_properties(&mut self) -> Vec<&Property> {
120        let key = CacheKey::OptionalProperties;
121
122        if let Some(CachedResult::Properties(urns)) = self.get_cached(&key) {
123            // Convert URNs back to references
124            return self
125                .query
126                .aspect()
127                .properties()
128                .iter()
129                .filter(|p| urns.contains(&p.urn().to_string()))
130                .collect();
131        }
132
133        // Compute and cache
134        let props = self.query.find_optional_properties();
135        let urns: Vec<String> = props.iter().map(|p| p.urn().to_string()).collect();
136        self.cache_result(key, CachedResult::Properties(urns));
137        props
138    }
139
140    /// Find required properties (cached)
141    pub fn find_required_properties(&mut self) -> Vec<&Property> {
142        let key = CacheKey::RequiredProperties;
143
144        if let Some(CachedResult::Properties(urns)) = self.get_cached(&key) {
145            return self
146                .query
147                .aspect()
148                .properties()
149                .iter()
150                .filter(|p| urns.contains(&p.urn().to_string()))
151                .collect();
152        }
153
154        let props = self.query.find_required_properties();
155        let urns: Vec<String> = props.iter().map(|p| p.urn().to_string()).collect();
156        self.cache_result(key, CachedResult::Properties(urns));
157        props
158    }
159
160    /// Find collection properties (cached)
161    pub fn find_properties_with_collection_characteristic(&mut self) -> Vec<&Property> {
162        let key = CacheKey::CollectionProperties;
163
164        if let Some(CachedResult::Properties(urns)) = self.get_cached(&key) {
165            return self
166                .query
167                .aspect()
168                .properties()
169                .iter()
170                .filter(|p| urns.contains(&p.urn().to_string()))
171                .collect();
172        }
173
174        let props = self.query.find_properties_with_collection_characteristic();
175        let urns: Vec<String> = props.iter().map(|p| p.urn().to_string()).collect();
176        self.cache_result(key, CachedResult::Properties(urns));
177        props
178    }
179
180    /// Build dependency graph (cached)
181    pub fn build_dependency_graph(&mut self) -> Vec<Dependency> {
182        let key = CacheKey::DependencyGraph;
183
184        if let Some(CachedResult::Dependencies(deps)) = self.get_cached(&key) {
185            return deps;
186        }
187
188        let deps = self.query.build_dependency_graph();
189        self.cache_result(key, CachedResult::Dependencies(deps.clone()));
190        deps
191    }
192
193    /// Detect circular dependencies (cached)
194    pub fn detect_circular_dependencies(&mut self) -> Vec<Vec<String>> {
195        let key = CacheKey::CircularDependencies;
196
197        // For now, compute without caching (complex return type)
198        self.query.detect_circular_dependencies()
199    }
200
201    /// Clear the cache
202    pub fn clear_cache(&mut self) {
203        let mut cache = self
204            .cache
205            .write()
206            .expect("cache mutex should not be poisoned");
207        cache.clear();
208    }
209
210    /// Get cache statistics
211    pub fn cache_statistics(&self) -> CacheStatistics {
212        let hits = *self.hits.read().expect("hits mutex should not be poisoned");
213        let misses = *self
214            .misses
215            .read()
216            .expect("misses mutex should not be poisoned");
217        let total = hits + misses;
218        let hit_rate = if total == 0 {
219            0.0
220        } else {
221            hits as f64 / total as f64
222        };
223
224        let cache = self
225            .cache
226            .read()
227            .expect("cache mutex should not be poisoned");
228
229        CacheStatistics {
230            size: cache.len(),
231            capacity: self.max_cache_size,
232            hits,
233            misses,
234            hit_rate,
235        }
236    }
237
238    /// Get cached value if it exists
239    fn get_cached(&self, key: &CacheKey) -> Option<CachedResult> {
240        let mut cache = self
241            .cache
242            .write()
243            .expect("cache mutex should not be poisoned");
244
245        if let Some(entry) = cache.get_mut(key) {
246            entry.access_count += 1;
247            entry.last_accessed = std::time::Instant::now();
248            *self
249                .hits
250                .write()
251                .expect("write lock should not be poisoned") += 1;
252            Some(entry.value.clone())
253        } else {
254            *self
255                .misses
256                .write()
257                .expect("write lock should not be poisoned") += 1;
258            None
259        }
260    }
261
262    /// Cache a result
263    fn cache_result(&self, key: CacheKey, value: CachedResult) {
264        let mut cache = self
265            .cache
266            .write()
267            .expect("cache mutex should not be poisoned");
268
269        // Evict LRU if at capacity
270        if cache.len() >= self.max_cache_size && !cache.contains_key(&key) {
271            if let Some((lru_key, _)) = cache
272                .iter()
273                .min_by_key(|(_, v)| v.last_accessed)
274                .map(|(k, v)| (k.clone(), v.clone()))
275            {
276                cache.remove(&lru_key);
277            }
278        }
279
280        cache.insert(
281            key,
282            CachedValue {
283                value,
284                access_count: 0,
285                last_accessed: std::time::Instant::now(),
286            },
287        );
288    }
289
290    /// Get the underlying query (bypasses cache)
291    pub fn query(&self) -> &ModelQuery<'a> {
292        &self.query
293    }
294}
295
296/// Cache statistics
297#[derive(Debug, Clone)]
298pub struct CacheStatistics {
299    /// Current cache size
300    pub size: usize,
301    /// Maximum capacity
302    pub capacity: usize,
303    /// Total cache hits
304    pub hits: usize,
305    /// Total cache misses
306    pub misses: usize,
307    /// Hit rate (0.0 to 1.0)
308    pub hit_rate: f64,
309}
310
311impl CacheStatistics {
312    /// Get total accesses
313    pub fn total_accesses(&self) -> usize {
314        self.hits + self.misses
315    }
316
317    /// Get fill percentage
318    pub fn fill_percentage(&self) -> f64 {
319        if self.capacity == 0 {
320            0.0
321        } else {
322            (self.size as f64 / self.capacity as f64) * 100.0
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::metamodel::{Characteristic, CharacteristicKind};
331
332    fn create_test_aspect() -> Aspect {
333        let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
334
335        let mut prop1 = Property::new("urn:samm:test:1.0.0#prop1".to_string());
336        prop1.optional = false;
337
338        let mut prop2 = Property::new("urn:samm:test:1.0.0#prop2".to_string());
339        prop2.optional = true;
340
341        let mut prop3 = Property::new("urn:samm:test:1.0.0#prop3".to_string());
342        prop3.is_collection = true;
343        // Add a Collection characteristic so it can be found by find_properties_with_collection_characteristic
344        let collection_char = Characteristic::new(
345            "urn:samm:test:1.0.0#CollectionChar".to_string(),
346            CharacteristicKind::Collection {
347                element_characteristic: None,
348            },
349        );
350        prop3.characteristic = Some(collection_char);
351
352        aspect.add_property(prop1);
353        aspect.add_property(prop2);
354        aspect.add_property(prop3);
355
356        aspect
357    }
358
359    #[test]
360    fn test_cached_complexity_metrics() {
361        let aspect = create_test_aspect();
362        let mut query = CachedModelQuery::new(&aspect, 10);
363
364        // First call - cache miss
365        let metrics1 = query.complexity_metrics();
366        assert_eq!(metrics1.total_properties, 3);
367
368        let stats1 = query.cache_statistics();
369        assert_eq!(stats1.misses, 1);
370        assert_eq!(stats1.hits, 0);
371
372        // Second call - cache hit
373        let metrics2 = query.complexity_metrics();
374        assert_eq!(metrics2.total_properties, 3);
375
376        let stats2 = query.cache_statistics();
377        assert_eq!(stats2.misses, 1);
378        assert_eq!(stats2.hits, 1);
379        assert!((stats2.hit_rate - 0.5).abs() < 0.01);
380    }
381
382    #[test]
383    fn test_cached_optional_properties() {
384        let aspect = create_test_aspect();
385        let mut query = CachedModelQuery::new(&aspect, 10);
386
387        let props1 = query.find_optional_properties();
388        assert_eq!(props1.len(), 1);
389
390        let stats1 = query.cache_statistics();
391        assert_eq!(stats1.misses, 1);
392
393        let props2 = query.find_optional_properties();
394        assert_eq!(props2.len(), 1);
395
396        let stats2 = query.cache_statistics();
397        assert_eq!(stats2.hits, 1);
398    }
399
400    #[test]
401    fn test_cached_required_properties() {
402        let aspect = create_test_aspect();
403        let mut query = CachedModelQuery::new(&aspect, 10);
404
405        let props1 = query.find_required_properties();
406        assert_eq!(props1.len(), 2);
407
408        let props2 = query.find_required_properties();
409        assert_eq!(props2.len(), 2);
410
411        let stats = query.cache_statistics();
412        assert_eq!(stats.hits, 1);
413        assert_eq!(stats.misses, 1);
414    }
415
416    #[test]
417    fn test_cache_clear() {
418        let aspect = create_test_aspect();
419        let mut query = CachedModelQuery::new(&aspect, 10);
420
421        query.complexity_metrics();
422        query.complexity_metrics(); // Hit
423
424        let stats_before = query.cache_statistics();
425        assert_eq!(stats_before.size, 1);
426
427        query.clear_cache();
428
429        let stats_after = query.cache_statistics();
430        assert_eq!(stats_after.size, 0);
431        assert_eq!(stats_after.hits, 1); // Stats not cleared
432    }
433
434    #[test]
435    fn test_cache_lru_eviction() {
436        let aspect = create_test_aspect();
437        let mut query = CachedModelQuery::new(&aspect, 2); // Small cache
438
439        // Fill cache
440        query.complexity_metrics();
441        query.find_optional_properties();
442
443        let stats1 = query.cache_statistics();
444        assert_eq!(stats1.size, 2);
445
446        // This should evict LRU
447        query.find_required_properties();
448
449        let stats2 = query.cache_statistics();
450        assert_eq!(stats2.size, 2); // Still at capacity
451    }
452
453    #[test]
454    fn test_cache_statistics() {
455        let aspect = create_test_aspect();
456        let mut query = CachedModelQuery::new(&aspect, 10);
457
458        query.complexity_metrics();
459        query.complexity_metrics();
460        query.find_optional_properties();
461
462        let stats = query.cache_statistics();
463        assert_eq!(stats.total_accesses(), 3);
464        assert_eq!(stats.hits, 1);
465        assert_eq!(stats.misses, 2);
466        assert!((stats.hit_rate - 0.333).abs() < 0.01);
467        assert_eq!(stats.size, 2);
468    }
469
470    #[test]
471    fn test_collection_properties_caching() {
472        let aspect = create_test_aspect();
473        let mut query = CachedModelQuery::new(&aspect, 10);
474
475        let props1 = query.find_properties_with_collection_characteristic();
476        assert_eq!(props1.len(), 1);
477
478        let props2 = query.find_properties_with_collection_characteristic();
479        assert_eq!(props2.len(), 1);
480
481        let stats = query.cache_statistics();
482        assert_eq!(stats.hits, 1);
483    }
484
485    #[test]
486    fn test_dependency_graph_caching() {
487        let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
488
489        let mut prop = Property::new("urn:samm:test:1.0.0#prop1".to_string());
490        let char = Characteristic::new(
491            "urn:samm:test:1.0.0#Char1".to_string(),
492            CharacteristicKind::Trait,
493        );
494        prop.characteristic = Some(char);
495        aspect.add_property(prop);
496
497        let mut query = CachedModelQuery::new(&aspect, 10);
498
499        let deps1 = query.build_dependency_graph();
500        assert!(!deps1.is_empty());
501
502        let deps2 = query.build_dependency_graph();
503        assert!(!deps2.is_empty());
504
505        let stats = query.cache_statistics();
506        assert_eq!(stats.hits, 1);
507    }
508}