1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum CachePolicy {
23 Lru,
25 Lfu,
27 Fifo,
29 Ttl,
31}
32
33#[derive(Debug, Default)]
39pub struct CacheStats {
40 hits: AtomicU64,
41 misses: AtomicU64,
42 evictions: AtomicU64,
43 insertions: AtomicU64,
44}
45
46impl CacheStats {
47 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 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#[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
143struct 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 Some(&self.entries[key].value)
174 } else {
175 None
176 }
177 }
178
179 fn insert(&mut self, key: K, value: V, stats: &CacheStats) {
180 self.entries.retain(|_, e| !e.is_expired());
182
183 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
245pub 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 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 pub fn lru(capacity: usize) -> Self {
268 Self::new(capacity, CachePolicy::Lru, None)
269 }
270
271 pub fn lru_ttl(capacity: usize, ttl: Duration) -> Self {
273 Self::new(capacity, CachePolicy::Ttl, Some(ttl))
274 }
275
276 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 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 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 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 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 pub fn clear(&self) {
318 let mut inner = self.inner.lock().expect("TripleCache lock poisoned");
319 inner.clear();
320 }
321
322 pub fn keys(&self) -> Vec<K> {
324 let inner = self.inner.lock().expect("TripleCache lock poisoned");
325 inner.keys()
326 }
327
328 pub fn stats(&self) -> Arc<CacheStats> {
330 Arc::clone(&self.stats)
331 }
332}
333
334pub type SparqlRow = HashMap<String, String>;
340
341#[derive(Debug, Clone)]
343pub struct QueryCacheEntry {
344 pub query_hash: u64,
346 pub rows: Vec<SparqlRow>,
348 pub variables: Vec<String>,
350 pub accessed_predicates: Vec<String>,
352 pub created_at: Instant,
354}
355
356pub struct QueryResultCache {
360 cache: TripleCache<(String, u64), QueryCacheEntry>,
361}
362
363impl QueryResultCache {
364 pub fn new(capacity: usize, ttl: Duration) -> Self {
366 Self {
367 cache: TripleCache::new(capacity, CachePolicy::Ttl, Some(ttl)),
368 }
369 }
370
371 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 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 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 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 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 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 pub fn stats(&self) -> Arc<CacheStats> {
456 self.cache.stats()
457 }
458}
459
460#[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 pub fn register(&mut self, prefix: &str, iri: &str) {
483 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 pub fn resolve_prefix(&self, prefix: &str) -> Option<&str> {
495 self.prefix_to_iri.get(prefix).map(|s| s.as_str())
496 }
497
498 pub fn resolve_iri(&self, iri: &str) -> Option<&str> {
500 self.iri_to_prefix.get(iri).map(|s| s.as_str())
501 }
502
503 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 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 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 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 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 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#[cfg(test)]
575mod tests {
576 use super::*;
577 use std::thread;
578 use std::time::Duration;
579
580 #[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 #[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 #[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 let _ = cache.get(&1);
685 let _ = cache.get(&2);
686 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 for _ in 0..5 {
762 let _ = cache.get(&1);
763 }
764 let _ = cache.get(&2);
766 cache.put(4, 4);
768 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); cache.put(2, 2); cache.put(3, 3); 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)); }
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 #[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 #[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")); }
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 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}