1use vibesql_ast::CreateIndexStmt;
4use vibesql_storage::{
5 index::{extract_mbr_from_sql_value, SpatialIndex, SpatialIndexEntry},
6 Database, SpatialIndexMetadata,
7};
8
9use crate::{errors::ExecutorError, privilege_checker::PrivilegeChecker};
10
11pub struct CreateIndexExecutor;
13
14impl CreateIndexExecutor {
15 pub fn execute(
53 stmt: &CreateIndexStmt,
54 database: &mut Database,
55 ) -> Result<String, ExecutorError> {
56 let (schema_name, table_name) =
58 if let Some((schema_part, table_part)) = stmt.table_name.split_once('.') {
59 (schema_part.to_string(), table_part.to_string())
60 } else {
61 (database.catalog.get_current_schema().to_string(), stmt.table_name.clone())
62 };
63
64 PrivilegeChecker::check_create(database, &schema_name)?;
66
67 let qualified_table_name = format!("{}.{}", schema_name, table_name);
69
70 if !database.catalog.table_exists(&qualified_table_name) {
72 return Err(ExecutorError::TableNotFound(qualified_table_name.clone()));
73 }
74
75 let table_schema = database
77 .catalog
78 .get_table(&qualified_table_name)
79 .ok_or_else(|| ExecutorError::TableNotFound(qualified_table_name.clone()))?;
80
81 for index_col in &stmt.columns {
83 if table_schema.get_column(&index_col.column_name).is_none() {
84 let available_columns =
85 table_schema.columns.iter().map(|c| c.name.clone()).collect();
86 return Err(ExecutorError::ColumnNotFound {
87 column_name: index_col.column_name.clone(),
88 table_name: qualified_table_name.clone(),
89 searched_tables: vec![qualified_table_name.clone()],
90 available_columns,
91 });
92 }
93 }
94
95 for index_col in &stmt.columns {
97 if let Some(prefix_len) = index_col.prefix_length {
98 if prefix_len == 0 {
100 return Err(ExecutorError::InvalidIndexDefinition(format!(
101 "Prefix length must be greater than 0 for column '{}'",
102 index_col.column_name
103 )));
104 }
105
106 let column = table_schema.get_column(&index_col.column_name).unwrap(); match column.data_type {
109 vibesql_types::DataType::Varchar { .. }
110 | vibesql_types::DataType::Character { .. } => {
111 }
113 _ => {
114 return Err(ExecutorError::InvalidIndexDefinition(
115 format!(
116 "Prefix length can only be specified for string columns, but column '{}' has type {:?}",
117 index_col.column_name, column.data_type
118 ),
119 ));
120 }
121 }
122
123 const MAX_PREFIX_LENGTH: u64 = 65536;
126 if prefix_len > MAX_PREFIX_LENGTH {
127 return Err(ExecutorError::InvalidIndexDefinition(format!(
128 "Prefix length {} is too large for column '{}' (maximum: {})",
129 prefix_len, index_col.column_name, MAX_PREFIX_LENGTH
130 )));
131 }
132 }
133 }
134
135 let index_name = &stmt.index_name;
137 let index_exists =
138 database.index_exists(index_name) || database.spatial_index_exists(index_name);
139
140 if index_exists {
141 if stmt.if_not_exists {
142 return Ok(format!("Index '{}' already exists (skipped)", index_name));
144 } else {
145 return Err(ExecutorError::IndexAlreadyExists(index_name.clone()));
146 }
147 }
148
149 match &stmt.index_type {
151 vibesql_ast::IndexType::BTree { unique } => {
152 let column_indices: Vec<u32> = stmt
154 .columns
155 .iter()
156 .filter_map(|col| table_schema.get_column_index(&col.column_name))
157 .map(|idx| idx as u32)
158 .collect();
159
160 let index_metadata = vibesql_catalog::IndexMetadata::new(
162 index_name.clone(),
163 table_name.clone(),
164 vibesql_catalog::IndexType::BTree,
165 stmt.columns
166 .iter()
167 .map(|col| vibesql_catalog::IndexedColumn {
168 column_name: col.column_name.clone(),
169 order: match col.direction {
170 vibesql_ast::OrderDirection::Asc => {
171 vibesql_catalog::SortOrder::Ascending
172 }
173 vibesql_ast::OrderDirection::Desc => {
174 vibesql_catalog::SortOrder::Descending
175 }
176 },
177 prefix_length: col.prefix_length,
178 })
179 .collect(),
180 *unique,
181 );
182 database.catalog.add_index(index_metadata)?;
183
184 database.create_index(
186 index_name.clone(),
187 table_name.clone(),
188 *unique,
189 stmt.columns.clone(),
190 )?;
191
192 database.emit_wal_create_index(
194 index_name_to_id(index_name),
195 index_name,
196 &qualified_table_name,
197 column_indices,
198 *unique,
199 );
200
201 Ok(format!(
202 "Index '{}' created successfully on table '{}'",
203 index_name, qualified_table_name
204 ))
205 }
206 vibesql_ast::IndexType::Fulltext => Err(ExecutorError::UnsupportedFeature(
207 "FULLTEXT indexes are not yet implemented".to_string(),
208 )),
209 vibesql_ast::IndexType::Spatial => {
210 if stmt.columns.len() != 1 {
212 return Err(ExecutorError::InvalidIndexDefinition(
213 "SPATIAL indexes must be defined on exactly one column".to_string(),
214 ));
215 }
216
217 let column_name = &stmt.columns[0].column_name;
218
219 let col_idx = table_schema.get_column_index(column_name).ok_or_else(|| {
221 ExecutorError::ColumnNotFound {
222 column_name: column_name.clone(),
223 table_name: qualified_table_name.clone(),
224 searched_tables: vec![qualified_table_name.clone()],
225 available_columns: table_schema
226 .columns
227 .iter()
228 .map(|c| c.name.clone())
229 .collect(),
230 }
231 })?;
232
233 let table = database
235 .get_table(&table_name)
236 .ok_or_else(|| ExecutorError::TableNotFound(qualified_table_name.clone()))?;
237
238 let mut entries = Vec::new();
239 for (row_idx, row) in table.scan_live() {
241 let geom_value = &row.values[col_idx];
242
243 if let Some(mbr) = extract_mbr_from_sql_value(geom_value) {
245 entries.push(SpatialIndexEntry { row_id: row_idx, mbr });
246 }
247 }
248
249 let spatial_index = SpatialIndex::bulk_load(column_name.clone(), entries);
251
252 let index_metadata = vibesql_catalog::IndexMetadata::new(
254 index_name.clone(),
255 table_name.clone(),
256 vibesql_catalog::IndexType::RTree,
257 vec![vibesql_catalog::IndexedColumn {
258 column_name: column_name.clone(),
259 order: vibesql_catalog::SortOrder::Ascending,
260 prefix_length: None, }],
262 false,
263 );
264 database.catalog.add_index(index_metadata)?;
265
266 let metadata = SpatialIndexMetadata {
268 index_name: index_name.clone(),
269 table_name: table_name.clone(),
270 column_name: column_name.clone(),
271 created_at: Some(chrono::Utc::now()),
272 };
273
274 database.create_spatial_index(metadata, spatial_index)?;
275
276 database.emit_wal_create_index(
278 index_name_to_id(index_name),
279 index_name,
280 &qualified_table_name,
281 vec![col_idx as u32],
282 false,
283 );
284
285 Ok(format!(
286 "Spatial index '{}' created successfully on table '{}'",
287 index_name, qualified_table_name
288 ))
289 }
290 vibesql_ast::IndexType::IVFFlat { metric, lists } => {
291 if stmt.columns.len() != 1 {
293 return Err(ExecutorError::InvalidIndexDefinition(
294 "IVFFlat indexes must be defined on exactly one vector column".to_string(),
295 ));
296 }
297
298 let column_name = &stmt.columns[0].column_name;
299
300 let col_idx = table_schema.get_column_index(column_name).ok_or_else(|| {
302 ExecutorError::ColumnNotFound {
303 column_name: column_name.clone(),
304 table_name: qualified_table_name.clone(),
305 searched_tables: vec![qualified_table_name.clone()],
306 available_columns: table_schema
307 .columns
308 .iter()
309 .map(|c| c.name.clone())
310 .collect(),
311 }
312 })?;
313
314 let col_type = &table_schema.columns[col_idx].data_type;
316 let dimensions = match col_type {
317 vibesql_types::DataType::Vector { dimensions } => *dimensions as usize,
318 _ => {
319 return Err(ExecutorError::InvalidIndexDefinition(format!(
320 "IVFFlat indexes can only be created on VECTOR columns, but '{}' has type {:?}",
321 column_name, col_type
322 )));
323 }
324 };
325
326 let catalog_metric = match metric {
328 vibesql_ast::VectorDistanceMetric::L2 => {
329 vibesql_catalog::VectorDistanceMetric::L2
330 }
331 vibesql_ast::VectorDistanceMetric::Cosine => {
332 vibesql_catalog::VectorDistanceMetric::Cosine
333 }
334 vibesql_ast::VectorDistanceMetric::InnerProduct => {
335 vibesql_catalog::VectorDistanceMetric::InnerProduct
336 }
337 };
338
339 let index_metadata = vibesql_catalog::IndexMetadata::new(
341 index_name.clone(),
342 table_name.clone(),
343 vibesql_catalog::IndexType::IVFFlat { metric: catalog_metric, lists: *lists },
344 vec![vibesql_catalog::IndexedColumn {
345 column_name: column_name.clone(),
346 order: vibesql_catalog::SortOrder::Ascending, prefix_length: None,
348 }],
349 false, );
351 database.catalog.add_index(index_metadata)?;
352
353 database.create_ivfflat_index(
355 index_name.clone(),
356 table_name.clone(),
357 column_name.clone(),
358 col_idx,
359 dimensions,
360 *lists as usize,
361 *metric,
362 )?;
363
364 database.emit_wal_create_index(
366 index_name_to_id(index_name),
367 index_name,
368 &qualified_table_name,
369 vec![col_idx as u32],
370 false,
371 );
372
373 Ok(format!(
374 "IVFFlat index '{}' created successfully on table '{}' column '{}'",
375 index_name, qualified_table_name, column_name
376 ))
377 }
378 vibesql_ast::IndexType::Hnsw { metric, m, ef_construction } => {
379 if stmt.columns.len() != 1 {
381 return Err(ExecutorError::InvalidIndexDefinition(
382 "HNSW indexes must be defined on exactly one vector column".to_string(),
383 ));
384 }
385
386 let column_name = &stmt.columns[0].column_name;
387
388 let col_idx = table_schema.get_column_index(column_name).ok_or_else(|| {
390 ExecutorError::ColumnNotFound {
391 column_name: column_name.clone(),
392 table_name: qualified_table_name.clone(),
393 searched_tables: vec![qualified_table_name.clone()],
394 available_columns: table_schema
395 .columns
396 .iter()
397 .map(|c| c.name.clone())
398 .collect(),
399 }
400 })?;
401
402 let col_type = &table_schema.columns[col_idx].data_type;
404 let dimensions = match col_type {
405 vibesql_types::DataType::Vector { dimensions } => *dimensions as usize,
406 _ => {
407 return Err(ExecutorError::InvalidIndexDefinition(format!(
408 "HNSW indexes can only be created on VECTOR columns, but '{}' has type {:?}",
409 column_name, col_type
410 )));
411 }
412 };
413
414 let catalog_metric = match metric {
416 vibesql_ast::VectorDistanceMetric::L2 => {
417 vibesql_catalog::VectorDistanceMetric::L2
418 }
419 vibesql_ast::VectorDistanceMetric::Cosine => {
420 vibesql_catalog::VectorDistanceMetric::Cosine
421 }
422 vibesql_ast::VectorDistanceMetric::InnerProduct => {
423 vibesql_catalog::VectorDistanceMetric::InnerProduct
424 }
425 };
426
427 let index_metadata = vibesql_catalog::IndexMetadata::new(
429 index_name.clone(),
430 table_name.clone(),
431 vibesql_catalog::IndexType::Hnsw {
432 metric: catalog_metric,
433 m: *m,
434 ef_construction: *ef_construction,
435 },
436 vec![vibesql_catalog::IndexedColumn {
437 column_name: column_name.clone(),
438 order: vibesql_catalog::SortOrder::Ascending, prefix_length: None,
440 }],
441 false, );
443 database.catalog.add_index(index_metadata)?;
444
445 database.create_hnsw_index(
447 index_name.clone(),
448 table_name.clone(),
449 column_name.clone(),
450 col_idx,
451 dimensions,
452 *m,
453 *ef_construction,
454 *metric,
455 )?;
456
457 database.emit_wal_create_index(
459 index_name_to_id(index_name),
460 index_name,
461 &qualified_table_name,
462 vec![col_idx as u32],
463 false,
464 );
465
466 Ok(format!(
467 "HNSW index '{}' created successfully on table '{}' column '{}'",
468 index_name, qualified_table_name, column_name
469 ))
470 }
471 }
472 }
473}
474
475fn index_name_to_id(name: &str) -> u32 {
477 use std::hash::{Hash, Hasher};
478 let mut hasher = std::collections::hash_map::DefaultHasher::new();
479 name.hash(&mut hasher);
480 hasher.finish() as u32
481}
482
483#[cfg(test)]
484mod tests {
485 use vibesql_ast::{ColumnDef, CreateTableStmt, IndexColumn, OrderDirection};
486 use vibesql_storage::Row;
487 use vibesql_types::{DataType, SqlValue};
488
489 use super::*;
490 use crate::CreateTableExecutor;
491
492 fn create_test_table(db: &mut Database) {
493 let stmt = CreateTableStmt {
494 if_not_exists: false,
495 table_name: "users".to_string(),
496 columns: vec![
497 ColumnDef {
498 name: "id".to_string(),
499 data_type: DataType::Integer,
500 nullable: false,
501 constraints: vec![],
502 default_value: None,
503 comment: None,
504 },
505 ColumnDef {
506 name: "email".to_string(),
507 data_type: DataType::Varchar { max_length: Some(255) },
508 nullable: false,
509 constraints: vec![],
510 default_value: None,
511 comment: None,
512 },
513 ColumnDef {
514 name: "name".to_string(),
515 data_type: DataType::Varchar { max_length: Some(100) },
516 nullable: true,
517 constraints: vec![],
518 default_value: None,
519 comment: None,
520 },
521 ],
522 table_constraints: vec![],
523 table_options: vec![],
524 };
525
526 CreateTableExecutor::execute(&stmt, db).unwrap();
527 }
528
529 #[test]
530 fn test_create_simple_index() {
531 let mut db = Database::new();
532 create_test_table(&mut db);
533
534 let stmt = CreateIndexStmt {
535 index_name: "idx_users_email".to_string(),
536 if_not_exists: false,
537 table_name: "users".to_string(),
538 index_type: vibesql_ast::IndexType::BTree { unique: false },
539 columns: vec![IndexColumn {
540 column_name: "email".to_string(),
541 direction: OrderDirection::Asc,
542 prefix_length: None,
543 }],
544 };
545
546 let result = CreateIndexExecutor::execute(&stmt, &mut db);
547 assert!(result.is_ok());
548 assert_eq!(
549 result.unwrap(),
550 "Index 'idx_users_email' created successfully on table 'public.users'"
551 );
552
553 assert!(db.index_exists("idx_users_email"));
555 }
556
557 #[test]
558 fn test_create_unique_index() {
559 let mut db = Database::new();
560 create_test_table(&mut db);
561
562 let stmt = CreateIndexStmt {
563 index_name: "idx_users_email_unique".to_string(),
564 if_not_exists: false,
565 table_name: "users".to_string(),
566 index_type: vibesql_ast::IndexType::BTree { unique: true },
567 columns: vec![IndexColumn {
568 column_name: "email".to_string(),
569 direction: OrderDirection::Asc,
570 prefix_length: None,
571 }],
572 };
573
574 let result = CreateIndexExecutor::execute(&stmt, &mut db);
575 assert!(result.is_ok());
576 assert!(db.index_exists("idx_users_email_unique"));
577 }
578
579 #[test]
580 fn test_create_multi_column_index() {
581 let mut db = Database::new();
582 create_test_table(&mut db);
583
584 let stmt = CreateIndexStmt {
585 index_name: "idx_users_email_name".to_string(),
586 if_not_exists: false,
587 table_name: "users".to_string(),
588 index_type: vibesql_ast::IndexType::BTree { unique: false },
589 columns: vec![
590 IndexColumn {
591 column_name: "email".to_string(),
592 direction: OrderDirection::Asc,
593 prefix_length: None,
594 },
595 IndexColumn {
596 column_name: "name".to_string(),
597 direction: OrderDirection::Desc,
598 prefix_length: None,
599 },
600 ],
601 };
602
603 let result = CreateIndexExecutor::execute(&stmt, &mut db);
604 assert!(result.is_ok());
605 }
606
607 #[test]
608 fn test_create_index_duplicate_name() {
609 let mut db = Database::new();
610 create_test_table(&mut db);
611
612 let stmt = CreateIndexStmt {
613 index_name: "idx_users_email".to_string(),
614 if_not_exists: false,
615 table_name: "users".to_string(),
616 index_type: vibesql_ast::IndexType::BTree { unique: false },
617 columns: vec![IndexColumn {
618 column_name: "email".to_string(),
619 direction: OrderDirection::Asc,
620 prefix_length: None,
621 }],
622 };
623
624 let result = CreateIndexExecutor::execute(&stmt, &mut db);
626 assert!(result.is_ok());
627
628 let result = CreateIndexExecutor::execute(&stmt, &mut db);
630 assert!(result.is_err());
631 assert!(matches!(result, Err(ExecutorError::IndexAlreadyExists(_))));
632 }
633
634 #[test]
635 fn test_create_index_on_nonexistent_table() {
636 let mut db = Database::new();
637
638 let stmt = CreateIndexStmt {
639 index_name: "idx_nonexistent".to_string(),
640 if_not_exists: false,
641 table_name: "nonexistent_table".to_string(),
642 index_type: vibesql_ast::IndexType::BTree { unique: false },
643 columns: vec![IndexColumn {
644 column_name: "id".to_string(),
645 direction: OrderDirection::Asc,
646 prefix_length: None,
647 }],
648 };
649
650 let result = CreateIndexExecutor::execute(&stmt, &mut db);
651 assert!(result.is_err());
652 assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
653 }
654
655 #[test]
656 fn test_create_index_on_nonexistent_column() {
657 let mut db = Database::new();
658 create_test_table(&mut db);
659
660 let stmt = CreateIndexStmt {
661 index_name: "idx_users_nonexistent".to_string(),
662 if_not_exists: false,
663 table_name: "users".to_string(),
664 index_type: vibesql_ast::IndexType::BTree { unique: false },
665 columns: vec![IndexColumn {
666 column_name: "nonexistent_column".to_string(),
667 direction: OrderDirection::Asc,
668 prefix_length: None,
669 }],
670 };
671
672 let result = CreateIndexExecutor::execute(&stmt, &mut db);
673 assert!(result.is_err());
674 assert!(matches!(result, Err(ExecutorError::ColumnNotFound { .. })));
675 }
676
677 #[test]
678 fn test_create_index_if_not_exists_when_not_exists() {
679 let mut db = Database::new();
680 create_test_table(&mut db);
681
682 let stmt = CreateIndexStmt {
683 index_name: "idx_users_email".to_string(),
684 if_not_exists: true,
685 table_name: "users".to_string(),
686 index_type: vibesql_ast::IndexType::BTree { unique: false },
687 columns: vec![IndexColumn {
688 column_name: "email".to_string(),
689 direction: OrderDirection::Asc,
690 prefix_length: None,
691 }],
692 };
693
694 let result = CreateIndexExecutor::execute(&stmt, &mut db);
695 assert!(result.is_ok());
696 assert_eq!(
697 result.unwrap(),
698 "Index 'idx_users_email' created successfully on table 'public.users'"
699 );
700 assert!(db.index_exists("idx_users_email"));
701 }
702
703 #[test]
704 fn test_create_index_if_not_exists_when_exists() {
705 let mut db = Database::new();
706 create_test_table(&mut db);
707
708 let stmt = CreateIndexStmt {
710 index_name: "idx_users_email".to_string(),
711 if_not_exists: false,
712 table_name: "users".to_string(),
713 index_type: vibesql_ast::IndexType::BTree { unique: false },
714 columns: vec![IndexColumn {
715 column_name: "email".to_string(),
716 direction: OrderDirection::Asc,
717 prefix_length: None,
718 }],
719 };
720 CreateIndexExecutor::execute(&stmt, &mut db).unwrap();
721
722 let stmt_with_if_not_exists = CreateIndexStmt {
724 index_name: "idx_users_email".to_string(),
725 if_not_exists: true,
726 table_name: "users".to_string(),
727 index_type: vibesql_ast::IndexType::BTree { unique: false },
728 columns: vec![IndexColumn {
729 column_name: "email".to_string(),
730 direction: OrderDirection::Asc,
731 prefix_length: None,
732 }],
733 };
734 let result = CreateIndexExecutor::execute(&stmt_with_if_not_exists, &mut db);
735 assert!(result.is_ok());
736 assert!(db.index_exists("idx_users_email"));
737 }
738
739 #[test]
740 fn test_create_index_with_schema_qualified_table() {
741 let mut db = Database::new();
742 create_test_table(&mut db);
743
744 let index_stmt = CreateIndexStmt {
746 index_name: "idx_users_email_qualified".to_string(),
747 if_not_exists: false,
748 table_name: "public.users".to_string(), index_type: vibesql_ast::IndexType::BTree { unique: false },
750 columns: vec![IndexColumn {
751 column_name: "email".to_string(),
752 direction: OrderDirection::Asc,
753 prefix_length: None,
754 }],
755 };
756
757 let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
758 assert!(result.is_ok());
759 assert_eq!(
760 result.unwrap(),
761 "Index 'idx_users_email_qualified' created successfully on table 'public.users'"
762 );
763
764 assert!(db.index_exists("idx_users_email_qualified"));
766 }
767
768 #[test]
769 fn test_create_index_on_nonexistent_schema_qualified_table() {
770 let mut db = Database::new();
771
772 db.catalog.create_schema("test_schema".to_string()).unwrap();
774
775 let index_stmt = CreateIndexStmt {
777 index_name: "idx_nonexistent".to_string(),
778 if_not_exists: false,
779 table_name: "test_schema.nonexistent_table".to_string(),
780 index_type: vibesql_ast::IndexType::BTree { unique: false },
781 columns: vec![IndexColumn {
782 column_name: "id".to_string(),
783 direction: OrderDirection::Asc,
784 prefix_length: None,
785 }],
786 };
787
788 let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
789 assert!(result.is_err());
790 assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
791 }
792
793 fn create_vector_table(db: &mut Database) {
798 let stmt = CreateTableStmt {
799 if_not_exists: false,
800 table_name: "documents".to_string(),
801 columns: vec![
802 ColumnDef {
803 name: "id".to_string(),
804 data_type: DataType::Integer,
805 nullable: false,
806 constraints: vec![],
807 default_value: None,
808 comment: None,
809 },
810 ColumnDef {
811 name: "embedding".to_string(),
812 data_type: DataType::Vector { dimensions: 3 },
813 nullable: true,
814 constraints: vec![],
815 default_value: None,
816 comment: None,
817 },
818 ColumnDef {
819 name: "content".to_string(),
820 data_type: DataType::Varchar { max_length: Some(1000) },
821 nullable: true,
822 constraints: vec![],
823 default_value: None,
824 comment: None,
825 },
826 ],
827 table_constraints: vec![],
828 table_options: vec![],
829 };
830
831 CreateTableExecutor::execute(&stmt, db).unwrap();
832 }
833
834 #[test]
835 fn test_create_ivfflat_index_l2() {
836 let mut db = Database::new();
837 create_vector_table(&mut db);
838
839 let stmt = CreateIndexStmt {
840 index_name: "idx_documents_embedding".to_string(),
841 if_not_exists: false,
842 table_name: "documents".to_string(),
843 index_type: vibesql_ast::IndexType::IVFFlat {
844 metric: vibesql_ast::VectorDistanceMetric::L2,
845 lists: 4,
846 },
847 columns: vec![IndexColumn {
848 column_name: "embedding".to_string(),
849 direction: OrderDirection::Asc,
850 prefix_length: None,
851 }],
852 };
853
854 let result = CreateIndexExecutor::execute(&stmt, &mut db);
855 assert!(result.is_ok(), "IVFFlat index creation failed: {:?}", result.err());
856 assert!(result
857 .unwrap()
858 .contains("IVFFlat index 'idx_documents_embedding' created successfully"));
859 assert!(db.index_exists("idx_documents_embedding"));
860 }
861
862 #[test]
863 fn test_create_ivfflat_index_cosine() {
864 let mut db = Database::new();
865 create_vector_table(&mut db);
866
867 let stmt = CreateIndexStmt {
868 index_name: "idx_documents_cosine".to_string(),
869 if_not_exists: false,
870 table_name: "documents".to_string(),
871 index_type: vibesql_ast::IndexType::IVFFlat {
872 metric: vibesql_ast::VectorDistanceMetric::Cosine,
873 lists: 4,
874 },
875 columns: vec![IndexColumn {
876 column_name: "embedding".to_string(),
877 direction: OrderDirection::Asc,
878 prefix_length: None,
879 }],
880 };
881
882 let result = CreateIndexExecutor::execute(&stmt, &mut db);
883 assert!(result.is_ok());
884 assert!(db.index_exists("idx_documents_cosine"));
885 }
886
887 #[test]
888 fn test_create_ivfflat_index_inner_product() {
889 let mut db = Database::new();
890 create_vector_table(&mut db);
891
892 let stmt = CreateIndexStmt {
893 index_name: "idx_documents_ip".to_string(),
894 if_not_exists: false,
895 table_name: "documents".to_string(),
896 index_type: vibesql_ast::IndexType::IVFFlat {
897 metric: vibesql_ast::VectorDistanceMetric::InnerProduct,
898 lists: 4,
899 },
900 columns: vec![IndexColumn {
901 column_name: "embedding".to_string(),
902 direction: OrderDirection::Asc,
903 prefix_length: None,
904 }],
905 };
906
907 let result = CreateIndexExecutor::execute(&stmt, &mut db);
908 assert!(result.is_ok());
909 assert!(db.index_exists("idx_documents_ip"));
910 }
911
912 #[test]
913 fn test_create_ivfflat_index_on_non_vector_column() {
914 let mut db = Database::new();
915 create_vector_table(&mut db);
916
917 let stmt = CreateIndexStmt {
919 index_name: "idx_documents_content".to_string(),
920 if_not_exists: false,
921 table_name: "documents".to_string(),
922 index_type: vibesql_ast::IndexType::IVFFlat {
923 metric: vibesql_ast::VectorDistanceMetric::L2,
924 lists: 4,
925 },
926 columns: vec![IndexColumn {
927 column_name: "content".to_string(),
928 direction: OrderDirection::Asc,
929 prefix_length: None,
930 }],
931 };
932
933 let result = CreateIndexExecutor::execute(&stmt, &mut db);
934 assert!(result.is_err());
935 assert!(matches!(result, Err(ExecutorError::InvalidIndexDefinition(_))));
936 }
937
938 #[test]
939 fn test_create_ivfflat_index_multiple_columns_fails() {
940 let mut db = Database::new();
941 create_vector_table(&mut db);
942
943 let stmt = CreateIndexStmt {
945 index_name: "idx_documents_multi".to_string(),
946 if_not_exists: false,
947 table_name: "documents".to_string(),
948 index_type: vibesql_ast::IndexType::IVFFlat {
949 metric: vibesql_ast::VectorDistanceMetric::L2,
950 lists: 4,
951 },
952 columns: vec![
953 IndexColumn {
954 column_name: "embedding".to_string(),
955 direction: OrderDirection::Asc,
956 prefix_length: None,
957 },
958 IndexColumn {
959 column_name: "id".to_string(),
960 direction: OrderDirection::Asc,
961 prefix_length: None,
962 },
963 ],
964 };
965
966 let result = CreateIndexExecutor::execute(&stmt, &mut db);
967 assert!(result.is_err());
968 assert!(matches!(result, Err(ExecutorError::InvalidIndexDefinition(_))));
969 }
970
971 #[test]
972 fn test_create_ivfflat_index_if_not_exists() {
973 let mut db = Database::new();
974 create_vector_table(&mut db);
975
976 let stmt = CreateIndexStmt {
977 index_name: "idx_documents_embedding".to_string(),
978 if_not_exists: true,
979 table_name: "documents".to_string(),
980 index_type: vibesql_ast::IndexType::IVFFlat {
981 metric: vibesql_ast::VectorDistanceMetric::L2,
982 lists: 4,
983 },
984 columns: vec![IndexColumn {
985 column_name: "embedding".to_string(),
986 direction: OrderDirection::Asc,
987 prefix_length: None,
988 }],
989 };
990
991 let result = CreateIndexExecutor::execute(&stmt, &mut db);
993 assert!(result.is_ok());
994
995 let result2 = CreateIndexExecutor::execute(&stmt, &mut db);
997 assert!(result2.is_ok());
998 assert!(result2.unwrap().contains("already exists"));
999 }
1000
1001 #[test]
1002 fn test_ivfflat_index_search() {
1003 let mut db = Database::new();
1004 create_vector_table(&mut db);
1005
1006 db.insert_row(
1009 "documents",
1010 Row::new(vec![
1011 SqlValue::Integer(1),
1012 SqlValue::Vector(vec![1.0, 0.0, 0.0]),
1013 SqlValue::Varchar(arcstr::ArcStr::from("doc1")),
1014 ]),
1015 )
1016 .unwrap();
1017
1018 db.insert_row(
1020 "documents",
1021 Row::new(vec![
1022 SqlValue::Integer(2),
1023 SqlValue::Vector(vec![0.0, 1.0, 0.0]),
1024 SqlValue::Varchar(arcstr::ArcStr::from("doc2")),
1025 ]),
1026 )
1027 .unwrap();
1028
1029 db.insert_row(
1031 "documents",
1032 Row::new(vec![
1033 SqlValue::Integer(3),
1034 SqlValue::Vector(vec![0.0, 0.0, 1.0]),
1035 SqlValue::Varchar(arcstr::ArcStr::from("doc3")),
1036 ]),
1037 )
1038 .unwrap();
1039
1040 db.insert_row(
1042 "documents",
1043 Row::new(vec![
1044 SqlValue::Integer(4),
1045 SqlValue::Vector(vec![0.5, 0.5, 0.0]),
1046 SqlValue::Varchar(arcstr::ArcStr::from("doc4")),
1047 ]),
1048 )
1049 .unwrap();
1050
1051 let stmt = CreateIndexStmt {
1053 index_name: "idx_embedding".to_string(),
1054 if_not_exists: false,
1055 table_name: "documents".to_string(),
1056 index_type: vibesql_ast::IndexType::IVFFlat {
1057 metric: vibesql_ast::VectorDistanceMetric::L2,
1058 lists: 2, },
1060 columns: vec![IndexColumn {
1061 column_name: "embedding".to_string(),
1062 direction: OrderDirection::Asc,
1063 prefix_length: None,
1064 }],
1065 };
1066
1067 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1068 assert!(result.is_ok(), "Index creation failed: {:?}", result.err());
1069
1070 let query_vector = vec![0.9, 0.1, 0.0];
1073 let results = db.search_ivfflat_index("idx_embedding", &query_vector, 2);
1074 assert!(results.is_ok(), "Search should succeed: {:?}", results.err());
1075
1076 let neighbors = results.unwrap();
1077 assert!(!neighbors.is_empty(), "Should find at least one neighbor");
1079
1080 let (first_row_id, first_distance) = neighbors[0];
1082 assert!(first_distance >= 0.0, "Distance should be non-negative");
1083 assert_eq!(first_row_id, 0, "First result should be the closest vector");
1085 }
1086
1087 #[test]
1088 fn test_ivfflat_get_indexes_for_table() {
1089 let mut db = Database::new();
1090 create_vector_table(&mut db);
1091
1092 let stmt = CreateIndexStmt {
1094 index_name: "idx_vec".to_string(),
1095 if_not_exists: false,
1096 table_name: "documents".to_string(),
1097 index_type: vibesql_ast::IndexType::IVFFlat {
1098 metric: vibesql_ast::VectorDistanceMetric::Cosine,
1099 lists: 2,
1100 },
1101 columns: vec![IndexColumn {
1102 column_name: "embedding".to_string(),
1103 direction: OrderDirection::Asc,
1104 prefix_length: None,
1105 }],
1106 };
1107
1108 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1109 assert!(result.is_ok());
1110
1111 let ivfflat_indexes = db.get_ivfflat_indexes_for_table("documents");
1113 assert_eq!(ivfflat_indexes.len(), 1, "Should have one IVFFlat index");
1114
1115 let (metadata, index) = &ivfflat_indexes[0];
1116 assert!(metadata.index_name.to_uppercase().contains("IDX_VEC"));
1117 assert_eq!(index.metric(), vibesql_ast::VectorDistanceMetric::Cosine);
1118 }
1119
1120 #[test]
1121 fn test_ivfflat_set_probes() {
1122 let mut db = Database::new();
1123 create_vector_table(&mut db);
1124
1125 let stmt = CreateIndexStmt {
1127 index_name: "idx_probes".to_string(),
1128 if_not_exists: false,
1129 table_name: "documents".to_string(),
1130 index_type: vibesql_ast::IndexType::IVFFlat {
1131 metric: vibesql_ast::VectorDistanceMetric::L2,
1132 lists: 4,
1133 },
1134 columns: vec![IndexColumn {
1135 column_name: "embedding".to_string(),
1136 direction: OrderDirection::Asc,
1137 prefix_length: None,
1138 }],
1139 };
1140
1141 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1142 assert!(result.is_ok());
1143
1144 let set_probes_result = db.set_ivfflat_probes("idx_probes", 3);
1146 assert!(set_probes_result.is_ok());
1147
1148 let query_vector = vec![0.5, 0.5, 0.5];
1150 let search_result = db.search_ivfflat_index("idx_probes", &query_vector, 3);
1151 assert!(search_result.is_ok());
1152 }
1153}