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 pub id: ColumnId,
15
16 pub name: String,
18
19 pub ty: stmt::Type,
21
22 pub storage_ty: Type,
24
25 pub nullable: bool,
27
28 pub primary_key: bool,
30
31 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, 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 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 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), make_column(0, 1, "new_name", Type::Text, false), make_column(0, 2, "added", Type::Integer(8), false), ];
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 assert_eq!(diff.items.len(), 4);
360 }
361}