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