reddb_server/storage/index/
registry.rs1use std::collections::HashMap;
28use std::sync::{Arc, RwLock};
29
30use super::{IndexBase, IndexStats};
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub enum IndexScope {
35 Table { table: String, column: String },
37 Graph { collection: String },
39 Timeseries { series: String },
41}
42
43impl IndexScope {
44 pub fn table(table: impl Into<String>, column: impl Into<String>) -> Self {
46 Self::Table {
47 table: table.into(),
48 column: column.into(),
49 }
50 }
51
52 pub fn graph(collection: impl Into<String>) -> Self {
54 Self::Graph {
55 collection: collection.into(),
56 }
57 }
58
59 pub fn timeseries(series: impl Into<String>) -> Self {
61 Self::Timeseries {
62 series: series.into(),
63 }
64 }
65}
66
67pub type SharedIndex = Arc<dyn IndexBase>;
70
71pub struct IndexRegistry {
73 entries: RwLock<HashMap<IndexScope, SharedIndex>>,
74}
75
76impl IndexRegistry {
77 pub fn new() -> Self {
79 Self {
80 entries: RwLock::new(HashMap::new()),
81 }
82 }
83
84 pub fn register(&self, scope: IndexScope, index: SharedIndex) -> Option<SharedIndex> {
87 self.entries
88 .write()
89 .ok()
90 .and_then(|mut map| map.insert(scope, index))
91 }
92
93 pub fn unregister(&self, scope: &IndexScope) -> Option<SharedIndex> {
95 self.entries
96 .write()
97 .ok()
98 .and_then(|mut map| map.remove(scope))
99 }
100
101 pub fn get(&self, scope: &IndexScope) -> Option<SharedIndex> {
103 self.entries
104 .read()
105 .ok()
106 .and_then(|map| map.get(scope).cloned())
107 }
108
109 pub fn table_index_stats(&self, table: &str, column: &str) -> Option<IndexStats> {
111 self.get(&IndexScope::table(table, column))
112 .map(|idx| idx.stats())
113 }
114
115 pub fn graph_index_stats(&self, collection: &str) -> Option<IndexStats> {
117 self.get(&IndexScope::graph(collection))
118 .map(|idx| idx.stats())
119 }
120
121 pub fn timeseries_index_stats(&self, series: &str) -> Option<IndexStats> {
123 self.get(&IndexScope::timeseries(series))
124 .map(|idx| idx.stats())
125 }
126
127 pub fn len(&self) -> usize {
129 self.entries.read().map(|m| m.len()).unwrap_or(0)
130 }
131
132 pub fn is_empty(&self) -> bool {
134 self.len() == 0
135 }
136
137 pub fn snapshot(&self) -> Vec<(IndexScope, IndexStats)> {
140 self.entries
141 .read()
142 .map(|map| {
143 map.iter()
144 .map(|(scope, idx)| (scope.clone(), idx.stats()))
145 .collect()
146 })
147 .unwrap_or_default()
148 }
149
150 pub fn clear(&self) {
152 if let Ok(mut map) = self.entries.write() {
153 map.clear();
154 }
155 }
156}
157
158impl Default for IndexRegistry {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::storage::index::{IndexBase, IndexKind, IndexStats};
168
169 struct FakeIndex {
170 name: String,
171 kind: IndexKind,
172 stats: IndexStats,
173 }
174
175 impl FakeIndex {
176 fn new(name: &str, kind: IndexKind, distinct: usize) -> Self {
177 Self {
178 name: name.to_string(),
179 kind,
180 stats: IndexStats {
181 entries: distinct,
182 distinct_keys: distinct,
183 approx_bytes: 0,
184 kind,
185 has_bloom: false,
186 index_correlation: 0.0,
187 },
188 }
189 }
190 }
191
192 impl IndexBase for FakeIndex {
193 fn name(&self) -> &str {
194 &self.name
195 }
196 fn kind(&self) -> IndexKind {
197 self.kind
198 }
199 fn stats(&self) -> IndexStats {
200 self.stats.clone()
201 }
202 }
203
204 fn shared(name: &str, kind: IndexKind, distinct: usize) -> SharedIndex {
205 Arc::new(FakeIndex::new(name, kind, distinct))
206 }
207
208 #[test]
209 fn register_and_lookup_table_scope() {
210 let reg = IndexRegistry::new();
211 let prev = reg.register(
212 IndexScope::table("users", "email"),
213 shared("users.email", IndexKind::Hash, 1_000_000),
214 );
215 assert!(prev.is_none());
216
217 let stats = reg.table_index_stats("users", "email").unwrap();
218 assert_eq!(stats.distinct_keys, 1_000_000);
219 assert_eq!(stats.kind, IndexKind::Hash);
220 }
221
222 #[test]
223 fn register_replaces_existing() {
224 let reg = IndexRegistry::new();
225 reg.register(
226 IndexScope::table("t", "c"),
227 shared("old", IndexKind::BTree, 10),
228 );
229 let replaced = reg.register(
230 IndexScope::table("t", "c"),
231 shared("new", IndexKind::Hash, 100),
232 );
233 assert!(replaced.is_some());
234 let stats = reg.table_index_stats("t", "c").unwrap();
235 assert_eq!(stats.distinct_keys, 100);
236 assert_eq!(stats.kind, IndexKind::Hash);
237 }
238
239 #[test]
240 fn unregister_removes_entry() {
241 let reg = IndexRegistry::new();
242 reg.register(
243 IndexScope::graph("hosts"),
244 shared("adjacency", IndexKind::GraphAdjacency, 50),
245 );
246 assert_eq!(reg.len(), 1);
247
248 let removed = reg.unregister(&IndexScope::graph("hosts"));
249 assert!(removed.is_some());
250 assert_eq!(reg.len(), 0);
251 assert!(reg.graph_index_stats("hosts").is_none());
252 }
253
254 #[test]
255 fn multi_scope_registration() {
256 let reg = IndexRegistry::new();
257 reg.register(
258 IndexScope::table("users", "id"),
259 shared("u.id", IndexKind::BTree, 10_000),
260 );
261 reg.register(
262 IndexScope::graph("social"),
263 shared("social.adj", IndexKind::GraphAdjacency, 5_000),
264 );
265 reg.register(
266 IndexScope::timeseries("cpu.idle"),
267 shared("cpu.temporal", IndexKind::Temporal, 2_000),
268 );
269
270 assert_eq!(reg.len(), 3);
271 assert!(reg.table_index_stats("users", "id").is_some());
272 assert!(reg.graph_index_stats("social").is_some());
273 assert!(reg.timeseries_index_stats("cpu.idle").is_some());
274 }
275
276 #[test]
277 fn snapshot_returns_all_entries() {
278 let reg = IndexRegistry::new();
279 reg.register(
280 IndexScope::table("a", "x"),
281 shared("a.x", IndexKind::Hash, 5),
282 );
283 reg.register(
284 IndexScope::table("a", "y"),
285 shared("a.y", IndexKind::Hash, 6),
286 );
287
288 let snap = reg.snapshot();
289 assert_eq!(snap.len(), 2);
290 let kinds: Vec<IndexKind> = snap.iter().map(|(_, s)| s.kind).collect();
291 assert!(kinds.iter().all(|k| *k == IndexKind::Hash));
292 }
293
294 #[test]
295 fn clear_drops_everything() {
296 let reg = IndexRegistry::new();
297 reg.register(
298 IndexScope::table("a", "x"),
299 shared("a.x", IndexKind::Hash, 5),
300 );
301 reg.clear();
302 assert!(reg.is_empty());
303 }
304
305 #[test]
306 fn concurrent_registration_is_safe() {
307 use std::thread;
308
309 let reg = Arc::new(IndexRegistry::new());
310 let mut handles = vec![];
311 for t in 0..4u32 {
312 let reg_c = Arc::clone(®);
313 handles.push(thread::spawn(move || {
314 for i in 0..50u32 {
315 let scope = IndexScope::table(format!("t{t}"), format!("c{i}"));
316 reg_c.register(
317 scope,
318 shared(&format!("t{t}.c{i}"), IndexKind::BTree, i as usize + 1),
319 );
320 }
321 }));
322 }
323 for h in handles {
324 h.join().unwrap();
325 }
326 assert_eq!(reg.len(), 200);
327 }
328}