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}