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