1use super::SQLiteSnapshot;
7use super::collection::{DiffType, EntityDiff, SQLiteDDL, diff_ddl};
8use super::ddl::SqliteEntity;
9use super::statements::{
10 AddColumnStatement, CreateIndexStatement, CreateTableStatement, CreateViewStatement,
11 DropColumnStatement, DropIndexStatement, DropTableStatement, DropViewStatement, JsonStatement,
12 RecreateTableStatement, RenameColumnStatement, RenameTableStatement, TableFull, from_json,
13};
14use crate::traits::EntityKind;
15use std::collections::{BTreeMap, BTreeSet, HashSet};
16
17pub use super::collection::{DiffType as SchemaDiffType, EntityDiff as SchemaEntityDiff};
19
20#[derive(Debug, Clone, Default)]
22pub struct SchemaDiff {
23 pub diffs: Vec<EntityDiff>,
25}
26
27impl SchemaDiff {
28 #[must_use]
30 pub const fn has_changes(&self) -> bool {
31 !self.diffs.is_empty()
32 }
33
34 #[must_use]
36 pub const fn is_empty(&self) -> bool {
37 self.diffs.is_empty()
38 }
39
40 #[must_use]
42 pub fn created(&self) -> Vec<&EntityDiff> {
43 self.diffs
44 .iter()
45 .filter(|d| d.diff_type == DiffType::Create)
46 .collect()
47 }
48
49 #[must_use]
51 pub fn dropped(&self) -> Vec<&EntityDiff> {
52 self.diffs
53 .iter()
54 .filter(|d| d.diff_type == DiffType::Drop)
55 .collect()
56 }
57
58 #[must_use]
60 pub fn altered(&self) -> Vec<&EntityDiff> {
61 self.diffs
62 .iter()
63 .filter(|d| d.diff_type == DiffType::Alter)
64 .collect()
65 }
66
67 #[must_use]
69 pub fn by_kind(&self, kind: EntityKind) -> Vec<&EntityDiff> {
70 self.diffs.iter().filter(|d| d.kind == kind).collect()
71 }
72
73 #[must_use]
75 pub fn created_tables(&self) -> Vec<&EntityDiff> {
76 self.diffs
77 .iter()
78 .filter(|d| d.diff_type == DiffType::Create && d.kind == EntityKind::Table)
79 .collect()
80 }
81
82 #[must_use]
84 pub fn dropped_tables(&self) -> Vec<&EntityDiff> {
85 self.diffs
86 .iter()
87 .filter(|d| d.diff_type == DiffType::Drop && d.kind == EntityKind::Table)
88 .collect()
89 }
90}
91
92#[must_use]
94pub fn diff_snapshots(prev: &SQLiteSnapshot, cur: &SQLiteSnapshot) -> SchemaDiff {
95 let prev_ddl = SQLiteDDL::from_entities(prev.ddl.clone());
96 let cur_ddl = SQLiteDDL::from_entities(cur.ddl.clone());
97
98 SchemaDiff {
99 diffs: diff_ddl(&prev_ddl, &cur_ddl),
100 }
101}
102
103#[must_use]
105pub fn diff_collections(prev: &SQLiteDDL, cur: &SQLiteDDL) -> SchemaDiff {
106 SchemaDiff {
107 diffs: diff_ddl(prev, cur),
108 }
109}
110
111#[derive(Debug, Clone)]
117pub struct TableRename {
118 pub from: String,
119 pub to: String,
120}
121
122#[derive(Debug, Clone)]
124pub struct ColumnRename {
125 pub table: String,
126 pub from: String,
127 pub to: String,
128}
129
130#[derive(Debug, Clone, Default)]
132pub struct MigrationDiff {
133 pub statements: Vec<JsonStatement>,
135 pub sql_statements: Vec<String>,
137 pub renames: Vec<String>,
139 pub warnings: Vec<String>,
141}
142
143#[must_use]
145pub fn table_from_ddl(table_name: &str, ddl: &SQLiteDDL) -> TableFull {
146 let entities = ddl.table_entities(table_name);
147
148 let (strict, without_rowid) = ddl
150 .tables
151 .one(table_name)
152 .map_or((false, false), |t| (t.strict, t.without_rowid));
153
154 TableFull {
155 name: table_name.to_string(),
156 columns: entities.columns.into_iter().cloned().collect(),
157 pk: entities.pk.cloned(),
158 fks: entities.fks.into_iter().cloned().collect(),
159 uniques: entities.uniques.into_iter().cloned().collect(),
160 checks: entities.checks.into_iter().cloned().collect(),
161 strict,
162 without_rowid,
163 }
164}
165
166fn entity_table_name(entity: &SqliteEntity) -> Option<String> {
167 match entity {
168 SqliteEntity::Column(c) => Some(c.table.to_string()),
169 SqliteEntity::ForeignKey(fk) => Some(fk.table.to_string()),
170 SqliteEntity::PrimaryKey(pk) => Some(pk.table.to_string()),
171 SqliteEntity::UniqueConstraint(uc) => Some(uc.table.to_string()),
172 SqliteEntity::CheckConstraint(cc) => Some(cc.table.to_string()),
173 _ => None,
174 }
175}
176
177fn collect_tables_to_recreate(
178 schema_diff: &SchemaDiff,
179 created: &HashSet<String>,
180 dropped: &HashSet<String>,
181) -> BTreeSet<String> {
182 let mut out: BTreeSet<String> = BTreeSet::new();
185
186 for table_diff in schema_diff.by_kind(EntityKind::Table) {
189 if table_diff.diff_type == DiffType::Alter
190 && let Some(SqliteEntity::Table(table)) = &table_diff.right
191 && !created.contains(table.name.as_ref())
192 && !dropped.contains(table.name.as_ref())
193 {
194 out.insert(table.name.to_string());
195 }
196 }
197
198 for col_diff in schema_diff.by_kind(EntityKind::Column) {
200 if col_diff.diff_type == DiffType::Alter
201 && let Some(SqliteEntity::Column(col)) = &col_diff.right
202 && !created.contains(col.table.as_ref())
203 && !dropped.contains(col.table.as_ref())
204 {
205 out.insert(col.table.to_string());
206 }
207 }
208
209 for col_diff in schema_diff.by_kind(EntityKind::Column) {
212 if col_diff.diff_type == DiffType::Create
213 && let Some(SqliteEntity::Column(col)) = &col_diff.right
214 && col
215 .generated
216 .as_ref()
217 .is_some_and(|g| g.gen_type == super::ddl::GeneratedType::Stored)
218 && !created.contains(col.table.as_ref())
219 && !dropped.contains(col.table.as_ref())
220 {
221 out.insert(col.table.to_string());
222 }
223 }
224
225 for kind in [
227 EntityKind::ForeignKey,
228 EntityKind::PrimaryKey,
229 EntityKind::UniqueConstraint,
230 EntityKind::CheckConstraint,
231 ] {
232 for diff in schema_diff.by_kind(kind) {
233 if !matches!(
234 diff.diff_type,
235 DiffType::Create | DiffType::Drop | DiffType::Alter
236 ) {
237 continue;
238 }
239 let table = diff
240 .right
241 .as_ref()
242 .and_then(entity_table_name)
243 .or_else(|| diff.left.as_ref().and_then(entity_table_name));
244 if let Some(table) = table
245 && !created.contains(&table)
246 && !dropped.contains(&table)
247 {
248 out.insert(table);
249 }
250 }
251 }
252
253 out
254}
255
256#[must_use]
262pub fn compute_migration(prev: &SQLiteDDL, cur: &SQLiteDDL) -> MigrationDiff {
263 let mut prev_normalized = prev.clone();
267 let mut rename_statements: Vec<JsonStatement> = Vec::new();
268 let mut table_renames: Vec<TableRename> = Vec::new();
269 let mut column_renames: Vec<ColumnRename> = Vec::new();
270 let mut warnings = Vec::new();
271
272 detect_and_apply_renames(
273 &mut prev_normalized,
274 cur,
275 &mut rename_statements,
276 &mut table_renames,
277 &mut column_renames,
278 &mut warnings,
279 );
280
281 let schema_diff = diff_collections(&prev_normalized, cur);
282 let mut statements = Vec::new();
283 let renames = prepare_migration_renames(&table_renames, &column_renames);
284
285 statements.extend(rename_statements);
287
288 let created_table_names: HashSet<String> = schema_diff
290 .created_tables()
291 .iter()
292 .map(|d| d.name.clone())
293 .collect();
294
295 let dropped_table_names: HashSet<String> = schema_diff
296 .dropped_tables()
297 .iter()
298 .map(|d| d.name.clone())
299 .collect();
300
301 let tables_to_recreate =
304 collect_tables_to_recreate(&schema_diff, &created_table_names, &dropped_table_names);
305
306 append_table_create_recreate_stmts(
307 &mut statements,
308 &schema_diff,
309 prev,
310 cur,
311 &tables_to_recreate,
312 );
313 append_add_column_stmts(
314 &mut statements,
315 &schema_diff,
316 cur,
317 &created_table_names,
318 &tables_to_recreate,
319 );
320 append_index_stmts(&mut statements, &schema_diff, cur, &tables_to_recreate);
321 append_drop_column_and_view_stmts(
322 &mut statements,
323 &schema_diff,
324 &dropped_table_names,
325 &tables_to_recreate,
326 );
327 append_drop_table_stmts(&mut statements, &schema_diff);
328 collect_stored_generated_warnings(&mut warnings, &schema_diff);
329
330 let result = from_json(statements.clone());
332
333 MigrationDiff {
334 statements,
335 sql_statements: result.sql_statements,
336 renames,
337 warnings,
338 }
339}
340
341fn append_table_create_recreate_stmts(
342 statements: &mut Vec<JsonStatement>,
343 schema_diff: &SchemaDiff,
344 prev: &SQLiteDDL,
345 cur: &SQLiteDDL,
346 tables_to_recreate: &BTreeSet<String>,
347) {
348 for table_diff in schema_diff.created_tables() {
350 if let Some(SqliteEntity::Table(table)) = &table_diff.right {
351 let table_full = table_from_ddl(&table.name, cur);
352 statements.push(JsonStatement::CreateTable(CreateTableStatement {
353 table: table_full,
354 }));
355 }
356 }
357
358 for table_name in tables_to_recreate {
360 let from_table = table_from_ddl(table_name, prev);
361 let to_table = table_from_ddl(table_name, cur);
362 statements.push(JsonStatement::RecreateTable(RecreateTableStatement {
363 from: from_table,
364 to: to_table,
365 data: None,
366 }));
367 }
368}
369
370fn append_add_column_stmts(
371 statements: &mut Vec<JsonStatement>,
372 schema_diff: &SchemaDiff,
373 cur: &SQLiteDDL,
374 created_table_names: &HashSet<String>,
375 tables_to_recreate: &BTreeSet<String>,
376) {
377 for col_diff in schema_diff.by_kind(EntityKind::Column) {
379 if col_diff.diff_type == DiffType::Create
380 && let Some(SqliteEntity::Column(col)) = &col_diff.right
381 && !created_table_names.contains(col.table.as_ref())
383 && !tables_to_recreate.contains(col.table.as_ref())
385 {
386 let fk = cur
388 .fks
389 .for_table(&col.table)
390 .into_iter()
391 .find(|fk| fk.columns.len() == 1 && fk.columns[0] == col.name)
392 .cloned();
393
394 statements.push(JsonStatement::AddColumn(AddColumnStatement {
395 column: col.clone(),
396 fk,
397 }));
398 }
399 }
400}
401
402fn append_index_stmts(
403 statements: &mut Vec<JsonStatement>,
404 schema_diff: &SchemaDiff,
405 cur: &SQLiteDDL,
406 tables_to_recreate: &BTreeSet<String>,
407) {
408 for idx_diff in schema_diff.by_kind(EntityKind::Index) {
410 if idx_diff.diff_type == DiffType::Drop
411 && let Some(SqliteEntity::Index(idx)) = &idx_diff.left
412 && !tables_to_recreate.contains(idx.table.as_ref())
413 {
414 statements.push(JsonStatement::DropIndex(DropIndexStatement {
415 index: idx.clone(),
416 }));
417 }
418 }
419
420 for idx_diff in schema_diff.by_kind(EntityKind::Index) {
422 if idx_diff.diff_type == DiffType::Create
423 && let Some(SqliteEntity::Index(idx)) = &idx_diff.right
424 && !tables_to_recreate.contains(idx.table.as_ref())
425 {
426 statements.push(JsonStatement::CreateIndex(CreateIndexStatement {
427 index: idx.clone(),
428 }));
429 }
430 }
431
432 for table_name in tables_to_recreate {
435 for idx in cur.indexes.for_table(table_name) {
436 statements.push(JsonStatement::CreateIndex(CreateIndexStatement {
437 index: idx.clone(),
438 }));
439 }
440 }
441
442 for idx_diff in schema_diff.by_kind(EntityKind::Index) {
444 if idx_diff.diff_type == DiffType::Alter {
445 if let Some(SqliteEntity::Index(old_idx)) = &idx_diff.left
446 && !tables_to_recreate.contains(old_idx.table.as_ref())
447 {
448 statements.push(JsonStatement::DropIndex(DropIndexStatement {
449 index: old_idx.clone(),
450 }));
451 }
452 if let Some(SqliteEntity::Index(new_idx)) = &idx_diff.right
453 && !tables_to_recreate.contains(new_idx.table.as_ref())
454 {
455 statements.push(JsonStatement::CreateIndex(CreateIndexStatement {
456 index: new_idx.clone(),
457 }));
458 }
459 }
460 }
461}
462
463fn append_drop_column_and_view_stmts(
464 statements: &mut Vec<JsonStatement>,
465 schema_diff: &SchemaDiff,
466 dropped_table_names: &HashSet<String>,
467 tables_to_recreate: &BTreeSet<String>,
468) {
469 for col_diff in schema_diff.by_kind(EntityKind::Column) {
471 if col_diff.diff_type == DiffType::Drop
472 && let Some(SqliteEntity::Column(col)) = &col_diff.left
473 && !dropped_table_names.contains(col.table.as_ref())
475 && !tables_to_recreate.contains(col.table.as_ref())
477 {
478 statements.push(JsonStatement::DropColumn(DropColumnStatement {
479 column: col.clone(),
480 }));
481 }
482 }
483
484 for view_diff in schema_diff.by_kind(EntityKind::View) {
486 if view_diff.diff_type == DiffType::Drop
487 && let Some(SqliteEntity::View(view)) = &view_diff.left
488 && !view.is_existing
489 {
490 statements.push(JsonStatement::DropView(DropViewStatement {
491 view: view.clone(),
492 }));
493 }
494 }
495
496 for view_diff in schema_diff.by_kind(EntityKind::View) {
498 if view_diff.diff_type == DiffType::Create
499 && let Some(SqliteEntity::View(view)) = &view_diff.right
500 && !view.is_existing
501 {
502 statements.push(JsonStatement::CreateView(CreateViewStatement {
503 view: view.clone(),
504 }));
505 }
506 }
507
508 for view_diff in schema_diff.by_kind(EntityKind::View) {
510 if view_diff.diff_type == DiffType::Alter {
511 if let Some(SqliteEntity::View(old_view)) = &view_diff.left {
512 statements.push(JsonStatement::DropView(DropViewStatement {
513 view: old_view.clone(),
514 }));
515 }
516 if let Some(SqliteEntity::View(new_view)) = &view_diff.right {
517 statements.push(JsonStatement::CreateView(CreateViewStatement {
518 view: new_view.clone(),
519 }));
520 }
521 }
522 }
523}
524
525fn append_drop_table_stmts(statements: &mut Vec<JsonStatement>, schema_diff: &SchemaDiff) {
526 for table_diff in schema_diff.dropped_tables() {
528 statements.push(JsonStatement::DropTable(DropTableStatement {
529 table_name: table_diff.name.clone(),
530 }));
531 }
532}
533
534fn collect_stored_generated_warnings(warnings: &mut Vec<String>, schema_diff: &SchemaDiff) {
535 for col_diff in schema_diff.by_kind(EntityKind::Column) {
537 if col_diff.diff_type == DiffType::Alter
538 && let Some(SqliteEntity::Column(col)) = &col_diff.right
539 && col
540 .generated
541 .as_ref()
542 .is_some_and(|g| g.gen_type == super::ddl::GeneratedType::Stored)
543 {
544 warnings.push(format!(
545 "Column '{}' in table '{}' has STORED generated column which requires table recreation",
546 col.name, col.table
547 ));
548 }
549 }
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
553struct TableColumnFingerprint {
554 name: String,
555 sql_type: String,
556 not_null: bool,
557}
558
559#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
560struct TableFingerprint {
561 columns: Vec<TableColumnFingerprint>,
562 pk_columns: Vec<String>,
563}
564
565fn table_fingerprint(table_name: &str, ddl: &SQLiteDDL) -> TableFingerprint {
566 let mut columns: Vec<_> = ddl
567 .columns
568 .for_table(table_name)
569 .into_iter()
570 .map(|c| TableColumnFingerprint {
571 name: c.name.to_string(),
572 sql_type: c.sql_type.to_string(),
573 not_null: c.not_null,
574 })
575 .collect();
576 columns.sort();
577
578 let pk_columns = if let Some(pk) = ddl.pks.for_table(table_name) {
579 pk.columns.iter().map(ToString::to_string).collect()
580 } else {
581 let mut inline_pk_columns: Vec<_> = ddl
582 .columns
583 .for_table(table_name)
584 .into_iter()
585 .filter(|c| c.primary_key.unwrap_or(false))
586 .map(|c| (c.ordinal_position.unwrap_or(i32::MAX), c.name.to_string()))
587 .collect();
588 inline_pk_columns.sort();
589 inline_pk_columns
590 .into_iter()
591 .map(|(_, name)| name)
592 .collect()
593 };
594
595 TableFingerprint {
596 columns,
597 pk_columns,
598 }
599}
600
601fn detect_and_apply_renames(
602 prev: &mut SQLiteDDL,
603 cur: &SQLiteDDL,
604 rename_statements: &mut Vec<JsonStatement>,
605 table_renames: &mut Vec<TableRename>,
606 column_renames: &mut Vec<ColumnRename>,
607 warnings: &mut Vec<String>,
608) {
609 let prev_tables: Vec<String> = prev
611 .tables
612 .list()
613 .iter()
614 .map(|t| t.name.to_string())
615 .collect();
616 let cur_tables: Vec<String> = cur
617 .tables
618 .list()
619 .iter()
620 .map(|t| t.name.to_string())
621 .collect();
622
623 let dropped: Vec<String> = prev_tables
624 .iter()
625 .filter(|t| !cur_tables.contains(t))
626 .cloned()
627 .collect();
628 let created: Vec<String> = cur_tables
629 .iter()
630 .filter(|t| !prev_tables.contains(t))
631 .cloned()
632 .collect();
633
634 let mut candidates: BTreeMap<TableFingerprint, (Vec<String>, Vec<String>)> = BTreeMap::new();
635 for from in dropped {
636 candidates
637 .entry(table_fingerprint(&from, prev))
638 .or_default()
639 .0
640 .push(from);
641 }
642 for to in created {
643 candidates
644 .entry(table_fingerprint(&to, cur))
645 .or_default()
646 .1
647 .push(to);
648 }
649
650 for (_, (mut dropped, mut created)) in candidates {
651 if dropped.is_empty() || created.is_empty() {
652 continue;
653 }
654
655 dropped.sort();
656 created.sort();
657
658 if dropped.len() == 1 && created.len() == 1 {
659 let from = &dropped[0];
660 let to = &created[0];
661 table_renames.push(TableRename {
662 from: from.clone(),
663 to: to.clone(),
664 });
665 rename_statements.push(JsonStatement::RenameTable(RenameTableStatement {
666 from: from.clone(),
667 to: to.clone(),
668 }));
669 apply_table_rename(prev, from, to);
670 } else {
671 warnings.push(format!(
672 "Ambiguous SQLite table rename candidates between dropped tables [{}] and created tables [{}]; no rename was inferred. Use DiffOptions::rename_table(...) with diff_with or diff_schemas_with to provide an explicit rename hint.",
673 dropped.join(", "),
674 created.join(", ")
675 ));
676 }
677 }
678
679 let common_tables: Vec<String> = prev
681 .tables
682 .list()
683 .iter()
684 .map(|t| t.name.to_string())
685 .filter(|t| cur.tables.one(t).is_some())
686 .collect();
687
688 for table in common_tables {
689 let prev_cols: Vec<_> = prev.columns.for_table(&table);
690 let cur_cols: Vec<_> = cur.columns.for_table(&table);
691
692 let prev_names: Vec<String> = prev_cols.iter().map(|c| c.name.to_string()).collect();
693 let cur_names: Vec<String> = cur_cols.iter().map(|c| c.name.to_string()).collect();
694
695 let dropped_cols: Vec<String> = prev_names
696 .iter()
697 .filter(|c| !cur_names.contains(c))
698 .cloned()
699 .collect();
700 let created_cols: Vec<String> = cur_names
701 .iter()
702 .filter(|c| !prev_names.contains(c))
703 .cloned()
704 .collect();
705
706 if dropped_cols.len() != 1 || created_cols.len() != 1 {
707 continue;
708 }
709
710 let from = &dropped_cols[0];
711 let to = &created_cols[0];
712
713 let prev_col = prev.columns.one(&table, from);
714 let cur_col = cur.columns.one(&table, to);
715 if let (Some(prev_col), Some(cur_col)) = (prev_col, cur_col) {
716 let mut prev_cmp = prev_col.clone();
717 prev_cmp.name.clone_from(&cur_col.name);
718 if prev_cmp == *cur_col {
719 column_renames.push(ColumnRename {
720 table: table.clone(),
721 from: from.clone(),
722 to: to.clone(),
723 });
724 rename_statements.push(JsonStatement::RenameColumn(RenameColumnStatement {
725 table: table.clone(),
726 from: from.clone(),
727 to: to.clone(),
728 }));
729 apply_column_rename(prev, &table, from, to);
730 }
731 }
732 }
733}
734
735fn apply_table_rename(ddl: &mut SQLiteDDL, from: &str, to: &str) {
736 let to = to.to_string();
737 if let Some(t) = ddl
739 .tables
740 .list_mut()
741 .iter_mut()
742 .find(|t| t.name.as_ref() == from)
743 {
744 t.name = to.clone().into();
745 }
746 for c in ddl
748 .columns
749 .list_mut()
750 .iter_mut()
751 .filter(|c| c.table.as_ref() == from)
752 {
753 c.table = to.clone().into();
754 }
755 for pk in ddl
757 .pks
758 .list_mut()
759 .iter_mut()
760 .filter(|pk| pk.table.as_ref() == from)
761 {
762 pk.table = to.clone().into();
763 }
764 for u in ddl
766 .uniques
767 .list_mut()
768 .iter_mut()
769 .filter(|u| u.table.as_ref() == from)
770 {
771 u.table = to.clone().into();
772 }
773 for fk in ddl.fks.list_mut().iter_mut() {
775 if fk.table.as_ref() == from {
776 fk.table = to.clone().into();
777 }
778 if fk.table_to.as_ref() == from {
779 fk.table_to = to.clone().into();
780 }
781 }
782 for idx in ddl
784 .indexes
785 .list_mut()
786 .iter_mut()
787 .filter(|i| i.table.as_ref() == from)
788 {
789 idx.table = to.clone().into();
790 }
791 for chk in ddl
793 .checks
794 .list_mut()
795 .iter_mut()
796 .filter(|c| c.table.as_ref() == from)
797 {
798 chk.table = to.clone().into();
799 }
800}
801
802fn apply_column_rename(ddl: &mut SQLiteDDL, table: &str, from: &str, to: &str) {
803 let to = to.to_string();
804 if let Some(c) = ddl
806 .columns
807 .list_mut()
808 .iter_mut()
809 .find(|c| c.table.as_ref() == table && c.name.as_ref() == from)
810 {
811 c.name = to.clone().into();
812 }
813 for pk in ddl
815 .pks
816 .list_mut()
817 .iter_mut()
818 .filter(|pk| pk.table.as_ref() == table)
819 {
820 for col in pk.columns.to_mut().iter_mut() {
821 if col.as_ref() == from {
822 *col = to.clone().into();
823 }
824 }
825 }
826 for u in ddl
828 .uniques
829 .list_mut()
830 .iter_mut()
831 .filter(|u| u.table.as_ref() == table)
832 {
833 for col in u.columns.to_mut().iter_mut() {
834 if col.as_ref() == from {
835 *col = to.clone().into();
836 }
837 }
838 }
839 for fk in ddl.fks.list_mut().iter_mut() {
841 if fk.table.as_ref() == table {
842 for col in fk.columns.to_mut().iter_mut() {
843 if col.as_ref() == from {
844 *col = to.clone().into();
845 }
846 }
847 }
848 if fk.table_to.as_ref() == table {
849 for col in fk.columns_to.to_mut().iter_mut() {
850 if col.as_ref() == from {
851 *col = to.clone().into();
852 }
853 }
854 }
855 }
856 for idx in ddl
858 .indexes
859 .list_mut()
860 .iter_mut()
861 .filter(|i| i.table.as_ref() == table)
862 {
863 for col in &mut idx.columns {
864 if !col.is_expression && col.value.as_ref() == from {
865 col.value = to.clone().into();
866 }
867 }
868 }
869}
870
871#[must_use]
873pub fn prepare_migration_renames(
874 table_renames: &[TableRename],
875 column_renames: &[ColumnRename],
876) -> Vec<String> {
877 let mut renames = Vec::new();
878
879 for tr in table_renames {
880 renames.push(format!("table:{}:{}", tr.from, tr.to));
881 }
882
883 for cr in column_renames {
884 renames.push(format!("column:{}:{}:{}", cr.table, cr.from, cr.to));
885 }
886
887 renames
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893 use crate::sqlite::ddl::{Column, ForeignKey, Index, IndexColumn, SqliteEntity, Table};
894 use std::borrow::Cow;
895
896 #[test]
897 fn test_empty_diff() {
898 let prev = SQLiteSnapshot::new();
899 let cur = SQLiteSnapshot::new();
900
901 let diff = diff_snapshots(&prev, &cur);
902 assert!(!diff.has_changes());
903 }
904
905 #[test]
906 fn test_table_creation() {
907 let prev = SQLiteSnapshot::new();
908 let mut cur = SQLiteSnapshot::new();
909
910 cur.add_entity(SqliteEntity::Table(Table::new("users")));
911 cur.add_entity(SqliteEntity::Column(
912 Column::new("users", "id", "integer").not_null(),
913 ));
914
915 let diff = diff_snapshots(&prev, &cur);
916 assert!(diff.has_changes());
917 assert_eq!(diff.created_tables().len(), 1);
918 }
919
920 #[test]
921 fn test_table_deletion() {
922 let mut prev = SQLiteSnapshot::new();
923 let cur = SQLiteSnapshot::new();
924
925 prev.add_entity(SqliteEntity::Table(Table::new("users")));
926
927 let diff = diff_snapshots(&prev, &cur);
928 assert!(diff.has_changes());
929 assert_eq!(diff.dropped_tables().len(), 1);
930 }
931
932 fn sqlite_table_with_id(table: &str) -> SQLiteDDL {
933 let mut ddl = SQLiteDDL::new();
934 ddl.tables.push(Table::new(table.to_string()));
935 ddl.columns
936 .push(Column::new(table.to_string(), "id", "integer").not_null());
937 ddl
938 }
939
940 #[test]
941 fn pure_table_rename_emits_single_rename_statement() {
942 let prev = sqlite_table_with_id("users");
943 let cur = sqlite_table_with_id("accounts");
944
945 let migration = compute_migration(&prev, &cur);
946
947 assert_eq!(migration.statements.len(), 1);
948 assert!(matches!(
949 migration.statements[0],
950 JsonStatement::RenameTable(_)
951 ));
952 assert_eq!(
953 migration.sql_statements,
954 vec!["ALTER TABLE `users` RENAME TO `accounts`;"]
955 );
956 assert!(
957 !migration
958 .statements
959 .iter()
960 .any(|statement| matches!(statement, JsonStatement::DropTable(_)))
961 );
962 }
963
964 #[test]
965 fn table_rename_rewrites_indexes_and_foreign_keys() {
966 let mut prev = sqlite_table_with_id("users");
967 prev.tables.push(Table::new("posts"));
968 prev.columns
969 .push(Column::new("posts", "id", "integer").not_null());
970 prev.columns
971 .push(Column::new("posts", "user_id", "integer").not_null());
972 prev.indexes.push(Index::new(
973 "users",
974 "idx_users_id",
975 vec![IndexColumn::new("id")],
976 ));
977 prev.fks.push(ForeignKey::new(
978 "posts",
979 "fk_posts_user",
980 vec![Cow::Borrowed("user_id")],
981 "users",
982 vec![Cow::Borrowed("id")],
983 ));
984
985 let mut cur = sqlite_table_with_id("accounts");
986 cur.tables.push(Table::new("posts"));
987 cur.columns
988 .push(Column::new("posts", "id", "integer").not_null());
989 cur.columns
990 .push(Column::new("posts", "user_id", "integer").not_null());
991 cur.indexes.push(Index::new(
992 "accounts",
993 "idx_users_id",
994 vec![IndexColumn::new("id")],
995 ));
996 cur.fks.push(ForeignKey::new(
997 "posts",
998 "fk_posts_user",
999 vec![Cow::Borrowed("user_id")],
1000 "accounts",
1001 vec![Cow::Borrowed("id")],
1002 ));
1003
1004 let migration = compute_migration(&prev, &cur);
1005
1006 assert_eq!(
1007 migration.sql_statements,
1008 vec!["ALTER TABLE `users` RENAME TO `accounts`;"]
1009 );
1010 assert!(
1011 !migration.sql_statements.iter().any(|statement| {
1012 statement.starts_with("DROP")
1013 || statement.starts_with("CREATE INDEX")
1014 || statement.contains("fk_posts_user")
1015 }),
1016 "unexpected dependent churn: {:?}",
1017 migration.sql_statements
1018 );
1019 }
1020
1021 #[test]
1022 fn ambiguous_table_rename_does_not_guess_and_warns() {
1023 let mut prev = sqlite_table_with_id("users");
1024 let mut admins = sqlite_table_with_id("admins");
1025 prev.tables.list_mut().append(admins.tables.list_mut());
1026 prev.columns.list_mut().append(admins.columns.list_mut());
1027 let cur = sqlite_table_with_id("accounts");
1028
1029 let migration = compute_migration(&prev, &cur);
1030
1031 assert!(
1032 migration.warnings.iter().any(|warning| warning
1033 .contains("Ambiguous SQLite table rename candidates")
1034 && warning.contains("rename_table")),
1035 "expected ambiguous rename warning, got {:?}",
1036 migration.warnings
1037 );
1038 assert!(
1039 !migration
1040 .statements
1041 .iter()
1042 .any(|statement| matches!(statement, JsonStatement::RenameTable(_)))
1043 );
1044 }
1045
1046 #[test]
1047 fn test_column_nullable_change() {
1048 let mut prev = SQLiteSnapshot::new();
1050 prev.add_entity(SqliteEntity::Table(Table::new("users")));
1051 prev.add_entity(SqliteEntity::Column(Column::new("users", "email", "text"))); let mut cur = SQLiteSnapshot::new();
1054 cur.add_entity(SqliteEntity::Table(Table::new("users")));
1055 cur.add_entity(SqliteEntity::Column(
1056 Column::new("users", "email", "text").not_null(),
1057 )); let diff = diff_snapshots(&prev, &cur);
1060 assert!(diff.has_changes(), "Should detect nullable change");
1061
1062 let altered = diff.altered();
1064 assert_eq!(altered.len(), 1, "Should have one altered entity");
1065 assert_eq!(altered[0].kind, crate::traits::EntityKind::Column);
1066 assert_eq!(altered[0].name, "users:email");
1067 }
1068
1069 #[test]
1070 fn test_column_not_null_to_nullable() {
1071 let mut prev = SQLiteSnapshot::new();
1073 prev.add_entity(SqliteEntity::Table(Table::new("users")));
1074 prev.add_entity(SqliteEntity::Column(
1075 Column::new("users", "email", "text").not_null(),
1076 )); let mut cur = SQLiteSnapshot::new();
1079 cur.add_entity(SqliteEntity::Table(Table::new("users")));
1080 cur.add_entity(SqliteEntity::Column(Column::new("users", "email", "text"))); let diff = diff_snapshots(&prev, &cur);
1083 assert!(diff.has_changes(), "Should detect nullable change");
1084
1085 let altered = diff.altered();
1087 assert_eq!(altered.len(), 1, "Should have one altered entity");
1088 assert_eq!(altered[0].kind, crate::traits::EntityKind::Column);
1089 }
1090
1091 #[test]
1092 fn test_column_nullable_change_generates_sql() {
1093 let mut prev_ddl = SQLiteDDL::new();
1095 prev_ddl.tables.push(Table::new("users"));
1096 prev_ddl
1097 .columns
1098 .push(Column::new("users", "id", "integer").not_null());
1099 prev_ddl.columns.push(Column::new("users", "email", "text")); let mut cur_ddl = SQLiteDDL::new();
1102 cur_ddl.tables.push(Table::new("users"));
1103 cur_ddl
1104 .columns
1105 .push(Column::new("users", "id", "integer").not_null());
1106 cur_ddl
1107 .columns
1108 .push(Column::new("users", "email", "text").not_null()); let migration = compute_migration(&prev_ddl, &cur_ddl);
1111
1112 assert!(
1114 !migration.sql_statements.is_empty(),
1115 "Should generate SQL statements"
1116 );
1117
1118 let has_recreate = migration
1120 .statements
1121 .iter()
1122 .any(|s| matches!(s, JsonStatement::RecreateTable(_)));
1123 assert!(
1124 has_recreate,
1125 "Should have RecreateTable statement for column alteration"
1126 );
1127
1128 assert_eq!(migration.sql_statements[0], "PRAGMA foreign_keys=OFF;");
1130 assert!(
1131 migration.sql_statements[1].starts_with("CREATE TABLE `__new_users`"),
1132 "Expected CREATE TABLE `__new_users`, got: {}",
1133 migration.sql_statements[1]
1134 );
1135 assert!(
1136 migration.sql_statements[1].contains("`email` TEXT NOT NULL"),
1137 "New table should have NOT NULL on email: {}",
1138 migration.sql_statements[1]
1139 );
1140 assert_eq!(
1141 migration.sql_statements[2],
1142 "INSERT INTO `__new_users`(`id`, `email`) SELECT `id`, `email` FROM `users`;"
1143 );
1144 assert_eq!(migration.sql_statements[3], "DROP TABLE `users`;");
1145 assert_eq!(
1146 migration.sql_statements[4],
1147 "ALTER TABLE `__new_users` RENAME TO `users`;"
1148 );
1149 assert_eq!(migration.sql_statements[5], "PRAGMA foreign_keys=ON;");
1150 }
1151
1152 #[test]
1153 fn strict_toggle_generates_table_recreate() {
1154 let mut prev = SQLiteDDL::new();
1155 prev.tables.push(Table::new("users"));
1156 prev.columns
1157 .push(Column::new("users", "id", "integer").not_null());
1158
1159 let mut cur = SQLiteDDL::new();
1160 cur.tables.push(Table::new("users").strict());
1161 cur.columns
1162 .push(Column::new("users", "id", "integer").not_null());
1163
1164 let migration = compute_migration(&prev, &cur);
1165
1166 let has_recreate = migration
1167 .statements
1168 .iter()
1169 .any(|s| matches!(s, JsonStatement::RecreateTable(_)));
1170 assert!(
1171 has_recreate,
1172 "toggling STRICT must recreate the table, got: {:?}",
1173 migration.statements
1174 );
1175 assert!(
1176 migration.sql_statements.iter().any(|sql| sql
1177 .starts_with("CREATE TABLE `__new_users`")
1178 && sql.ends_with("STRICT;")),
1179 "recreated table must carry STRICT: {:?}",
1180 migration.sql_statements
1181 );
1182 }
1183
1184 #[test]
1185 fn partial_index_predicate_change_recreates_index() {
1186 let mut prev = sqlite_table_with_id("jobs");
1187 let mut previous_index =
1188 Index::new("jobs", "idx_jobs_unclaimed", vec![IndexColumn::new("id")]);
1189 previous_index.where_clause = Some(Cow::Borrowed("builder IS NULL"));
1190 prev.indexes.push(previous_index);
1191
1192 let mut cur = sqlite_table_with_id("jobs");
1193 let mut current_index =
1194 Index::new("jobs", "idx_jobs_unclaimed", vec![IndexColumn::new("id")]);
1195 current_index.where_clause = Some(Cow::Borrowed("builder IS NOT NULL"));
1196 cur.indexes.push(current_index);
1197
1198 let migration = compute_migration(&prev, &cur);
1199 assert_eq!(
1200 migration.sql_statements,
1201 vec![
1202 "DROP INDEX IF EXISTS `idx_jobs_unclaimed`;",
1203 "CREATE INDEX `idx_jobs_unclaimed` ON `jobs`(`id`) WHERE builder IS NOT NULL;",
1204 ]
1205 );
1206 }
1207
1208 #[test]
1209 fn without_rowid_toggle_generates_table_recreate() {
1210 let mut prev = SQLiteDDL::new();
1211 prev.tables.push(Table::new("kv"));
1212 prev.columns
1213 .push(Column::new("kv", "key", "text").not_null());
1214
1215 let mut cur = SQLiteDDL::new();
1216 cur.tables.push(Table::new("kv").without_rowid());
1217 cur.columns
1218 .push(Column::new("kv", "key", "text").not_null());
1219
1220 let migration = compute_migration(&prev, &cur);
1221
1222 assert!(
1223 migration
1224 .statements
1225 .iter()
1226 .any(|s| matches!(s, JsonStatement::RecreateTable(_))),
1227 "toggling WITHOUT ROWID must recreate the table, got: {:?}",
1228 migration.statements
1229 );
1230 }
1231
1232 #[test]
1233 fn multi_table_recreation_order_is_deterministic() {
1234 let make = |not_null: bool| {
1235 let mut ddl = SQLiteDDL::new();
1236 for table in ["zeta", "alpha", "midway"] {
1237 ddl.tables.push(Table::new(table.to_string()));
1238 let col = Column::new(table.to_string(), "name", "text");
1239 ddl.columns
1240 .push(if not_null { col.not_null() } else { col });
1241 }
1242 ddl
1243 };
1244
1245 let migration = compute_migration(&make(false), &make(true));
1246 let recreate_order: Vec<String> = migration
1247 .statements
1248 .iter()
1249 .filter_map(|s| match s {
1250 JsonStatement::RecreateTable(st) => Some(st.to.name.clone()),
1251 _ => None,
1252 })
1253 .collect();
1254 assert_eq!(
1255 recreate_order,
1256 vec!["alpha", "midway", "zeta"],
1257 "table recreation must be emitted in sorted order"
1258 );
1259 }
1260
1261 #[test]
1262 fn test_column_type_change_generates_recreate() {
1263 let mut prev_ddl = SQLiteDDL::new();
1265 prev_ddl.tables.push(Table::new("users"));
1266 prev_ddl.columns.push(Column::new("users", "age", "text")); let mut cur_ddl = SQLiteDDL::new();
1269 cur_ddl.tables.push(Table::new("users"));
1270 cur_ddl.columns.push(Column::new("users", "age", "integer")); let migration = compute_migration(&prev_ddl, &cur_ddl);
1273
1274 let has_recreate = migration
1276 .statements
1277 .iter()
1278 .any(|s| matches!(s, JsonStatement::RecreateTable(_)));
1279 assert!(
1280 has_recreate,
1281 "Should have RecreateTable statement for type change"
1282 );
1283 }
1284}