Skip to main content

oxirs_core/cache/
triple_cache.rs

1//! Generic LRU/TTL cache for RDF triple data, query results, and prefix lookups.
2//!
3//! This module provides:
4//! - [`TripleCache`]: Generic LRU cache with TTL for arbitrary RDF key-value data
5//! - [`QueryResultCache`]: Caches SPARQL query results keyed by query hash
6//! - [`PrefixCache`]: Fast namespace prefix → IRI lookup cache
7//! - [`CacheStats`]: Shared hit/miss/eviction counters
8//! - [`CachePolicy`]: Eviction strategy selector
9
10use std::collections::HashMap;
11use std::hash::Hash;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15
16// ---------------------------------------------------------------------------
17// CachePolicy
18// ---------------------------------------------------------------------------
19
20/// Eviction strategy for cache entries.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum CachePolicy {
23    /// Least-recently-used: evict the entry accessed least recently.
24    Lru,
25    /// Least-frequently-used: evict the entry accessed the fewest times.
26    Lfu,
27    /// First-in, first-out: evict the oldest entry by insertion order.
28    Fifo,
29    /// Time-to-live: evict entries whose TTL has expired; fall back to LRU on capacity.
30    Ttl,
31}
32
33// ---------------------------------------------------------------------------
34// CacheStats
35// ---------------------------------------------------------------------------
36
37/// Shared, atomically-updated cache statistics.
38#[derive(Debug, Default)]
39pub struct CacheStats {
40    hits: AtomicU64,
41    misses: AtomicU64,
42    evictions: AtomicU64,
43    insertions: AtomicU64,
44}
45
46impl CacheStats {
47    /// Create a new zeroed stats counter.
48    pub fn new() -> Arc<Self> {
49        Arc::new(Self::default())
50    }
51
52    pub fn record_hit(&self) {
53        self.hits.fetch_add(1, Ordering::Relaxed);
54    }
55
56    pub fn record_miss(&self) {
57        self.misses.fetch_add(1, Ordering::Relaxed);
58    }
59
60    pub fn record_eviction(&self) {
61        self.evictions.fetch_add(1, Ordering::Relaxed);
62    }
63
64    pub fn record_insertion(&self) {
65        self.insertions.fetch_add(1, Ordering::Relaxed);
66    }
67
68    pub fn hits(&self) -> u64 {
69        self.hits.load(Ordering::Relaxed)
70    }
71
72    pub fn misses(&self) -> u64 {
73        self.misses.load(Ordering::Relaxed)
74    }
75
76    pub fn evictions(&self) -> u64 {
77        self.evictions.load(Ordering::Relaxed)
78    }
79
80    pub fn insertions(&self) -> u64 {
81        self.insertions.load(Ordering::Relaxed)
82    }
83
84    /// Hit rate in [0, 1].  Returns 0.0 if no lookups have been made.
85    pub fn hit_rate(&self) -> f64 {
86        let h = self.hits.load(Ordering::Relaxed) as f64;
87        let m = self.misses.load(Ordering::Relaxed) as f64;
88        let total = h + m;
89        if total == 0.0 {
90            0.0
91        } else {
92            h / total
93        }
94    }
95
96    pub fn reset(&self) {
97        self.hits.store(0, Ordering::Relaxed);
98        self.misses.store(0, Ordering::Relaxed);
99        self.evictions.store(0, Ordering::Relaxed);
100        self.insertions.store(0, Ordering::Relaxed);
101    }
102}
103
104// ---------------------------------------------------------------------------
105// Internal entry
106// ---------------------------------------------------------------------------
107
108#[allow(dead_code)]
109struct Entry<V> {
110    value: V,
111    created_at: Instant,
112    expires_at: Option<Instant>,
113    last_accessed: Instant,
114    access_count: u64,
115    insertion_seq: u64,
116}
117
118impl<V: Clone> Entry<V> {
119    fn new(value: V, ttl: Option<Duration>, seq: u64) -> Self {
120        let now = Instant::now();
121        Self {
122            value,
123            created_at: now,
124            expires_at: ttl.map(|d| now + d),
125            last_accessed: now,
126            access_count: 0,
127            insertion_seq: seq,
128        }
129    }
130
131    fn is_expired(&self) -> bool {
132        self.expires_at
133            .map(|e| Instant::now() >= e)
134            .unwrap_or(false)
135    }
136
137    fn touch(&mut self) {
138        self.last_accessed = Instant::now();
139        self.access_count += 1;
140    }
141}
142
143// ---------------------------------------------------------------------------
144// TripleCache
145// ---------------------------------------------------------------------------
146
147struct TripleCacheInner<K, V> {
148    entries: HashMap<K, Entry<V>>,
149    policy: CachePolicy,
150    capacity: usize,
151    ttl: Option<Duration>,
152    seq_counter: u64,
153}
154
155impl<K: Eq + Hash + Clone, V: Clone> TripleCacheInner<K, V> {
156    fn new(capacity: usize, policy: CachePolicy, ttl: Option<Duration>) -> Self {
157        Self {
158            entries: HashMap::new(),
159            policy,
160            capacity,
161            ttl,
162            seq_counter: 0,
163        }
164    }
165
166    fn get_mut(&mut self, key: &K) -> Option<&V> {
167        if let Some(entry) = self.entries.get_mut(key) {
168            if entry.is_expired() {
169                return None;
170            }
171            entry.touch();
172            // SAFETY: reborrow as immutable after touch
173            Some(&self.entries[key].value)
174        } else {
175            None
176        }
177    }
178
179    fn insert(&mut self, key: K, value: V, stats: &CacheStats) {
180        // Purge expired entries first
181        self.entries.retain(|_, e| !e.is_expired());
182
183        // Evict if at capacity
184        if !self.entries.contains_key(&key) && self.entries.len() >= self.capacity {
185            if let Some(victim) = self.select_victim() {
186                self.entries.remove(&victim);
187                stats.record_eviction();
188            }
189        }
190
191        self.seq_counter += 1;
192        let entry = Entry::new(value, self.ttl, self.seq_counter);
193        self.entries.insert(key, entry);
194        stats.record_insertion();
195    }
196
197    fn select_victim(&self) -> Option<K> {
198        match self.policy {
199            CachePolicy::Lru | CachePolicy::Ttl => self
200                .entries
201                .iter()
202                .min_by_key(|(_, e)| e.last_accessed)
203                .map(|(k, _)| k.clone()),
204            CachePolicy::Lfu => self
205                .entries
206                .iter()
207                .min_by_key(|(_, e)| e.access_count)
208                .map(|(k, _)| k.clone()),
209            CachePolicy::Fifo => self
210                .entries
211                .iter()
212                .min_by_key(|(_, e)| e.insertion_seq)
213                .map(|(k, _)| k.clone()),
214        }
215    }
216
217    fn remove(&mut self, key: &K) -> bool {
218        self.entries.remove(key).is_some()
219    }
220
221    fn len(&self) -> usize {
222        self.entries.iter().filter(|(_, e)| !e.is_expired()).count()
223    }
224
225    fn clear(&mut self) {
226        self.entries.clear();
227    }
228
229    fn contains_key(&self, key: &K) -> bool {
230        self.entries
231            .get(key)
232            .map(|e| !e.is_expired())
233            .unwrap_or(false)
234    }
235
236    fn keys(&self) -> Vec<K> {
237        self.entries
238            .iter()
239            .filter(|(_, e)| !e.is_expired())
240            .map(|(k, _)| k.clone())
241            .collect()
242    }
243}
244
245/// Generic LRU/LFU/FIFO/TTL cache for RDF data.
246///
247/// Thread-safe via an internal `Mutex`.  The policy is selected at construction time.
248///
249/// # Type Parameters
250/// - `K`: Cache key type (must implement `Eq + Hash + Clone`).
251/// - `V`: Cached value type (must implement `Clone`).
252pub struct TripleCache<K, V> {
253    inner: Mutex<TripleCacheInner<K, V>>,
254    stats: Arc<CacheStats>,
255}
256
257impl<K: Eq + Hash + Clone, V: Clone> TripleCache<K, V> {
258    /// Create a new cache with the given capacity, eviction policy, and optional TTL.
259    pub fn new(capacity: usize, policy: CachePolicy, ttl: Option<Duration>) -> Self {
260        Self {
261            inner: Mutex::new(TripleCacheInner::new(capacity, policy, ttl)),
262            stats: CacheStats::new(),
263        }
264    }
265
266    /// Create an LRU cache with the given capacity and no TTL.
267    pub fn lru(capacity: usize) -> Self {
268        Self::new(capacity, CachePolicy::Lru, None)
269    }
270
271    /// Create an LRU+TTL cache.
272    pub fn lru_ttl(capacity: usize, ttl: Duration) -> Self {
273        Self::new(capacity, CachePolicy::Ttl, Some(ttl))
274    }
275
276    /// Look up `key` and return a clone of the value if present and not expired.
277    pub fn get(&self, key: &K) -> Option<V> {
278        let mut inner = self.inner.lock().expect("TripleCache lock poisoned");
279        let result = inner.get_mut(key).cloned();
280        if result.is_some() {
281            self.stats.record_hit();
282        } else {
283            self.stats.record_miss();
284        }
285        result
286    }
287
288    /// Insert or replace `key` → `value`.
289    pub fn put(&self, key: K, value: V) {
290        let mut inner = self.inner.lock().expect("TripleCache lock poisoned");
291        inner.insert(key, value, &self.stats);
292    }
293
294    /// Remove `key` from the cache.  Returns `true` if it was present.
295    pub fn remove(&self, key: &K) -> bool {
296        let mut inner = self.inner.lock().expect("TripleCache lock poisoned");
297        inner.remove(key)
298    }
299
300    /// Returns `true` if `key` is in the cache and not expired.
301    pub fn contains(&self, key: &K) -> bool {
302        let inner = self.inner.lock().expect("TripleCache lock poisoned");
303        inner.contains_key(key)
304    }
305
306    /// Number of live (non-expired) entries.
307    pub fn len(&self) -> usize {
308        let inner = self.inner.lock().expect("TripleCache lock poisoned");
309        inner.len()
310    }
311
312    pub fn is_empty(&self) -> bool {
313        self.len() == 0
314    }
315
316    /// Remove all entries.
317    pub fn clear(&self) {
318        let mut inner = self.inner.lock().expect("TripleCache lock poisoned");
319        inner.clear();
320    }
321
322    /// All live keys.
323    pub fn keys(&self) -> Vec<K> {
324        let inner = self.inner.lock().expect("TripleCache lock poisoned");
325        inner.keys()
326    }
327
328    /// Shared statistics counter.
329    pub fn stats(&self) -> Arc<CacheStats> {
330        Arc::clone(&self.stats)
331    }
332}
333
334// ---------------------------------------------------------------------------
335// QueryResultCache
336// ---------------------------------------------------------------------------
337
338/// A row of SPARQL variable bindings.
339pub type SparqlRow = HashMap<String, String>;
340
341/// Cache entry for a SPARQL query result set.
342#[derive(Debug, Clone)]
343pub struct QueryCacheEntry {
344    /// Hashed SPARQL query string.
345    pub query_hash: u64,
346    /// Cached result rows.
347    pub rows: Vec<SparqlRow>,
348    /// Variable names in result order.
349    pub variables: Vec<String>,
350    /// Predicates accessed by this query (for invalidation).
351    pub accessed_predicates: Vec<String>,
352    /// Insertion timestamp.
353    pub created_at: Instant,
354}
355
356/// Caches SPARQL query results keyed by a (dataset_id, query_hash) pair.
357///
358/// Internally delegates to `TripleCache<(String, u64), QueryCacheEntry>`.
359pub struct QueryResultCache {
360    cache: TripleCache<(String, u64), QueryCacheEntry>,
361}
362
363impl QueryResultCache {
364    /// Create a new result cache with `capacity` entries and a TTL.
365    pub fn new(capacity: usize, ttl: Duration) -> Self {
366        Self {
367            cache: TripleCache::new(capacity, CachePolicy::Ttl, Some(ttl)),
368        }
369    }
370
371    /// FNV-1a hash of the SPARQL query text.
372    fn hash_query(query: &str) -> u64 {
373        const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
374        const FNV_PRIME: u64 = 1_099_511_628_211;
375        let mut h = FNV_OFFSET;
376        for b in query.bytes() {
377            h ^= b as u64;
378            h = h.wrapping_mul(FNV_PRIME);
379        }
380        h
381    }
382
383    /// Store a result set for `(dataset_id, query)`.
384    pub fn put(
385        &self,
386        dataset_id: &str,
387        query: &str,
388        rows: Vec<SparqlRow>,
389        variables: Vec<String>,
390        accessed_predicates: Vec<String>,
391    ) {
392        let hash = Self::hash_query(query);
393        let entry = QueryCacheEntry {
394            query_hash: hash,
395            rows,
396            variables,
397            accessed_predicates,
398            created_at: Instant::now(),
399        };
400        self.cache.put((dataset_id.to_string(), hash), entry);
401    }
402
403    /// Look up a cached result for `(dataset_id, query)`.
404    pub fn get(&self, dataset_id: &str, query: &str) -> Option<QueryCacheEntry> {
405        let hash = Self::hash_query(query);
406        self.cache.get(&(dataset_id.to_string(), hash))
407    }
408
409    /// Invalidate all entries for `dataset_id` that accessed `predicate`.
410    pub fn invalidate_by_predicate(&self, dataset_id: &str, predicate: &str) -> usize {
411        let keys_to_remove: Vec<_> = self
412            .cache
413            .keys()
414            .into_iter()
415            .filter(|(ds, _)| ds == dataset_id)
416            .filter(|k| {
417                self.cache
418                    .get(k)
419                    .map(|e| e.accessed_predicates.iter().any(|p| p == predicate))
420                    .unwrap_or(false)
421            })
422            .collect();
423        let count = keys_to_remove.len();
424        for k in keys_to_remove {
425            self.cache.remove(&k);
426        }
427        count
428    }
429
430    /// Invalidate all entries for `dataset_id`.
431    pub fn invalidate_dataset(&self, dataset_id: &str) -> usize {
432        let keys_to_remove: Vec<_> = self
433            .cache
434            .keys()
435            .into_iter()
436            .filter(|(ds, _)| ds == dataset_id)
437            .collect();
438        let count = keys_to_remove.len();
439        for k in keys_to_remove {
440            self.cache.remove(&k);
441        }
442        count
443    }
444
445    /// Number of cached entries.
446    pub fn len(&self) -> usize {
447        self.cache.len()
448    }
449
450    pub fn is_empty(&self) -> bool {
451        self.cache.is_empty()
452    }
453
454    /// Shared stats.
455    pub fn stats(&self) -> Arc<CacheStats> {
456        self.cache.stats()
457    }
458}
459
460// ---------------------------------------------------------------------------
461// PrefixCache
462// ---------------------------------------------------------------------------
463
464/// Bidirectional namespace prefix ↔ IRI lookup cache.
465///
466/// Stores the registered prefix→IRI mappings and provides fast lookups in
467/// both directions (prefix→IRI and IRI→prefix).
468#[derive(Debug, Default, Clone)]
469pub struct PrefixCache {
470    prefix_to_iri: HashMap<String, String>,
471    iri_to_prefix: HashMap<String, String>,
472}
473
474impl PrefixCache {
475    pub fn new() -> Self {
476        Self::default()
477    }
478
479    /// Register a new prefix → IRI mapping.
480    ///
481    /// Replaces any existing mapping for the same prefix.
482    pub fn register(&mut self, prefix: &str, iri: &str) {
483        // Remove stale reverse mapping if prefix was previously mapped to a different IRI
484        if let Some(old_iri) = self.prefix_to_iri.get(prefix) {
485            self.iri_to_prefix.remove(old_iri.as_str());
486        }
487        self.prefix_to_iri
488            .insert(prefix.to_string(), iri.to_string());
489        self.iri_to_prefix
490            .insert(iri.to_string(), prefix.to_string());
491    }
492
493    /// Look up the IRI for a given prefix.  Returns `None` if not registered.
494    pub fn resolve_prefix(&self, prefix: &str) -> Option<&str> {
495        self.prefix_to_iri.get(prefix).map(|s| s.as_str())
496    }
497
498    /// Look up the prefix for a given namespace IRI.  Returns `None` if not registered.
499    pub fn resolve_iri(&self, iri: &str) -> Option<&str> {
500        self.iri_to_prefix.get(iri).map(|s| s.as_str())
501    }
502
503    /// Expand a prefixed name (e.g. `"rdf:type"`) to a full IRI.
504    ///
505    /// Returns `None` if the prefix is not registered or the input has no colon.
506    pub fn expand(&self, prefixed: &str) -> Option<String> {
507        let colon = prefixed.find(':')?;
508        let prefix = &prefixed[..colon];
509        let local = &prefixed[colon + 1..];
510        let namespace = self.prefix_to_iri.get(prefix)?;
511        Some(format!("{namespace}{local}"))
512    }
513
514    /// Compact a full IRI to a prefixed name (e.g. `"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"` → `"rdf:type"`).
515    ///
516    /// Tries all registered namespaces; picks the longest matching namespace.
517    pub fn compact(&self, iri: &str) -> Option<String> {
518        let mut best: Option<(&str, &str)> = None;
519        for (namespace, prefix) in &self.iri_to_prefix {
520            if iri.starts_with(namespace.as_str())
521                && best.map(|(ns, _)| ns.len()).unwrap_or(0) < namespace.len()
522            {
523                best = Some((namespace.as_str(), prefix.as_str()));
524            }
525        }
526        best.map(|(ns, pfx)| format!("{}:{}", pfx, &iri[ns.len()..]))
527    }
528
529    /// Remove a prefix mapping.
530    pub fn remove(&mut self, prefix: &str) -> bool {
531        if let Some(iri) = self.prefix_to_iri.remove(prefix) {
532            self.iri_to_prefix.remove(&iri);
533            true
534        } else {
535            false
536        }
537    }
538
539    /// Number of registered prefixes.
540    pub fn len(&self) -> usize {
541        self.prefix_to_iri.len()
542    }
543
544    pub fn is_empty(&self) -> bool {
545        self.prefix_to_iri.is_empty()
546    }
547
548    /// All registered (prefix, iri) pairs.
549    pub fn entries(&self) -> Vec<(&str, &str)> {
550        self.prefix_to_iri
551            .iter()
552            .map(|(p, i)| (p.as_str(), i.as_str()))
553            .collect()
554    }
555
556    /// Register the standard RDF/RDFS/OWL/XSD prefixes.
557    pub fn with_standard_prefixes(mut self) -> Self {
558        self.register("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
559        self.register("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
560        self.register("owl", "http://www.w3.org/2002/07/owl#");
561        self.register("xsd", "http://www.w3.org/2001/XMLSchema#");
562        self.register("dc", "http://purl.org/dc/elements/1.1/");
563        self.register("dcterms", "http://purl.org/dc/terms/");
564        self.register("foaf", "http://xmlns.com/foaf/0.1/");
565        self.register("skos", "http://www.w3.org/2004/02/skos/core#");
566        self
567    }
568}
569
570// ---------------------------------------------------------------------------
571// Tests
572// ---------------------------------------------------------------------------
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use std::thread;
578    use std::time::Duration;
579
580    // ---- CachePolicy ----
581
582    #[test]
583    fn test_cache_policy_variants_are_distinct() {
584        assert_ne!(CachePolicy::Lru, CachePolicy::Lfu);
585        assert_ne!(CachePolicy::Lfu, CachePolicy::Fifo);
586        assert_ne!(CachePolicy::Fifo, CachePolicy::Ttl);
587    }
588
589    // ---- CacheStats ----
590
591    #[test]
592    fn test_cache_stats_initial_zero() {
593        let s = CacheStats::new();
594        assert_eq!(s.hits(), 0);
595        assert_eq!(s.misses(), 0);
596        assert_eq!(s.evictions(), 0);
597        assert_eq!(s.insertions(), 0);
598    }
599
600    #[test]
601    fn test_cache_stats_hit_rate_empty() {
602        let s = CacheStats::new();
603        assert_eq!(s.hit_rate(), 0.0);
604    }
605
606    #[test]
607    fn test_cache_stats_hit_rate_all_hits() {
608        let s = CacheStats::new();
609        s.record_hit();
610        s.record_hit();
611        assert_eq!(s.hit_rate(), 1.0);
612    }
613
614    #[test]
615    fn test_cache_stats_hit_rate_half() {
616        let s = CacheStats::new();
617        s.record_hit();
618        s.record_miss();
619        assert!((s.hit_rate() - 0.5).abs() < 1e-9);
620    }
621
622    #[test]
623    fn test_cache_stats_reset() {
624        let s = CacheStats::new();
625        s.record_hit();
626        s.record_miss();
627        s.record_eviction();
628        s.reset();
629        assert_eq!(s.hits(), 0);
630        assert_eq!(s.misses(), 0);
631        assert_eq!(s.evictions(), 0);
632    }
633
634    #[test]
635    fn test_cache_stats_concurrent_updates() {
636        let s = Arc::new(CacheStats::default());
637        let mut handles = vec![];
638        for _ in 0..4 {
639            let sc = Arc::clone(&s);
640            handles.push(thread::spawn(move || {
641                for _ in 0..100 {
642                    sc.record_hit();
643                    sc.record_miss();
644                }
645            }));
646        }
647        for h in handles {
648            h.join().expect("thread panicked");
649        }
650        assert_eq!(s.hits(), 400);
651        assert_eq!(s.misses(), 400);
652    }
653
654    // ---- TripleCache (LRU) ----
655
656    #[test]
657    fn test_triple_cache_lru_empty_get_returns_none() {
658        let cache: TripleCache<String, String> = TripleCache::lru(10);
659        assert!(cache.get(&"missing".to_string()).is_none());
660    }
661
662    #[test]
663    fn test_triple_cache_lru_put_and_get() {
664        let cache = TripleCache::lru(10);
665        cache.put("key1".to_string(), "val1".to_string());
666        assert_eq!(cache.get(&"key1".to_string()), Some("val1".to_string()));
667    }
668
669    #[test]
670    fn test_triple_cache_lru_overwrite() {
671        let cache = TripleCache::lru(10);
672        cache.put("k".to_string(), "v1".to_string());
673        cache.put("k".to_string(), "v2".to_string());
674        assert_eq!(cache.get(&"k".to_string()), Some("v2".to_string()));
675    }
676
677    #[test]
678    fn test_triple_cache_lru_eviction_at_capacity() {
679        let cache: TripleCache<usize, usize> = TripleCache::lru(3);
680        cache.put(1, 10);
681        cache.put(2, 20);
682        cache.put(3, 30);
683        // Access 1 and 2 so 3 is LRU
684        let _ = cache.get(&1);
685        let _ = cache.get(&2);
686        // Insert 4 → should evict 3 (LRU)
687        cache.put(4, 40);
688        assert_eq!(cache.len(), 3);
689        assert!(cache.contains(&1));
690        assert!(cache.contains(&2));
691        assert!(cache.contains(&4));
692    }
693
694    #[test]
695    fn test_triple_cache_lru_remove() {
696        let cache = TripleCache::lru(10);
697        cache.put("a".to_string(), 1i32);
698        assert!(cache.remove(&"a".to_string()));
699        assert!(!cache.remove(&"a".to_string()));
700        assert!(cache.get(&"a".to_string()).is_none());
701    }
702
703    #[test]
704    fn test_triple_cache_lru_clear() {
705        let cache: TripleCache<i32, i32> = TripleCache::lru(10);
706        for i in 0..5 {
707            cache.put(i, i * 2);
708        }
709        cache.clear();
710        assert_eq!(cache.len(), 0);
711        assert!(cache.is_empty());
712    }
713
714    #[test]
715    fn test_triple_cache_lru_contains() {
716        let cache: TripleCache<i32, i32> = TripleCache::lru(10);
717        cache.put(42, 84);
718        assert!(cache.contains(&42));
719        assert!(!cache.contains(&43));
720    }
721
722    #[test]
723    fn test_triple_cache_lru_len() {
724        let cache: TripleCache<i32, i32> = TripleCache::lru(10);
725        assert_eq!(cache.len(), 0);
726        cache.put(1, 1);
727        assert_eq!(cache.len(), 1);
728        cache.put(2, 2);
729        assert_eq!(cache.len(), 2);
730    }
731
732    #[test]
733    fn test_triple_cache_lru_keys() {
734        let cache: TripleCache<i32, i32> = TripleCache::lru(10);
735        cache.put(1, 10);
736        cache.put(2, 20);
737        let mut keys = cache.keys();
738        keys.sort();
739        assert_eq!(keys, vec![1, 2]);
740    }
741
742    #[test]
743    fn test_triple_cache_lru_stats_incremented() {
744        let cache: TripleCache<i32, i32> = TripleCache::lru(10);
745        cache.put(1, 100);
746        let _ = cache.get(&1);
747        let _ = cache.get(&99);
748        let s = cache.stats();
749        assert_eq!(s.hits(), 1);
750        assert_eq!(s.misses(), 1);
751        assert_eq!(s.insertions(), 1);
752    }
753
754    #[test]
755    fn test_triple_cache_lfu_evicts_least_used() {
756        let cache: TripleCache<i32, i32> = TripleCache::new(3, CachePolicy::Lfu, None);
757        cache.put(1, 1);
758        cache.put(2, 2);
759        cache.put(3, 3);
760        // Access 1 many times
761        for _ in 0..5 {
762            let _ = cache.get(&1);
763        }
764        // Access 2 once
765        let _ = cache.get(&2);
766        // 3 has zero accesses → should be LFU victim
767        cache.put(4, 4);
768        // Either 3 is evicted (access_count=0) or 2 (access_count=1); 1 has most
769        assert!(cache.contains(&1));
770    }
771
772    #[test]
773    fn test_triple_cache_fifo_evicts_oldest() {
774        let cache: TripleCache<i32, i32> = TripleCache::new(3, CachePolicy::Fifo, None);
775        cache.put(1, 1); // seq 1
776        cache.put(2, 2); // seq 2
777        cache.put(3, 3); // seq 3
778                         // Access 1 to distinguish from LRU — FIFO should still evict 1 (oldest)
779        let _ = cache.get(&1);
780        let _ = cache.get(&1);
781        cache.put(4, 4);
782        assert_eq!(cache.len(), 3);
783        assert!(!cache.contains(&1)); // oldest evicted
784    }
785
786    #[test]
787    fn test_triple_cache_ttl_expiry() {
788        let ttl = Duration::from_millis(50);
789        let cache: TripleCache<i32, i32> = TripleCache::lru_ttl(10, ttl);
790        cache.put(1, 100);
791        assert!(cache.get(&1).is_some());
792        thread::sleep(Duration::from_millis(60));
793        assert!(cache.get(&1).is_none());
794    }
795
796    // ---- QueryResultCache ----
797
798    #[test]
799    fn test_query_result_cache_miss_on_empty() {
800        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
801        assert!(qrc.get("ds", "SELECT * WHERE { ?s ?p ?o }").is_none());
802    }
803
804    #[test]
805    fn test_query_result_cache_put_and_get() {
806        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
807        let rows = vec![[("s".to_string(), "Alice".to_string())]
808            .into_iter()
809            .collect()];
810        qrc.put(
811            "ds1",
812            "SELECT ?s WHERE {?s a :Person}",
813            rows.clone(),
814            vec!["s".to_string()],
815            vec![],
816        );
817        let entry = qrc
818            .get("ds1", "SELECT ?s WHERE {?s a :Person}")
819            .expect("should hit");
820        assert_eq!(entry.rows.len(), 1);
821    }
822
823    #[test]
824    fn test_query_result_cache_different_datasets_no_collision() {
825        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
826        let q = "SELECT * WHERE { ?s ?p ?o }";
827        let rows1 = vec![[("x".to_string(), "A".to_string())].into_iter().collect()];
828        let rows2 = vec![[("x".to_string(), "B".to_string())].into_iter().collect()];
829        qrc.put("ds1", q, rows1, vec![], vec![]);
830        qrc.put("ds2", q, rows2, vec![], vec![]);
831        let e1 = qrc.get("ds1", q).expect("hit");
832        let e2 = qrc.get("ds2", q).expect("hit");
833        assert_ne!(e1.rows[0]["x"], e2.rows[0]["x"],);
834    }
835
836    #[test]
837    fn test_query_result_cache_invalidate_by_predicate() {
838        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
839        qrc.put("ds", "q1", vec![], vec![], vec!["http://p/age".to_string()]);
840        qrc.put(
841            "ds",
842            "q2",
843            vec![],
844            vec![],
845            vec!["http://p/name".to_string()],
846        );
847        let removed = qrc.invalidate_by_predicate("ds", "http://p/age");
848        assert_eq!(removed, 1);
849        assert!(qrc.get("ds", "q1").is_none());
850        assert!(qrc.get("ds", "q2").is_some());
851    }
852
853    #[test]
854    fn test_query_result_cache_invalidate_dataset() {
855        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
856        qrc.put("ds", "q1", vec![], vec![], vec![]);
857        qrc.put("ds", "q2", vec![], vec![], vec![]);
858        qrc.put("other_ds", "q1", vec![], vec![], vec![]);
859        let removed = qrc.invalidate_dataset("ds");
860        assert_eq!(removed, 2);
861        assert!(qrc.get("other_ds", "q1").is_some());
862    }
863
864    #[test]
865    fn test_query_result_cache_len() {
866        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
867        assert_eq!(qrc.len(), 0);
868        qrc.put("ds", "q1", vec![], vec![], vec![]);
869        assert_eq!(qrc.len(), 1);
870    }
871
872    #[test]
873    fn test_query_result_cache_is_empty() {
874        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
875        assert!(qrc.is_empty());
876        qrc.put("ds", "q", vec![], vec![], vec![]);
877        assert!(!qrc.is_empty());
878    }
879
880    #[test]
881    fn test_query_result_cache_variables_preserved() {
882        let qrc = QueryResultCache::new(100, Duration::from_secs(60));
883        let vars = vec!["?s".to_string(), "?p".to_string(), "?o".to_string()];
884        qrc.put("ds", "q", vec![], vars.clone(), vec![]);
885        let entry = qrc.get("ds", "q").expect("hit");
886        assert_eq!(entry.variables, vars);
887    }
888
889    // ---- PrefixCache ----
890
891    #[test]
892    fn test_prefix_cache_empty() {
893        let pc = PrefixCache::new();
894        assert!(pc.is_empty());
895        assert_eq!(pc.len(), 0);
896    }
897
898    #[test]
899    fn test_prefix_cache_register_and_resolve_prefix() {
900        let mut pc = PrefixCache::new();
901        pc.register("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
902        assert_eq!(
903            pc.resolve_prefix("rdf"),
904            Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
905        );
906    }
907
908    #[test]
909    fn test_prefix_cache_register_and_resolve_iri() {
910        let mut pc = PrefixCache::new();
911        pc.register("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
912        assert_eq!(
913            pc.resolve_iri("http://www.w3.org/2000/01/rdf-schema#"),
914            Some("rdfs")
915        );
916    }
917
918    #[test]
919    fn test_prefix_cache_expand() {
920        let mut pc = PrefixCache::new();
921        pc.register("owl", "http://www.w3.org/2002/07/owl#");
922        assert_eq!(
923            pc.expand("owl:Class"),
924            Some("http://www.w3.org/2002/07/owl#Class".to_string())
925        );
926    }
927
928    #[test]
929    fn test_prefix_cache_compact() {
930        let mut pc = PrefixCache::new();
931        pc.register("xsd", "http://www.w3.org/2001/XMLSchema#");
932        assert_eq!(
933            pc.compact("http://www.w3.org/2001/XMLSchema#string"),
934            Some("xsd:string".to_string())
935        );
936    }
937
938    #[test]
939    fn test_prefix_cache_expand_unknown_prefix_returns_none() {
940        let pc = PrefixCache::new();
941        assert!(pc.expand("unknown:Term").is_none());
942    }
943
944    #[test]
945    fn test_prefix_cache_compact_no_match_returns_none() {
946        let pc = PrefixCache::new();
947        assert!(pc.compact("http://example.org/x").is_none());
948    }
949
950    #[test]
951    fn test_prefix_cache_overwrite_prefix() {
952        let mut pc = PrefixCache::new();
953        pc.register("ex", "http://example.org/");
954        pc.register("ex", "http://example.com/");
955        assert_eq!(pc.resolve_prefix("ex"), Some("http://example.com/"));
956        assert_eq!(pc.len(), 1);
957    }
958
959    #[test]
960    fn test_prefix_cache_remove() {
961        let mut pc = PrefixCache::new();
962        pc.register("ex", "http://example.org/");
963        assert!(pc.remove("ex"));
964        assert!(pc.resolve_prefix("ex").is_none());
965        assert!(!pc.remove("ex")); // already gone
966    }
967
968    #[test]
969    fn test_prefix_cache_standard_prefixes() {
970        let pc = PrefixCache::new().with_standard_prefixes();
971        assert!(pc.resolve_prefix("rdf").is_some());
972        assert!(pc.resolve_prefix("rdfs").is_some());
973        assert!(pc.resolve_prefix("owl").is_some());
974        assert!(pc.resolve_prefix("xsd").is_some());
975        assert!(pc.resolve_prefix("foaf").is_some());
976    }
977
978    #[test]
979    fn test_prefix_cache_longest_namespace_wins_on_compact() {
980        let mut pc = PrefixCache::new();
981        pc.register("schema", "http://schema.org/");
982        pc.register("schema_person", "http://schema.org/Person/");
983        // The longer namespace should be chosen
984        let result = pc.compact("http://schema.org/Person/name");
985        assert_eq!(result, Some("schema_person:name".to_string()));
986    }
987
988    #[test]
989    fn test_prefix_cache_entries_returns_all() {
990        let mut pc = PrefixCache::new();
991        pc.register("a", "http://a.org/");
992        pc.register("b", "http://b.org/");
993        let mut entries: Vec<_> = pc.entries().into_iter().map(|(p, _)| p).collect();
994        entries.sort();
995        assert_eq!(entries, vec!["a", "b"]);
996    }
997}