Skip to main content

toasty_core/schema/db/
index.rs

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