Skip to main content

uqa_storage/
index_manager.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Index manager: registry that creates / drops / looks up indexes.
8//!
9//! Owns the in-memory map of
10//! `Box<dyn Index>` and resolves `find_covering_index` lookups for
11//! the planner. The registry stays in memory and delegates persistence to the
12//! catalog when wired by the engine.
13
14#![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
26/// Thin [`Index`] adapter over an in-memory [`BTreeIndex`]. Each
27/// adapter owns its [`IndexDef`] so the registry can route lookups
28/// by table/column.
29pub 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        // Cost proxy: equality predicates are cheap (one bucket lookup);
83        // ranges scale with the predicted matching cardinality. The
84        // planner reads relative numbers so the absolute scale is
85        // unimportant.
86        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/// In-memory physical index registry, independent of catalog persistence.
102#[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    /// Build a physical index and register the definition under
115    /// `index_def.name`. Returns an error if an index with the same
116    /// name is already registered.
117    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    /// Like [`Self::find_covering_index_name`] but returns the chosen
174    /// index's name together with its `scan_cost(predicate)` so the caller can
175    /// require `scan_cost < full_scan_cost` before committing to the rewrite.
176    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}