Skip to main content

toasty_core/schema/diff/
column.rs

1use super::Context;
2use crate::schema::db;
3
4use hashbrown::{HashMap, HashSet};
5
6/// A single column-level change between two table versions.
7///
8/// Computed by [`Column::diff`].
9pub enum Column<'a> {
10    /// A new column was added.
11    Add(&'a db::Column),
12    /// An existing column was removed.
13    Drop(&'a db::Column),
14    /// A column was modified (name, type, nullability, or other property changed).
15    Alter {
16        /// The column definition before the change.
17        previous: &'a db::Column,
18        /// The column definition after the change.
19        next: &'a db::Column,
20    },
21}
22
23impl<'a> Column<'a> {
24    /// Computes the diff between two column slices.
25    ///
26    /// Uses [`Context`] to resolve rename hints. Columns matched by name (or
27    /// by rename hint) are compared field-by-field; unmatched columns in
28    /// `previous` become drops, and unmatched columns in `next` become adds.
29    ///
30    /// # Examples
31    ///
32    /// ```ignore
33    /// use toasty_core::schema::{db, diff};
34    ///
35    /// let previous = db::Schema::default();
36    /// let next = db::Schema::default();
37    /// let hints = diff::RenameHints::new();
38    /// let cx = diff::Context::new(&previous, &next, &hints);
39    /// assert!(diff::Column::diff(&cx, &[], &[]).is_empty());
40    /// ```
41    pub fn diff(cx: &Context<'a>, previous: &'a [db::Column], next: &'a [db::Column]) -> Vec<Self> {
42        fn has_diff(previous: &db::Column, next: &db::Column) -> bool {
43            previous.name != next.name
44                || previous.storage_ty != next.storage_ty
45                || previous.nullable != next.nullable
46                || previous.primary_key != next.primary_key
47                || previous.auto_increment != next.auto_increment
48                || previous.versionable != next.versionable
49        }
50
51        let mut changes = vec![];
52        let mut add_ids: HashSet<_> = next.iter().map(|next| next.id).collect();
53
54        let next_map = HashMap::<&str, &'a db::Column>::from_iter(
55            next.iter().map(|to| (to.name.as_str(), to)),
56        );
57
58        for previous in previous {
59            let next = if let Some(next_id) = cx.rename_hints().get_column(previous.id) {
60                cx.next().column(next_id)
61            } else if let Some(next) = next_map.get(previous.name.as_str()) {
62                next
63            } else {
64                changes.push(Self::Drop(previous));
65                continue;
66            };
67
68            add_ids.remove(&next.id);
69
70            if has_diff(previous, next) {
71                changes.push(Self::Alter { previous, next });
72            }
73        }
74
75        for column_id in add_ids {
76            changes.push(Self::Add(cx.next().column(column_id)));
77        }
78
79        changes
80    }
81}