1#[derive(Debug, Clone, PartialEq)]
8pub struct IndexMetadata {
9 pub name: String,
11 pub table_name: String,
13 pub index_type: IndexType,
15 pub columns: Vec<IndexedColumn>,
17 pub is_unique: bool,
19}
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum IndexType {
24 BTree,
26 Hash,
28 RTree,
30 Fulltext,
32 IVFFlat {
34 metric: VectorDistanceMetric,
36 lists: u32,
38 },
39 Hnsw {
41 metric: VectorDistanceMetric,
43 m: u32,
45 ef_construction: u32,
47 },
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum VectorDistanceMetric {
53 L2,
55 Cosine,
57 InnerProduct,
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum IndexedColumn {
64 Column {
66 column_name: String,
68 order: SortOrder,
70 prefix_length: Option<u64>,
74 },
75 Expression {
78 expr: Box<vibesql_ast::Expression>,
80 order: SortOrder,
82 },
83}
84
85impl IndexedColumn {
86 pub fn new_column(column_name: String, order: SortOrder) -> Self {
88 IndexedColumn::Column { column_name, order, prefix_length: None }
89 }
90
91 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 pub fn new_expression(expr: vibesql_ast::Expression, order: SortOrder) -> Self {
98 IndexedColumn::Expression { expr: Box::new(expr), order }
99 }
100
101 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 pub fn order(&self) -> &SortOrder {
111 match self {
112 IndexedColumn::Column { order, .. } => order,
113 IndexedColumn::Expression { order, .. } => order,
114 }
115 }
116
117 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 pub fn is_expression(&self) -> bool {
127 matches!(self, IndexedColumn::Expression { .. })
128 }
129
130 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#[derive(Debug, Clone, PartialEq)]
141pub enum SortOrder {
142 Ascending,
143 Descending,
144}
145
146impl IndexMetadata {
147 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 pub fn qualified_name(&self) -> String {
160 format!("{}.{}", self.table_name, self.name)
161 }
162
163 pub fn can_index_column(&self, column_name: &str) -> bool {
165 self.columns
169 .first()
170 .and_then(|col| col.column_name())
171 .map(|name| name == column_name)
172 .unwrap_or(false)
173 }
174
175 pub fn can_index_columns(&self, column_names: &[String]) -> bool {
177 if column_names.is_empty() {
178 return false;
179 }
180
181 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 pub fn has_expression_columns(&self) -> bool {
191 self.columns.iter().any(|col| col.is_expression())
192 }
193
194 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 assert!(index.can_index_columns(&["name".to_string()]));
246
247 assert!(index.can_index_columns(&["name".to_string(), "email".to_string()]));
249
250 assert!(!index.can_index_columns(&["email".to_string()]));
252
253 assert!(!index.can_index_columns(&["age".to_string()]));
255 }
256
257 #[test]
258 fn test_indexed_column_helpers() {
259 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 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 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 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 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 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()); 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 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 assert!(!expr_index.can_index_column("x"));
350 assert!(!expr_index.can_index_columns(&["x".to_string()]));
351 }
352}