1use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct TriplePattern {
17 pub subject: Option<String>,
19 pub predicate: Option<String>,
21 pub object: Option<String>,
23}
24
25impl TriplePattern {
26 pub fn new(subject: Option<String>, predicate: Option<String>, object: Option<String>) -> Self {
28 Self {
29 subject,
30 predicate,
31 object,
32 }
33 }
34
35 pub fn bound(s: &str, p: &str, o: &str) -> Self {
37 Self {
38 subject: Some(s.to_string()),
39 predicate: Some(p.to_string()),
40 object: Some(o.to_string()),
41 }
42 }
43
44 pub fn for_subject(s: &str) -> Self {
46 Self {
47 subject: Some(s.to_string()),
48 predicate: None,
49 object: None,
50 }
51 }
52
53 pub fn wildcard() -> Self {
55 Self {
56 subject: None,
57 predicate: None,
58 object: None,
59 }
60 }
61
62 pub fn matches(&self, s: &str, p: &str, o: &str) -> bool {
67 self.subject.as_deref().map_or(true, |ps| ps == s)
68 && self.predicate.as_deref().map_or(true, |pp| pp == p)
69 && self.object.as_deref().map_or(true, |po| po == o)
70 }
71}
72
73pub type CacheKey = TriplePattern;
76
77#[derive(Debug, Clone)]
83pub struct CacheEntry {
84 pub results: Vec<(String, String, String)>,
86 pub hit_count: usize,
88 pub last_accessed: u64,
90}
91
92#[derive(Debug, Clone, Default)]
98pub struct CacheStats {
99 pub hits: u64,
101 pub misses: u64,
103 pub evictions: u64,
105 pub size: usize,
107}
108
109pub struct TripleCache {
115 store: HashMap<CacheKey, CacheEntry>,
116 capacity: usize,
117 stats: CacheStats,
118}
119
120impl TripleCache {
121 pub fn new(capacity: usize) -> Self {
123 Self {
124 store: HashMap::new(),
125 capacity,
126 stats: CacheStats::default(),
127 }
128 }
129
130 pub fn get(
139 &mut self,
140 pattern: &TriplePattern,
141 now: u64,
142 ) -> Option<&Vec<(String, String, String)>> {
143 if let Some(entry) = self.store.get_mut(pattern) {
144 entry.hit_count += 1;
145 entry.last_accessed = now;
146 self.stats.hits += 1;
147 return Some(&self.store[pattern].results);
149 }
150 self.stats.misses += 1;
151 None
152 }
153
154 pub fn put(
159 &mut self,
160 pattern: TriplePattern,
161 results: Vec<(String, String, String)>,
162 now: u64,
163 ) {
164 if !self.store.contains_key(&pattern) && self.store.len() >= self.capacity {
166 self.evict_lru();
167 }
168
169 let entry = CacheEntry {
170 results,
171 hit_count: 0,
172 last_accessed: now,
173 };
174 self.store.insert(pattern, entry);
175 self.stats.size = self.store.len();
176 }
177
178 pub fn invalidate_matching(&mut self, s: &str, p: &str, o: &str) {
184 self.store.retain(|pattern, _| !pattern.matches(s, p, o));
185 self.stats.size = self.store.len();
186 }
187
188 pub fn stats(&self) -> &CacheStats {
194 &self.stats
195 }
196
197 pub fn len(&self) -> usize {
199 self.store.len()
200 }
201
202 pub fn is_empty(&self) -> bool {
204 self.store.is_empty()
205 }
206
207 pub fn clear(&mut self) {
209 self.store.clear();
210 self.stats.size = 0;
211 }
212
213 fn evict_lru(&mut self) {
218 if self.store.is_empty() {
219 return;
220 }
221 let lru_key = self
223 .store
224 .iter()
225 .min_by_key(|(_, entry)| entry.last_accessed)
226 .map(|(k, _)| k.clone());
227
228 if let Some(key) = lru_key {
229 self.store.remove(&key);
230 self.stats.evictions += 1;
231 }
232 }
233}
234
235#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn triple(s: &str, p: &str, o: &str) -> (String, String, String) {
244 (s.to_string(), p.to_string(), o.to_string())
245 }
246
247 fn pattern_sp(s: &str, p: &str) -> TriplePattern {
248 TriplePattern::new(Some(s.to_string()), Some(p.to_string()), None)
249 }
250
251 #[test]
256 fn test_pattern_matches_wildcard() {
257 let p = TriplePattern::wildcard();
258 assert!(p.matches("s", "p", "o"));
259 }
260
261 #[test]
262 fn test_pattern_matches_bound_exact() {
263 let p = TriplePattern::bound("s", "p", "o");
264 assert!(p.matches("s", "p", "o"));
265 assert!(!p.matches("s2", "p", "o"));
266 }
267
268 #[test]
269 fn test_pattern_matches_subject_bound() {
270 let p = TriplePattern::for_subject("Alice");
271 assert!(p.matches("Alice", "knows", "Bob"));
272 assert!(!p.matches("Bob", "knows", "Alice"));
273 }
274
275 #[test]
276 fn test_pattern_matches_partial_wildcard() {
277 let p = pattern_sp(":Alice", ":knows");
278 assert!(p.matches(":Alice", ":knows", ":Bob"));
279 assert!(p.matches(":Alice", ":knows", ":Carol"));
280 assert!(!p.matches(":Alice", ":likes", ":Bob"));
281 }
282
283 #[test]
288 fn test_cache_miss() {
289 let mut cache = TripleCache::new(10);
290 let p = TriplePattern::wildcard();
291 assert!(cache.get(&p, 1).is_none());
292 assert_eq!(cache.stats().misses, 1);
293 }
294
295 #[test]
296 fn test_cache_hit_after_put() {
297 let mut cache = TripleCache::new(10);
298 let p = TriplePattern::for_subject("Alice");
299 let results = vec![triple(":Alice", ":knows", ":Bob")];
300 cache.put(p.clone(), results.clone(), 1);
301 let got = cache.get(&p, 2);
302 assert!(got.is_some());
303 assert_eq!(got.unwrap(), &results);
304 assert_eq!(cache.stats().hits, 1);
305 }
306
307 #[test]
308 fn test_cache_size_after_put() {
309 let mut cache = TripleCache::new(10);
310 assert_eq!(cache.len(), 0);
311 cache.put(TriplePattern::wildcard(), vec![], 1);
312 assert_eq!(cache.len(), 1);
313 }
314
315 #[test]
316 fn test_cache_is_empty() {
317 let cache = TripleCache::new(5);
318 assert!(cache.is_empty());
319 }
320
321 #[test]
322 fn test_cache_put_updates_existing() {
323 let mut cache = TripleCache::new(5);
324 let p = TriplePattern::for_subject("X");
325 cache.put(p.clone(), vec![triple("X", "p", "o1")], 1);
326 cache.put(p.clone(), vec![triple("X", "p", "o2")], 2);
327 let got = cache.get(&p, 3).unwrap();
328 assert_eq!(got.len(), 1);
329 assert_eq!(got[0].2, "o2");
331 assert_eq!(cache.len(), 1);
333 }
334
335 #[test]
340 fn test_lru_eviction_on_capacity() {
341 let mut cache = TripleCache::new(3);
342 let p1 = TriplePattern::for_subject("A");
343 let p2 = TriplePattern::for_subject("B");
344 let p3 = TriplePattern::for_subject("C");
345 let p4 = TriplePattern::for_subject("D");
346
347 cache.put(p1.clone(), vec![], 1);
348 cache.put(p2.clone(), vec![], 2);
349 cache.put(p3.clone(), vec![], 3);
350 cache.put(p4.clone(), vec![], 4);
352
353 assert_eq!(cache.len(), 3);
354 assert_eq!(cache.stats().evictions, 1);
355 assert!(cache.get(&p1, 5).is_none());
357 assert!(cache.get(&p4, 6).is_some());
359 }
360
361 #[test]
362 fn test_lru_eviction_respects_access_time() {
363 let mut cache = TripleCache::new(2);
364 let p1 = TriplePattern::for_subject("A");
365 let p2 = TriplePattern::for_subject("B");
366 let p3 = TriplePattern::for_subject("C");
367
368 cache.put(p1.clone(), vec![], 1);
369 cache.put(p2.clone(), vec![], 2);
370 cache.get(&p1, 10);
372 cache.put(p3.clone(), vec![], 3);
374
375 assert!(cache.get(&p1, 11).is_some());
376 assert!(cache.get(&p3, 12).is_some());
377 assert!(cache.get(&p2, 13).is_none());
379 }
380
381 #[test]
386 fn test_invalidate_bound_pattern() {
387 let mut cache = TripleCache::new(10);
388 let p = TriplePattern::bound(":Alice", ":knows", ":Bob");
389 cache.put(p.clone(), vec![triple(":Alice", ":knows", ":Bob")], 1);
390 cache.invalidate_matching(":Alice", ":knows", ":Bob");
391 assert!(cache.get(&p, 2).is_none());
392 }
393
394 #[test]
395 fn test_invalidate_wildcard_pattern_removes_all() {
396 let mut cache = TripleCache::new(10);
397 cache.put(TriplePattern::for_subject("A"), vec![], 1);
398 cache.put(TriplePattern::for_subject("B"), vec![], 2);
399 cache.put(TriplePattern::wildcard(), vec![], 3);
400 cache.invalidate_matching("A", "p", "o");
402 assert!(cache.get(&TriplePattern::wildcard(), 4).is_none());
404 }
405
406 #[test]
407 fn test_invalidate_non_matching_pattern_not_removed() {
408 let mut cache = TripleCache::new(10);
409 let p = TriplePattern::for_subject("Bob");
410 cache.put(p.clone(), vec![triple("Bob", "age", "30")], 1);
411 cache.invalidate_matching("Alice", "knows", "Carol");
413 assert!(cache.get(&p, 2).is_some());
414 }
415
416 #[test]
417 fn test_invalidate_updates_stats_size() {
418 let mut cache = TripleCache::new(10);
419 cache.put(TriplePattern::bound("s", "p", "o"), vec![], 1);
420 assert_eq!(cache.stats().size, 1);
421 cache.invalidate_matching("s", "p", "o");
422 assert_eq!(cache.stats().size, 0);
423 }
424
425 #[test]
430 fn test_stats_hits_and_misses() {
431 let mut cache = TripleCache::new(10);
432 let p = TriplePattern::wildcard();
433 cache.get(&p, 1); cache.put(p.clone(), vec![], 2);
435 cache.get(&p, 3); cache.get(&p, 4); assert_eq!(cache.stats().misses, 1);
438 assert_eq!(cache.stats().hits, 2);
439 }
440
441 #[test]
442 fn test_stats_eviction_count() {
443 let mut cache = TripleCache::new(2);
444 cache.put(TriplePattern::for_subject("A"), vec![], 1);
445 cache.put(TriplePattern::for_subject("B"), vec![], 2);
446 cache.put(TriplePattern::for_subject("C"), vec![], 3); cache.put(TriplePattern::for_subject("D"), vec![], 4); assert_eq!(cache.stats().evictions, 2);
449 }
450
451 #[test]
456 fn test_clear_empties_cache() {
457 let mut cache = TripleCache::new(10);
458 cache.put(TriplePattern::wildcard(), vec![], 1);
459 cache.put(TriplePattern::for_subject("X"), vec![], 2);
460 cache.clear();
461 assert!(cache.is_empty());
462 assert_eq!(cache.stats().size, 0);
463 }
464
465 #[test]
466 fn test_clear_then_reuse() {
467 let mut cache = TripleCache::new(5);
468 cache.put(TriplePattern::for_subject("A"), vec![], 1);
469 cache.clear();
470 let p = TriplePattern::for_subject("B");
471 cache.put(p.clone(), vec![triple("B", "p", "o")], 2);
472 assert!(cache.get(&p, 3).is_some());
473 }
474
475 #[test]
480 fn test_capacity_one() {
481 let mut cache = TripleCache::new(1);
482 let p1 = TriplePattern::for_subject("A");
483 let p2 = TriplePattern::for_subject("B");
484 cache.put(p1.clone(), vec![], 1);
485 cache.put(p2.clone(), vec![], 2); assert!(cache.get(&p1, 3).is_none());
487 assert!(cache.get(&p2, 4).is_some());
488 }
489
490 #[test]
491 fn test_empty_results_cached() {
492 let mut cache = TripleCache::new(5);
493 let p = TriplePattern::wildcard();
494 cache.put(p.clone(), vec![], 1);
495 let got = cache.get(&p, 2);
496 assert!(got.is_some());
497 assert!(got.unwrap().is_empty());
498 }
499}