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