toasty_core/schema/db/
column.rs

1use super::{table, DiffContext, TableId, Type};
2use crate::stmt;
3
4use std::{
5    collections::{HashMap, HashSet},
6    fmt,
7    ops::Deref,
8};
9
10#[derive(Debug, Clone, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Column {
13    /// Uniquely identifies the column in the schema.
14    pub id: ColumnId,
15
16    /// The name of the column in the database.
17    pub name: String,
18
19    /// The column type, from Toasty's point of view.
20    pub ty: stmt::Type,
21
22    /// The database storage type of the column.
23    pub storage_ty: Type,
24
25    /// Whether or not the column is nullable
26    pub nullable: bool,
27
28    /// True if the column is part of the table's primary key
29    pub primary_key: bool,
30
31    /// True if the column is an integer that should be auto-incremented
32    /// with each insertion of a new row. This should be false if a `storage_ty`
33    /// of type `Serial` is used.
34    pub auto_increment: bool,
35}
36
37#[derive(PartialEq, Eq, Clone, Copy, Hash)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct ColumnId {
40    pub table: TableId,
41    pub index: usize,
42}
43
44impl ColumnId {
45    pub(crate) fn placeholder() -> Self {
46        Self {
47            table: table::TableId::placeholder(),
48            index: usize::MAX,
49        }
50    }
51}
52
53impl From<&Column> for ColumnId {
54    fn from(value: &Column) -> Self {
55        value.id
56    }
57}
58
59impl fmt::Debug for ColumnId {
60    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(fmt, "ColumnId({}/{})", self.table.0, self.index)
62    }
63}
64
65pub struct ColumnsDiff<'a> {
66    items: Vec<ColumnsDiffItem<'a>>,
67}
68
69impl<'a> ColumnsDiff<'a> {
70    pub fn from(cx: &DiffContext<'a>, previous: &'a [Column], next: &'a [Column]) -> Self {
71        fn has_diff(previous: &Column, next: &Column) -> bool {
72            previous.name != next.name
73                || previous.storage_ty != next.storage_ty
74                || previous.nullable != next.nullable
75                || previous.primary_key != next.primary_key
76                || previous.auto_increment != next.auto_increment
77        }
78
79        let mut items = vec![];
80        let mut add_ids: HashSet<_> = next.iter().map(|next| next.id).collect();
81
82        let next_map =
83            HashMap::<&str, &'a Column>::from_iter(next.iter().map(|to| (to.name.as_str(), to)));
84
85        for previous in previous {
86            let next = if let Some(next_id) = cx.rename_hints().get_column(previous.id) {
87                cx.next().column(next_id)
88            } else if let Some(next) = next_map.get(previous.name.as_str()) {
89                next
90            } else {
91                items.push(ColumnsDiffItem::DropColumn(previous));
92                continue;
93            };
94
95            add_ids.remove(&next.id);
96
97            if has_diff(previous, next) {
98                items.push(ColumnsDiffItem::AlterColumn { previous, next });
99            }
100        }
101
102        for column_id in add_ids {
103            items.push(ColumnsDiffItem::AddColumn(cx.next().column(column_id)));
104        }
105
106        Self { items }
107    }
108
109    pub const fn is_empty(&self) -> bool {
110        self.items.is_empty()
111    }
112}
113
114impl<'a> Deref for ColumnsDiff<'a> {
115    type Target = Vec<ColumnsDiffItem<'a>>;
116
117    fn deref(&self) -> &Self::Target {
118        &self.items
119    }
120}
121
122pub enum ColumnsDiffItem<'a> {
123    AddColumn(&'a Column),
124    DropColumn(&'a Column),
125    AlterColumn {
126        previous: &'a Column,
127        next: &'a Column,
128    },
129}
130
131#[cfg(test)]
132mod tests {
133    use crate::schema::db::{
134        Column, ColumnId, ColumnsDiff, ColumnsDiffItem, DiffContext, PrimaryKey, RenameHints,
135        Schema, Table, TableId, Type,
136    };
137    use crate::stmt;
138
139    fn make_column(
140        table_id: usize,
141        index: usize,
142        name: &str,
143        storage_ty: Type,
144        nullable: bool,
145    ) -> Column {
146        Column {
147            id: ColumnId {
148                table: TableId(table_id),
149                index,
150            },
151            name: name.to_string(),
152            ty: stmt::Type::String, // Simplified for tests
153            storage_ty,
154            nullable,
155            primary_key: false,
156            auto_increment: false,
157        }
158    }
159
160    fn make_schema_with_columns(table_id: usize, columns: Vec<Column>) -> Schema {
161        let mut schema = Schema::default();
162        schema.tables.push(Table {
163            id: TableId(table_id),
164            name: "test_table".to_string(),
165            columns,
166            primary_key: PrimaryKey {
167                columns: vec![],
168                index: super::super::IndexId {
169                    table: TableId(table_id),
170                    index: 0,
171                },
172            },
173            indices: vec![],
174        });
175        schema
176    }
177
178    #[test]
179    fn test_no_diff_same_columns() {
180        let from_cols = vec![
181            make_column(0, 0, "id", Type::Integer(8), false),
182            make_column(0, 1, "name", Type::Text, false),
183        ];
184        let to_cols = vec![
185            make_column(0, 0, "id", Type::Integer(8), false),
186            make_column(0, 1, "name", Type::Text, false),
187        ];
188
189        let from_schema = make_schema_with_columns(0, from_cols.clone());
190        let to_schema = make_schema_with_columns(0, to_cols.clone());
191        let hints = RenameHints::new();
192        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
193
194        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
195        assert!(diff.is_empty());
196    }
197
198    #[test]
199    fn test_add_column() {
200        let from_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
201        let to_cols = vec![
202            make_column(0, 0, "id", Type::Integer(8), false),
203            make_column(0, 1, "name", Type::Text, false),
204        ];
205
206        let from_schema = make_schema_with_columns(0, from_cols.clone());
207        let to_schema = make_schema_with_columns(0, to_cols.clone());
208        let hints = RenameHints::new();
209        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
210
211        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
212        assert_eq!(diff.items.len(), 1);
213        assert!(matches!(diff.items[0], ColumnsDiffItem::AddColumn(_)));
214        if let ColumnsDiffItem::AddColumn(col) = diff.items[0] {
215            assert_eq!(col.name, "name");
216        }
217    }
218
219    #[test]
220    fn test_drop_column() {
221        let from_cols = vec![
222            make_column(0, 0, "id", Type::Integer(8), false),
223            make_column(0, 1, "name", Type::Text, false),
224        ];
225        let to_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
226
227        let from_schema = make_schema_with_columns(0, from_cols.clone());
228        let to_schema = make_schema_with_columns(0, to_cols.clone());
229        let hints = RenameHints::new();
230        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
231
232        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
233        assert_eq!(diff.items.len(), 1);
234        assert!(matches!(diff.items[0], ColumnsDiffItem::DropColumn(_)));
235        if let ColumnsDiffItem::DropColumn(col) = diff.items[0] {
236            assert_eq!(col.name, "name");
237        }
238    }
239
240    #[test]
241    fn test_alter_column_type() {
242        let from_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
243        let to_cols = vec![make_column(0, 0, "id", Type::Text, false)];
244
245        let from_schema = make_schema_with_columns(0, from_cols.clone());
246        let to_schema = make_schema_with_columns(0, to_cols.clone());
247        let hints = RenameHints::new();
248        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
249
250        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
251        assert_eq!(diff.items.len(), 1);
252        assert!(matches!(diff.items[0], ColumnsDiffItem::AlterColumn { .. }));
253    }
254
255    #[test]
256    fn test_alter_column_nullable() {
257        let from_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
258        let to_cols = vec![make_column(0, 0, "id", Type::Integer(8), true)];
259
260        let from_schema = make_schema_with_columns(0, from_cols.clone());
261        let to_schema = make_schema_with_columns(0, to_cols.clone());
262        let hints = RenameHints::new();
263        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
264
265        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
266        assert_eq!(diff.items.len(), 1);
267        assert!(matches!(diff.items[0], ColumnsDiffItem::AlterColumn { .. }));
268    }
269
270    #[test]
271    fn test_rename_column_with_hint() {
272        // Column renamed from "old_name" to "new_name"
273        let from_cols = vec![make_column(0, 0, "old_name", Type::Text, false)];
274        let to_cols = vec![make_column(0, 0, "new_name", Type::Text, false)];
275
276        let from_schema = make_schema_with_columns(0, from_cols.clone());
277        let to_schema = make_schema_with_columns(0, to_cols.clone());
278
279        let mut hints = RenameHints::new();
280        hints.add_column_hint(
281            ColumnId {
282                table: TableId(0),
283                index: 0,
284            },
285            ColumnId {
286                table: TableId(0),
287                index: 0,
288            },
289        );
290        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
291
292        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
293        assert_eq!(diff.items.len(), 1);
294        assert!(matches!(diff.items[0], ColumnsDiffItem::AlterColumn { .. }));
295        if let ColumnsDiffItem::AlterColumn { previous, next } = diff.items[0] {
296            assert_eq!(previous.name, "old_name");
297            assert_eq!(next.name, "new_name");
298        }
299    }
300
301    #[test]
302    fn test_rename_column_without_hint_is_drop_and_add() {
303        // Column renamed from "old_name" to "new_name", but no hint provided
304        // Should be treated as drop + add
305        let from_cols = vec![make_column(0, 0, "old_name", Type::Text, false)];
306        let to_cols = vec![make_column(0, 0, "new_name", Type::Text, false)];
307
308        let from_schema = make_schema_with_columns(0, from_cols.clone());
309        let to_schema = make_schema_with_columns(0, to_cols.clone());
310        let hints = RenameHints::new();
311        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
312
313        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
314        assert_eq!(diff.items.len(), 2);
315
316        let has_drop = diff
317            .items
318            .iter()
319            .any(|item| matches!(item, ColumnsDiffItem::DropColumn(_)));
320        let has_add = diff
321            .items
322            .iter()
323            .any(|item| matches!(item, ColumnsDiffItem::AddColumn(_)));
324        assert!(has_drop);
325        assert!(has_add);
326    }
327
328    #[test]
329    fn test_multiple_operations() {
330        let from_cols = vec![
331            make_column(0, 0, "id", Type::Integer(8), false),
332            make_column(0, 1, "old_name", Type::Text, false),
333            make_column(0, 2, "to_drop", Type::Text, false),
334        ];
335        let to_cols = vec![
336            make_column(0, 0, "id", Type::Text, false), // type changed
337            make_column(0, 1, "new_name", Type::Text, false), // renamed
338            make_column(0, 2, "added", Type::Integer(8), false), // new column
339        ];
340
341        let from_schema = make_schema_with_columns(0, from_cols.clone());
342        let to_schema = make_schema_with_columns(0, to_cols.clone());
343
344        let mut hints = RenameHints::new();
345        hints.add_column_hint(
346            ColumnId {
347                table: TableId(0),
348                index: 1,
349            },
350            ColumnId {
351                table: TableId(0),
352                index: 1,
353            },
354        );
355        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
356
357        let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
358        // Should have: 1 alter (id type changed), 1 alter (renamed), 1 drop (to_drop), 1 add (added)
359        assert_eq!(diff.items.len(), 4);
360    }
361}