vibesql_catalog/
index.rs

1//! Index metadata definitions for catalog management
2//!
3//! This module provides the structures for tracking index metadata
4//! in the database catalog, independent of the physical index storage.
5
6/// Index metadata stored in the catalog
7#[derive(Debug, Clone, PartialEq)]
8pub struct IndexMetadata {
9    /// Name of the index
10    pub name: String,
11    /// Name of the table this index belongs to
12    pub table_name: String,
13    /// Type of index
14    pub index_type: IndexType,
15    /// Columns included in the index
16    pub columns: Vec<IndexedColumn>,
17    /// Whether this index enforces uniqueness
18    pub is_unique: bool,
19}
20
21/// Type of physical index structure
22#[derive(Debug, Clone, PartialEq)]
23pub enum IndexType {
24    /// B-tree index for general-purpose indexing
25    BTree,
26    /// Hash index for equality lookups (PRIMARY KEY, UNIQUE)
27    Hash,
28    /// R-tree spatial index for geometric data
29    RTree,
30    /// Full-text index for text search
31    Fulltext,
32    /// IVFFlat index for approximate nearest neighbor search on vectors
33    IVFFlat {
34        /// Distance metric used for similarity calculations
35        metric: VectorDistanceMetric,
36        /// Number of clusters/lists
37        lists: u32,
38    },
39    /// HNSW index for high-performance approximate nearest neighbor search
40    Hnsw {
41        /// Distance metric used for similarity calculations
42        metric: VectorDistanceMetric,
43        /// Maximum number of connections per node
44        m: u32,
45        /// Size of dynamic candidate list during construction
46        ef_construction: u32,
47    },
48}
49
50/// Distance metric for vector index operations
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum VectorDistanceMetric {
53    /// Euclidean distance (L2 norm)
54    L2,
55    /// Cosine similarity
56    Cosine,
57    /// Inner product (dot product)
58    InnerProduct,
59}
60
61/// Column specification within an index - can be either a simple column or an expression
62#[derive(Debug, Clone, PartialEq)]
63pub enum IndexedColumn {
64    /// Simple column reference with optional prefix length
65    Column {
66        /// Column name
67        column_name: String,
68        /// Sort order for ordered indexes (B-tree, R-tree)
69        order: SortOrder,
70        /// Optional prefix length for indexed columns (MySQL/SQLite feature)
71        /// When present, only the first N characters/bytes of the column value are indexed
72        /// Example: UNIQUE (email(50)) indexes only first 50 characters
73        prefix_length: Option<u64>,
74    },
75    /// Expression index (functional index)
76    /// Example: CREATE INDEX idx ON t(lower(name)) or CREATE INDEX idx ON t(a + b)
77    Expression {
78        /// The expression AST to evaluate for index keys
79        expr: Box<vibesql_ast::Expression>,
80        /// Sort order for ordered indexes
81        order: SortOrder,
82    },
83}
84
85impl IndexedColumn {
86    /// Create a new simple column index
87    pub fn new_column(column_name: String, order: SortOrder) -> Self {
88        IndexedColumn::Column { column_name, order, prefix_length: None }
89    }
90
91    /// Create a new column index with prefix length
92    pub fn new_column_with_prefix(column_name: String, order: SortOrder, prefix_length: u64) -> Self {
93        IndexedColumn::Column { column_name, order, prefix_length: Some(prefix_length) }
94    }
95
96    /// Create a new expression index
97    pub fn new_expression(expr: vibesql_ast::Expression, order: SortOrder) -> Self {
98        IndexedColumn::Expression { expr: Box::new(expr), order }
99    }
100
101    /// Get the column name if this is a simple column reference
102    pub fn column_name(&self) -> Option<&str> {
103        match self {
104            IndexedColumn::Column { column_name, .. } => Some(column_name),
105            IndexedColumn::Expression { .. } => None,
106        }
107    }
108
109    /// Get the sort order
110    pub fn order(&self) -> &SortOrder {
111        match self {
112            IndexedColumn::Column { order, .. } => order,
113            IndexedColumn::Expression { order, .. } => order,
114        }
115    }
116
117    /// Get the prefix length if this is a column with prefix
118    pub fn prefix_length(&self) -> Option<u64> {
119        match self {
120            IndexedColumn::Column { prefix_length, .. } => *prefix_length,
121            IndexedColumn::Expression { .. } => None,
122        }
123    }
124
125    /// Check if this is an expression index
126    pub fn is_expression(&self) -> bool {
127        matches!(self, IndexedColumn::Expression { .. })
128    }
129
130    /// Get the expression if this is an expression index
131    pub fn get_expression(&self) -> Option<&vibesql_ast::Expression> {
132        match self {
133            IndexedColumn::Expression { expr, .. } => Some(expr),
134            IndexedColumn::Column { .. } => None,
135        }
136    }
137}
138
139/// Sort order for indexed columns
140#[derive(Debug, Clone, PartialEq)]
141pub enum SortOrder {
142    Ascending,
143    Descending,
144}
145
146impl IndexMetadata {
147    /// Create a new index metadata entry
148    pub fn new(
149        name: String,
150        table_name: String,
151        index_type: IndexType,
152        columns: Vec<IndexedColumn>,
153        is_unique: bool,
154    ) -> Self {
155        Self { name, table_name, index_type, columns, is_unique }
156    }
157
158    /// Get the fully qualified index name (table.index)
159    pub fn qualified_name(&self) -> String {
160        format!("{}.{}", self.table_name, self.name)
161    }
162
163    /// Check if this index can be used for the given column
164    pub fn can_index_column(&self, column_name: &str) -> bool {
165        // For now, check if the column is the first column in the index
166        // More sophisticated matching can be added later (composite index prefixes)
167        // Expression indexes cannot be used for simple column lookups
168        self.columns
169            .first()
170            .and_then(|col| col.column_name())
171            .map(|name| name == column_name)
172            .unwrap_or(false)
173    }
174
175    /// Check if this index can be used for the given columns
176    pub fn can_index_columns(&self, column_names: &[String]) -> bool {
177        if column_names.is_empty() {
178            return false;
179        }
180
181        // Check if the index columns match the query columns as a prefix
182        // Expression indexes cannot be matched by column name alone
183        column_names.len() <= self.columns.len()
184            && column_names.iter().zip(self.columns.iter()).all(|(query_col, index_col)| {
185                index_col.column_name().map(|name| name == query_col).unwrap_or(false)
186            })
187    }
188
189    /// Check if this index contains any expression columns
190    pub fn has_expression_columns(&self) -> bool {
191        self.columns.iter().any(|col| col.is_expression())
192    }
193
194    /// Check if this index contains only expression columns
195    pub fn is_expression_index(&self) -> bool {
196        !self.columns.is_empty() && self.columns.iter().all(|col| col.is_expression())
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_qualified_name() {
206        let index = IndexMetadata::new(
207            "idx_name".to_string(),
208            "users".to_string(),
209            IndexType::BTree,
210            vec![IndexedColumn::new_column("name".to_string(), SortOrder::Ascending)],
211            false,
212        );
213
214        assert_eq!(index.qualified_name(), "users.idx_name");
215    }
216
217    #[test]
218    fn test_can_index_column() {
219        let index = IndexMetadata::new(
220            "idx_name".to_string(),
221            "users".to_string(),
222            IndexType::BTree,
223            vec![IndexedColumn::new_column("name".to_string(), SortOrder::Ascending)],
224            false,
225        );
226
227        assert!(index.can_index_column("name"));
228        assert!(!index.can_index_column("email"));
229    }
230
231    #[test]
232    fn test_can_index_columns_composite() {
233        let index = IndexMetadata::new(
234            "idx_name_email".to_string(),
235            "users".to_string(),
236            IndexType::BTree,
237            vec![
238                IndexedColumn::new_column("name".to_string(), SortOrder::Ascending),
239                IndexedColumn::new_column("email".to_string(), SortOrder::Ascending),
240            ],
241            false,
242        );
243
244        // Can use index for name prefix
245        assert!(index.can_index_columns(&["name".to_string()]));
246
247        // Can use index for name+email
248        assert!(index.can_index_columns(&["name".to_string(), "email".to_string()]));
249
250        // Cannot use index for email alone (not a prefix)
251        assert!(!index.can_index_columns(&["email".to_string()]));
252
253        // Cannot use index for wrong column
254        assert!(!index.can_index_columns(&["age".to_string()]));
255    }
256
257    #[test]
258    fn test_indexed_column_helpers() {
259        // Test column index
260        let col = IndexedColumn::new_column("name".to_string(), SortOrder::Ascending);
261        assert_eq!(col.column_name(), Some("name"));
262        assert_eq!(*col.order(), SortOrder::Ascending);
263        assert!(!col.is_expression());
264        assert!(col.get_expression().is_none());
265        assert!(col.prefix_length().is_none());
266
267        // Test column with prefix
268        let col_prefix =
269            IndexedColumn::new_column_with_prefix("email".to_string(), SortOrder::Descending, 50);
270        assert_eq!(col_prefix.column_name(), Some("email"));
271        assert_eq!(*col_prefix.order(), SortOrder::Descending);
272        assert_eq!(col_prefix.prefix_length(), Some(50));
273        assert!(!col_prefix.is_expression());
274
275        // Test expression index - use a literal expression
276        let expr = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(42));
277        let expr_col = IndexedColumn::new_expression(expr.clone(), SortOrder::Ascending);
278        assert!(expr_col.column_name().is_none());
279        assert_eq!(*expr_col.order(), SortOrder::Ascending);
280        assert!(expr_col.is_expression());
281        assert!(expr_col.get_expression().is_some());
282        assert!(expr_col.prefix_length().is_none());
283    }
284
285    #[test]
286    fn test_expression_index_detection() {
287        // Pure column index
288        let column_index = IndexMetadata::new(
289            "idx_col".to_string(),
290            "test".to_string(),
291            IndexType::BTree,
292            vec![IndexedColumn::new_column("a".to_string(), SortOrder::Ascending)],
293            false,
294        );
295        assert!(!column_index.has_expression_columns());
296        assert!(!column_index.is_expression_index());
297
298        // Pure expression index - use a literal expression
299        let expr = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(1));
300        let expr_index = IndexMetadata::new(
301            "idx_expr".to_string(),
302            "test".to_string(),
303            IndexType::BTree,
304            vec![IndexedColumn::new_expression(expr.clone(), SortOrder::Ascending)],
305            false,
306        );
307        assert!(expr_index.has_expression_columns());
308        assert!(expr_index.is_expression_index());
309
310        // Mixed index (column + expression)
311        let mixed_index = IndexMetadata::new(
312            "idx_mixed".to_string(),
313            "test".to_string(),
314            IndexType::BTree,
315            vec![
316                IndexedColumn::new_column("a".to_string(), SortOrder::Ascending),
317                IndexedColumn::new_expression(expr, SortOrder::Ascending),
318            ],
319            false,
320        );
321        assert!(mixed_index.has_expression_columns());
322        assert!(!mixed_index.is_expression_index()); // Not purely expression
323
324        // Empty index
325        let empty_index = IndexMetadata::new(
326            "idx_empty".to_string(),
327            "test".to_string(),
328            IndexType::BTree,
329            vec![],
330            false,
331        );
332        assert!(!empty_index.has_expression_columns());
333        assert!(!empty_index.is_expression_index());
334    }
335
336    #[test]
337    fn test_expression_index_cannot_match_columns() {
338        // Use a literal expression
339        let expr = vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(1));
340        let expr_index = IndexMetadata::new(
341            "idx_expr".to_string(),
342            "test".to_string(),
343            IndexType::BTree,
344            vec![IndexedColumn::new_expression(expr, SortOrder::Ascending)],
345            false,
346        );
347
348        // Expression index cannot be used for simple column lookups
349        assert!(!expr_index.can_index_column("x"));
350        assert!(!expr_index.can_index_columns(&["x".to_string()]));
351    }
352}