1use vibesql_ast::CreateIndexStmt;
13use vibesql_storage::Database;
14
15use super::{
16 btree_index::create_btree_index,
17 spatial_index::create_spatial_index,
18 validation::{index_already_exists, validate_create_index},
19 vector_index::{create_hnsw_index, create_ivfflat_index},
20};
21use crate::errors::ExecutorError;
22
23pub struct CreateIndexExecutor;
25
26impl CreateIndexExecutor {
27 pub fn execute(
66 stmt: &CreateIndexStmt,
67 database: &mut Database,
68 ) -> Result<String, ExecutorError> {
69 if stmt.if_not_exists && index_already_exists(stmt, database) {
71 return Ok(format!("Index '{}' already exists (skipped)", stmt.index_name));
72 }
73
74 let validation = validate_create_index(stmt, database)?;
76
77 match &stmt.index_type {
90 vibesql_ast::IndexType::BTree { unique } => create_btree_index(
91 database,
92 stmt,
93 &validation.table_name,
94 &validation.qualified_table_name,
95 &validation.table_schema,
96 *unique,
97 ),
98
99 vibesql_ast::IndexType::Fulltext => Err(ExecutorError::UnsupportedFeature(
100 "FULLTEXT indexes are not yet implemented".to_string(),
101 )),
102
103 vibesql_ast::IndexType::Spatial => create_spatial_index(
104 database,
105 stmt,
106 &validation.table_name,
107 &validation.qualified_table_name,
108 &validation.table_schema,
109 ),
110
111 vibesql_ast::IndexType::IVFFlat { metric, lists } => create_ivfflat_index(
112 database,
113 stmt,
114 &validation.table_name,
115 &validation.qualified_table_name,
116 &validation.table_schema,
117 *metric,
118 *lists,
119 ),
120
121 vibesql_ast::IndexType::Hnsw { metric, m, ef_construction } => create_hnsw_index(
122 database,
123 stmt,
124 &validation.table_name,
125 &validation.qualified_table_name,
126 &validation.table_schema,
127 *metric,
128 *m,
129 *ef_construction,
130 ),
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use vibesql_ast::{ColumnDef, CreateTableStmt, IndexColumn, OrderDirection};
138 use vibesql_storage::Row;
139 use vibesql_types::{DataType, SqlValue};
140
141 use super::*;
142 use crate::CreateTableExecutor;
143
144 fn create_test_table(db: &mut Database) {
145 let stmt = CreateTableStmt {
146 temporary: false,
147 if_not_exists: false,
148 table_name: "users".to_string(),
149 columns: vec![
150 ColumnDef {
151 name: "id".to_string(),
152 data_type: DataType::Integer,
153 nullable: false,
154 constraints: vec![],
155 default_value: None,
156 comment: None,
157 generated_expr: None,
158 is_exact_integer_type: false,
159 },
160 ColumnDef {
161 name: "email".to_string(),
162 data_type: DataType::Varchar { max_length: Some(255) },
163 nullable: false,
164 constraints: vec![],
165 default_value: None,
166 comment: None,
167 generated_expr: None,
168 is_exact_integer_type: false,
169 },
170 ColumnDef {
171 name: "name".to_string(),
172 data_type: DataType::Varchar { max_length: Some(100) },
173 nullable: true,
174 constraints: vec![],
175 default_value: None,
176 comment: None,
177 generated_expr: None,
178 is_exact_integer_type: false,
179 },
180 ],
181 table_constraints: vec![],
182 table_options: vec![],
183 quoted: false,
184 name_source: None,
185 as_query: None,
186 without_rowid: false,
187 };
188
189 CreateTableExecutor::execute(&stmt, db).unwrap();
190 }
191
192 #[test]
193 fn test_create_simple_index() {
194 let mut db = Database::new();
195 create_test_table(&mut db);
196
197 let stmt = CreateIndexStmt {
198 index_name: "idx_users_email".to_string(),
199 if_not_exists: false,
200 table_name: "users".to_string(),
201 index_type: vibesql_ast::IndexType::BTree { unique: false },
202 columns: vec![IndexColumn::Column {
203 column_name: "email".to_string(),
204 direction: OrderDirection::Asc,
205 prefix_length: None,
206 }],
207 where_clause: None,
208 };
209
210 let result = CreateIndexExecutor::execute(&stmt, &mut db);
211 assert!(result.is_ok());
212 assert_eq!(
213 result.unwrap(),
214 "Index 'idx_users_email' created successfully on table 'main.users'"
215 );
216
217 assert!(db.index_exists("idx_users_email"));
219 }
220
221 #[test]
222 fn test_create_unique_index() {
223 let mut db = Database::new();
224 create_test_table(&mut db);
225
226 let stmt = CreateIndexStmt {
227 index_name: "idx_users_email_unique".to_string(),
228 if_not_exists: false,
229 table_name: "users".to_string(),
230 index_type: vibesql_ast::IndexType::BTree { unique: true },
231 columns: vec![IndexColumn::Column {
232 column_name: "email".to_string(),
233 direction: OrderDirection::Asc,
234 prefix_length: None,
235 }],
236 where_clause: None,
237 };
238
239 let result = CreateIndexExecutor::execute(&stmt, &mut db);
240 assert!(result.is_ok());
241 assert!(db.index_exists("idx_users_email_unique"));
242 }
243
244 #[test]
245 fn test_create_multi_column_index() {
246 let mut db = Database::new();
247 create_test_table(&mut db);
248
249 let stmt = CreateIndexStmt {
250 index_name: "idx_users_email_name".to_string(),
251 if_not_exists: false,
252 table_name: "users".to_string(),
253 index_type: vibesql_ast::IndexType::BTree { unique: false },
254 columns: vec![
255 IndexColumn::Column {
256 column_name: "email".to_string(),
257 direction: OrderDirection::Asc,
258 prefix_length: None,
259 },
260 IndexColumn::Column {
261 column_name: "name".to_string(),
262 direction: OrderDirection::Desc,
263 prefix_length: None,
264 },
265 ],
266 where_clause: None,
267 };
268
269 let result = CreateIndexExecutor::execute(&stmt, &mut db);
270 assert!(result.is_ok());
271 }
272
273 #[test]
274 fn test_create_index_duplicate_name() {
275 let mut db = Database::new();
276 create_test_table(&mut db);
277
278 let stmt = CreateIndexStmt {
279 index_name: "idx_users_email".to_string(),
280 if_not_exists: false,
281 table_name: "users".to_string(),
282 index_type: vibesql_ast::IndexType::BTree { unique: false },
283 columns: vec![IndexColumn::Column {
284 column_name: "email".to_string(),
285 direction: OrderDirection::Asc,
286 prefix_length: None,
287 }],
288 where_clause: None,
289 };
290
291 let result = CreateIndexExecutor::execute(&stmt, &mut db);
293 assert!(result.is_ok());
294
295 let result = CreateIndexExecutor::execute(&stmt, &mut db);
297 assert!(result.is_err());
298 assert!(matches!(result, Err(ExecutorError::IndexAlreadyExists(_))));
299 }
300
301 #[test]
302 fn test_create_index_on_nonexistent_table() {
303 let mut db = Database::new();
304
305 let stmt = CreateIndexStmt {
306 index_name: "idx_nonexistent".to_string(),
307 if_not_exists: false,
308 table_name: "nonexistent_table".to_string(),
309 index_type: vibesql_ast::IndexType::BTree { unique: false },
310 columns: vec![IndexColumn::Column {
311 column_name: "id".to_string(),
312 direction: OrderDirection::Asc,
313 prefix_length: None,
314 }],
315 where_clause: None,
316 };
317
318 let result = CreateIndexExecutor::execute(&stmt, &mut db);
319 assert!(result.is_err());
320 assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
321 }
322
323 #[test]
324 fn test_create_index_on_nonexistent_column() {
325 let mut db = Database::new();
326 create_test_table(&mut db);
327
328 let stmt = CreateIndexStmt {
329 index_name: "idx_users_nonexistent".to_string(),
330 if_not_exists: false,
331 table_name: "users".to_string(),
332 index_type: vibesql_ast::IndexType::BTree { unique: false },
333 columns: vec![IndexColumn::Column {
334 column_name: "nonexistent_column".to_string(),
335 direction: OrderDirection::Asc,
336 prefix_length: None,
337 }],
338 where_clause: None,
339 };
340
341 let result = CreateIndexExecutor::execute(&stmt, &mut db);
342 assert!(result.is_err());
343 assert!(matches!(result, Err(ExecutorError::ColumnNotFound { .. })));
344 }
345
346 #[test]
347 fn test_create_index_if_not_exists_when_not_exists() {
348 let mut db = Database::new();
349 create_test_table(&mut db);
350
351 let stmt = CreateIndexStmt {
352 index_name: "idx_users_email".to_string(),
353 if_not_exists: true,
354 table_name: "users".to_string(),
355 index_type: vibesql_ast::IndexType::BTree { unique: false },
356 columns: vec![IndexColumn::Column {
357 column_name: "email".to_string(),
358 direction: OrderDirection::Asc,
359 prefix_length: None,
360 }],
361 where_clause: None,
362 };
363
364 let result = CreateIndexExecutor::execute(&stmt, &mut db);
365 assert!(result.is_ok());
366 assert_eq!(
367 result.unwrap(),
368 "Index 'idx_users_email' created successfully on table 'main.users'"
369 );
370 assert!(db.index_exists("idx_users_email"));
371 }
372
373 #[test]
374 fn test_create_index_if_not_exists_when_exists() {
375 let mut db = Database::new();
376 create_test_table(&mut db);
377
378 let stmt = CreateIndexStmt {
380 index_name: "idx_users_email".to_string(),
381 if_not_exists: false,
382 table_name: "users".to_string(),
383 index_type: vibesql_ast::IndexType::BTree { unique: false },
384 columns: vec![IndexColumn::Column {
385 column_name: "email".to_string(),
386 direction: OrderDirection::Asc,
387 prefix_length: None,
388 }],
389 where_clause: None,
390 };
391 CreateIndexExecutor::execute(&stmt, &mut db).unwrap();
392
393 let stmt_with_if_not_exists = CreateIndexStmt {
395 index_name: "idx_users_email".to_string(),
396 if_not_exists: true,
397 table_name: "users".to_string(),
398 index_type: vibesql_ast::IndexType::BTree { unique: false },
399 columns: vec![IndexColumn::Column {
400 column_name: "email".to_string(),
401 direction: OrderDirection::Asc,
402 prefix_length: None,
403 }],
404 where_clause: None,
405 };
406 let result = CreateIndexExecutor::execute(&stmt_with_if_not_exists, &mut db);
407 assert!(result.is_ok());
408 assert!(db.index_exists("idx_users_email"));
409 }
410
411 #[test]
412 fn test_create_index_with_schema_qualified_table() {
413 let mut db = Database::new();
414 create_test_table(&mut db);
415
416 let index_stmt = CreateIndexStmt {
418 index_name: "idx_users_email_qualified".to_string(),
419 if_not_exists: false,
420 table_name: "main.users".to_string(), index_type: vibesql_ast::IndexType::BTree { unique: false },
422 columns: vec![IndexColumn::Column {
423 column_name: "email".to_string(),
424 direction: OrderDirection::Asc,
425 prefix_length: None,
426 }],
427 where_clause: None,
428 };
429
430 let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
431 assert!(result.is_ok());
432 assert_eq!(
433 result.unwrap(),
434 "Index 'idx_users_email_qualified' created successfully on table 'main.users'"
435 );
436
437 assert!(db.index_exists("idx_users_email_qualified"));
439 }
440
441 fn simple_ab_table(name: &str, temporary: bool) -> CreateTableStmt {
444 CreateTableStmt {
445 temporary,
446 if_not_exists: false,
447 table_name: name.to_string(),
448 columns: vec![
449 ColumnDef {
450 name: "a".to_string(),
451 data_type: DataType::Integer,
452 nullable: true,
453 constraints: vec![],
454 default_value: None,
455 comment: None,
456 generated_expr: None,
457 is_exact_integer_type: false,
458 },
459 ColumnDef {
460 name: "b".to_string(),
461 data_type: DataType::Integer,
462 nullable: true,
463 constraints: vec![],
464 default_value: None,
465 comment: None,
466 generated_expr: None,
467 is_exact_integer_type: false,
468 },
469 ],
470 table_constraints: vec![],
471 table_options: vec![],
472 quoted: false,
473 name_source: None,
474 as_query: None,
475 without_rowid: false,
476 }
477 }
478
479 fn index_on_b(index_name: &str, table_name: &str) -> CreateIndexStmt {
480 CreateIndexStmt {
481 index_name: index_name.to_string(),
482 if_not_exists: false,
483 table_name: table_name.to_string(),
484 index_type: vibesql_ast::IndexType::BTree { unique: false },
485 columns: vec![IndexColumn::Column {
486 column_name: "b".to_string(),
487 direction: OrderDirection::Asc,
488 prefix_length: None,
489 }],
490 where_clause: None,
491 }
492 }
493
494 #[test]
497 fn test_create_index_on_temp_table_resolves_temp_schema() {
498 let mut db = Database::new();
499
500 CreateTableExecutor::execute(&simple_ab_table("tbl", true), &mut db).unwrap();
502
503 let result = CreateIndexExecutor::execute(&index_on_b("tbl_idx", "tbl"), &mut db);
505 assert!(
506 result.is_ok(),
507 "CREATE INDEX on a temp table should succeed (got {:?})",
508 result.err()
509 );
510
511 let msg = result.unwrap();
514 assert!(
515 msg.contains(&format!("{}.tbl", db.catalog.temp_schema_name())),
516 "index should be created in the temp schema, got: {msg}"
517 );
518
519 assert!(db.index_exists("tbl_idx"));
521
522 db.insert_row("tbl", Row::new(vec![SqlValue::Integer(1), SqlValue::Integer(2)])).unwrap();
523 db.insert_row("tbl", Row::new(vec![SqlValue::Integer(3), SqlValue::Integer(4)])).unwrap();
524
525 let rows: Vec<_> = db.get_table("tbl").unwrap().scan_live().collect();
526 assert_eq!(rows.len(), 2, "temp table should hold the inserted rows");
527 }
528
529 #[test]
533 fn test_create_index_temp_shadows_main() {
534 let mut db = Database::new();
535
536 CreateTableExecutor::execute(&simple_ab_table("t", false), &mut db).unwrap();
538 CreateTableExecutor::execute(&simple_ab_table("t", true), &mut db).unwrap();
539
540 let result = CreateIndexExecutor::execute(&index_on_b("ix", "t"), &mut db);
541 assert!(result.is_ok(), "CREATE INDEX should resolve to temp.t (got {:?})", result.err());
542
543 let msg = result.unwrap();
544 assert!(
545 msg.contains(&format!("{}.t", db.catalog.temp_schema_name())),
546 "unqualified index target must shadow to the temp table, got: {msg}"
547 );
548 assert!(db.index_exists("ix"));
549 }
550
551 #[test]
552 fn test_create_index_on_nonexistent_schema_qualified_table() {
553 let mut db = Database::new();
554
555 db.catalog.create_schema("test_schema".to_string()).unwrap();
557
558 let index_stmt = CreateIndexStmt {
560 index_name: "idx_nonexistent".to_string(),
561 if_not_exists: false,
562 table_name: "test_schema.nonexistent_table".to_string(),
563 index_type: vibesql_ast::IndexType::BTree { unique: false },
564 columns: vec![IndexColumn::Column {
565 column_name: "id".to_string(),
566 direction: OrderDirection::Asc,
567 prefix_length: None,
568 }],
569 where_clause: None,
570 };
571
572 let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
573 assert!(result.is_err());
574 assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
575 }
576
577 fn create_vector_table(db: &mut Database) {
582 let stmt = CreateTableStmt {
583 temporary: false,
584 if_not_exists: false,
585 table_name: "documents".to_string(),
586 columns: vec![
587 ColumnDef {
588 name: "id".to_string(),
589 data_type: DataType::Integer,
590 nullable: false,
591 constraints: vec![],
592 default_value: None,
593 comment: None,
594 generated_expr: None,
595 is_exact_integer_type: false,
596 },
597 ColumnDef {
598 name: "embedding".to_string(),
599 data_type: DataType::Vector { dimensions: 3 },
600 nullable: true,
601 constraints: vec![],
602 default_value: None,
603 comment: None,
604 generated_expr: None,
605 is_exact_integer_type: false,
606 },
607 ColumnDef {
608 name: "content".to_string(),
609 data_type: DataType::Varchar { max_length: Some(1000) },
610 nullable: true,
611 constraints: vec![],
612 default_value: None,
613 comment: None,
614 generated_expr: None,
615 is_exact_integer_type: false,
616 },
617 ],
618 table_constraints: vec![],
619 table_options: vec![],
620 quoted: false,
621 name_source: None,
622 as_query: None,
623 without_rowid: false,
624 };
625
626 CreateTableExecutor::execute(&stmt, db).unwrap();
627 }
628
629 #[test]
630 fn test_create_ivfflat_index_l2() {
631 let mut db = Database::new();
632 create_vector_table(&mut db);
633
634 let stmt = CreateIndexStmt {
635 index_name: "idx_documents_embedding".to_string(),
636 if_not_exists: false,
637 table_name: "documents".to_string(),
638 index_type: vibesql_ast::IndexType::IVFFlat {
639 metric: vibesql_ast::VectorDistanceMetric::L2,
640 lists: 4,
641 },
642 columns: vec![IndexColumn::Column {
643 column_name: "embedding".to_string(),
644 direction: OrderDirection::Asc,
645 prefix_length: None,
646 }],
647 where_clause: None,
648 };
649
650 let result = CreateIndexExecutor::execute(&stmt, &mut db);
651 assert!(result.is_ok(), "IVFFlat index creation failed: {:?}", result.err());
652 assert!(result
653 .unwrap()
654 .contains("IVFFlat index 'idx_documents_embedding' created successfully"));
655 assert!(db.index_exists("idx_documents_embedding"));
656 }
657
658 #[test]
659 fn test_create_ivfflat_index_cosine() {
660 let mut db = Database::new();
661 create_vector_table(&mut db);
662
663 let stmt = CreateIndexStmt {
664 index_name: "idx_documents_cosine".to_string(),
665 if_not_exists: false,
666 table_name: "documents".to_string(),
667 index_type: vibesql_ast::IndexType::IVFFlat {
668 metric: vibesql_ast::VectorDistanceMetric::Cosine,
669 lists: 4,
670 },
671 columns: vec![IndexColumn::Column {
672 column_name: "embedding".to_string(),
673 direction: OrderDirection::Asc,
674 prefix_length: None,
675 }],
676 where_clause: None,
677 };
678
679 let result = CreateIndexExecutor::execute(&stmt, &mut db);
680 assert!(result.is_ok());
681 assert!(db.index_exists("idx_documents_cosine"));
682 }
683
684 #[test]
685 fn test_create_ivfflat_index_inner_product() {
686 let mut db = Database::new();
687 create_vector_table(&mut db);
688
689 let stmt = CreateIndexStmt {
690 index_name: "idx_documents_ip".to_string(),
691 if_not_exists: false,
692 table_name: "documents".to_string(),
693 index_type: vibesql_ast::IndexType::IVFFlat {
694 metric: vibesql_ast::VectorDistanceMetric::InnerProduct,
695 lists: 4,
696 },
697 columns: vec![IndexColumn::Column {
698 column_name: "embedding".to_string(),
699 direction: OrderDirection::Asc,
700 prefix_length: None,
701 }],
702 where_clause: None,
703 };
704
705 let result = CreateIndexExecutor::execute(&stmt, &mut db);
706 assert!(result.is_ok());
707 assert!(db.index_exists("idx_documents_ip"));
708 }
709
710 #[test]
711 fn test_create_ivfflat_index_on_non_vector_column() {
712 let mut db = Database::new();
713 create_vector_table(&mut db);
714
715 let stmt = CreateIndexStmt {
717 index_name: "idx_documents_content".to_string(),
718 if_not_exists: false,
719 table_name: "documents".to_string(),
720 index_type: vibesql_ast::IndexType::IVFFlat {
721 metric: vibesql_ast::VectorDistanceMetric::L2,
722 lists: 4,
723 },
724 columns: vec![IndexColumn::Column {
725 column_name: "content".to_string(),
726 direction: OrderDirection::Asc,
727 prefix_length: None,
728 }],
729 where_clause: None,
730 };
731
732 let result = CreateIndexExecutor::execute(&stmt, &mut db);
733 assert!(result.is_err());
734 assert!(matches!(result, Err(ExecutorError::InvalidIndexDefinition(_))));
735 }
736
737 #[test]
738 fn test_create_ivfflat_index_multiple_columns_fails() {
739 let mut db = Database::new();
740 create_vector_table(&mut db);
741
742 let stmt = CreateIndexStmt {
744 index_name: "idx_documents_multi".to_string(),
745 if_not_exists: false,
746 table_name: "documents".to_string(),
747 index_type: vibesql_ast::IndexType::IVFFlat {
748 metric: vibesql_ast::VectorDistanceMetric::L2,
749 lists: 4,
750 },
751 columns: vec![
752 IndexColumn::Column {
753 column_name: "embedding".to_string(),
754 direction: OrderDirection::Asc,
755 prefix_length: None,
756 },
757 IndexColumn::Column {
758 column_name: "id".to_string(),
759 direction: OrderDirection::Asc,
760 prefix_length: None,
761 },
762 ],
763 where_clause: None,
764 };
765
766 let result = CreateIndexExecutor::execute(&stmt, &mut db);
767 assert!(result.is_err());
768 assert!(matches!(result, Err(ExecutorError::InvalidIndexDefinition(_))));
769 }
770
771 #[test]
772 fn test_create_ivfflat_index_if_not_exists() {
773 let mut db = Database::new();
774 create_vector_table(&mut db);
775
776 let stmt = CreateIndexStmt {
777 index_name: "idx_documents_embedding".to_string(),
778 if_not_exists: true,
779 table_name: "documents".to_string(),
780 index_type: vibesql_ast::IndexType::IVFFlat {
781 metric: vibesql_ast::VectorDistanceMetric::L2,
782 lists: 4,
783 },
784 columns: vec![IndexColumn::Column {
785 column_name: "embedding".to_string(),
786 direction: OrderDirection::Asc,
787 prefix_length: None,
788 }],
789 where_clause: None,
790 };
791
792 let result = CreateIndexExecutor::execute(&stmt, &mut db);
794 assert!(result.is_ok());
795
796 let result2 = CreateIndexExecutor::execute(&stmt, &mut db);
798 assert!(result2.is_ok());
799 assert!(result2.unwrap().contains("already exists"));
800 }
801
802 #[test]
803 fn test_ivfflat_index_search() {
804 let mut db = Database::new();
805 create_vector_table(&mut db);
806
807 db.insert_row(
810 "documents",
811 Row::new(vec![
812 SqlValue::Integer(1),
813 SqlValue::Vector(vec![1.0, 0.0, 0.0]),
814 SqlValue::Varchar(arcstr::ArcStr::from("doc1")),
815 ]),
816 )
817 .unwrap();
818
819 db.insert_row(
821 "documents",
822 Row::new(vec![
823 SqlValue::Integer(2),
824 SqlValue::Vector(vec![0.0, 1.0, 0.0]),
825 SqlValue::Varchar(arcstr::ArcStr::from("doc2")),
826 ]),
827 )
828 .unwrap();
829
830 db.insert_row(
832 "documents",
833 Row::new(vec![
834 SqlValue::Integer(3),
835 SqlValue::Vector(vec![0.0, 0.0, 1.0]),
836 SqlValue::Varchar(arcstr::ArcStr::from("doc3")),
837 ]),
838 )
839 .unwrap();
840
841 db.insert_row(
843 "documents",
844 Row::new(vec![
845 SqlValue::Integer(4),
846 SqlValue::Vector(vec![0.5, 0.5, 0.0]),
847 SqlValue::Varchar(arcstr::ArcStr::from("doc4")),
848 ]),
849 )
850 .unwrap();
851
852 let stmt = CreateIndexStmt {
854 index_name: "idx_embedding".to_string(),
855 if_not_exists: false,
856 table_name: "documents".to_string(),
857 index_type: vibesql_ast::IndexType::IVFFlat {
858 metric: vibesql_ast::VectorDistanceMetric::L2,
859 lists: 2, },
861 columns: vec![IndexColumn::Column {
862 column_name: "embedding".to_string(),
863 direction: OrderDirection::Asc,
864 prefix_length: None,
865 }],
866 where_clause: None,
867 };
868
869 let result = CreateIndexExecutor::execute(&stmt, &mut db);
870 assert!(result.is_ok(), "Index creation failed: {:?}", result.err());
871
872 let query_vector = vec![0.9, 0.1, 0.0];
875 let results = db.search_ivfflat_index("idx_embedding", &query_vector, 2);
876 assert!(results.is_ok(), "Search should succeed: {:?}", results.err());
877
878 let neighbors = results.unwrap();
879 assert!(!neighbors.is_empty(), "Should find at least one neighbor");
881
882 let (first_row_id, first_distance) = neighbors[0];
884 assert!(first_distance >= 0.0, "Distance should be non-negative");
885 assert_eq!(first_row_id, 0, "First result should be the closest vector");
887 }
888
889 #[test]
890 fn test_ivfflat_get_indexes_for_table() {
891 let mut db = Database::new();
892 create_vector_table(&mut db);
893
894 let stmt = CreateIndexStmt {
896 index_name: "idx_vec".to_string(),
897 if_not_exists: false,
898 table_name: "documents".to_string(),
899 index_type: vibesql_ast::IndexType::IVFFlat {
900 metric: vibesql_ast::VectorDistanceMetric::Cosine,
901 lists: 2,
902 },
903 columns: vec![IndexColumn::Column {
904 column_name: "embedding".to_string(),
905 direction: OrderDirection::Asc,
906 prefix_length: None,
907 }],
908 where_clause: None,
909 };
910
911 let result = CreateIndexExecutor::execute(&stmt, &mut db);
912 assert!(result.is_ok());
913
914 let ivfflat_indexes = db.get_ivfflat_indexes_for_table("documents");
916 assert_eq!(ivfflat_indexes.len(), 1, "Should have one IVFFlat index");
917
918 let (metadata, index) = &ivfflat_indexes[0];
919 assert!(metadata.index_name.to_uppercase().contains("IDX_VEC"));
920 assert_eq!(index.metric(), vibesql_ast::VectorDistanceMetric::Cosine);
921 }
922
923 #[test]
924 fn test_ivfflat_set_probes() {
925 let mut db = Database::new();
926 create_vector_table(&mut db);
927
928 let stmt = CreateIndexStmt {
930 index_name: "idx_probes".to_string(),
931 if_not_exists: false,
932 table_name: "documents".to_string(),
933 index_type: vibesql_ast::IndexType::IVFFlat {
934 metric: vibesql_ast::VectorDistanceMetric::L2,
935 lists: 4,
936 },
937 columns: vec![IndexColumn::Column {
938 column_name: "embedding".to_string(),
939 direction: OrderDirection::Asc,
940 prefix_length: None,
941 }],
942 where_clause: None,
943 };
944
945 let result = CreateIndexExecutor::execute(&stmt, &mut db);
946 assert!(result.is_ok());
947
948 let set_probes_result = db.set_ivfflat_probes("idx_probes", 3);
950 assert!(set_probes_result.is_ok());
951
952 let query_vector = vec![0.5, 0.5, 0.5];
954 let search_result = db.search_ivfflat_index("idx_probes", &query_vector, 3);
955 assert!(search_result.is_ok());
956 }
957
958 #[test]
963 fn test_create_expression_index_lower() {
964 let mut db = Database::new();
965 create_test_table(&mut db);
966
967 db.insert_row(
969 "users",
970 Row::new(vec![
971 SqlValue::Integer(1),
972 SqlValue::Varchar(arcstr::ArcStr::from("Test@Example.COM")),
973 SqlValue::Varchar(arcstr::ArcStr::from("John")),
974 ]),
975 )
976 .unwrap();
977
978 let stmt = CreateIndexStmt {
980 index_name: "idx_users_email_lower".to_string(),
981 if_not_exists: false,
982 table_name: "users".to_string(),
983 index_type: vibesql_ast::IndexType::BTree { unique: false },
984 columns: vec![IndexColumn::new_expression(
985 vibesql_ast::Expression::Function {
986 name: vibesql_ast::FunctionIdentifier::new("lower"),
987 args: vec![vibesql_ast::Expression::ColumnRef(
988 vibesql_ast::ColumnIdentifier::simple("email", false),
989 )],
990 character_unit: None,
991 },
992 OrderDirection::Asc,
993 )],
994 where_clause: None,
995 };
996
997 let result = CreateIndexExecutor::execute(&stmt, &mut db);
998 assert!(result.is_ok(), "Expression index creation failed: {:?}", result.err());
999 assert!(db.index_exists("idx_users_email_lower"));
1000 }
1001
1002 #[test]
1003 fn test_create_expression_index_arithmetic() {
1004 let mut db = Database::new();
1005
1006 let table_stmt = vibesql_ast::CreateTableStmt {
1008 temporary: false,
1009 if_not_exists: false,
1010 table_name: "products".to_string(),
1011 columns: vec![
1012 vibesql_ast::ColumnDef {
1013 name: "id".to_string(),
1014 data_type: DataType::Integer,
1015 nullable: false,
1016 constraints: vec![],
1017 default_value: None,
1018 comment: None,
1019 generated_expr: None,
1020 is_exact_integer_type: false,
1021 },
1022 vibesql_ast::ColumnDef {
1023 name: "price".to_string(),
1024 data_type: DataType::Integer,
1025 nullable: false,
1026 constraints: vec![],
1027 default_value: None,
1028 comment: None,
1029 generated_expr: None,
1030 is_exact_integer_type: false,
1031 },
1032 vibesql_ast::ColumnDef {
1033 name: "discount".to_string(),
1034 data_type: DataType::Integer,
1035 nullable: false,
1036 constraints: vec![],
1037 default_value: None,
1038 comment: None,
1039 generated_expr: None,
1040 is_exact_integer_type: false,
1041 },
1042 ],
1043 table_constraints: vec![],
1044 table_options: vec![],
1045 quoted: false,
1046 name_source: None,
1047 as_query: None,
1048 without_rowid: false,
1049 };
1050 crate::CreateTableExecutor::execute(&table_stmt, &mut db).unwrap();
1051
1052 db.insert_row(
1054 "products",
1055 Row::new(vec![SqlValue::Integer(1), SqlValue::Integer(100), SqlValue::Integer(10)]),
1056 )
1057 .unwrap();
1058
1059 let stmt = CreateIndexStmt {
1061 index_name: "idx_products_net_price".to_string(),
1062 if_not_exists: false,
1063 table_name: "products".to_string(),
1064 index_type: vibesql_ast::IndexType::BTree { unique: false },
1065 columns: vec![IndexColumn::new_expression(
1066 vibesql_ast::Expression::BinaryOp {
1067 op: vibesql_ast::BinaryOperator::Minus,
1068 left: Box::new(vibesql_ast::Expression::ColumnRef(
1069 vibesql_ast::ColumnIdentifier::simple("price", false),
1070 )),
1071 right: Box::new(vibesql_ast::Expression::ColumnRef(
1072 vibesql_ast::ColumnIdentifier::simple("discount", false),
1073 )),
1074 },
1075 OrderDirection::Asc,
1076 )],
1077 where_clause: None,
1078 };
1079
1080 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1081 assert!(result.is_ok(), "Arithmetic expression index creation failed: {:?}", result.err());
1082 assert!(db.index_exists("idx_products_net_price"));
1083 }
1084
1085 #[test]
1086 fn test_create_unique_expression_index() {
1087 let mut db = Database::new();
1088 create_test_table(&mut db);
1089
1090 db.insert_row(
1092 "users",
1093 Row::new(vec![
1094 SqlValue::Integer(1),
1095 SqlValue::Varchar(arcstr::ArcStr::from("user@example.com")),
1096 SqlValue::Varchar(arcstr::ArcStr::from("John")),
1097 ]),
1098 )
1099 .unwrap();
1100
1101 let stmt = CreateIndexStmt {
1103 index_name: "idx_users_email_lower_unique".to_string(),
1104 if_not_exists: false,
1105 table_name: "users".to_string(),
1106 index_type: vibesql_ast::IndexType::BTree { unique: true },
1107 columns: vec![IndexColumn::new_expression(
1108 vibesql_ast::Expression::Function {
1109 name: vibesql_ast::FunctionIdentifier::new("lower"),
1110 args: vec![vibesql_ast::Expression::ColumnRef(
1111 vibesql_ast::ColumnIdentifier::simple("email", false),
1112 )],
1113 character_unit: None,
1114 },
1115 OrderDirection::Asc,
1116 )],
1117 where_clause: None,
1118 };
1119
1120 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1121 assert!(result.is_ok(), "Unique expression index creation failed: {:?}", result.err());
1122 assert!(db.index_exists("idx_users_email_lower_unique"));
1123 }
1124
1125 #[test]
1126 fn test_create_expression_index_rejects_non_deterministic() {
1127 let mut db = Database::new();
1128 create_test_table(&mut db);
1129
1130 let stmt = CreateIndexStmt {
1132 index_name: "idx_users_random".to_string(),
1133 if_not_exists: false,
1134 table_name: "users".to_string(),
1135 index_type: vibesql_ast::IndexType::BTree { unique: false },
1136 columns: vec![IndexColumn::new_expression(
1137 vibesql_ast::Expression::Function {
1138 name: vibesql_ast::FunctionIdentifier::new("random"),
1139 args: vec![],
1140 character_unit: None,
1141 },
1142 OrderDirection::Asc,
1143 )],
1144 where_clause: None,
1145 };
1146
1147 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1148 assert!(result.is_err(), "Non-deterministic expression index should fail");
1149 assert!(matches!(result, Err(ExecutorError::UnsupportedFeature(_))));
1150 }
1151
1152 #[test]
1153 fn test_create_expression_index_validates_column_references() {
1154 let mut db = Database::new();
1155 create_test_table(&mut db);
1156
1157 let stmt = CreateIndexStmt {
1159 index_name: "idx_users_bad_col".to_string(),
1160 if_not_exists: false,
1161 table_name: "users".to_string(),
1162 index_type: vibesql_ast::IndexType::BTree { unique: false },
1163 columns: vec![IndexColumn::new_expression(
1164 vibesql_ast::Expression::Function {
1165 name: vibesql_ast::FunctionIdentifier::new("lower"),
1166 args: vec![vibesql_ast::Expression::ColumnRef(
1167 vibesql_ast::ColumnIdentifier::simple("nonexistent_column", false),
1168 )],
1169 character_unit: None,
1170 },
1171 OrderDirection::Asc,
1172 )],
1173 where_clause: None,
1174 };
1175
1176 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1177 assert!(result.is_err(), "Expression index with non-existent column should fail");
1178 assert!(matches!(result, Err(ExecutorError::ColumnNotFound { .. })));
1179 }
1180
1181 #[test]
1182 fn test_create_expression_index_handles_null() {
1183 let mut db = Database::new();
1184 create_test_table(&mut db);
1185
1186 db.insert_row(
1188 "users",
1189 Row::new(vec![
1190 SqlValue::Integer(1),
1191 SqlValue::Varchar(arcstr::ArcStr::from("user@example.com")),
1192 SqlValue::Null, ]),
1194 )
1195 .unwrap();
1196
1197 let stmt = CreateIndexStmt {
1199 index_name: "idx_users_name_lower".to_string(),
1200 if_not_exists: false,
1201 table_name: "users".to_string(),
1202 index_type: vibesql_ast::IndexType::BTree { unique: false },
1203 columns: vec![IndexColumn::new_expression(
1204 vibesql_ast::Expression::Function {
1205 name: vibesql_ast::FunctionIdentifier::new("lower"),
1206 args: vec![vibesql_ast::Expression::ColumnRef(
1207 vibesql_ast::ColumnIdentifier::simple("name", false),
1208 )],
1209 character_unit: None,
1210 },
1211 OrderDirection::Asc,
1212 )],
1213 where_clause: None,
1214 };
1215
1216 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1217 assert!(result.is_ok(), "Expression index with NULL should succeed: {:?}", result.err());
1218 assert!(db.index_exists("idx_users_name_lower"));
1219 }
1220}