1use crate::types::*;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum SchemaOperation {
11 CreateTable(TableSchema),
13 DropTable(String),
15 RenameTable { from: String, to: String },
17 AddColumn { table: String, column: ColumnSchema },
19 DropColumn { table: String, column: String },
21 RenameColumn { table: String, from: String, to: String },
23 AlterColumn {
25 table: String,
26 column: String,
27 changes: ColumnChanges,
28 },
29 CreateIndex { table: String, index: IndexSchema },
31 DropIndex { table: String, name: String },
33 AddForeignKey {
35 table: String,
36 foreign_key: ForeignKeySchema,
37 },
38 DropForeignKey { table: String, name: String },
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Default)]
44pub struct ColumnChanges {
45 pub column_type: Option<ColumnType>,
47 pub nullable: Option<bool>,
49 pub default: Option<Option<String>>,
51 pub max_length: Option<Option<u32>>,
53 pub unique: Option<bool>,
55}
56
57impl ColumnChanges {
58 pub fn is_empty(&self) -> bool {
60 self.column_type.is_none()
61 && self.nullable.is_none()
62 && self.default.is_none()
63 && self.max_length.is_none()
64 && self.unique.is_none()
65 }
66}
67
68pub fn generate_diff(current: &[TableSchema], target: &[TableSchema]) -> Vec<SchemaOperation> {
72 let mut operations = Vec::new();
73
74 let current_tables: std::collections::HashMap<&str, &TableSchema> =
75 current.iter().map(|t| (t.name.as_str(), t)).collect();
76 let target_tables: std::collections::HashMap<&str, &TableSchema> =
77 target.iter().map(|t| (t.name.as_str(), t)).collect();
78
79 for (name, schema) in &target_tables {
81 if !current_tables.contains_key(name) {
82 operations.push(SchemaOperation::CreateTable((*schema).clone()));
83 }
84 }
85
86 for name in current_tables.keys() {
88 if !target_tables.contains_key(name) {
89 operations.push(SchemaOperation::DropTable((*name).to_string()));
90 }
91 }
92
93 for (name, target_table) in &target_tables {
95 if let Some(current_table) = current_tables.get(name) {
96 operations.extend(diff_table(current_table, target_table));
97 }
98 }
99
100 order_operations(operations)
102}
103
104fn diff_table(current: &TableSchema, target: &TableSchema) -> Vec<SchemaOperation> {
106 let mut ops = Vec::new();
107 let table = ¤t.name;
108
109 let current_cols: std::collections::HashMap<&str, &ColumnSchema> =
110 current.columns.iter().map(|c| (c.name.as_str(), c)).collect();
111 let target_cols: std::collections::HashMap<&str, &ColumnSchema> =
112 target.columns.iter().map(|c| (c.name.as_str(), c)).collect();
113
114 let mut renamed_from: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
116 for (name, col) in &target_cols {
117 if let Some(ref from) = col.renamed_from {
118 renamed_from.insert(from.as_str(), name);
119 }
120 }
121
122 for (name, col) in &target_cols {
124 if col.dropped {
125 ops.push(SchemaOperation::DropColumn {
126 table: table.clone(),
127 column: name.to_string(),
128 });
129 }
130 }
131
132 for (from, to) in &renamed_from {
134 if current_cols.contains_key(from) {
135 ops.push(SchemaOperation::RenameColumn {
136 table: table.clone(),
137 from: (*from).to_string(),
138 to: (*to).to_string(),
139 });
140 }
141 }
142
143 for (name, col) in &target_cols {
145 if col.dropped {
146 continue;
147 }
148
149 if col.renamed_from.is_some() {
151 continue;
152 }
153
154 if !current_cols.contains_key(name) {
155 ops.push(SchemaOperation::AddColumn {
156 table: table.clone(),
157 column: (*col).clone(),
158 });
159 }
160 }
161
162 for name in current_cols.keys() {
164 if !target_cols.contains_key(name) && !renamed_from.contains_key(name) {
165 let explicitly_dropped = target_cols.values().any(|c| c.dropped && c.name == *name);
167
168 if !explicitly_dropped {
169 ops.push(SchemaOperation::DropColumn {
170 table: table.clone(),
171 column: (*name).to_string(),
172 });
173 }
174 }
175 }
176
177 for (name, target_col) in &target_cols {
179 if target_col.dropped || target_col.renamed_from.is_some() {
180 continue;
181 }
182
183 let current_col = if let Some(ref from) = target_col.renamed_from {
185 current_cols.get(from.as_str())
186 } else {
187 current_cols.get(name)
188 };
189
190 if let Some(current_col) = current_col {
191 if let Some(changes) = diff_column(current_col, target_col) {
192 ops.push(SchemaOperation::AlterColumn {
193 table: table.clone(),
194 column: name.to_string(),
195 changes,
196 });
197 }
198 }
199 }
200
201 ops.extend(diff_indexes(table, ¤t.indexes, &target.indexes));
203
204 ops.extend(diff_foreign_keys(table, ¤t.foreign_keys, &target.foreign_keys));
206
207 ops
208}
209
210fn diff_column(current: &ColumnSchema, target: &ColumnSchema) -> Option<ColumnChanges> {
212 let mut changes = ColumnChanges::default();
213
214 if current.column_type != target.column_type {
215 changes.column_type = Some(target.column_type.clone());
216 }
217
218 if current.nullable != target.nullable {
219 changes.nullable = Some(target.nullable);
220 }
221
222 if current.default != target.default {
223 changes.default = Some(target.default.clone());
224 }
225
226 if current.max_length != target.max_length {
227 changes.max_length = Some(target.max_length);
228 }
229
230 if current.unique != target.unique {
231 changes.unique = Some(target.unique);
232 }
233
234 if changes.is_empty() {
235 None
236 } else {
237 Some(changes)
238 }
239}
240
241fn diff_indexes(
243 table: &str,
244 current: &[IndexSchema],
245 target: &[IndexSchema],
246) -> Vec<SchemaOperation> {
247 let mut ops = Vec::new();
248
249 let current_by_name: std::collections::HashMap<&str, &IndexSchema> =
250 current.iter().map(|i| (i.name.as_str(), i)).collect();
251 let target_by_name: std::collections::HashMap<&str, &IndexSchema> =
252 target.iter().map(|i| (i.name.as_str(), i)).collect();
253
254 for (name, idx) in &target_by_name {
256 if !current_by_name.contains_key(name) {
257 ops.push(SchemaOperation::CreateIndex {
258 table: table.to_string(),
259 index: (*idx).clone(),
260 });
261 }
262 }
263
264 for name in current_by_name.keys() {
266 if !target_by_name.contains_key(name) {
267 ops.push(SchemaOperation::DropIndex {
268 table: table.to_string(),
269 name: (*name).to_string(),
270 });
271 }
272 }
273
274 for (name, target_idx) in &target_by_name {
276 if let Some(current_idx) = current_by_name.get(name) {
277 if current_idx != target_idx {
278 ops.push(SchemaOperation::DropIndex {
279 table: table.to_string(),
280 name: (*name).to_string(),
281 });
282 ops.push(SchemaOperation::CreateIndex {
283 table: table.to_string(),
284 index: (*target_idx).clone(),
285 });
286 }
287 }
288 }
289
290 ops
291}
292
293fn diff_foreign_keys(
295 table: &str,
296 current: &[ForeignKeySchema],
297 target: &[ForeignKeySchema],
298) -> Vec<SchemaOperation> {
299 let mut ops = Vec::new();
300
301 let current_by_col: std::collections::HashMap<&str, &ForeignKeySchema> =
303 current.iter().map(|fk| (fk.column.as_str(), fk)).collect();
304 let target_by_col: std::collections::HashMap<&str, &ForeignKeySchema> =
305 target.iter().map(|fk| (fk.column.as_str(), fk)).collect();
306
307 for (col, fk) in &target_by_col {
309 if !current_by_col.contains_key(col) {
310 ops.push(SchemaOperation::AddForeignKey {
311 table: table.to_string(),
312 foreign_key: (*fk).clone(),
313 });
314 }
315 }
316
317 for (col, fk) in ¤t_by_col {
319 if !target_by_col.contains_key(col) {
320 let name = fk.name.clone().unwrap_or_else(|| format!("fk_{table}_{col}"));
321 ops.push(SchemaOperation::DropForeignKey { table: table.to_string(), name });
322 }
323 }
324
325 for (col, target_fk) in &target_by_col {
327 if let Some(current_fk) = current_by_col.get(col) {
328 if current_fk != target_fk {
329 let name = current_fk.name.clone().unwrap_or_else(|| format!("fk_{table}_{col}"));
330 ops.push(SchemaOperation::DropForeignKey { table: table.to_string(), name });
331 ops.push(SchemaOperation::AddForeignKey {
332 table: table.to_string(),
333 foreign_key: (*target_fk).clone(),
334 });
335 }
336 }
337 }
338
339 ops
340}
341
342fn order_operations(mut ops: Vec<SchemaOperation>) -> Vec<SchemaOperation> {
356 ops.sort_by_key(|op| match op {
357 SchemaOperation::DropForeignKey { .. } => 0,
358 SchemaOperation::DropIndex { .. } => 1,
359 SchemaOperation::DropColumn { .. } => 2,
360 SchemaOperation::DropTable(_) => 3,
361 SchemaOperation::CreateTable(_) => 4,
362 SchemaOperation::AddColumn { .. } => 5,
363 SchemaOperation::RenameColumn { .. } => 6,
364 SchemaOperation::RenameTable { .. } => 6,
365 SchemaOperation::AlterColumn { .. } => 7,
366 SchemaOperation::CreateIndex { .. } => 8,
367 SchemaOperation::AddForeignKey { .. } => 9,
368 });
369
370 ops
371}
372
373pub fn resolve_delta(base: &TableSchema, delta: &TableSchema) -> TableSchema {
378 let mut result = base.clone();
379 result.migration_id = delta.migration_id.clone();
380
381 for delta_col in &delta.columns {
383 if delta_col.dropped {
384 result.columns.retain(|c| c.name != delta_col.name);
386 continue;
387 }
388
389 if let Some(ref from) = delta_col.renamed_from {
390 if let Some(col) = result.columns.iter_mut().find(|c| c.name == *from) {
392 col.name = delta_col.name.clone();
393 if delta_col.max_length.is_some() {
395 col.max_length = delta_col.max_length;
396 }
397 if delta_col.unique {
398 col.unique = true;
399 }
400 if delta_col.indexed {
401 col.indexed = true;
402 }
403 }
404 continue;
405 }
406
407 if let Some(existing) = result.columns.iter_mut().find(|c| c.name == delta_col.name) {
409 *existing = delta_col.clone();
411 } else {
412 result.columns.push(delta_col.clone());
414 }
415 }
416
417 for idx in &delta.indexes {
419 if !result.indexes.iter().any(|i| i.name == idx.name) {
420 result.indexes.push(idx.clone());
421 }
422 }
423
424 for fk in &delta.foreign_keys {
426 if !result.foreign_keys.iter().any(|f| f.column == fk.column) {
427 result.foreign_keys.push(fk.clone());
428 }
429 }
430
431 result
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 fn make_table(name: &str, columns: Vec<ColumnSchema>) -> TableSchema {
439 let mut table = TableSchema::new(name);
440 for col in columns {
441 if col.primary_key {
442 table.primary_key.push(col.name.clone());
443 }
444 table.columns.push(col);
445 }
446 table
447 }
448
449 #[test]
450 fn test_diff_new_table() {
451 let current: Vec<TableSchema> = vec![];
452 let target = vec![make_table(
453 "books",
454 vec![
455 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
456 ColumnSchema::new("title", ColumnType::String(Some(200))),
457 ],
458 )];
459
460 let ops = generate_diff(¤t, &target);
461
462 assert_eq!(ops.len(), 1);
463 assert!(matches!(&ops[0], SchemaOperation::CreateTable(t) if t.name == "books"));
464 }
465
466 #[test]
467 fn test_diff_drop_table() {
468 let current = vec![make_table(
469 "books",
470 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
471 )];
472 let target: Vec<TableSchema> = vec![];
473
474 let ops = generate_diff(¤t, &target);
475
476 assert_eq!(ops.len(), 1);
477 assert!(matches!(&ops[0], SchemaOperation::DropTable(name) if name == "books"));
478 }
479
480 #[test]
481 fn test_diff_add_column() {
482 let current = vec![make_table(
483 "books",
484 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
485 )];
486 let target = vec![make_table(
487 "books",
488 vec![
489 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
490 ColumnSchema::new("title", ColumnType::String(Some(200))),
491 ],
492 )];
493
494 let ops = generate_diff(¤t, &target);
495
496 assert_eq!(ops.len(), 1);
497 assert!(matches!(
498 &ops[0],
499 SchemaOperation::AddColumn { table, column }
500 if table == "books" && column.name == "title"
501 ));
502 }
503
504 #[test]
505 fn test_diff_drop_column() {
506 let current = vec![make_table(
507 "books",
508 vec![
509 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
510 ColumnSchema::new("title", ColumnType::String(Some(200))),
511 ],
512 )];
513 let target = vec![make_table(
514 "books",
515 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
516 )];
517
518 let ops = generate_diff(¤t, &target);
519
520 assert_eq!(ops.len(), 1);
521 assert!(matches!(
522 &ops[0],
523 SchemaOperation::DropColumn { table, column }
524 if table == "books" && column == "title"
525 ));
526 }
527
528 #[test]
529 fn test_diff_rename_column() {
530 let current = vec![make_table(
531 "books",
532 vec![
533 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
534 ColumnSchema::new("title", ColumnType::String(Some(200))),
535 ],
536 )];
537
538 let mut name_col = ColumnSchema::new("name", ColumnType::String(Some(200)));
539 name_col.renamed_from = Some("title".to_string());
540
541 let target = vec![make_table(
542 "books",
543 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true), name_col],
544 )];
545
546 let ops = generate_diff(¤t, &target);
547
548 assert!(ops.iter().any(|op| matches!(
549 op,
550 SchemaOperation::RenameColumn { table, from, to }
551 if table == "books" && from == "title" && to == "name"
552 )));
553 }
554
555 #[test]
556 fn test_resolve_delta_add_column() {
557 let base = make_table(
558 "books",
559 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
560 );
561
562 let mut delta = TableSchema::new("books");
563 delta.migration_id = Some("002".to_string());
564 delta.columns.push(ColumnSchema::new("title", ColumnType::String(Some(200))));
565
566 let result = resolve_delta(&base, &delta);
567
568 assert_eq!(result.columns.len(), 2);
569 assert!(result.find_column("id").is_some());
570 assert!(result.find_column("title").is_some());
571 assert_eq!(result.migration_id, Some("002".to_string()));
572 }
573
574 #[test]
575 fn test_resolve_delta_drop_column() {
576 let base = make_table(
577 "books",
578 vec![
579 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
580 ColumnSchema::new("legacy", ColumnType::String(None)),
581 ],
582 );
583
584 let mut delta = TableSchema::new("books");
585 let mut drop_col = ColumnSchema::new("legacy", ColumnType::String(None));
586 drop_col.dropped = true;
587 delta.columns.push(drop_col);
588
589 let result = resolve_delta(&base, &delta);
590
591 assert_eq!(result.columns.len(), 1);
592 assert!(result.find_column("id").is_some());
593 assert!(result.find_column("legacy").is_none());
594 }
595
596 #[test]
597 fn test_operation_ordering() {
598 let ops = vec![
599 SchemaOperation::AddColumn {
600 table: "t".to_string(),
601 column: ColumnSchema::new("c", ColumnType::Integer),
602 },
603 SchemaOperation::DropForeignKey {
604 table: "t".to_string(),
605 name: "fk".to_string(),
606 },
607 SchemaOperation::CreateTable(TableSchema::new("new")),
608 SchemaOperation::DropTable("old".to_string()),
609 ];
610
611 let ordered = order_operations(ops);
612
613 assert!(matches!(&ordered[0], SchemaOperation::DropForeignKey { .. }));
615 assert!(matches!(&ordered[1], SchemaOperation::DropTable(_)));
617 assert!(matches!(&ordered[2], SchemaOperation::CreateTable(_)));
618 assert!(matches!(&ordered[3], SchemaOperation::AddColumn { .. }));
620 }
621
622 #[test]
623 fn test_diff_alter_column_type() {
624 let current = vec![make_table(
625 "books",
626 vec![
627 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
628 ColumnSchema::new("price", ColumnType::Integer),
629 ],
630 )];
631 let target = vec![make_table(
632 "books",
633 vec![
634 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
635 ColumnSchema::new("price", ColumnType::Double),
636 ],
637 )];
638
639 let ops = generate_diff(¤t, &target);
640
641 assert_eq!(ops.len(), 1);
642 assert!(matches!(
643 &ops[0],
644 SchemaOperation::AlterColumn { table, column, changes }
645 if table == "books" && column == "price" && changes.column_type.is_some()
646 ));
647 }
648
649 #[test]
650 fn test_diff_multiple_tables() {
651 let current: Vec<TableSchema> = vec![];
652 let target = vec![
653 make_table(
654 "authors",
655 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
656 ),
657 make_table(
658 "books",
659 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
660 ),
661 ];
662
663 let ops = generate_diff(¤t, &target);
664
665 assert_eq!(ops.len(), 2);
666 let table_names: Vec<_> = ops
667 .iter()
668 .filter_map(|op| {
669 if let SchemaOperation::CreateTable(t) = op {
670 Some(t.name.as_str())
671 } else {
672 None
673 }
674 })
675 .collect();
676 assert!(table_names.contains(&"authors"));
677 assert!(table_names.contains(&"books"));
678 }
679
680 #[test]
681 fn test_diff_no_changes() {
682 let schema = vec![make_table(
683 "books",
684 vec![
685 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
686 ColumnSchema::new("title", ColumnType::String(Some(200))),
687 ],
688 )];
689
690 let ops = generate_diff(&schema, &schema);
691
692 assert!(ops.is_empty());
693 }
694
695 #[test]
696 fn test_resolve_delta_rename_column() {
697 let base = make_table(
698 "authors",
699 vec![
700 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
701 ColumnSchema::new("name", ColumnType::String(Some(100))),
702 ],
703 );
704
705 let mut delta = TableSchema::new("authors");
706 let mut renamed_col = ColumnSchema::new("full_name", ColumnType::String(Some(100)));
707 renamed_col.renamed_from = Some("name".to_string());
708 delta.columns.push(renamed_col);
709
710 let result = resolve_delta(&base, &delta);
711
712 assert_eq!(result.columns.len(), 2);
713 assert!(result.find_column("id").is_some());
714 assert!(result.find_column("full_name").is_some());
715 assert!(result.find_column("name").is_none());
716 }
717
718 #[test]
719 fn test_resolve_delta_modify_existing_column() {
720 let base = make_table(
721 "books",
722 vec![
723 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
724 ColumnSchema::new("title", ColumnType::String(Some(100))),
725 ],
726 );
727
728 let mut delta = TableSchema::new("books");
729 let mut modified_col = ColumnSchema::new("title", ColumnType::String(Some(500)));
730 modified_col.unique = true;
731 modified_col.max_length = Some(500);
732 delta.columns.push(modified_col);
733
734 let result = resolve_delta(&base, &delta);
735
736 assert_eq!(result.columns.len(), 2);
737 let title_col = result.find_column("title").unwrap();
738 assert!(title_col.unique);
740 assert_eq!(title_col.column_type, ColumnType::String(Some(500)));
741 }
742
743 #[test]
744 fn test_diff_add_index() {
745 let current = vec![make_table(
746 "books",
747 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
748 )];
749
750 let mut target_table = make_table(
751 "books",
752 vec![ColumnSchema::new("id", ColumnType::Integer).primary_key(true)],
753 );
754 target_table
755 .indexes
756 .push(crate::types::IndexSchema::new("idx_books_title", vec!["title".to_string()]));
757 let target = vec![target_table];
758
759 let ops = generate_diff(¤t, &target);
760
761 assert!(ops.iter().any(|op| matches!(op, SchemaOperation::CreateIndex { .. })));
762 }
763
764 #[test]
765 fn test_diff_add_foreign_key() {
766 let current = vec![make_table(
767 "books",
768 vec![
769 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
770 ColumnSchema::new("author_id", ColumnType::Integer),
771 ],
772 )];
773
774 let mut target_table = make_table(
775 "books",
776 vec![
777 ColumnSchema::new("id", ColumnType::Integer).primary_key(true),
778 ColumnSchema::new("author_id", ColumnType::Integer),
779 ],
780 );
781 target_table.foreign_keys.push(crate::types::ForeignKeySchema::new(
782 "author_id",
783 "authors",
784 "id",
785 ));
786 let target = vec![target_table];
787
788 let ops = generate_diff(¤t, &target);
789
790 assert!(ops.iter().any(|op| matches!(op, SchemaOperation::AddForeignKey { .. })));
791 }
792}