1use super::{DiffContext, TableId, Type, table};
2use crate::stmt;
3
4use hashbrown::{HashMap, HashSet};
5use std::{fmt, ops::Deref};
6
7#[derive(Debug, Clone, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct Column {
34 pub id: ColumnId,
36
37 pub name: String,
39
40 pub ty: stmt::Type,
42
43 pub storage_ty: Type,
45
46 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
48 pub nullable: bool,
49
50 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
52 pub primary_key: bool,
53
54 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
58 pub auto_increment: bool,
59
60 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
62 pub versionable: bool,
63}
64
65#[cfg(feature = "serde")]
66fn is_false(b: &bool) -> bool {
67 !*b
68}
69
70#[derive(PartialEq, Eq, Clone, Copy, Hash)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct ColumnId {
86 pub table: TableId,
88 pub index: usize,
90}
91
92impl ColumnId {
93 pub(crate) fn placeholder() -> Self {
94 Self {
95 table: table::TableId::placeholder(),
96 index: usize::MAX,
97 }
98 }
99}
100
101impl From<&Column> for ColumnId {
102 fn from(value: &Column) -> Self {
103 value.id
104 }
105}
106
107impl fmt::Debug for ColumnId {
108 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(fmt, "ColumnId({}/{})", self.table.0, self.index)
110 }
111}
112
113pub struct ColumnsDiff<'a> {
131 items: Vec<ColumnsDiffItem<'a>>,
132}
133
134impl<'a> ColumnsDiff<'a> {
135 pub fn from(cx: &DiffContext<'a>, previous: &'a [Column], next: &'a [Column]) -> Self {
141 fn has_diff(previous: &Column, next: &Column) -> bool {
142 previous.name != next.name
143 || previous.storage_ty != next.storage_ty
144 || previous.nullable != next.nullable
145 || previous.primary_key != next.primary_key
146 || previous.auto_increment != next.auto_increment
147 || previous.versionable != next.versionable
148 }
149
150 let mut items = vec![];
151 let mut add_ids: HashSet<_> = next.iter().map(|next| next.id).collect();
152
153 let next_map =
154 HashMap::<&str, &'a Column>::from_iter(next.iter().map(|to| (to.name.as_str(), to)));
155
156 for previous in previous {
157 let next = if let Some(next_id) = cx.rename_hints().get_column(previous.id) {
158 cx.next().column(next_id)
159 } else if let Some(next) = next_map.get(previous.name.as_str()) {
160 next
161 } else {
162 items.push(ColumnsDiffItem::DropColumn(previous));
163 continue;
164 };
165
166 add_ids.remove(&next.id);
167
168 if has_diff(previous, next) {
169 items.push(ColumnsDiffItem::AlterColumn { previous, next });
170 }
171 }
172
173 for column_id in add_ids {
174 items.push(ColumnsDiffItem::AddColumn(cx.next().column(column_id)));
175 }
176
177 Self { items }
178 }
179
180 pub const fn is_empty(&self) -> bool {
182 self.items.is_empty()
183 }
184}
185
186impl<'a> Deref for ColumnsDiff<'a> {
187 type Target = Vec<ColumnsDiffItem<'a>>;
188
189 fn deref(&self) -> &Self::Target {
190 &self.items
191 }
192}
193
194pub enum ColumnsDiffItem<'a> {
196 AddColumn(&'a Column),
198 DropColumn(&'a Column),
200 AlterColumn {
202 previous: &'a Column,
204 next: &'a Column,
206 },
207}
208
209#[cfg(test)]
210mod tests {
211 use crate::schema::db::{
212 Column, ColumnId, ColumnsDiff, ColumnsDiffItem, DiffContext, PrimaryKey, RenameHints,
213 Schema, Table, TableId, Type,
214 };
215 use crate::stmt;
216
217 fn make_column(
218 table_id: usize,
219 index: usize,
220 name: &str,
221 storage_ty: Type,
222 nullable: bool,
223 ) -> Column {
224 Column {
225 id: ColumnId {
226 table: TableId(table_id),
227 index,
228 },
229 name: name.to_string(),
230 ty: stmt::Type::String, storage_ty,
232 nullable,
233 primary_key: false,
234 auto_increment: false,
235 versionable: false,
236 }
237 }
238
239 fn make_schema_with_columns(table_id: usize, columns: Vec<Column>) -> Schema {
240 let mut schema = Schema::default();
241 schema.tables.push(Table {
242 id: TableId(table_id),
243 name: "test_table".to_string(),
244 columns,
245 primary_key: PrimaryKey {
246 columns: vec![],
247 index: super::super::IndexId {
248 table: TableId(table_id),
249 index: 0,
250 },
251 },
252 indices: vec![],
253 });
254 schema
255 }
256
257 #[test]
258 fn test_no_diff_same_columns() {
259 let from_cols = vec![
260 make_column(0, 0, "id", Type::Integer(8), false),
261 make_column(0, 1, "name", Type::Text, false),
262 ];
263 let to_cols = vec![
264 make_column(0, 0, "id", Type::Integer(8), false),
265 make_column(0, 1, "name", Type::Text, false),
266 ];
267
268 let from_schema = make_schema_with_columns(0, from_cols.clone());
269 let to_schema = make_schema_with_columns(0, to_cols.clone());
270 let hints = RenameHints::new();
271 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
272
273 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
274 assert!(diff.is_empty());
275 }
276
277 #[test]
278 fn test_add_column() {
279 let from_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
280 let to_cols = vec![
281 make_column(0, 0, "id", Type::Integer(8), false),
282 make_column(0, 1, "name", Type::Text, false),
283 ];
284
285 let from_schema = make_schema_with_columns(0, from_cols.clone());
286 let to_schema = make_schema_with_columns(0, to_cols.clone());
287 let hints = RenameHints::new();
288 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
289
290 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
291 assert_eq!(diff.items.len(), 1);
292 assert!(matches!(diff.items[0], ColumnsDiffItem::AddColumn(_)));
293 if let ColumnsDiffItem::AddColumn(col) = diff.items[0] {
294 assert_eq!(col.name, "name");
295 }
296 }
297
298 #[test]
299 fn test_drop_column() {
300 let from_cols = vec![
301 make_column(0, 0, "id", Type::Integer(8), false),
302 make_column(0, 1, "name", Type::Text, false),
303 ];
304 let to_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
305
306 let from_schema = make_schema_with_columns(0, from_cols.clone());
307 let to_schema = make_schema_with_columns(0, to_cols.clone());
308 let hints = RenameHints::new();
309 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
310
311 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
312 assert_eq!(diff.items.len(), 1);
313 assert!(matches!(diff.items[0], ColumnsDiffItem::DropColumn(_)));
314 if let ColumnsDiffItem::DropColumn(col) = diff.items[0] {
315 assert_eq!(col.name, "name");
316 }
317 }
318
319 #[test]
320 fn test_alter_column_type() {
321 let from_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
322 let to_cols = vec![make_column(0, 0, "id", Type::Text, false)];
323
324 let from_schema = make_schema_with_columns(0, from_cols.clone());
325 let to_schema = make_schema_with_columns(0, to_cols.clone());
326 let hints = RenameHints::new();
327 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
328
329 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
330 assert_eq!(diff.items.len(), 1);
331 assert!(matches!(diff.items[0], ColumnsDiffItem::AlterColumn { .. }));
332 }
333
334 #[test]
335 fn test_alter_column_nullable() {
336 let from_cols = vec![make_column(0, 0, "id", Type::Integer(8), false)];
337 let to_cols = vec![make_column(0, 0, "id", Type::Integer(8), true)];
338
339 let from_schema = make_schema_with_columns(0, from_cols.clone());
340 let to_schema = make_schema_with_columns(0, to_cols.clone());
341 let hints = RenameHints::new();
342 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
343
344 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
345 assert_eq!(diff.items.len(), 1);
346 assert!(matches!(diff.items[0], ColumnsDiffItem::AlterColumn { .. }));
347 }
348
349 #[test]
350 fn test_rename_column_with_hint() {
351 let from_cols = vec![make_column(0, 0, "old_name", Type::Text, false)];
353 let to_cols = vec![make_column(0, 0, "new_name", Type::Text, false)];
354
355 let from_schema = make_schema_with_columns(0, from_cols.clone());
356 let to_schema = make_schema_with_columns(0, to_cols.clone());
357
358 let mut hints = RenameHints::new();
359 hints.add_column_hint(
360 ColumnId {
361 table: TableId(0),
362 index: 0,
363 },
364 ColumnId {
365 table: TableId(0),
366 index: 0,
367 },
368 );
369 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
370
371 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
372 assert_eq!(diff.items.len(), 1);
373 assert!(matches!(diff.items[0], ColumnsDiffItem::AlterColumn { .. }));
374 if let ColumnsDiffItem::AlterColumn { previous, next } = diff.items[0] {
375 assert_eq!(previous.name, "old_name");
376 assert_eq!(next.name, "new_name");
377 }
378 }
379
380 #[test]
381 fn test_rename_column_without_hint_is_drop_and_add() {
382 let from_cols = vec![make_column(0, 0, "old_name", Type::Text, false)];
385 let to_cols = vec![make_column(0, 0, "new_name", Type::Text, false)];
386
387 let from_schema = make_schema_with_columns(0, from_cols.clone());
388 let to_schema = make_schema_with_columns(0, to_cols.clone());
389 let hints = RenameHints::new();
390 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
391
392 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
393 assert_eq!(diff.items.len(), 2);
394
395 let has_drop = diff
396 .items
397 .iter()
398 .any(|item| matches!(item, ColumnsDiffItem::DropColumn(_)));
399 let has_add = diff
400 .items
401 .iter()
402 .any(|item| matches!(item, ColumnsDiffItem::AddColumn(_)));
403 assert!(has_drop);
404 assert!(has_add);
405 }
406
407 #[cfg(feature = "serde")]
408 mod serde_tests {
409 use crate::schema::db::{Column, ColumnId, TableId, Type};
410 use crate::stmt;
411
412 fn base_column() -> Column {
413 Column {
414 id: ColumnId {
415 table: TableId(0),
416 index: 0,
417 },
418 name: "test".to_string(),
419 ty: stmt::Type::String,
420 storage_ty: Type::Text,
421 nullable: false,
422 primary_key: false,
423 auto_increment: false,
424 versionable: false,
425 }
426 }
427
428 #[test]
429 fn false_booleans_are_omitted() {
430 let toml = toml::to_string(&base_column()).unwrap();
431 assert!(!toml.contains("nullable"), "toml: {toml}");
432 assert!(!toml.contains("primary_key"), "toml: {toml}");
433 assert!(!toml.contains("auto_increment"), "toml: {toml}");
434 assert!(!toml.contains("versionable"), "toml: {toml}");
435 }
436
437 #[test]
438 fn nullable_true_is_included() {
439 let col = Column {
440 nullable: true,
441 ..base_column()
442 };
443 let toml = toml::to_string(&col).unwrap();
444 assert!(toml.contains("nullable = true"), "toml: {toml}");
445 }
446
447 #[test]
448 fn primary_key_true_is_included() {
449 let col = Column {
450 primary_key: true,
451 ..base_column()
452 };
453 let toml = toml::to_string(&col).unwrap();
454 assert!(toml.contains("primary_key = true"), "toml: {toml}");
455 }
456
457 #[test]
458 fn auto_increment_true_is_included() {
459 let col = Column {
460 auto_increment: true,
461 ..base_column()
462 };
463 let toml = toml::to_string(&col).unwrap();
464 assert!(toml.contains("auto_increment = true"), "toml: {toml}");
465 }
466
467 #[test]
468 fn missing_bool_fields_deserialize_as_false() {
469 let toml = "name = \"test\"\nty = \"String\"\nstorage_ty = \"Text\"\n\n[id]\ntable = 0\nindex = 0\n";
470 let col: Column = toml::from_str(toml).unwrap();
471 assert!(!col.nullable);
472 assert!(!col.primary_key);
473 assert!(!col.auto_increment);
474 assert!(!col.versionable);
475 }
476
477 #[test]
478 fn round_trip_all_true() {
479 let original = Column {
480 nullable: true,
481 primary_key: true,
482 auto_increment: true,
483 ..base_column()
484 };
485 let decoded: Column = toml::from_str(&toml::to_string(&original).unwrap()).unwrap();
486 assert_eq!(original, decoded);
487 }
488 }
489
490 #[test]
491 fn test_multiple_operations() {
492 let from_cols = vec![
493 make_column(0, 0, "id", Type::Integer(8), false),
494 make_column(0, 1, "old_name", Type::Text, false),
495 make_column(0, 2, "to_drop", Type::Text, false),
496 ];
497 let to_cols = vec![
498 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), ];
502
503 let from_schema = make_schema_with_columns(0, from_cols.clone());
504 let to_schema = make_schema_with_columns(0, to_cols.clone());
505
506 let mut hints = RenameHints::new();
507 hints.add_column_hint(
508 ColumnId {
509 table: TableId(0),
510 index: 1,
511 },
512 ColumnId {
513 table: TableId(0),
514 index: 1,
515 },
516 );
517 let cx = DiffContext::new(&from_schema, &to_schema, &hints);
518
519 let diff = ColumnsDiff::from(&cx, &from_cols, &to_cols);
520 assert_eq!(diff.items.len(), 4);
522 }
523}