toasty_core/schema/db/
index.rs

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/// A database index over one or more columns of a table.
11///
12/// Indices can be unique or non-unique, and can cover the primary key.
13/// Each indexed column specifies an [`IndexOp`] (equality or sort) and an
14/// [`IndexScope`] (partition or local).
15///
16/// # Examples
17///
18/// ```ignore
19/// use toasty_core::schema::db::{Index, IndexColumn, IndexId, IndexOp, IndexScope, ColumnId, TableId};
20///
21/// let index = Index {
22///     id: IndexId { table: TableId(0), index: 0 },
23///     name: "idx_users_email".to_string(),
24///     on: TableId(0),
25///     columns: vec![IndexColumn {
26///         column: ColumnId { table: TableId(0), index: 1 },
27///         op: IndexOp::Eq,
28///         scope: IndexScope::Local,
29///     }],
30///     unique: true,
31///     primary_key: false,
32/// };
33///
34/// assert!(index.unique);
35/// assert_eq!(index.columns.len(), 1);
36/// ```
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct Index {
40    /// Uniquely identifies the index within the schema
41    pub id: IndexId,
42
43    /// Index name is unique within the schema
44    pub name: String,
45
46    /// The table being indexed
47    pub on: TableId,
48
49    /// Fields included in the index.
50    pub columns: Vec<IndexColumn>,
51
52    /// When `true`, indexed entries are unique
53    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
54    pub unique: bool,
55
56    /// When `true`, the index indexes the model's primary key fields.
57    #[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/// Uniquely identifies an index within a schema.
67///
68/// Combines the [`TableId`] of the owning table with the index's positional
69/// offset in that table's index list.
70///
71/// # Examples
72///
73/// ```ignore
74/// use toasty_core::schema::db::{IndexId, TableId};
75///
76/// let id = IndexId { table: TableId(0), index: 1 };
77/// assert_eq!(id.index, 1);
78/// ```
79#[derive(Copy, Clone, Eq, PartialEq, Hash)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub struct IndexId {
82    /// The table this index belongs to.
83    pub table: TableId,
84    /// Zero-based position of this index in the table's index list.
85    pub index: usize,
86}
87
88/// A single column entry within an [`Index`].
89///
90/// Specifies which column is indexed, the comparison operation, and the scope
91/// (partition vs. local).
92///
93/// # Examples
94///
95/// ```ignore
96/// use toasty_core::schema::db::{IndexColumn, IndexOp, IndexScope, ColumnId, TableId};
97///
98/// let ic = IndexColumn {
99///     column: ColumnId { table: TableId(0), index: 0 },
100///     op: IndexOp::Eq,
101///     scope: IndexScope::Local,
102/// };
103///
104/// assert!(ic.scope.is_local());
105/// ```
106#[derive(Debug, Clone)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct IndexColumn {
109    /// The column being indexed
110    pub column: ColumnId,
111
112    /// The comparison operation used to index the column
113    pub op: IndexOp,
114
115    /// Scope of the index
116    pub scope: IndexScope,
117}
118
119/// The comparison operation used by an index column.
120///
121/// # Examples
122///
123/// ```ignore
124/// use toasty_core::schema::db::IndexOp;
125/// use toasty_core::stmt::Direction;
126///
127/// let op = IndexOp::Sort(Direction::Asc);
128/// assert!(matches!(op, IndexOp::Sort(_)));
129/// ```
130#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub enum IndexOp {
133    /// Equality lookup.
134    Eq,
135    /// Sorted scan in the given direction.
136    Sort(stmt::Direction),
137}
138
139/// Scope of an index column, relevant for distributed databases.
140///
141/// # Examples
142///
143/// ```ignore
144/// use toasty_core::schema::db::IndexScope;
145///
146/// let scope = IndexScope::Partition;
147/// assert!(scope.is_partition());
148/// assert!(!scope.is_local());
149/// ```
150#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub enum IndexScope {
153    /// The index column is used to partition rows across nodes of a distributed database.
154    Partition,
155
156    /// The index column is scoped to a physical node.
157    Local,
158}
159
160impl IndexColumn {
161    /// Returns the [`Column`] referenced by this index column.
162    pub fn table_column<'a>(&self, schema: &'a Schema) -> &'a Column {
163        schema.column(self.column)
164    }
165}
166
167impl IndexScope {
168    /// Returns `true` if this is the [`Partition`](IndexScope::Partition) scope.
169    pub fn is_partition(self) -> bool {
170        matches!(self, Self::Partition)
171    }
172
173    /// Returns `true` if this is the [`Local`](IndexScope::Local) scope.
174    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
194/// The set of differences between two index lists.
195///
196/// Computed by [`IndicesDiff::from`] and dereferences to
197/// `Vec<IndicesDiffItem>` for iteration.
198///
199/// # Examples
200///
201/// ```ignore
202/// use toasty_core::schema::db::{IndicesDiff, DiffContext, RenameHints, Schema};
203///
204/// let previous = Schema::default();
205/// let next = Schema::default();
206/// let hints = RenameHints::new();
207/// let cx = DiffContext::new(&previous, &next, &hints);
208/// let diff = IndicesDiff::from(&cx, &[], &[]);
209/// assert!(diff.is_empty());
210/// ```
211pub struct IndicesDiff<'a> {
212    items: Vec<IndicesDiffItem<'a>>,
213}
214
215impl<'a> IndicesDiff<'a> {
216    /// Computes the diff between two index slices.
217    ///
218    /// Uses [`DiffContext`] to resolve rename hints for both indices and columns.
219    /// Indices matched by name (or by rename hint) are compared; unmatched
220    /// indices in `previous` become drops, and unmatched indices in `next`
221    /// become creates.
222    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            // Check basic properties
225            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            // Check if index columns have changed
234            for (previous_col, next_col) in previous.columns.iter().zip(next.columns.iter()) {
235                // Check if op or scope changed
236                if previous_col.op != next_col.op || previous_col.scope != next_col.scope {
237                    return true;
238                }
239
240                // Check if the column changed (accounting for renames)
241                let columns_match =
242                    if let Some(renamed_to) = cx.rename_hints().get_column(previous_col.column) {
243                        // Column was renamed - check if it matches the target column
244                        renamed_to == next_col.column
245                    } else {
246                        // No rename hint - check if columns match by name
247                        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    /// Returns `true` if there are no index changes.
291    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
304/// A single change detected between two index lists.
305pub enum IndicesDiffItem<'a> {
306    /// A new index was created.
307    CreateIndex(&'a Index),
308    /// An existing index was dropped.
309    DropIndex(&'a Index),
310    /// An index was modified (name, columns, uniqueness, or other property changed).
311    AlterIndex {
312        /// The index definition before the change.
313        previous: &'a Index,
314        /// The index definition after the change.
315        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, // changed to unique
491        )];
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        // Index should remain unchanged when column is renamed with hint
718        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, // changed to unique
835            ),
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        // Should have: 1 alter (idx_name unique changed), 1 alter (renamed), 1 drop (idx_to_drop), 1 create (idx_added)
870        assert_eq!(diff.items.len(), 4);
871    }
872}