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, 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
27/// Thin [`Index`] adapter over an in-memory [`BTreeIndex`]. Each
28/// adapter owns its [`IndexDef`] so the registry can route lookups
29/// by table/column.
30pub 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        // Cost proxy: equality predicates are cheap (one bucket lookup);
84        // ranges scale with the predicted matching cardinality. The
85        // planner reads relative numbers so the absolute scale is
86        // unimportant.
87        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
102/// Index registry. Constructed once per [`crate::Catalog`] and shared
103/// across the engine's tables.
104pub 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    /// Build a physical index and register the definition under
119    /// `index_def.name`. Returns an error if an index with the same
120    /// name is already registered.
121    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    /// Like [`Self::find_covering_index_name`] but returns the chosen
178    /// index's name together with its `scan_cost(predicate)` so the caller can
179    /// require `scan_cost < full_scan_cost` before committing to the rewrite.
180    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}