1use super::{Column, ColumnId, DiffContext, Schema, TableId};
2use crate::stmt;
3
4use hashbrown::{HashMap, HashSet};
5use std::{fmt, ops::Deref};
6
7#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Index {
37 pub id: IndexId,
39
40 pub name: String,
42
43 pub on: TableId,
45
46 pub columns: Vec<IndexColumn>,
48
49 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
51 pub unique: bool,
52
53 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
55 pub primary_key: bool,
56}
57
58#[cfg(feature = "serde")]
59fn is_false(b: &bool) -> bool {
60 !*b
61}
62
63#[derive(Copy, Clone, Eq, PartialEq, Hash)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct IndexId {
79 pub table: TableId,
81 pub index: usize,
83}
84
85#[derive(Debug, Clone)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub struct IndexColumn {
106 pub column: ColumnId,
108
109 pub op: IndexOp,
111
112 pub scope: IndexScope,
114}
115
116#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129pub enum IndexOp {
130 Eq,
132 Sort(stmt::Direction),
134}
135
136#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub enum IndexScope {
150 Partition,
152
153 Local,
155}
156
157impl IndexColumn {
158 pub fn table_column<'a>(&self, schema: &'a Schema) -> &'a Column {
160 schema.column(self.column)
161 }
162}
163
164impl IndexScope {
165 pub fn is_partition(self) -> bool {
167 matches!(self, Self::Partition)
168 }
169
170 pub fn is_local(self) -> bool {
172 matches!(self, Self::Local)
173 }
174}
175
176impl IndexId {
177 pub(crate) fn placeholder() -> Self {
178 Self {
179 table: TableId::placeholder(),
180 index: usize::MAX,
181 }
182 }
183}
184
185impl fmt::Debug for IndexId {
186 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(fmt, "IndexId({}/{})", self.table.0, self.index)
188 }
189}
190
191pub struct IndicesDiff<'a> {
209 items: Vec<IndicesDiffItem<'a>>,
210}
211
212impl<'a> IndicesDiff<'a> {
213 pub fn from(cx: &DiffContext<'a>, previous: &'a [Index], next: &'a [Index]) -> Self {
220 fn has_diff(cx: &DiffContext<'_>, previous: &Index, next: &Index) -> bool {
221 if previous.name != next.name
223 || previous.columns.len() != next.columns.len()
224 || previous.unique != next.unique
225 || previous.primary_key != next.primary_key
226 {
227 return true;
228 }
229
230 for (previous_col, next_col) in previous.columns.iter().zip(next.columns.iter()) {
232 if previous_col.op != next_col.op || previous_col.scope != next_col.scope {
234 return true;
235 }
236
237 let columns_match =
239 if let Some(renamed_to) = cx.rename_hints().get_column(previous_col.column) {
240 renamed_to == next_col.column
242 } else {
243 let previous_column = cx.previous().column(previous_col.column);
245 let next_column = cx.next().column(next_col.column);
246 previous_column.name == next_column.name
247 };
248
249 if !columns_match {
250 return true;
251 }
252 }
253
254 false
255 }
256
257 let mut items = vec![];
258 let mut create_ids: HashSet<_> = next.iter().map(|to| to.id).collect();
259
260 let next_map =
261 HashMap::<&str, &'a Index>::from_iter(next.iter().map(|to| (to.name.as_str(), to)));
262
263 for previous in previous {
264 let next = if let Some(next_id) = cx.rename_hints().get_index(previous.id) {
265 cx.next().index(next_id)
266 } else if let Some(next) = next_map.get(previous.name.as_str()) {
267 next
268 } else {
269 items.push(IndicesDiffItem::DropIndex(previous));
270 continue;
271 };
272
273 create_ids.remove(&next.id);
274
275 if has_diff(cx, previous, next) {
276 items.push(IndicesDiffItem::AlterIndex { previous, next });
277 }
278 }
279
280 for index_id in create_ids {
281 items.push(IndicesDiffItem::CreateIndex(cx.next().index(index_id)));
282 }
283
284 Self { items }
285 }
286
287 pub const fn is_empty(&self) -> bool {
289 self.items.is_empty()
290 }
291}
292
293impl<'a> Deref for IndicesDiff<'a> {
294 type Target = Vec<IndicesDiffItem<'a>>;
295
296 fn deref(&self) -> &Self::Target {
297 &self.items
298 }
299}
300
301pub enum IndicesDiffItem<'a> {
303 CreateIndex(&'a Index),
305 DropIndex(&'a Index),
307 AlterIndex {
309 previous: &'a Index,
311 next: &'a Index,
313 },
314}
315
316#[cfg(test)]
317mod tests {
318 use crate::schema::db::{
319 Column, ColumnId, DiffContext, Index, IndexColumn, IndexId, IndexOp, IndexScope,
320 IndicesDiff, IndicesDiffItem, PrimaryKey, RenameHints, Schema, Table, TableId, Type,
321 };
322 use crate::stmt;
323
324 fn make_column(table_id: usize, index: usize, name: &str) -> Column {
325 Column {
326 id: ColumnId {
327 table: TableId(table_id),
328 index,
329 },
330 name: name.to_string(),
331 ty: stmt::Type::String,
332 storage_ty: Type::Text,
333 nullable: false,
334 primary_key: false,
335 auto_increment: false,
336 versionable: false,
337 }
338 }
339
340 fn make_index(
341 table_id: usize,
342 index: usize,
343 name: &str,
344 columns: Vec<(usize, IndexOp, IndexScope)>,
345 unique: bool,
346 ) -> Index {
347 Index {
348 id: IndexId {
349 table: TableId(table_id),
350 index,
351 },
352 name: name.to_string(),
353 on: TableId(table_id),
354 columns: columns
355 .into_iter()
356 .map(|(col_idx, op, scope)| IndexColumn {
357 column: ColumnId {
358 table: TableId(table_id),
359 index: col_idx,
360 },
361 op,
362 scope,
363 })
364 .collect(),
365 unique,
366 primary_key: false,
367 }
368 }
369
370 fn make_schema_with_indices(
371 table_id: usize,
372 columns: Vec<Column>,
373 indices: Vec<Index>,
374 ) -> Schema {
375 let mut schema = Schema::default();
376 schema.tables.push(Table {
377 id: TableId(table_id),
378 name: "test_table".to_string(),
379 columns,
380 primary_key: PrimaryKey {
381 columns: vec![],
382 index: IndexId {
383 table: TableId(table_id),
384 index: 0,
385 },
386 },
387 indices,
388 });
389 schema
390 }
391
392 #[test]
393 fn test_no_diff_same_indices() {
394 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
395
396 let from_indices = vec![make_index(
397 0,
398 0,
399 "idx_name",
400 vec![(1, IndexOp::Eq, IndexScope::Local)],
401 false,
402 )];
403 let to_indices = vec![make_index(
404 0,
405 0,
406 "idx_name",
407 vec![(1, IndexOp::Eq, IndexScope::Local)],
408 false,
409 )];
410
411 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
412 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
413 let hints = RenameHints::new();
414 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
415
416 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
417 assert!(diff.is_empty());
418 }
419
420 #[test]
421 fn test_create_index() {
422 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
423
424 let from_indices = vec![];
425 let to_indices = vec![make_index(
426 0,
427 0,
428 "idx_name",
429 vec![(1, IndexOp::Eq, IndexScope::Local)],
430 false,
431 )];
432
433 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
434 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
435 let hints = RenameHints::new();
436 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
437
438 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
439 assert_eq!(diff.items.len(), 1);
440 assert!(matches!(diff.items[0], IndicesDiffItem::CreateIndex(_)));
441 if let IndicesDiffItem::CreateIndex(idx) = diff.items[0] {
442 assert_eq!(idx.name, "idx_name");
443 }
444 }
445
446 #[test]
447 fn test_drop_index() {
448 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
449
450 let from_indices = vec![make_index(
451 0,
452 0,
453 "idx_name",
454 vec![(1, IndexOp::Eq, IndexScope::Local)],
455 false,
456 )];
457 let to_indices = vec![];
458
459 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
460 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
461 let hints = RenameHints::new();
462 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
463
464 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
465 assert_eq!(diff.items.len(), 1);
466 assert!(matches!(diff.items[0], IndicesDiffItem::DropIndex(_)));
467 if let IndicesDiffItem::DropIndex(idx) = diff.items[0] {
468 assert_eq!(idx.name, "idx_name");
469 }
470 }
471
472 #[test]
473 fn test_alter_index_unique() {
474 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
475
476 let from_indices = vec![make_index(
477 0,
478 0,
479 "idx_name",
480 vec![(1, IndexOp::Eq, IndexScope::Local)],
481 false,
482 )];
483 let to_indices = vec![make_index(
484 0,
485 0,
486 "idx_name",
487 vec![(1, IndexOp::Eq, IndexScope::Local)],
488 true, )];
490
491 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
492 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
493 let hints = RenameHints::new();
494 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
495
496 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
497 assert_eq!(diff.items.len(), 1);
498 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
499 }
500
501 #[test]
502 fn test_alter_index_columns() {
503 let columns = vec![
504 make_column(0, 0, "id"),
505 make_column(0, 1, "name"),
506 make_column(0, 2, "email"),
507 ];
508
509 let from_indices = vec![make_index(
510 0,
511 0,
512 "idx_name",
513 vec![(1, IndexOp::Eq, IndexScope::Local)],
514 false,
515 )];
516 let to_indices = vec![make_index(
517 0,
518 0,
519 "idx_name",
520 vec![
521 (1, IndexOp::Eq, IndexScope::Local),
522 (2, IndexOp::Eq, IndexScope::Local),
523 ],
524 false,
525 )];
526
527 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
528 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
529 let hints = RenameHints::new();
530 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
531
532 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
533 assert_eq!(diff.items.len(), 1);
534 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
535 }
536
537 #[test]
538 fn test_alter_index_op() {
539 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
540
541 let from_indices = vec![make_index(
542 0,
543 0,
544 "idx_name",
545 vec![(1, IndexOp::Eq, IndexScope::Local)],
546 false,
547 )];
548 let to_indices = vec![make_index(
549 0,
550 0,
551 "idx_name",
552 vec![(1, IndexOp::Sort(stmt::Direction::Asc), IndexScope::Local)],
553 false,
554 )];
555
556 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
557 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
558 let hints = RenameHints::new();
559 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
560
561 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
562 assert_eq!(diff.items.len(), 1);
563 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
564 }
565
566 #[test]
567 fn test_alter_index_scope() {
568 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
569
570 let from_indices = vec![make_index(
571 0,
572 0,
573 "idx_name",
574 vec![(1, IndexOp::Eq, IndexScope::Local)],
575 false,
576 )];
577 let to_indices = vec![make_index(
578 0,
579 0,
580 "idx_name",
581 vec![(1, IndexOp::Eq, IndexScope::Partition)],
582 false,
583 )];
584
585 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
586 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
587 let hints = RenameHints::new();
588 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
589
590 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
591 assert_eq!(diff.items.len(), 1);
592 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
593 }
594
595 #[test]
596 fn test_rename_index_with_hint() {
597 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
598
599 let from_indices = vec![make_index(
600 0,
601 0,
602 "old_idx_name",
603 vec![(1, IndexOp::Eq, IndexScope::Local)],
604 false,
605 )];
606 let to_indices = vec![make_index(
607 0,
608 0,
609 "new_idx_name",
610 vec![(1, IndexOp::Eq, IndexScope::Local)],
611 false,
612 )];
613
614 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
615 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
616
617 let mut hints = RenameHints::new();
618 hints.add_index_hint(
619 IndexId {
620 table: TableId(0),
621 index: 0,
622 },
623 IndexId {
624 table: TableId(0),
625 index: 0,
626 },
627 );
628 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
629
630 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
631 assert_eq!(diff.items.len(), 1);
632 assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
633 if let IndicesDiffItem::AlterIndex { previous, next } = diff.items[0] {
634 assert_eq!(previous.name, "old_idx_name");
635 assert_eq!(next.name, "new_idx_name");
636 }
637 }
638
639 #[test]
640 fn test_rename_index_without_hint_is_drop_and_create() {
641 let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
642
643 let from_indices = vec![make_index(
644 0,
645 0,
646 "old_idx_name",
647 vec![(1, IndexOp::Eq, IndexScope::Local)],
648 false,
649 )];
650 let to_indices = vec![make_index(
651 0,
652 0,
653 "new_idx_name",
654 vec![(1, IndexOp::Eq, IndexScope::Local)],
655 false,
656 )];
657
658 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
659 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
660 let hints = RenameHints::new();
661 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
662
663 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
664 assert_eq!(diff.items.len(), 2);
665
666 let has_drop = diff
667 .items
668 .iter()
669 .any(|item| matches!(item, IndicesDiffItem::DropIndex(_)));
670 let has_create = diff
671 .items
672 .iter()
673 .any(|item| matches!(item, IndicesDiffItem::CreateIndex(_)));
674 assert!(has_drop);
675 assert!(has_create);
676 }
677
678 #[test]
679 fn test_index_with_renamed_column() {
680 let from_columns = vec![make_column(0, 0, "id"), make_column(0, 1, "old_name")];
681 let to_columns = vec![make_column(0, 0, "id"), make_column(0, 1, "new_name")];
682
683 let from_indices = vec![make_index(
684 0,
685 0,
686 "idx_name",
687 vec![(1, IndexOp::Eq, IndexScope::Local)],
688 false,
689 )];
690 let to_indices = vec![make_index(
691 0,
692 0,
693 "idx_name",
694 vec![(1, IndexOp::Eq, IndexScope::Local)],
695 false,
696 )];
697
698 let from_schema = make_schema_with_indices(0, from_columns, from_indices.clone());
699 let to_schema = make_schema_with_indices(0, to_columns, to_indices.clone());
700
701 let mut hints = RenameHints::new();
702 hints.add_column_hint(
703 ColumnId {
704 table: TableId(0),
705 index: 1,
706 },
707 ColumnId {
708 table: TableId(0),
709 index: 1,
710 },
711 );
712 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
713
714 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
715 assert!(diff.is_empty());
717 }
718
719 #[cfg(feature = "serde")]
720 mod serde_tests {
721 use crate::schema::db::{
722 ColumnId, Index, IndexColumn, IndexId, IndexOp, IndexScope, TableId,
723 };
724
725 fn base_index() -> Index {
726 Index {
727 id: IndexId {
728 table: TableId(0),
729 index: 0,
730 },
731 name: "idx".to_string(),
732 on: TableId(0),
733 columns: vec![IndexColumn {
734 column: ColumnId {
735 table: TableId(0),
736 index: 0,
737 },
738 op: IndexOp::Eq,
739 scope: IndexScope::Local,
740 }],
741 unique: false,
742 primary_key: false,
743 }
744 }
745
746 #[test]
747 fn false_booleans_are_omitted() {
748 let toml = toml::to_string(&base_index()).unwrap();
749 assert!(!toml.contains("unique"), "toml: {toml}");
750 assert!(!toml.contains("primary_key"), "toml: {toml}");
751 }
752
753 #[test]
754 fn unique_true_is_included() {
755 let idx = Index {
756 unique: true,
757 ..base_index()
758 };
759 let toml = toml::to_string(&idx).unwrap();
760 assert!(toml.contains("unique = true"), "toml: {toml}");
761 }
762
763 #[test]
764 fn primary_key_true_is_included() {
765 let idx = Index {
766 primary_key: true,
767 ..base_index()
768 };
769 let toml = toml::to_string(&idx).unwrap();
770 assert!(toml.contains("primary_key = true"), "toml: {toml}");
771 }
772
773 #[test]
774 fn missing_bool_fields_deserialize_as_false() {
775 let toml = "name = \"idx\"\non = 0\n\n[id]\ntable = 0\nindex = 0\n\n[[columns]]\nop = \"Eq\"\nscope = \"Local\"\n\n[columns.column]\ntable = 0\nindex = 0\n";
776 let idx: Index = toml::from_str(toml).unwrap();
777 assert!(!idx.unique);
778 assert!(!idx.primary_key);
779 }
780
781 #[test]
782 fn round_trip_all_true() {
783 let original = Index {
784 unique: true,
785 primary_key: true,
786 ..base_index()
787 };
788 let decoded: Index = toml::from_str(&toml::to_string(&original).unwrap()).unwrap();
789 assert_eq!(decoded.unique, original.unique);
790 assert_eq!(decoded.primary_key, original.primary_key);
791 assert_eq!(decoded.name, original.name);
792 }
793 }
794
795 #[test]
796 fn test_multiple_operations() {
797 let columns = vec![
798 make_column(0, 0, "id"),
799 make_column(0, 1, "name"),
800 make_column(0, 2, "email"),
801 ];
802
803 let from_indices = vec![
804 make_index(
805 0,
806 0,
807 "idx_name",
808 vec![(1, IndexOp::Eq, IndexScope::Local)],
809 false,
810 ),
811 make_index(
812 0,
813 1,
814 "old_idx",
815 vec![(2, IndexOp::Eq, IndexScope::Local)],
816 false,
817 ),
818 make_index(
819 0,
820 2,
821 "idx_to_drop",
822 vec![(0, IndexOp::Eq, IndexScope::Local)],
823 false,
824 ),
825 ];
826 let to_indices = vec![
827 make_index(
828 0,
829 0,
830 "idx_name",
831 vec![(1, IndexOp::Eq, IndexScope::Local)],
832 true, ),
834 make_index(
835 0,
836 1,
837 "new_idx",
838 vec![(2, IndexOp::Eq, IndexScope::Local)],
839 false,
840 ),
841 make_index(
842 0,
843 2,
844 "idx_added",
845 vec![(1, IndexOp::Sort(stmt::Direction::Asc), IndexScope::Local)],
846 false,
847 ),
848 ];
849
850 let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
851 let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
852
853 let mut hints = RenameHints::new();
854 hints.add_index_hint(
855 IndexId {
856 table: TableId(0),
857 index: 1,
858 },
859 IndexId {
860 table: TableId(0),
861 index: 1,
862 },
863 );
864 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
865
866 let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
867 assert_eq!(diff.items.len(), 4);
869 }
870}