toasty_core/schema/db/
table.rs

1use super::{Column, ColumnId, Index, IndexId, PrimaryKey};
2use crate::{
3    schema::db::{column::ColumnsDiff, diff::DiffContext, index::IndicesDiff},
4    stmt,
5};
6
7use std::{
8    collections::{HashMap, HashSet},
9    fmt,
10    ops::Deref,
11};
12
13/// A database table
14#[derive(Debug, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Table {
17    /// Uniquely identifies a table
18    pub id: TableId,
19
20    /// Name of the table
21    pub name: String,
22
23    /// The table's columns
24    pub columns: Vec<Column>,
25
26    pub primary_key: PrimaryKey,
27
28    pub indices: Vec<Index>,
29}
30
31/// Uniquely identifies a table
32#[derive(PartialEq, Eq, Clone, Copy, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct TableId(pub usize);
35
36impl Table {
37    pub fn primary_key_column(&self, i: usize) -> &Column {
38        &self.columns[self.primary_key.columns[i].index]
39    }
40
41    pub fn primary_key_columns(&self) -> impl ExactSizeIterator<Item = &Column> + '_ {
42        self.primary_key
43            .columns
44            .iter()
45            .map(|column_id| &self.columns[column_id.index])
46    }
47
48    pub fn column(&self, id: impl Into<ColumnId>) -> &Column {
49        &self.columns[id.into().index]
50    }
51
52    /// The path must have exactly one step
53    pub fn resolve(&self, projection: &stmt::Projection) -> &Column {
54        let [first, rest @ ..] = projection.as_slice() else {
55            panic!("need at most one path step")
56        };
57        assert!(rest.is_empty());
58
59        &self.columns[*first]
60    }
61
62    pub(crate) fn new(id: TableId, name: String) -> Self {
63        Self {
64            id,
65            name,
66            columns: vec![],
67            primary_key: PrimaryKey {
68                columns: vec![],
69                index: IndexId {
70                    table: id,
71                    index: 0,
72                },
73            },
74            indices: vec![],
75        }
76    }
77}
78
79impl TableId {
80    pub(crate) fn placeholder() -> Self {
81        Self(usize::MAX)
82    }
83}
84
85impl fmt::Debug for TableId {
86    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(fmt, "TableId({})", self.0)
88    }
89}
90
91pub struct TablesDiff<'a> {
92    items: Vec<TablesDiffItem<'a>>,
93}
94
95impl<'a> TablesDiff<'a> {
96    pub fn from(cx: &DiffContext<'a>, previous: &'a [Table], next: &'a [Table]) -> Self {
97        let mut items = vec![];
98        let mut create_ids: HashSet<_> = next.iter().map(|next| next.id).collect();
99
100        let next_map = HashMap::<&str, &'a Table>::from_iter(
101            next.iter().map(|next| (next.name.as_str(), next)),
102        );
103
104        for previous in previous {
105            let next = if let Some(next_id) = cx.rename_hints().get_table(previous.id) {
106                cx.next().table(next_id)
107            } else if let Some(to) = next_map.get(previous.name.as_str()) {
108                to
109            } else {
110                items.push(TablesDiffItem::DropTable(previous));
111                continue;
112            };
113
114            create_ids.remove(&next.id);
115
116            let columns = ColumnsDiff::from(cx, &previous.columns, &next.columns);
117            let indices = IndicesDiff::from(cx, &previous.indices, &next.indices);
118            if previous.name != next.name || !columns.is_empty() || !indices.is_empty() {
119                items.push(TablesDiffItem::AlterTable {
120                    previous,
121                    next,
122                    columns,
123                    indices,
124                });
125            }
126        }
127
128        for table_id in create_ids {
129            items.push(TablesDiffItem::CreateTable(cx.next().table(table_id)));
130        }
131
132        Self { items }
133    }
134}
135
136impl<'a> Deref for TablesDiff<'a> {
137    type Target = Vec<TablesDiffItem<'a>>;
138
139    fn deref(&self) -> &Self::Target {
140        &self.items
141    }
142}
143
144pub enum TablesDiffItem<'a> {
145    CreateTable(&'a Table),
146    DropTable(&'a Table),
147    AlterTable {
148        previous: &'a Table,
149        next: &'a Table,
150        columns: ColumnsDiff<'a>,
151        indices: IndicesDiff<'a>,
152    },
153}
154
155#[cfg(test)]
156mod tests {
157    use crate::schema::db::{
158        Column, ColumnId, DiffContext, IndexId, PrimaryKey, RenameHints, Schema, Table, TableId,
159        TablesDiff, TablesDiffItem, Type,
160    };
161    use crate::stmt;
162
163    fn make_table(id: usize, name: &str, num_columns: usize) -> Table {
164        let mut columns = vec![];
165        for i in 0..num_columns {
166            columns.push(Column {
167                id: ColumnId {
168                    table: TableId(id),
169                    index: i,
170                },
171                name: format!("col{}", i),
172                ty: stmt::Type::String,
173                storage_ty: Type::Text,
174                nullable: false,
175                primary_key: false,
176                auto_increment: false,
177            });
178        }
179
180        Table {
181            id: TableId(id),
182            name: name.to_string(),
183            columns,
184            primary_key: PrimaryKey {
185                columns: vec![],
186                index: IndexId {
187                    table: TableId(id),
188                    index: 0,
189                },
190            },
191            indices: vec![],
192        }
193    }
194
195    fn make_schema(tables: Vec<Table>) -> Schema {
196        Schema { tables }
197    }
198
199    #[test]
200    fn test_no_diff_same_tables() {
201        let from_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
202        let to_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
203
204        let from_schema = make_schema(from_tables.clone());
205        let to_schema = make_schema(to_tables.clone());
206        let hints = RenameHints::new();
207        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
208
209        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
210        assert_eq!(diff.items.len(), 0);
211    }
212
213    #[test]
214    fn test_create_table() {
215        let from_tables = vec![make_table(0, "users", 2)];
216        let to_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
217
218        let from_schema = make_schema(from_tables.clone());
219        let to_schema = make_schema(to_tables.clone());
220        let hints = RenameHints::new();
221        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
222
223        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
224        assert_eq!(diff.items.len(), 1);
225        assert!(matches!(diff.items[0], TablesDiffItem::CreateTable(_)));
226        if let TablesDiffItem::CreateTable(table) = diff.items[0] {
227            assert_eq!(table.name, "posts");
228        }
229    }
230
231    #[test]
232    fn test_drop_table() {
233        let from_tables = vec![make_table(0, "users", 2), make_table(1, "posts", 3)];
234        let to_tables = vec![make_table(0, "users", 2)];
235
236        let from_schema = make_schema(from_tables.clone());
237        let to_schema = make_schema(to_tables.clone());
238        let hints = RenameHints::new();
239        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
240
241        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
242        assert_eq!(diff.items.len(), 1);
243        assert!(matches!(diff.items[0], TablesDiffItem::DropTable(_)));
244        if let TablesDiffItem::DropTable(table) = diff.items[0] {
245            assert_eq!(table.name, "posts");
246        }
247    }
248
249    #[test]
250    fn test_rename_table_with_hint() {
251        let from_tables = vec![make_table(0, "old_users", 2)];
252        let to_tables = vec![make_table(0, "new_users", 2)];
253
254        let from_schema = make_schema(from_tables.clone());
255        let to_schema = make_schema(to_tables.clone());
256
257        let mut hints = RenameHints::new();
258        hints.add_table_hint(TableId(0), TableId(0));
259        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
260
261        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
262        assert_eq!(diff.items.len(), 1);
263        assert!(matches!(diff.items[0], TablesDiffItem::AlterTable { .. }));
264        if let TablesDiffItem::AlterTable { previous, next, .. } = &diff.items[0] {
265            assert_eq!(previous.name, "old_users");
266            assert_eq!(next.name, "new_users");
267        }
268    }
269
270    #[test]
271    fn test_rename_table_without_hint_is_drop_and_create() {
272        let from_tables = vec![make_table(0, "old_users", 2)];
273        let to_tables = vec![make_table(0, "new_users", 2)];
274
275        let from_schema = make_schema(from_tables.clone());
276        let to_schema = make_schema(to_tables.clone());
277        let hints = RenameHints::new();
278        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
279
280        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
281        assert_eq!(diff.items.len(), 2);
282
283        let has_drop = diff
284            .items
285            .iter()
286            .any(|item| matches!(item, TablesDiffItem::DropTable(_)));
287        let has_create = diff
288            .items
289            .iter()
290            .any(|item| matches!(item, TablesDiffItem::CreateTable(_)));
291        assert!(has_drop);
292        assert!(has_create);
293    }
294
295    #[test]
296    fn test_alter_table_column_change() {
297        let from_tables = vec![make_table(0, "users", 2)];
298        let to_tables = vec![make_table(0, "users", 3)]; // added a column
299
300        let from_schema = make_schema(from_tables.clone());
301        let to_schema = make_schema(to_tables.clone());
302        let hints = RenameHints::new();
303        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
304
305        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
306        assert_eq!(diff.items.len(), 1);
307        assert!(matches!(diff.items[0], TablesDiffItem::AlterTable { .. }));
308    }
309
310    #[test]
311    fn test_multiple_operations() {
312        let from_tables = vec![
313            make_table(0, "users", 2),
314            make_table(1, "posts", 3),
315            make_table(2, "old_table", 1),
316        ];
317        let to_tables = vec![
318            make_table(0, "users", 3),     // added column
319            make_table(1, "new_posts", 3), // renamed
320            make_table(2, "comments", 2),  // new table (reused ID 2)
321        ];
322
323        let from_schema = make_schema(from_tables.clone());
324        let to_schema = make_schema(to_tables.clone());
325
326        let mut hints = RenameHints::new();
327        hints.add_table_hint(TableId(1), TableId(1));
328        let cx = DiffContext::new(&from_schema, &to_schema, &hints);
329
330        let diff = TablesDiff::from(&cx, &from_tables, &to_tables);
331        // Should have: 1 alter (users added column), 1 alter (posts renamed), 1 drop (old_table), 1 create (comments)
332        assert_eq!(diff.items.len(), 4);
333    }
334}