uqa_storage/
index_manager.rs1#![allow(clippy::needless_pass_by_value, clippy::map_unwrap_or)]
15
16use parking_lot::Mutex;
17use std::collections::BTreeMap;
18
19use uqa_core::{Predicate, Value};
20
21use crate::btree_index::BTreeIndex;
22use crate::index_abc::Index;
23use crate::index_types::{IndexDef, IndexType};
24use crate::StorageBackendError;
25
26pub struct BTreeIndexHandle {
30 def: IndexDef,
31 inner: BTreeIndex,
32}
33
34impl BTreeIndexHandle {
35 pub fn new(def: IndexDef) -> Result<Self, StorageBackendError> {
36 if def.columns.len() != 1 {
37 return Err(StorageBackendError::Other(format!(
38 "B-tree index `{}` requires exactly one column; got {}",
39 def.name,
40 def.columns.len()
41 )));
42 }
43 let field = def.columns[0].clone();
44 Ok(Self {
45 def,
46 inner: BTreeIndex::new(field),
47 })
48 }
49
50 pub fn insert(&mut self, doc_id: u64, value: Value) {
51 self.inner.insert(doc_id, value);
52 }
53
54 pub fn remove(&mut self, doc_id: u64, value: &Value) {
55 self.inner.remove(doc_id, value);
56 }
57
58 pub fn clear(&mut self) {
59 self.inner.clear();
60 }
61
62 pub fn inner(&self) -> &BTreeIndex {
63 &self.inner
64 }
65
66 pub fn inner_mut(&mut self) -> &mut BTreeIndex {
67 &mut self.inner
68 }
69}
70
71impl Index for BTreeIndexHandle {
72 fn index_def(&self) -> &IndexDef {
73 &self.def
74 }
75 fn scan(&self, predicate: &Predicate) -> uqa_core::PostingList {
76 self.inner.scan(predicate)
77 }
78 fn estimate_cardinality(&self, predicate: &Predicate) -> usize {
79 self.inner.scan(predicate).len()
80 }
81 fn scan_cost(&self, predicate: &Predicate) -> f64 {
82 let card = self.estimate_cardinality(predicate) as f64;
87 match predicate {
88 Predicate::Equals(_) => 1.0 + card * 0.1,
89 _ => card.max(1.0),
90 }
91 }
92 fn build(&mut self) -> Result<(), StorageBackendError> {
93 Ok(())
94 }
95 fn drop_index(&mut self) -> Result<(), StorageBackendError> {
96 self.inner.clear();
97 Ok(())
98 }
99}
100
101#[derive(Default)]
103pub struct IndexManager {
104 indexes: Mutex<BTreeMap<String, Box<dyn Index>>>,
105}
106
107impl IndexManager {
108 pub fn new() -> Self {
109 Self {
110 indexes: Mutex::new(BTreeMap::new()),
111 }
112 }
113
114 pub fn create_index(&self, index_def: IndexDef) -> Result<(), StorageBackendError> {
118 if index_def.index_type != IndexType::BTree {
119 return Err(StorageBackendError::Other(format!(
120 "IndexManager has no physical `{}` implementation for index `{}`; engine-specific index backends must be registered through their owning engine",
121 index_def.index_type.as_str(),
122 index_def.name
123 )));
124 }
125 let mut index: Box<dyn Index> = Box::new(BTreeIndexHandle::new(index_def.clone())?);
126 let mut guard = self.indexes.lock();
127 if guard.contains_key(&index_def.name) {
128 return Err(StorageBackendError::Other(format!(
129 "index `{}` is already registered",
130 index_def.name
131 )));
132 }
133 index.build()?;
134 guard.insert(index_def.name.clone(), index);
135 Ok(())
136 }
137
138 pub fn drop_index(&self, name: &str) -> Result<bool, StorageBackendError> {
139 let mut guard = self.indexes.lock();
140 if let Some(mut idx) = guard.remove(name) {
141 idx.drop_index()?;
142 Ok(true)
143 } else {
144 Ok(false)
145 }
146 }
147
148 pub fn drop_indexes_for_table(&self, table_name: &str) -> Result<(), StorageBackendError> {
149 let mut guard = self.indexes.lock();
150 let names: Vec<String> = guard
151 .iter()
152 .filter(|(_, idx)| idx.index_def().table_name == table_name)
153 .map(|(n, _)| n.clone())
154 .collect();
155 for name in names {
156 if let Some(mut idx) = guard.remove(&name) {
157 idx.drop_index()?;
158 }
159 }
160 Ok(())
161 }
162
163 pub fn find_covering_index_name(
164 &self,
165 table_name: &str,
166 column: &str,
167 predicate: &Predicate,
168 ) -> Option<String> {
169 self.find_covering_index_with_cost(table_name, column, predicate)
170 .map(|(name, _)| name)
171 }
172
173 pub fn find_covering_index_with_cost(
177 &self,
178 table_name: &str,
179 column: &str,
180 predicate: &Predicate,
181 ) -> Option<(String, f64)> {
182 let guard = self.indexes.lock();
183 let mut best: Option<(String, f64)> = None;
184 for (name, idx) in guard.iter() {
185 let def = idx.index_def();
186 if def.table_name != table_name {
187 continue;
188 }
189 if def.columns.first().map(String::as_str) != Some(column) {
190 continue;
191 }
192 let cost = idx.scan_cost(predicate);
193 if best.as_ref().map(|(_, c)| cost < *c).unwrap_or(true) {
194 best = Some((name.clone(), cost));
195 }
196 }
197 best
198 }
199
200 pub fn has_index(&self, name: &str) -> bool {
201 self.indexes.lock().contains_key(name)
202 }
203
204 pub fn index_count(&self) -> usize {
205 self.indexes.lock().len()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn fresh() -> IndexManager {
214 IndexManager::new()
215 }
216
217 #[test]
218 fn fresh_manager_has_no_indexes() {
219 let mgr = fresh();
220 assert_eq!(mgr.index_count(), 0);
221 assert!(!mgr.has_index("missing"));
222 }
223
224 #[test]
225 fn drop_unknown_index_is_noop() {
226 let mgr = fresh();
227 assert!(!mgr.drop_index("missing").unwrap());
228 }
229
230 #[test]
231 fn create_then_drop_btree_index_reflects_in_count() {
232 let mgr = fresh();
233 let def = IndexDef::new(
234 "users_age_idx",
235 IndexType::BTree,
236 "users",
237 vec!["age".into()],
238 );
239 mgr.create_index(def).unwrap();
240 assert_eq!(mgr.index_count(), 1);
241 assert!(mgr.has_index("users_age_idx"));
242 assert!(mgr.drop_index("users_age_idx").unwrap());
243 assert_eq!(mgr.index_count(), 0);
244 }
245
246 #[test]
247 fn find_covering_index_picks_matching_btree() {
248 let mgr = fresh();
249 mgr.create_index(IndexDef::new(
250 "users_age_idx",
251 IndexType::BTree,
252 "users",
253 vec!["age".into()],
254 ))
255 .unwrap();
256 let pred = Predicate::Equals(Value::Int(42));
257 let pick = mgr.find_covering_index_name("users", "age", &pred);
258 assert_eq!(pick, Some("users_age_idx".into()));
259 }
260
261 #[test]
262 fn invalid_or_unimplemented_physical_indexes_are_explicit_errors() {
263 let mgr = fresh();
264 for columns in [Vec::new(), vec!["a".into(), "b".into()]] {
265 let error = mgr
266 .create_index(IndexDef::new(
267 "bad_btree",
268 IndexType::BTree,
269 "users",
270 columns,
271 ))
272 .unwrap_err();
273 assert!(error.to_string().contains("requires exactly one column"));
274 }
275
276 let error = mgr
277 .create_index(IndexDef::new(
278 "not_a_btree",
279 IndexType::Gin,
280 "users",
281 vec!["body".into()],
282 ))
283 .unwrap_err();
284 assert!(error
285 .to_string()
286 .contains("no physical `gin` implementation"));
287 assert_eq!(mgr.index_count(), 0);
288 }
289}