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 index_metadata = vibesql_catalog::IndexMetadata::new(
154 index_name.clone(),
155 table_name.clone(),
156 vibesql_catalog::IndexType::BTree,
157 stmt.columns
158 .iter()
159 .map(|col| vibesql_catalog::IndexedColumn {
160 column_name: col.column_name.clone(),
161 order: match col.direction {
162 vibesql_ast::OrderDirection::Asc => {
163 vibesql_catalog::SortOrder::Ascending
164 }
165 vibesql_ast::OrderDirection::Desc => {
166 vibesql_catalog::SortOrder::Descending
167 }
168 },
169 prefix_length: col.prefix_length,
170 })
171 .collect(),
172 *unique,
173 );
174 database.catalog.add_index(index_metadata)?;
175
176 database.create_index(
178 index_name.clone(),
179 table_name.clone(),
180 *unique,
181 stmt.columns.clone(),
182 )?;
183
184 Ok(format!(
185 "Index '{}' created successfully on table '{}'",
186 index_name, qualified_table_name
187 ))
188 }
189 vibesql_ast::IndexType::Fulltext => Err(ExecutorError::UnsupportedFeature(
190 "FULLTEXT indexes are not yet implemented".to_string(),
191 )),
192 vibesql_ast::IndexType::Spatial => {
193 if stmt.columns.len() != 1 {
195 return Err(ExecutorError::InvalidIndexDefinition(
196 "SPATIAL indexes must be defined on exactly one column".to_string(),
197 ));
198 }
199
200 let column_name = &stmt.columns[0].column_name;
201
202 let col_idx = table_schema.get_column_index(column_name).ok_or_else(|| {
204 ExecutorError::ColumnNotFound {
205 column_name: column_name.clone(),
206 table_name: qualified_table_name.clone(),
207 searched_tables: vec![qualified_table_name.clone()],
208 available_columns: table_schema
209 .columns
210 .iter()
211 .map(|c| c.name.clone())
212 .collect(),
213 }
214 })?;
215
216 let table = database
218 .get_table(&table_name)
219 .ok_or_else(|| ExecutorError::TableNotFound(qualified_table_name.clone()))?;
220
221 let mut entries = Vec::new();
222 for (row_idx, row) in table.scan().iter().enumerate() {
223 let geom_value = &row.values[col_idx];
224
225 if let Some(mbr) = extract_mbr_from_sql_value(geom_value) {
227 entries.push(SpatialIndexEntry { row_id: row_idx, mbr });
228 }
229 }
230
231 let spatial_index = SpatialIndex::bulk_load(column_name.clone(), entries);
233
234 let index_metadata = vibesql_catalog::IndexMetadata::new(
236 index_name.clone(),
237 table_name.clone(),
238 vibesql_catalog::IndexType::RTree,
239 vec![vibesql_catalog::IndexedColumn {
240 column_name: column_name.clone(),
241 order: vibesql_catalog::SortOrder::Ascending,
242 prefix_length: None, }],
244 false,
245 );
246 database.catalog.add_index(index_metadata)?;
247
248 let metadata = SpatialIndexMetadata {
250 index_name: index_name.clone(),
251 table_name: table_name.clone(),
252 column_name: column_name.clone(),
253 created_at: Some(chrono::Utc::now()),
254 };
255
256 database.create_spatial_index(metadata, spatial_index)?;
257
258 Ok(format!(
259 "Spatial index '{}' created successfully on table '{}'",
260 index_name, qualified_table_name
261 ))
262 }
263 vibesql_ast::IndexType::IVFFlat { metric, lists } => {
264 if stmt.columns.len() != 1 {
266 return Err(ExecutorError::InvalidIndexDefinition(
267 "IVFFlat indexes must be defined on exactly one vector column".to_string(),
268 ));
269 }
270
271 let column_name = &stmt.columns[0].column_name;
272
273 let col_idx = table_schema.get_column_index(column_name).ok_or_else(|| {
275 ExecutorError::ColumnNotFound {
276 column_name: column_name.clone(),
277 table_name: qualified_table_name.clone(),
278 searched_tables: vec![qualified_table_name.clone()],
279 available_columns: table_schema
280 .columns
281 .iter()
282 .map(|c| c.name.clone())
283 .collect(),
284 }
285 })?;
286
287 let col_type = &table_schema.columns[col_idx].data_type;
289 let dimensions = match col_type {
290 vibesql_types::DataType::Vector { dimensions } => *dimensions as usize,
291 _ => {
292 return Err(ExecutorError::InvalidIndexDefinition(format!(
293 "IVFFlat indexes can only be created on VECTOR columns, but '{}' has type {:?}",
294 column_name, col_type
295 )));
296 }
297 };
298
299 let catalog_metric = match metric {
301 vibesql_ast::VectorDistanceMetric::L2 => {
302 vibesql_catalog::VectorDistanceMetric::L2
303 }
304 vibesql_ast::VectorDistanceMetric::Cosine => {
305 vibesql_catalog::VectorDistanceMetric::Cosine
306 }
307 vibesql_ast::VectorDistanceMetric::InnerProduct => {
308 vibesql_catalog::VectorDistanceMetric::InnerProduct
309 }
310 };
311
312 let index_metadata = vibesql_catalog::IndexMetadata::new(
314 index_name.clone(),
315 table_name.clone(),
316 vibesql_catalog::IndexType::IVFFlat { metric: catalog_metric, lists: *lists },
317 vec![vibesql_catalog::IndexedColumn {
318 column_name: column_name.clone(),
319 order: vibesql_catalog::SortOrder::Ascending, prefix_length: None,
321 }],
322 false, );
324 database.catalog.add_index(index_metadata)?;
325
326 database.create_ivfflat_index(
328 index_name.clone(),
329 table_name.clone(),
330 column_name.clone(),
331 col_idx,
332 dimensions,
333 *lists as usize,
334 *metric,
335 )?;
336
337 Ok(format!(
338 "IVFFlat index '{}' created successfully on table '{}' column '{}'",
339 index_name, qualified_table_name, column_name
340 ))
341 }
342 vibesql_ast::IndexType::Hnsw { metric, m, ef_construction } => {
343 if stmt.columns.len() != 1 {
345 return Err(ExecutorError::InvalidIndexDefinition(
346 "HNSW indexes must be defined on exactly one vector column".to_string(),
347 ));
348 }
349
350 let column_name = &stmt.columns[0].column_name;
351
352 let col_idx = table_schema.get_column_index(column_name).ok_or_else(|| {
354 ExecutorError::ColumnNotFound {
355 column_name: column_name.clone(),
356 table_name: qualified_table_name.clone(),
357 searched_tables: vec![qualified_table_name.clone()],
358 available_columns: table_schema
359 .columns
360 .iter()
361 .map(|c| c.name.clone())
362 .collect(),
363 }
364 })?;
365
366 let col_type = &table_schema.columns[col_idx].data_type;
368 let dimensions = match col_type {
369 vibesql_types::DataType::Vector { dimensions } => *dimensions as usize,
370 _ => {
371 return Err(ExecutorError::InvalidIndexDefinition(format!(
372 "HNSW indexes can only be created on VECTOR columns, but '{}' has type {:?}",
373 column_name, col_type
374 )));
375 }
376 };
377
378 let catalog_metric = match metric {
380 vibesql_ast::VectorDistanceMetric::L2 => {
381 vibesql_catalog::VectorDistanceMetric::L2
382 }
383 vibesql_ast::VectorDistanceMetric::Cosine => {
384 vibesql_catalog::VectorDistanceMetric::Cosine
385 }
386 vibesql_ast::VectorDistanceMetric::InnerProduct => {
387 vibesql_catalog::VectorDistanceMetric::InnerProduct
388 }
389 };
390
391 let index_metadata = vibesql_catalog::IndexMetadata::new(
393 index_name.clone(),
394 table_name.clone(),
395 vibesql_catalog::IndexType::Hnsw {
396 metric: catalog_metric,
397 m: *m,
398 ef_construction: *ef_construction,
399 },
400 vec![vibesql_catalog::IndexedColumn {
401 column_name: column_name.clone(),
402 order: vibesql_catalog::SortOrder::Ascending, prefix_length: None,
404 }],
405 false, );
407 database.catalog.add_index(index_metadata)?;
408
409 database.create_hnsw_index(
411 index_name.clone(),
412 table_name.clone(),
413 column_name.clone(),
414 col_idx,
415 dimensions,
416 *m,
417 *ef_construction,
418 *metric,
419 )?;
420
421 Ok(format!(
422 "HNSW index '{}' created successfully on table '{}' column '{}'",
423 index_name, qualified_table_name, column_name
424 ))
425 }
426 }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use vibesql_ast::{ColumnDef, CreateTableStmt, IndexColumn, OrderDirection};
433 use vibesql_storage::Row;
434 use vibesql_types::{DataType, SqlValue};
435
436 use super::*;
437 use crate::CreateTableExecutor;
438
439 fn create_test_table(db: &mut Database) {
440 let stmt = CreateTableStmt {
441 table_name: "users".to_string(),
442 columns: vec![
443 ColumnDef {
444 name: "id".to_string(),
445 data_type: DataType::Integer,
446 nullable: false,
447 constraints: vec![],
448 default_value: None,
449 comment: None,
450 },
451 ColumnDef {
452 name: "email".to_string(),
453 data_type: DataType::Varchar { max_length: Some(255) },
454 nullable: false,
455 constraints: vec![],
456 default_value: None,
457 comment: None,
458 },
459 ColumnDef {
460 name: "name".to_string(),
461 data_type: DataType::Varchar { max_length: Some(100) },
462 nullable: true,
463 constraints: vec![],
464 default_value: None,
465 comment: None,
466 },
467 ],
468 table_constraints: vec![],
469 table_options: vec![],
470 };
471
472 CreateTableExecutor::execute(&stmt, db).unwrap();
473 }
474
475 #[test]
476 fn test_create_simple_index() {
477 let mut db = Database::new();
478 create_test_table(&mut db);
479
480 let stmt = CreateIndexStmt {
481 index_name: "idx_users_email".to_string(),
482 if_not_exists: false,
483 table_name: "users".to_string(),
484 index_type: vibesql_ast::IndexType::BTree { unique: false },
485 columns: vec![IndexColumn {
486 column_name: "email".to_string(),
487 direction: OrderDirection::Asc,
488 prefix_length: None,
489 }],
490 };
491
492 let result = CreateIndexExecutor::execute(&stmt, &mut db);
493 assert!(result.is_ok());
494 assert_eq!(
495 result.unwrap(),
496 "Index 'idx_users_email' created successfully on table 'public.users'"
497 );
498
499 assert!(db.index_exists("idx_users_email"));
501 }
502
503 #[test]
504 fn test_create_unique_index() {
505 let mut db = Database::new();
506 create_test_table(&mut db);
507
508 let stmt = CreateIndexStmt {
509 index_name: "idx_users_email_unique".to_string(),
510 if_not_exists: false,
511 table_name: "users".to_string(),
512 index_type: vibesql_ast::IndexType::BTree { unique: true },
513 columns: vec![IndexColumn {
514 column_name: "email".to_string(),
515 direction: OrderDirection::Asc,
516 prefix_length: None,
517 }],
518 };
519
520 let result = CreateIndexExecutor::execute(&stmt, &mut db);
521 assert!(result.is_ok());
522 assert!(db.index_exists("idx_users_email_unique"));
523 }
524
525 #[test]
526 fn test_create_multi_column_index() {
527 let mut db = Database::new();
528 create_test_table(&mut db);
529
530 let stmt = CreateIndexStmt {
531 index_name: "idx_users_email_name".to_string(),
532 if_not_exists: false,
533 table_name: "users".to_string(),
534 index_type: vibesql_ast::IndexType::BTree { unique: false },
535 columns: vec![
536 IndexColumn {
537 column_name: "email".to_string(),
538 direction: OrderDirection::Asc,
539 prefix_length: None,
540 },
541 IndexColumn {
542 column_name: "name".to_string(),
543 direction: OrderDirection::Desc,
544 prefix_length: None,
545 },
546 ],
547 };
548
549 let result = CreateIndexExecutor::execute(&stmt, &mut db);
550 assert!(result.is_ok());
551 }
552
553 #[test]
554 fn test_create_index_duplicate_name() {
555 let mut db = Database::new();
556 create_test_table(&mut db);
557
558 let stmt = CreateIndexStmt {
559 index_name: "idx_users_email".to_string(),
560 if_not_exists: false,
561 table_name: "users".to_string(),
562 index_type: vibesql_ast::IndexType::BTree { unique: false },
563 columns: vec![IndexColumn {
564 column_name: "email".to_string(),
565 direction: OrderDirection::Asc,
566 prefix_length: None,
567 }],
568 };
569
570 let result = CreateIndexExecutor::execute(&stmt, &mut db);
572 assert!(result.is_ok());
573
574 let result = CreateIndexExecutor::execute(&stmt, &mut db);
576 assert!(result.is_err());
577 assert!(matches!(result, Err(ExecutorError::IndexAlreadyExists(_))));
578 }
579
580 #[test]
581 fn test_create_index_on_nonexistent_table() {
582 let mut db = Database::new();
583
584 let stmt = CreateIndexStmt {
585 index_name: "idx_nonexistent".to_string(),
586 if_not_exists: false,
587 table_name: "nonexistent_table".to_string(),
588 index_type: vibesql_ast::IndexType::BTree { unique: false },
589 columns: vec![IndexColumn {
590 column_name: "id".to_string(),
591 direction: OrderDirection::Asc,
592 prefix_length: None,
593 }],
594 };
595
596 let result = CreateIndexExecutor::execute(&stmt, &mut db);
597 assert!(result.is_err());
598 assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
599 }
600
601 #[test]
602 fn test_create_index_on_nonexistent_column() {
603 let mut db = Database::new();
604 create_test_table(&mut db);
605
606 let stmt = CreateIndexStmt {
607 index_name: "idx_users_nonexistent".to_string(),
608 if_not_exists: false,
609 table_name: "users".to_string(),
610 index_type: vibesql_ast::IndexType::BTree { unique: false },
611 columns: vec![IndexColumn {
612 column_name: "nonexistent_column".to_string(),
613 direction: OrderDirection::Asc,
614 prefix_length: None,
615 }],
616 };
617
618 let result = CreateIndexExecutor::execute(&stmt, &mut db);
619 assert!(result.is_err());
620 assert!(matches!(result, Err(ExecutorError::ColumnNotFound { .. })));
621 }
622
623 #[test]
624 fn test_create_index_if_not_exists_when_not_exists() {
625 let mut db = Database::new();
626 create_test_table(&mut db);
627
628 let stmt = CreateIndexStmt {
629 index_name: "idx_users_email".to_string(),
630 if_not_exists: true,
631 table_name: "users".to_string(),
632 index_type: vibesql_ast::IndexType::BTree { unique: false },
633 columns: vec![IndexColumn {
634 column_name: "email".to_string(),
635 direction: OrderDirection::Asc,
636 prefix_length: None,
637 }],
638 };
639
640 let result = CreateIndexExecutor::execute(&stmt, &mut db);
641 assert!(result.is_ok());
642 assert_eq!(
643 result.unwrap(),
644 "Index 'idx_users_email' created successfully on table 'public.users'"
645 );
646 assert!(db.index_exists("idx_users_email"));
647 }
648
649 #[test]
650 fn test_create_index_if_not_exists_when_exists() {
651 let mut db = Database::new();
652 create_test_table(&mut db);
653
654 let stmt = CreateIndexStmt {
656 index_name: "idx_users_email".to_string(),
657 if_not_exists: false,
658 table_name: "users".to_string(),
659 index_type: vibesql_ast::IndexType::BTree { unique: false },
660 columns: vec![IndexColumn {
661 column_name: "email".to_string(),
662 direction: OrderDirection::Asc,
663 prefix_length: None,
664 }],
665 };
666 CreateIndexExecutor::execute(&stmt, &mut db).unwrap();
667
668 let stmt_with_if_not_exists = CreateIndexStmt {
670 index_name: "idx_users_email".to_string(),
671 if_not_exists: true,
672 table_name: "users".to_string(),
673 index_type: vibesql_ast::IndexType::BTree { unique: false },
674 columns: vec![IndexColumn {
675 column_name: "email".to_string(),
676 direction: OrderDirection::Asc,
677 prefix_length: None,
678 }],
679 };
680 let result = CreateIndexExecutor::execute(&stmt_with_if_not_exists, &mut db);
681 assert!(result.is_ok());
682 assert!(db.index_exists("idx_users_email"));
683 }
684
685 #[test]
686 fn test_create_index_with_schema_qualified_table() {
687 let mut db = Database::new();
688 create_test_table(&mut db);
689
690 let index_stmt = CreateIndexStmt {
692 index_name: "idx_users_email_qualified".to_string(),
693 if_not_exists: false,
694 table_name: "public.users".to_string(), index_type: vibesql_ast::IndexType::BTree { unique: false },
696 columns: vec![IndexColumn {
697 column_name: "email".to_string(),
698 direction: OrderDirection::Asc,
699 prefix_length: None,
700 }],
701 };
702
703 let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
704 assert!(result.is_ok());
705 assert_eq!(
706 result.unwrap(),
707 "Index 'idx_users_email_qualified' created successfully on table 'public.users'"
708 );
709
710 assert!(db.index_exists("idx_users_email_qualified"));
712 }
713
714 #[test]
715 fn test_create_index_on_nonexistent_schema_qualified_table() {
716 let mut db = Database::new();
717
718 db.catalog.create_schema("test_schema".to_string()).unwrap();
720
721 let index_stmt = CreateIndexStmt {
723 index_name: "idx_nonexistent".to_string(),
724 if_not_exists: false,
725 table_name: "test_schema.nonexistent_table".to_string(),
726 index_type: vibesql_ast::IndexType::BTree { unique: false },
727 columns: vec![IndexColumn {
728 column_name: "id".to_string(),
729 direction: OrderDirection::Asc,
730 prefix_length: None,
731 }],
732 };
733
734 let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
735 assert!(result.is_err());
736 assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
737 }
738
739 fn create_vector_table(db: &mut Database) {
744 let stmt = CreateTableStmt {
745 table_name: "documents".to_string(),
746 columns: vec![
747 ColumnDef {
748 name: "id".to_string(),
749 data_type: DataType::Integer,
750 nullable: false,
751 constraints: vec![],
752 default_value: None,
753 comment: None,
754 },
755 ColumnDef {
756 name: "embedding".to_string(),
757 data_type: DataType::Vector { dimensions: 3 },
758 nullable: true,
759 constraints: vec![],
760 default_value: None,
761 comment: None,
762 },
763 ColumnDef {
764 name: "content".to_string(),
765 data_type: DataType::Varchar { max_length: Some(1000) },
766 nullable: true,
767 constraints: vec![],
768 default_value: None,
769 comment: None,
770 },
771 ],
772 table_constraints: vec![],
773 table_options: vec![],
774 };
775
776 CreateTableExecutor::execute(&stmt, db).unwrap();
777 }
778
779 #[test]
780 fn test_create_ivfflat_index_l2() {
781 let mut db = Database::new();
782 create_vector_table(&mut db);
783
784 let stmt = CreateIndexStmt {
785 index_name: "idx_documents_embedding".to_string(),
786 if_not_exists: false,
787 table_name: "documents".to_string(),
788 index_type: vibesql_ast::IndexType::IVFFlat {
789 metric: vibesql_ast::VectorDistanceMetric::L2,
790 lists: 4,
791 },
792 columns: vec![IndexColumn {
793 column_name: "embedding".to_string(),
794 direction: OrderDirection::Asc,
795 prefix_length: None,
796 }],
797 };
798
799 let result = CreateIndexExecutor::execute(&stmt, &mut db);
800 assert!(result.is_ok(), "IVFFlat index creation failed: {:?}", result.err());
801 assert!(result
802 .unwrap()
803 .contains("IVFFlat index 'idx_documents_embedding' created successfully"));
804 assert!(db.index_exists("idx_documents_embedding"));
805 }
806
807 #[test]
808 fn test_create_ivfflat_index_cosine() {
809 let mut db = Database::new();
810 create_vector_table(&mut db);
811
812 let stmt = CreateIndexStmt {
813 index_name: "idx_documents_cosine".to_string(),
814 if_not_exists: false,
815 table_name: "documents".to_string(),
816 index_type: vibesql_ast::IndexType::IVFFlat {
817 metric: vibesql_ast::VectorDistanceMetric::Cosine,
818 lists: 4,
819 },
820 columns: vec![IndexColumn {
821 column_name: "embedding".to_string(),
822 direction: OrderDirection::Asc,
823 prefix_length: None,
824 }],
825 };
826
827 let result = CreateIndexExecutor::execute(&stmt, &mut db);
828 assert!(result.is_ok());
829 assert!(db.index_exists("idx_documents_cosine"));
830 }
831
832 #[test]
833 fn test_create_ivfflat_index_inner_product() {
834 let mut db = Database::new();
835 create_vector_table(&mut db);
836
837 let stmt = CreateIndexStmt {
838 index_name: "idx_documents_ip".to_string(),
839 if_not_exists: false,
840 table_name: "documents".to_string(),
841 index_type: vibesql_ast::IndexType::IVFFlat {
842 metric: vibesql_ast::VectorDistanceMetric::InnerProduct,
843 lists: 4,
844 },
845 columns: vec![IndexColumn {
846 column_name: "embedding".to_string(),
847 direction: OrderDirection::Asc,
848 prefix_length: None,
849 }],
850 };
851
852 let result = CreateIndexExecutor::execute(&stmt, &mut db);
853 assert!(result.is_ok());
854 assert!(db.index_exists("idx_documents_ip"));
855 }
856
857 #[test]
858 fn test_create_ivfflat_index_on_non_vector_column() {
859 let mut db = Database::new();
860 create_vector_table(&mut db);
861
862 let stmt = CreateIndexStmt {
864 index_name: "idx_documents_content".to_string(),
865 if_not_exists: false,
866 table_name: "documents".to_string(),
867 index_type: vibesql_ast::IndexType::IVFFlat {
868 metric: vibesql_ast::VectorDistanceMetric::L2,
869 lists: 4,
870 },
871 columns: vec![IndexColumn {
872 column_name: "content".to_string(),
873 direction: OrderDirection::Asc,
874 prefix_length: None,
875 }],
876 };
877
878 let result = CreateIndexExecutor::execute(&stmt, &mut db);
879 assert!(result.is_err());
880 assert!(matches!(result, Err(ExecutorError::InvalidIndexDefinition(_))));
881 }
882
883 #[test]
884 fn test_create_ivfflat_index_multiple_columns_fails() {
885 let mut db = Database::new();
886 create_vector_table(&mut db);
887
888 let stmt = CreateIndexStmt {
890 index_name: "idx_documents_multi".to_string(),
891 if_not_exists: false,
892 table_name: "documents".to_string(),
893 index_type: vibesql_ast::IndexType::IVFFlat {
894 metric: vibesql_ast::VectorDistanceMetric::L2,
895 lists: 4,
896 },
897 columns: vec![
898 IndexColumn {
899 column_name: "embedding".to_string(),
900 direction: OrderDirection::Asc,
901 prefix_length: None,
902 },
903 IndexColumn {
904 column_name: "id".to_string(),
905 direction: OrderDirection::Asc,
906 prefix_length: None,
907 },
908 ],
909 };
910
911 let result = CreateIndexExecutor::execute(&stmt, &mut db);
912 assert!(result.is_err());
913 assert!(matches!(result, Err(ExecutorError::InvalidIndexDefinition(_))));
914 }
915
916 #[test]
917 fn test_create_ivfflat_index_if_not_exists() {
918 let mut db = Database::new();
919 create_vector_table(&mut db);
920
921 let stmt = CreateIndexStmt {
922 index_name: "idx_documents_embedding".to_string(),
923 if_not_exists: true,
924 table_name: "documents".to_string(),
925 index_type: vibesql_ast::IndexType::IVFFlat {
926 metric: vibesql_ast::VectorDistanceMetric::L2,
927 lists: 4,
928 },
929 columns: vec![IndexColumn {
930 column_name: "embedding".to_string(),
931 direction: OrderDirection::Asc,
932 prefix_length: None,
933 }],
934 };
935
936 let result = CreateIndexExecutor::execute(&stmt, &mut db);
938 assert!(result.is_ok());
939
940 let result2 = CreateIndexExecutor::execute(&stmt, &mut db);
942 assert!(result2.is_ok());
943 assert!(result2.unwrap().contains("already exists"));
944 }
945
946 #[test]
947 fn test_ivfflat_index_search() {
948 let mut db = Database::new();
949 create_vector_table(&mut db);
950
951 db.insert_row(
954 "documents",
955 Row::new(vec![
956 SqlValue::Integer(1),
957 SqlValue::Vector(vec![1.0, 0.0, 0.0]),
958 SqlValue::Varchar("doc1".to_string()),
959 ]),
960 )
961 .unwrap();
962
963 db.insert_row(
965 "documents",
966 Row::new(vec![
967 SqlValue::Integer(2),
968 SqlValue::Vector(vec![0.0, 1.0, 0.0]),
969 SqlValue::Varchar("doc2".to_string()),
970 ]),
971 )
972 .unwrap();
973
974 db.insert_row(
976 "documents",
977 Row::new(vec![
978 SqlValue::Integer(3),
979 SqlValue::Vector(vec![0.0, 0.0, 1.0]),
980 SqlValue::Varchar("doc3".to_string()),
981 ]),
982 )
983 .unwrap();
984
985 db.insert_row(
987 "documents",
988 Row::new(vec![
989 SqlValue::Integer(4),
990 SqlValue::Vector(vec![0.5, 0.5, 0.0]),
991 SqlValue::Varchar("doc4".to_string()),
992 ]),
993 )
994 .unwrap();
995
996 let stmt = CreateIndexStmt {
998 index_name: "idx_embedding".to_string(),
999 if_not_exists: false,
1000 table_name: "documents".to_string(),
1001 index_type: vibesql_ast::IndexType::IVFFlat {
1002 metric: vibesql_ast::VectorDistanceMetric::L2,
1003 lists: 2, },
1005 columns: vec![IndexColumn {
1006 column_name: "embedding".to_string(),
1007 direction: OrderDirection::Asc,
1008 prefix_length: None,
1009 }],
1010 };
1011
1012 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1013 assert!(result.is_ok(), "Index creation failed: {:?}", result.err());
1014
1015 let query_vector = vec![0.9, 0.1, 0.0];
1018 let results = db.search_ivfflat_index("idx_embedding", &query_vector, 2);
1019 assert!(results.is_ok(), "Search should succeed: {:?}", results.err());
1020
1021 let neighbors = results.unwrap();
1022 assert!(!neighbors.is_empty(), "Should find at least one neighbor");
1024
1025 let (first_row_id, first_distance) = neighbors[0];
1027 assert!(first_distance >= 0.0, "Distance should be non-negative");
1028 assert_eq!(first_row_id, 0, "First result should be the closest vector");
1030 }
1031
1032 #[test]
1033 fn test_ivfflat_get_indexes_for_table() {
1034 let mut db = Database::new();
1035 create_vector_table(&mut db);
1036
1037 let stmt = CreateIndexStmt {
1039 index_name: "idx_vec".to_string(),
1040 if_not_exists: false,
1041 table_name: "documents".to_string(),
1042 index_type: vibesql_ast::IndexType::IVFFlat {
1043 metric: vibesql_ast::VectorDistanceMetric::Cosine,
1044 lists: 2,
1045 },
1046 columns: vec![IndexColumn {
1047 column_name: "embedding".to_string(),
1048 direction: OrderDirection::Asc,
1049 prefix_length: None,
1050 }],
1051 };
1052
1053 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1054 assert!(result.is_ok());
1055
1056 let ivfflat_indexes = db.get_ivfflat_indexes_for_table("documents");
1058 assert_eq!(ivfflat_indexes.len(), 1, "Should have one IVFFlat index");
1059
1060 let (metadata, index) = &ivfflat_indexes[0];
1061 assert!(metadata.index_name.to_uppercase().contains("IDX_VEC"));
1062 assert_eq!(index.metric(), vibesql_ast::VectorDistanceMetric::Cosine);
1063 }
1064
1065 #[test]
1066 fn test_ivfflat_set_probes() {
1067 let mut db = Database::new();
1068 create_vector_table(&mut db);
1069
1070 let stmt = CreateIndexStmt {
1072 index_name: "idx_probes".to_string(),
1073 if_not_exists: false,
1074 table_name: "documents".to_string(),
1075 index_type: vibesql_ast::IndexType::IVFFlat {
1076 metric: vibesql_ast::VectorDistanceMetric::L2,
1077 lists: 4,
1078 },
1079 columns: vec![IndexColumn {
1080 column_name: "embedding".to_string(),
1081 direction: OrderDirection::Asc,
1082 prefix_length: None,
1083 }],
1084 };
1085
1086 let result = CreateIndexExecutor::execute(&stmt, &mut db);
1087 assert!(result.is_ok());
1088
1089 let set_probes_result = db.set_ivfflat_probes("idx_probes", 3);
1091 assert!(set_probes_result.is_ok());
1092
1093 let query_vector = vec![0.5, 0.5, 0.5];
1095 let search_result = db.search_ivfflat_index("idx_probes", &query_vector, 3);
1096 assert!(search_result.is_ok());
1097 }
1098}