Skip to main content

clt_database/index_method/
mod.rs

1use std::sync::Arc;
2
3use rustc_hash::FxHashMap as HashMap;
4use turso_parser::ast;
5
6use crate::{
7    schema::IndexColumn,
8    storage::btree::BTreeCursor,
9    types::{IOResult, IndexInfo, KeyInfo},
10    vdbe::Register,
11    Connection, LimboError, Result, Value,
12};
13
14pub mod backing_btree;
15#[cfg(all(clt_turso_feature = "fts", not(target_family = "wasm")))]
16pub mod fts;
17pub mod toy_vector_sparse_ivf;
18
19pub const BACKING_BTREE_INDEX_METHOD_NAME: &str = "backing_btree";
20pub const TOY_VECTOR_SPARSE_IVF_INDEX_METHOD_NAME: &str = "toy_vector_sparse_ivf";
21
22/// index method "entry point" which can create attachment of the method to the table with given configuration
23/// (this trait acts like a "factory")
24pub trait IndexMethod: std::fmt::Debug + Send + Sync {
25    /// create attachment of the index method to the specific table with specific method configuration
26    fn attach(
27        &self,
28        configuration: &IndexMethodConfiguration,
29    ) -> Result<Arc<dyn IndexMethodAttachment>>;
30}
31
32#[derive(Debug, Clone)]
33pub struct IndexMethodConfiguration {
34    /// table name for which index_method is defined
35    pub table_name: String,
36    /// index name
37    pub index_name: String,
38    /// columns c1, c2, c3, ... provided to the index method (e.g. create index t_idx on t using method (c1, c2, c3, ...))
39    pub columns: crate::alloc::Vec<IndexColumn>,
40    /// optional parameters provided to the index method through WITH clause
41    pub parameters: HashMap<String, Value>,
42}
43
44/// index method attached to the table with specific configuration
45/// the attachment is capable of generating SELECT patterns where index can be used and also can create cursor for query execution
46pub trait IndexMethodAttachment: std::fmt::Debug + Send + Sync {
47    fn definition<'a>(&'a self) -> IndexMethodDefinition<'a>;
48    fn init(&self) -> Result<Box<dyn IndexMethodCursor>>;
49}
50
51#[derive(Debug)]
52pub struct IndexMethodDefinition<'a> {
53    /// index method name
54    pub method_name: &'a str,
55    /// index name
56    pub index_name: &'a str,
57    /// SELECT patterns where index method can be used
58    /// the patterns can contain positional placeholder which will make planner to capture parameters from the original query and provide them to the index method
59    /// (for example, pattern 'SELECT * FROM {table} LIMIT ?' will capture LIMIT parameter and provide its value from the query to the index method query_start(...) call)
60    pub patterns: &'a [ast::Select],
61    /// special marker which forces tursodb core to treat index method as backing btree - so it will allocate real btree on disk for that index method
62    pub backing_btree: bool,
63    /// Whether `query_start()` materializes all matching rowids up front (e.g. into a Vec/VecDeque).
64    /// When `true`, the cursor is safe to use during DML because it does not lazily stream from
65    /// a live data structure that writes could invalidate.
66    /// When `false`, the emitter will collect rowids into a RowSet/ephemeral table before writing.
67    pub results_materialized: bool,
68}
69
70/// Cost estimate returned by custom index methods for optimizer integration.
71/// This enables the optimizer to make cost-based decisions when choosing between
72/// custom index methods and traditional BTree indexes.
73#[derive(Debug, Clone, Copy)]
74pub struct IndexMethodCostEstimate {
75    /// Estimated CPU/IO cost (lower is better, comparable to optimizer Cost values)
76    pub estimated_cost: f64,
77    /// Estimated number of rows returned by the query
78    pub estimated_rows: u64,
79}
80
81/// cursor opened for index method and capable of executing DML/DDL/DQL queries for the index method over fixed table
82pub trait IndexMethodCursor {
83    /// create necessary components for index method (usually, this is a bunch of btree-s)
84    fn create(&mut self, connection: &Arc<Connection>, database_id: usize) -> Result<IOResult<()>>;
85    /// destroy components created in the create(...) call for index method
86    fn destroy(&mut self, connection: &Arc<Connection>, database_id: usize)
87        -> Result<IOResult<()>>;
88
89    /// open necessary components for reading the index
90    fn open_read(
91        &mut self,
92        connection: &Arc<Connection>,
93        database_id: usize,
94    ) -> Result<IOResult<()>>;
95    /// open necessary components for writing the index
96    fn open_write(
97        &mut self,
98        connection: &Arc<Connection>,
99        database_id: usize,
100    ) -> Result<IOResult<()>>;
101
102    /// handle insert action
103    /// "values" argument contains registers with values for index columns followed by rowid Integer register
104    /// (e.g. for "CREATE INDEX i ON t USING method (x, z)" insert(...) call will have 3 registers in values: [x, z, rowid])
105    fn insert(&mut self, values: &[Register]) -> Result<IOResult<()>>;
106    /// handle delete action
107    /// "values" argument contains registers with values for index columns followed by rowid Integer register
108    /// (e.g. for "CREATE INDEX i ON t USING method (x, z)" insert(...) call will have 3 registers in values: [x, z, rowid])
109    fn delete(&mut self, values: &[Register]) -> Result<IOResult<()>>;
110
111    /// initialize query to the index method
112    /// first element of "values" slice is the Integer register which holds index of the chosen [IndexMethodDefinition::patterns] by query planner
113    /// next arguments of the "values" slice are values from the original query expression captured by pattern
114    ///
115    /// For example, for 2 patterns ["SELECT * FROM {table} LIMIT ?", "SELECT * FROM {table} WHERE x = ?"], query_start(...) call can have following arguments:
116    /// - [Integer(0), Integer(10)] - pattern "SELECT * FROM {table} LIMIT ?" was chosen with LIMIT parameter equals to 10
117    /// - [Integer(1), Text("turso")] - pattern "SELECT * FROM {table} WHERE x = ?" was chosen with equality comparison equals to "turso"
118    ///
119    /// Returns false if query will produce no rows (similar to VFilter/Rewind op codes)
120    fn query_start(&mut self, values: &[Register]) -> Result<IOResult<bool>>;
121
122    /// Moves cursor to the next response row
123    /// Returns false if query exhausted all rows
124    fn query_next(&mut self) -> Result<IOResult<bool>>;
125
126    /// Return column with given idx (zero-based) from current row
127    fn query_column(&mut self, idx: usize) -> Result<IOResult<Value>>;
128
129    /// Return rowid of the original table row which corresponds to the current cursor row
130    ///
131    /// This method is used by tursodb core in order to "enrich" response from query pattern with additional fields from original table
132    /// For example, consider pattern like this:
133    ///
134    /// > SELECT vector_distance_jaccard(embedding, ?) as d FROM table ORDER BY d LIMIT 10
135    ///
136    /// It can be used in more complex query:
137    ///
138    /// > SELECT name, comment, rating, vector_distance_jaccard(embedding, ?) as d FROM table ORDER BY d LIMIT 10
139    ///
140    /// In this case query planner will execute index method query first, and then
141    /// enrich its result with name, comment, rating columns from original table accessing original row by its rowid
142    /// returned from query_rowid(...) method
143    fn query_rowid(&mut self) -> Result<IOResult<Option<i64>>>;
144
145    /// Called before transaction commit to flush any pending writes.
146    /// This ensures index method writes are persisted as part of the transaction.
147    fn pre_commit(&mut self) -> Result<IOResult<()>> {
148        Ok(IOResult::Done(()))
149    }
150
151    /// Optimize the index by merging segments or performing other maintenance.
152    fn optimize(
153        &mut self,
154        _connection: &Arc<Connection>,
155        _database_id: usize,
156    ) -> Result<IOResult<()>> {
157        Ok(IOResult::Done(()))
158    }
159
160    /// Estimate the cost of executing a query with the given pattern.
161    ///
162    /// This method enables the optimizer to make cost-based decisions when choosing
163    /// between custom index methods and traditional BTree indexes.
164    fn estimate_cost(
165        &self,
166        pattern_idx: usize,
167        base_table_rows: f64,
168    ) -> Option<IndexMethodCostEstimate> {
169        let _ = (pattern_idx, base_table_rows);
170        None
171    }
172}
173
174/// helper method to open table BTree cursor in the index method implementation
175pub(crate) fn open_table_cursor(
176    connection: &Connection,
177    database_id: usize,
178    table: &str,
179) -> Result<BTreeCursor> {
180    let pager = connection.get_pager_from_database_index(&database_id)?;
181    let Some(table) = connection.with_schema(database_id, |schema| schema.get_table(table)) else {
182        return Err(LimboError::InternalError(format!(
183            "table {table} not found",
184        )));
185    };
186    let cursor = BTreeCursor::new_table(pager, table.get_root_page()?, table.columns().len());
187    Ok(cursor)
188}
189
190/// helper method to open index BTree cursor in the index method implementation
191pub(crate) fn open_index_cursor<I, E>(
192    connection: &Connection,
193    database_id: usize,
194    table: &str,
195    index: &str,
196    keys: I,
197) -> Result<BTreeCursor>
198where
199    I: IntoIterator<Item = KeyInfo, IntoIter = E>,
200    E: ExactSizeIterator<Item = KeyInfo>,
201{
202    let pager = connection.get_pager_from_database_index(&database_id)?;
203    let Some(scratch) = connection.with_schema(database_id, |schema| {
204        schema.get_index(table, index).cloned()
205    }) else {
206        return Err(LimboError::InternalError(format!(
207            "index {index} for table {table} not found",
208        )));
209    };
210    let keys = keys.into_iter();
211    let num_cols = keys.len();
212    let mut cursor = BTreeCursor::new(pager, scratch.root_page, num_cols);
213    cursor.index_info = Some(Arc::new(IndexInfo::new(
214        keys,
215        false,
216        num_cols,
217        scratch.unique,
218    )?));
219    Ok(cursor)
220}
221
222/// helper method to parse select patterns for [IndexMethodAttachment::definition] call
223pub(crate) fn parse_patterns(patterns: &[&str]) -> Result<Vec<ast::Select>> {
224    let mut parsed = Vec::new();
225    for pattern in patterns {
226        let mut parser = turso_parser::parser::Parser::new(pattern.as_bytes());
227        let Some(ast) = parser.next() else {
228            return Err(LimboError::ParseError(format!(
229                "unable to parse pattern statement: {pattern}",
230            )));
231        };
232        let ast = ast?;
233        let ast::Cmd::Stmt(ast::Stmt::Select(select)) = ast else {
234            return Err(LimboError::ParseError(format!(
235                "only select patterns are allowed: {pattern}",
236            )));
237        };
238        parsed.push(select);
239    }
240    Ok(parsed)
241}