oxihuman_core/
query_cache.rs1#![allow(dead_code)]
4
5use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct QueryEntry<V> {
12 pub value: V,
13 pub ttl: u32,
14 pub hits: u32,
15}
16
17pub struct QueryCache<V> {
19 entries: HashMap<String, QueryEntry<V>>,
20 total_hits: u64,
21 total_misses: u64,
22 default_ttl: u32,
23}
24
25#[allow(dead_code)]
26impl<V: Clone> QueryCache<V> {
27 pub fn new(default_ttl: u32) -> Self {
28 QueryCache {
29 entries: HashMap::new(),
30 total_hits: 0,
31 total_misses: 0,
32 default_ttl,
33 }
34 }
35
36 pub fn insert(&mut self, key: &str, value: V) {
37 let ttl = self.default_ttl;
38 self.entries.insert(
39 key.to_string(),
40 QueryEntry {
41 value,
42 ttl,
43 hits: 0,
44 },
45 );
46 }
47
48 pub fn insert_with_ttl(&mut self, key: &str, value: V, ttl: u32) {
49 self.entries.insert(
50 key.to_string(),
51 QueryEntry {
52 value,
53 ttl,
54 hits: 0,
55 },
56 );
57 }
58
59 pub fn get(&mut self, key: &str) -> Option<&V> {
60 if let Some(entry) = self.entries.get_mut(key) {
61 entry.hits += 1;
62 self.total_hits += 1;
63 Some(&entry.value)
64 } else {
65 self.total_misses += 1;
66 None
67 }
68 }
69
70 pub fn peek(&self, key: &str) -> Option<&V> {
71 self.entries.get(key).map(|e| &e.value)
72 }
73
74 pub fn contains(&self, key: &str) -> bool {
75 self.entries.contains_key(key)
76 }
77
78 pub fn remove(&mut self, key: &str) -> bool {
79 self.entries.remove(key).is_some()
80 }
81
82 pub fn tick(&mut self) {
83 self.entries.retain(|_, e| {
84 if e.ttl == 0 {
85 false
86 } else {
87 e.ttl -= 1;
88 true
89 }
90 });
91 }
92
93 pub fn len(&self) -> usize {
94 self.entries.len()
95 }
96
97 pub fn is_empty(&self) -> bool {
98 self.entries.is_empty()
99 }
100
101 pub fn hit_rate(&self) -> f64 {
102 let total = self.total_hits + self.total_misses;
103 if total == 0 {
104 0.0
105 } else {
106 self.total_hits as f64 / total as f64
107 }
108 }
109
110 pub fn total_hits(&self) -> u64 {
111 self.total_hits
112 }
113
114 pub fn total_misses(&self) -> u64 {
115 self.total_misses
116 }
117
118 pub fn clear(&mut self) {
119 self.entries.clear();
120 }
121
122 pub fn keys(&self) -> Vec<&str> {
123 self.entries.keys().map(|k| k.as_str()).collect()
124 }
125}
126
127pub fn new_query_cache<V: Clone>(default_ttl: u32) -> QueryCache<V> {
128 QueryCache::new(default_ttl)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn insert_and_get() {
137 let mut c: QueryCache<i32> = new_query_cache(10);
138 c.insert("k", 42);
139 assert_eq!(*c.get("k").expect("should succeed"), 42);
140 }
141
142 #[test]
143 fn miss_increments_counter() {
144 let mut c: QueryCache<i32> = new_query_cache(10);
145 assert!(c.get("none").is_none());
146 assert_eq!(c.total_misses(), 1);
147 }
148
149 #[test]
150 fn hit_rate_calculation() {
151 let mut c: QueryCache<i32> = new_query_cache(10);
152 c.insert("k", 1);
153 c.get("k");
154 c.get("k");
155 c.get("missing");
156 let hr = c.hit_rate();
157 assert!((hr - 2.0 / 3.0).abs() < 1e-9);
158 }
159
160 #[test]
161 fn ttl_expiry() {
162 let mut c: QueryCache<i32> = new_query_cache(1);
163 c.insert("k", 1);
164 c.tick();
165 assert!(c.contains("k"));
166 c.tick();
167 assert!(!c.contains("k"));
168 }
169
170 #[test]
171 fn contains_and_remove() {
172 let mut c: QueryCache<i32> = new_query_cache(5);
173 c.insert("x", 7);
174 assert!(c.contains("x"));
175 c.remove("x");
176 assert!(!c.contains("x"));
177 }
178
179 #[test]
180 fn clear() {
181 let mut c: QueryCache<i32> = new_query_cache(5);
182 c.insert("a", 1);
183 c.insert("b", 2);
184 c.clear();
185 assert!(c.is_empty());
186 }
187
188 #[test]
189 fn peek_no_hit_count() {
190 let mut c: QueryCache<i32> = new_query_cache(5);
191 c.insert("k", 1);
192 c.peek("k");
193 assert_eq!(c.total_hits(), 0);
194 }
195
196 #[test]
197 fn insert_with_custom_ttl() {
198 let mut c: QueryCache<i32> = new_query_cache(100);
199 c.insert_with_ttl("k", 5, 1);
200 c.tick();
201 c.tick();
202 assert!(!c.contains("k"));
203 }
204
205 #[test]
206 fn len_tracking() {
207 let mut c: QueryCache<i32> = new_query_cache(5);
208 assert_eq!(c.len(), 0);
209 c.insert("a", 1);
210 c.insert("b", 2);
211 assert_eq!(c.len(), 2);
212 }
213}