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#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Index {
13    /// Uniquely identifies the index within the schema
14    pub id: IndexId,
15
16    /// Index name is unique within the schema
17    pub name: String,
18
19    /// The table being indexed
20    pub on: TableId,
21
22    /// Fields included in the index.
23    pub columns: Vec<IndexColumn>,
24
25    /// When `true`, indexed entries are unique
26    pub unique: bool,
27
28    /// When `true`, the index indexes the model's primary key fields.
29    pub primary_key: bool,
30}
31
32#[derive(Copy, Clone, Eq, PartialEq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct IndexId {
35    pub table: TableId,
36    pub index: usize,
37}
38
39#[derive(Debug, Clone)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct IndexColumn {
42    /// The column being indexed
43    pub column: ColumnId,
44
45    /// The comparison operation used to index the column
46    pub op: IndexOp,
47
48    /// Scope of the index
49    pub scope: IndexScope,
50}
51
52#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub enum IndexOp {
55    Eq,
56    Sort(stmt::Direction),
57}
58
59#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub enum IndexScope {
62    /// The index column is used to partition rows across nodes of a distributed database.
63    Partition,
64
65    /// The index column is scoped to a physical node.
66    Local,
67}
68
69impl IndexColumn {
70    pub fn table_column<'a>(&self, schema: &'a Schema) -> &'a Column {
71        schema.column(self.column)
72    }
73}
74
75impl IndexScope {
76    pub fn is_partition(self) -> bool {
77        matches!(self, Self::Partition)
78    }
79
80    pub fn is_local(self) -> bool {
81        matches!(self, Self::Local)
82    }
83}
84
85impl IndexId {
86    pub(crate) fn placeholder() -> Self {
87        Self {
88            table: TableId::placeholder(),
89            index: usize::MAX,
90        }
91    }
92}
93
94impl fmt::Debug for IndexId {
95    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(fmt, "IndexId({}/{})", self.table.0, self.index)
97    }
98}
99
100pub struct IndicesDiff<'a> {
101    items: Vec<IndicesDiffItem<'a>>,
102}
103
104impl<'a> IndicesDiff<'a> {
105    pub fn from(cx: &DiffContext<'a>, previous: &'a [Index], next: &'a [Index]) -> Self {
106        fn has_diff(cx: &DiffContext<'_>, previous: &Index, next: &Index) -> bool {
107            // Check basic properties
108            if previous.name != next.name
109                || previous.columns.len() != next.columns.len()
110                || previous.unique != next.unique
111                || previous.primary_key != next.primary_key
112            {
113                return true;
114            }
115
116            // Check if index columns have changed
117            for (previous_col, next_col) in previous.columns.iter().zip(next.columns.iter()) {
118                // Check if op or scope changed
119                if previous_col.op != next_col.op || previous_col.scope != next_col.scope {
120                    return true;
121                }
122
123                // Check if the column changed (accounting for renames)
124                let columns_match =
125                    if let Some(renamed_to) = cx.rename_hints().get_column(previous_col.column) {
126                        // Column was renamed - check if it matches the target column
127                        renamed_to == next_col.column
128                    } else {
129                        // No rename hint - check if columns match by name
130                        let previous_column = cx.previous().column(previous_col.column);
131                        let next_column = cx.next().column(next_col.column);
132                        previous_column.name == next_column.name
133                    };
134
135                if !columns_match {
136                    return true;
137                }
138            }
139
140            false
141        }
142
143        let mut items = vec![];
144        let mut create_ids: HashSet<_> = next.iter().map(|to| to.id).collect();
145
146        let next_map =
147            HashMap::<&str, &'a Index>::from_iter(next.iter().map(|to| (to.name.as_str(), to)));
148
149        for previous in previous {
150            let next = if let Some(next_id) = cx.rename_hints().get_index(previous.id) {
151                cx.next().index(next_id)
152            } else if let Some(next) = next_map.get(previous.name.as_str()) {
153                next
154            } else {
155                items.push(IndicesDiffItem::DropIndex(previous));
156                continue;
157            };
158
159            create_ids.remove(&next.id);
160
161            if has_diff(cx, previous, next) {
162                items.push(IndicesDiffItem::AlterIndex { previous, next });
163            }
164        }
165
166        for index_id in create_ids {
167            items.push(IndicesDiffItem::CreateIndex(cx.next().index(index_id)));
168        }
169
170        Self { items }
171    }
172
173    pub const fn is_empty(&self) -> bool {
174        self.items.is_empty()
175    }
176}
177
178impl<'a> Deref for IndicesDiff<'a> {
179    type Target = Vec<IndicesDiffItem<'a>>;
180
181    fn deref(&self) -> &Self::Target {
182        &self.items
183    }
184}
185
186pub enum IndicesDiffItem<'a> {
187    CreateIndex(&'a Index),
188    DropIndex(&'a Index),
189    AlterIndex {
190        previous: &'a Index,
191        next: &'a Index,
192    },
193}
194
195#[cfg(test)]
196mod tests {
197    use crate::schema::db::{
198        Column, ColumnId, DiffContext, Index, IndexColumn, IndexId, IndexOp, IndexScope,
199        IndicesDiff, IndicesDiffItem, PrimaryKey, RenameHints, Schema, Table, TableId, Type,
200    };
201    use crate::stmt;
202
203    fn make_column(table_id: usize, index: usize, name: &str) -> Column {
204        Column {
205            id: ColumnId {
206                table: TableId(table_id),
207                index,
208            },
209            name: name.to_string(),
210            ty: stmt::Type::String,
211            storage_ty: Type::Text,
212            nullable: false,
213            primary_key: false,
214            auto_increment: false,
215        }
216    }
217
218    fn make_index(
219        table_id: usize,
220        index: usize,
221        name: &str,
222        columns: Vec<(usize, IndexOp, IndexScope)>,
223        unique: bool,
224    ) -> Index {
225        Index {
226            id: IndexId {
227                table: TableId(table_id),
228                index,
229            },
230            name: name.to_string(),
231            on: TableId(table_id),
232            columns: columns
233                .into_iter()
234                .map(|(col_idx, op, scope)| IndexColumn {
235                    column: ColumnId {
236                        table: TableId(table_id),
237                        index: col_idx,
238                    },
239                    op,
240                    scope,
241                })
242                .collect(),
243            unique,
244            primary_key: false,
245        }
246    }
247
248    fn make_schema_with_indices(
249        table_id: usize,
250        columns: Vec<Column>,
251        indices: Vec<Index>,
252    ) -> Schema {
253        let mut schema = Schema::default();
254        schema.tables.push(Table {
255            id: TableId(table_id),
256            name: "test_table".to_string(),
257            columns,
258            primary_key: PrimaryKey {
259                columns: vec![],
260                index: IndexId {
261                    table: TableId(table_id),
262                    index: 0,
263                },
264            },
265            indices,
266        });
267        schema
268    }
269
270    #[test]
271    fn test_no_diff_same_indices() {
272        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
273
274        let from_indices = vec![make_index(
275            0,
276            0,
277            "idx_name",
278            vec![(1, IndexOp::Eq, IndexScope::Local)],
279            false,
280        )];
281        let to_indices = vec![make_index(
282            0,
283            0,
284            "idx_name",
285            vec![(1, IndexOp::Eq, IndexScope::Local)],
286            false,
287        )];
288
289        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
290        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
291        let hints = RenameHints::new();
292        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
293
294        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
295        assert!(diff.is_empty());
296    }
297
298    #[test]
299    fn test_create_index() {
300        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
301
302        let from_indices = vec![];
303        let to_indices = vec![make_index(
304            0,
305            0,
306            "idx_name",
307            vec![(1, IndexOp::Eq, IndexScope::Local)],
308            false,
309        )];
310
311        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
312        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
313        let hints = RenameHints::new();
314        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
315
316        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
317        assert_eq!(diff.items.len(), 1);
318        assert!(matches!(diff.items[0], IndicesDiffItem::CreateIndex(_)));
319        if let IndicesDiffItem::CreateIndex(idx) = diff.items[0] {
320            assert_eq!(idx.name, "idx_name");
321        }
322    }
323
324    #[test]
325    fn test_drop_index() {
326        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
327
328        let from_indices = vec![make_index(
329            0,
330            0,
331            "idx_name",
332            vec![(1, IndexOp::Eq, IndexScope::Local)],
333            false,
334        )];
335        let to_indices = vec![];
336
337        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
338        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
339        let hints = RenameHints::new();
340        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
341
342        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
343        assert_eq!(diff.items.len(), 1);
344        assert!(matches!(diff.items[0], IndicesDiffItem::DropIndex(_)));
345        if let IndicesDiffItem::DropIndex(idx) = diff.items[0] {
346            assert_eq!(idx.name, "idx_name");
347        }
348    }
349
350    #[test]
351    fn test_alter_index_unique() {
352        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
353
354        let from_indices = vec![make_index(
355            0,
356            0,
357            "idx_name",
358            vec![(1, IndexOp::Eq, IndexScope::Local)],
359            false,
360        )];
361        let to_indices = vec![make_index(
362            0,
363            0,
364            "idx_name",
365            vec![(1, IndexOp::Eq, IndexScope::Local)],
366            true, // changed to unique
367        )];
368
369        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
370        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
371        let hints = RenameHints::new();
372        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
373
374        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
375        assert_eq!(diff.items.len(), 1);
376        assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
377    }
378
379    #[test]
380    fn test_alter_index_columns() {
381        let columns = vec![
382            make_column(0, 0, "id"),
383            make_column(0, 1, "name"),
384            make_column(0, 2, "email"),
385        ];
386
387        let from_indices = vec![make_index(
388            0,
389            0,
390            "idx_name",
391            vec![(1, IndexOp::Eq, IndexScope::Local)],
392            false,
393        )];
394        let to_indices = vec![make_index(
395            0,
396            0,
397            "idx_name",
398            vec![
399                (1, IndexOp::Eq, IndexScope::Local),
400                (2, IndexOp::Eq, IndexScope::Local),
401            ],
402            false,
403        )];
404
405        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
406        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
407        let hints = RenameHints::new();
408        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
409
410        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
411        assert_eq!(diff.items.len(), 1);
412        assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
413    }
414
415    #[test]
416    fn test_alter_index_op() {
417        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
418
419        let from_indices = vec![make_index(
420            0,
421            0,
422            "idx_name",
423            vec![(1, IndexOp::Eq, IndexScope::Local)],
424            false,
425        )];
426        let to_indices = vec![make_index(
427            0,
428            0,
429            "idx_name",
430            vec![(1, IndexOp::Sort(stmt::Direction::Asc), IndexScope::Local)],
431            false,
432        )];
433
434        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
435        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
436        let hints = RenameHints::new();
437        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
438
439        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
440        assert_eq!(diff.items.len(), 1);
441        assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
442    }
443
444    #[test]
445    fn test_alter_index_scope() {
446        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
447
448        let from_indices = vec![make_index(
449            0,
450            0,
451            "idx_name",
452            vec![(1, IndexOp::Eq, IndexScope::Local)],
453            false,
454        )];
455        let to_indices = vec![make_index(
456            0,
457            0,
458            "idx_name",
459            vec![(1, IndexOp::Eq, IndexScope::Partition)],
460            false,
461        )];
462
463        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
464        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
465        let hints = RenameHints::new();
466        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
467
468        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
469        assert_eq!(diff.items.len(), 1);
470        assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
471    }
472
473    #[test]
474    fn test_rename_index_with_hint() {
475        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
476
477        let from_indices = vec![make_index(
478            0,
479            0,
480            "old_idx_name",
481            vec![(1, IndexOp::Eq, IndexScope::Local)],
482            false,
483        )];
484        let to_indices = vec![make_index(
485            0,
486            0,
487            "new_idx_name",
488            vec![(1, IndexOp::Eq, IndexScope::Local)],
489            false,
490        )];
491
492        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
493        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
494
495        let mut hints = RenameHints::new();
496        hints.add_index_hint(
497            IndexId {
498                table: TableId(0),
499                index: 0,
500            },
501            IndexId {
502                table: TableId(0),
503                index: 0,
504            },
505        );
506        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
507
508        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
509        assert_eq!(diff.items.len(), 1);
510        assert!(matches!(diff.items[0], IndicesDiffItem::AlterIndex { .. }));
511        if let IndicesDiffItem::AlterIndex { previous, next } = diff.items[0] {
512            assert_eq!(previous.name, "old_idx_name");
513            assert_eq!(next.name, "new_idx_name");
514        }
515    }
516
517    #[test]
518    fn test_rename_index_without_hint_is_drop_and_create() {
519        let columns = vec![make_column(0, 0, "id"), make_column(0, 1, "name")];
520
521        let from_indices = vec![make_index(
522            0,
523            0,
524            "old_idx_name",
525            vec![(1, IndexOp::Eq, IndexScope::Local)],
526            false,
527        )];
528        let to_indices = vec![make_index(
529            0,
530            0,
531            "new_idx_name",
532            vec![(1, IndexOp::Eq, IndexScope::Local)],
533            false,
534        )];
535
536        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
537        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
538        let hints = RenameHints::new();
539        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
540
541        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
542        assert_eq!(diff.items.len(), 2);
543
544        let has_drop = diff
545            .items
546            .iter()
547            .any(|item| matches!(item, IndicesDiffItem::DropIndex(_)));
548        let has_create = diff
549            .items
550            .iter()
551            .any(|item| matches!(item, IndicesDiffItem::CreateIndex(_)));
552        assert!(has_drop);
553        assert!(has_create);
554    }
555
556    #[test]
557    fn test_index_with_renamed_column() {
558        let from_columns = vec![make_column(0, 0, "id"), make_column(0, 1, "old_name")];
559        let to_columns = vec![make_column(0, 0, "id"), make_column(0, 1, "new_name")];
560
561        let from_indices = vec![make_index(
562            0,
563            0,
564            "idx_name",
565            vec![(1, IndexOp::Eq, IndexScope::Local)],
566            false,
567        )];
568        let to_indices = vec![make_index(
569            0,
570            0,
571            "idx_name",
572            vec![(1, IndexOp::Eq, IndexScope::Local)],
573            false,
574        )];
575
576        let from_schema = make_schema_with_indices(0, from_columns, from_indices.clone());
577        let to_schema = make_schema_with_indices(0, to_columns, to_indices.clone());
578
579        let mut hints = RenameHints::new();
580        hints.add_column_hint(
581            ColumnId {
582                table: TableId(0),
583                index: 1,
584            },
585            ColumnId {
586                table: TableId(0),
587                index: 1,
588            },
589        );
590        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
591
592        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
593        // Index should remain unchanged when column is renamed with hint
594        assert!(diff.is_empty());
595    }
596
597    #[test]
598    fn test_multiple_operations() {
599        let columns = vec![
600            make_column(0, 0, "id"),
601            make_column(0, 1, "name"),
602            make_column(0, 2, "email"),
603        ];
604
605        let from_indices = vec![
606            make_index(
607                0,
608                0,
609                "idx_name",
610                vec![(1, IndexOp::Eq, IndexScope::Local)],
611                false,
612            ),
613            make_index(
614                0,
615                1,
616                "old_idx",
617                vec![(2, IndexOp::Eq, IndexScope::Local)],
618                false,
619            ),
620            make_index(
621                0,
622                2,
623                "idx_to_drop",
624                vec![(0, IndexOp::Eq, IndexScope::Local)],
625                false,
626            ),
627        ];
628        let to_indices = vec![
629            make_index(
630                0,
631                0,
632                "idx_name",
633                vec![(1, IndexOp::Eq, IndexScope::Local)],
634                true, // changed to unique
635            ),
636            make_index(
637                0,
638                1,
639                "new_idx",
640                vec![(2, IndexOp::Eq, IndexScope::Local)],
641                false,
642            ),
643            make_index(
644                0,
645                2,
646                "idx_added",
647                vec![(1, IndexOp::Sort(stmt::Direction::Asc), IndexScope::Local)],
648                false,
649            ),
650        ];
651
652        let from_schema = make_schema_with_indices(0, columns.clone(), from_indices.clone());
653        let to_schema = make_schema_with_indices(0, columns, to_indices.clone());
654
655        let mut hints = RenameHints::new();
656        hints.add_index_hint(
657            IndexId {
658                table: TableId(0),
659                index: 1,
660            },
661            IndexId {
662                table: TableId(0),
663                index: 1,
664            },
665        );
666        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
667
668        let diff = IndicesDiff::from(&cx, &from_indices, &to_indices);
669        // Should have: 1 alter (idx_name unique changed), 1 alter (renamed), 1 drop (idx_to_drop), 1 create (idx_added)
670        assert_eq!(diff.items.len(), 4);
671    }
672}