1use crate::postgres::collection::PostgresDDL;
52use crate::schema::{Schema, Snapshot};
53use crate::sqlite::collection::SQLiteDDL;
54use crate::version::ORIGIN_UUID;
55use crate::writer::MigrationError;
56use std::borrow::Cow;
57use std::io::{self, Write};
58
59#[derive(Clone, Debug)]
61pub struct Plan {
62 pub statements: Vec<String>,
64 pub warnings: Vec<String>,
66 pub snapshot: Snapshot,
68}
69
70impl Plan {
71 #[must_use]
73 pub fn is_empty(&self) -> bool {
74 self.statements.is_empty()
75 || self
76 .statements
77 .iter()
78 .all(|statement| statement.trim().is_empty())
79 }
80
81 #[must_use]
83 pub fn to_sql(&self) -> String {
84 self.statements.join("\n--> statement-breakpoint\n")
85 }
86
87 pub fn write(&self, writer: impl Write) -> io::Result<()> {
93 let mut writer = writer;
94 writer.write_all(self.to_sql().as_bytes())
95 }
96}
97
98#[derive(Clone, Debug, Default, PartialEq, Eq)]
100pub struct RenameHints {
101 pub schema_renames: Vec<SchemaRenameHint>,
103 pub table_renames: Vec<TableRenameHint>,
105 pub column_renames: Vec<ColumnRenameHint>,
107}
108
109impl RenameHints {
110 #[must_use]
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 #[must_use]
116 pub fn rename_schema(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
117 self.schema_renames.push(SchemaRenameHint {
118 from: from.into(),
119 to: to.into(),
120 });
121 self
122 }
123
124 #[must_use]
125 pub fn rename_table(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
126 self.table_renames.push(TableRenameHint {
127 schema: None,
128 from: from.into(),
129 to: to.into(),
130 });
131 self
132 }
133
134 #[must_use]
135 pub fn rename_table_in(
136 mut self,
137 schema: impl Into<String>,
138 from: impl Into<String>,
139 to: impl Into<String>,
140 ) -> Self {
141 self.table_renames.push(TableRenameHint {
142 schema: Some(schema.into()),
143 from: from.into(),
144 to: to.into(),
145 });
146 self
147 }
148
149 #[must_use]
150 pub fn rename_column(
151 mut self,
152 table: impl Into<String>,
153 from: impl Into<String>,
154 to: impl Into<String>,
155 ) -> Self {
156 self.column_renames.push(ColumnRenameHint {
157 schema: None,
158 table: table.into(),
159 from: from.into(),
160 to: to.into(),
161 });
162 self
163 }
164
165 #[must_use]
166 pub fn rename_column_in(
167 mut self,
168 schema: impl Into<String>,
169 table: impl Into<String>,
170 from: impl Into<String>,
171 to: impl Into<String>,
172 ) -> Self {
173 self.column_renames.push(ColumnRenameHint {
174 schema: Some(schema.into()),
175 table: table.into(),
176 from: from.into(),
177 to: to.into(),
178 });
179 self
180 }
181}
182
183#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct SchemaRenameHint {
186 pub from: String,
188 pub to: String,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct TableRenameHint {
195 pub schema: Option<String>,
197 pub from: String,
199 pub to: String,
201}
202
203#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct ColumnRenameHint {
206 pub schema: Option<String>,
208 pub table: String,
210 pub from: String,
212 pub to: String,
214}
215
216#[derive(Clone, Debug, Default, PartialEq, Eq)]
218pub struct DiffOptions {
219 pub renames: RenameHints,
221 pub strict_renames: bool,
223 pub sqlite_rebuild_data: Option<crate::sqlite::SqliteRebuildDataPlanRegistry>,
226}
227
228impl DiffOptions {
229 #[must_use]
230 pub fn new() -> Self {
231 Self::default()
232 }
233
234 #[must_use]
235 pub fn with_renames(mut self, renames: RenameHints) -> Self {
236 self.renames = renames;
237 self
238 }
239
240 #[must_use]
241 pub const fn strict_renames(mut self, strict: bool) -> Self {
242 self.strict_renames = strict;
243 self
244 }
245
246 #[must_use]
247 pub fn sqlite_rebuild_data(mut self, plan: crate::sqlite::SqliteRebuildDataPlan) -> Self {
248 self.sqlite_rebuild_data = Some(crate::sqlite::SqliteRebuildDataPlanRegistry::single(plan));
249 self
250 }
251
252 #[must_use]
253 pub fn sqlite_rebuild_data_registry(
254 mut self,
255 registry: crate::sqlite::SqliteRebuildDataPlanRegistry,
256 ) -> Self {
257 self.sqlite_rebuild_data = Some(registry);
258 self
259 }
260
261 #[must_use]
262 pub fn rename_schema(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
263 self.renames = self.renames.rename_schema(from, to);
264 self
265 }
266
267 #[must_use]
268 pub fn rename_table(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
269 self.renames = self.renames.rename_table(from, to);
270 self
271 }
272
273 #[must_use]
274 pub fn rename_table_in(
275 mut self,
276 schema: impl Into<String>,
277 from: impl Into<String>,
278 to: impl Into<String>,
279 ) -> Self {
280 self.renames = self.renames.rename_table_in(schema, from, to);
281 self
282 }
283
284 #[must_use]
285 pub fn rename_column(
286 mut self,
287 table: impl Into<String>,
288 from: impl Into<String>,
289 to: impl Into<String>,
290 ) -> Self {
291 self.renames = self.renames.rename_column(table, from, to);
292 self
293 }
294
295 #[must_use]
296 pub fn rename_column_in(
297 mut self,
298 schema: impl Into<String>,
299 table: impl Into<String>,
300 from: impl Into<String>,
301 to: impl Into<String>,
302 ) -> Self {
303 self.renames = self.renames.rename_column_in(schema, table, from, to);
304 self
305 }
306}
307
308pub fn diff(prev: &Snapshot, current: &Snapshot) -> Result<Plan, MigrationError> {
324 diff_with(prev, current, &DiffOptions::default())
325}
326
327pub fn diff_with(
338 prev: &Snapshot,
339 current: &Snapshot,
340 options: &DiffOptions,
341) -> Result<Plan, MigrationError> {
342 let (statements, warnings) = match (prev, current) {
343 (Snapshot::Sqlite(p), Snapshot::Sqlite(c)) => {
344 let mut prev_ddl = SQLiteDDL::from_entities(p.ddl.clone());
345 let cur_ddl = crate::sqlite::collection::SQLiteDDL::from_entities(c.ddl.clone());
346 let mut statements = apply_sqlite_rename_hints(&mut prev_ddl, &cur_ddl, options)?;
347 let mut diff = crate::sqlite::diff::compute_migration(&prev_ddl, &cur_ddl);
348 crate::sqlite::rebuild_data::apply_rebuild_data_plan(
349 prev.id(),
350 &prev_ddl,
351 &cur_ddl,
352 &mut diff.statements,
353 options.sqlite_rebuild_data.as_ref(),
354 )
355 .map_err(MigrationError::ConfigError)?;
356 diff.sql_statements =
357 crate::sqlite::statements::from_json(diff.statements.clone()).sql_statements;
358 statements.extend(diff.sql_statements);
359 (statements, diff.warnings)
360 }
361 (Snapshot::Postgres(p), Snapshot::Postgres(c)) => {
362 if options.sqlite_rebuild_data.is_some() {
363 return Err(MigrationError::ConfigError(
364 "SQLite rebuild-data plan cannot be used for a PostgreSQL migration"
365 .to_string(),
366 ));
367 }
368 let mut prev_ddl = PostgresDDL::from_entities(p.ddl.clone());
369 let cur_ddl = PostgresDDL::from_entities(c.ddl.clone());
370 let mut statements = apply_postgres_rename_hints(&mut prev_ddl, &cur_ddl, options)?;
371 let diff = crate::postgres::diff::compute_migration(&prev_ddl, &cur_ddl);
372 statements.extend(diff.sql_statements);
373 (statements, diff.warnings)
374 }
375 _ => return Err(MigrationError::DialectMismatch),
376 };
377
378 let mut snapshot = current.clone();
382 let prev_is_origin_baseline =
383 prev.is_empty() && matches!(prev.prev_ids(), [only] if only == ORIGIN_UUID);
384 if prev_is_origin_baseline {
385 snapshot.set_prev_ids(vec![ORIGIN_UUID.to_string()]);
386 } else {
387 snapshot.set_prev_ids(vec![prev.id().to_string()]);
388 }
389
390 Ok(Plan {
391 statements,
392 warnings,
393 snapshot,
394 })
395}
396
397pub fn diff_schemas<From: Schema, To: Schema>(
406 prev: &From,
407 current: &To,
408) -> Result<Plan, MigrationError> {
409 let prev = prev.to_snapshot();
410 let current = current.to_snapshot();
411 diff(&prev, ¤t)
412}
413
414pub fn diff_schemas_with<From: Schema, To: Schema>(
449 prev: &From,
450 current: &To,
451 options: &DiffOptions,
452) -> Result<Plan, MigrationError> {
453 let prev = prev.to_snapshot();
454 let current = current.to_snapshot();
455 diff_with(&prev, ¤t, options)
456}
457
458fn apply_sqlite_rename_hints(
459 prev: &mut SQLiteDDL,
460 cur: &SQLiteDDL,
461 options: &DiffOptions,
462) -> Result<Vec<String>, MigrationError> {
463 let mut statements = Vec::new();
464
465 if !options.renames.schema_renames.is_empty() && options.strict_renames {
466 return Err(MigrationError::ConfigError(
467 "sqlite rename_schema hint is not supported".to_string(),
468 ));
469 }
470
471 for hint in &options.renames.table_renames {
472 if hint.schema.is_some() {
473 if options.strict_renames {
474 return Err(MigrationError::ConfigError(
475 "sqlite rename_table hint does not support schema".to_string(),
476 ));
477 }
478 continue;
479 }
480
481 if !valid_rename_name(&hint.from) || !valid_rename_name(&hint.to) || hint.from == hint.to {
482 if options.strict_renames {
483 return Err(MigrationError::ConfigError(format!(
484 "invalid sqlite table rename hint: {} -> {}",
485 hint.from, hint.to
486 )));
487 }
488 continue;
489 }
490
491 let can_apply = prev.tables.one(&hint.from).is_some()
492 && cur.tables.one(&hint.to).is_some()
493 && prev.tables.one(&hint.to).is_none();
494
495 if !can_apply {
496 if options.strict_renames {
497 return Err(MigrationError::ConfigError(format!(
498 "sqlite table rename hint did not match snapshots: {} -> {}",
499 hint.from, hint.to
500 )));
501 }
502 continue;
503 }
504
505 statements.push(format!(
506 "ALTER TABLE `{}` RENAME TO `{}`;",
507 hint.from, hint.to
508 ));
509 apply_sqlite_table_rename(prev, &hint.from, &hint.to);
510 }
511
512 for hint in &options.renames.column_renames {
513 if hint.schema.is_some() {
514 if options.strict_renames {
515 return Err(MigrationError::ConfigError(
516 "sqlite rename_column hint does not support schema".to_string(),
517 ));
518 }
519 continue;
520 }
521
522 if !valid_rename_name(&hint.table)
523 || !valid_rename_name(&hint.from)
524 || !valid_rename_name(&hint.to)
525 || hint.from == hint.to
526 {
527 if options.strict_renames {
528 return Err(MigrationError::ConfigError(format!(
529 "invalid sqlite column rename hint: {}.{} -> {}",
530 hint.table, hint.from, hint.to
531 )));
532 }
533 continue;
534 }
535
536 let can_apply = prev.columns.one(&hint.table, &hint.from).is_some()
537 && cur.columns.one(&hint.table, &hint.to).is_some()
538 && prev.columns.one(&hint.table, &hint.to).is_none();
539
540 if !can_apply {
541 if options.strict_renames {
542 return Err(MigrationError::ConfigError(format!(
543 "sqlite column rename hint did not match snapshots: {}.{} -> {}",
544 hint.table, hint.from, hint.to
545 )));
546 }
547 continue;
548 }
549
550 statements.push(format!(
551 "ALTER TABLE `{}` RENAME COLUMN `{}` TO `{}`;",
552 hint.table, hint.from, hint.to
553 ));
554 apply_sqlite_column_rename(prev, &hint.table, &hint.from, &hint.to);
555 }
556
557 Ok(statements)
558}
559
560fn apply_postgres_rename_hints(
561 prev: &mut PostgresDDL,
562 cur: &PostgresDDL,
563 options: &DiffOptions,
564) -> Result<Vec<String>, MigrationError> {
565 let mut statements = Vec::new();
566
567 for hint in &options.renames.schema_renames {
568 if !valid_rename_name(&hint.from) || !valid_rename_name(&hint.to) || hint.from == hint.to {
569 if options.strict_renames {
570 return Err(MigrationError::ConfigError(format!(
571 "invalid postgres schema rename hint: {} -> {}",
572 hint.from, hint.to
573 )));
574 }
575 continue;
576 }
577
578 let can_apply = prev.schemas.one(&hint.from).is_some()
579 && cur.schemas.one(&hint.to).is_some()
580 && prev.schemas.one(&hint.to).is_none();
581
582 if !can_apply {
583 if options.strict_renames {
584 return Err(MigrationError::ConfigError(format!(
585 "postgres schema rename hint did not match snapshots: {} -> {}",
586 hint.from, hint.to
587 )));
588 }
589 continue;
590 }
591
592 statements.push(format!(
593 "ALTER SCHEMA \"{}\" RENAME TO \"{}\";",
594 hint.from, hint.to
595 ));
596 apply_postgres_schema_rename(prev, &hint.from, &hint.to);
597 }
598
599 for hint in &options.renames.table_renames {
600 let schema = hint.schema.as_deref().unwrap_or("public");
601 if !valid_rename_name(schema)
602 || !valid_rename_name(&hint.from)
603 || !valid_rename_name(&hint.to)
604 || hint.from == hint.to
605 {
606 if options.strict_renames {
607 return Err(MigrationError::ConfigError(format!(
608 "invalid postgres table rename hint: {}.{} -> {}",
609 schema, hint.from, hint.to
610 )));
611 }
612 continue;
613 }
614
615 let can_apply = prev.tables.one(schema, &hint.from).is_some()
616 && cur.tables.one(schema, &hint.to).is_some()
617 && prev.tables.one(schema, &hint.to).is_none();
618
619 if !can_apply {
620 if options.strict_renames {
621 return Err(MigrationError::ConfigError(format!(
622 "postgres table rename hint did not match snapshots: {}.{} -> {}",
623 schema, hint.from, hint.to
624 )));
625 }
626 continue;
627 }
628
629 statements.push(format!(
630 "ALTER TABLE \"{}\".\"{}\" RENAME TO \"{}\";",
631 schema, hint.from, hint.to
632 ));
633 apply_postgres_table_rename(prev, schema, &hint.from, &hint.to);
634 }
635
636 for hint in &options.renames.column_renames {
637 let schema = hint.schema.as_deref().unwrap_or("public");
638 if !valid_rename_name(schema)
639 || !valid_rename_name(&hint.table)
640 || !valid_rename_name(&hint.from)
641 || !valid_rename_name(&hint.to)
642 || hint.from == hint.to
643 {
644 if options.strict_renames {
645 return Err(MigrationError::ConfigError(format!(
646 "invalid postgres column rename hint: {}.{}.{} -> {}",
647 schema, hint.table, hint.from, hint.to
648 )));
649 }
650 continue;
651 }
652
653 let can_apply = prev.columns.one(schema, &hint.table, &hint.from).is_some()
654 && cur.columns.one(schema, &hint.table, &hint.to).is_some()
655 && prev.columns.one(schema, &hint.table, &hint.to).is_none();
656
657 if !can_apply {
658 if options.strict_renames {
659 return Err(MigrationError::ConfigError(format!(
660 "postgres column rename hint did not match snapshots: {}.{}.{} -> {}",
661 schema, hint.table, hint.from, hint.to
662 )));
663 }
664 continue;
665 }
666
667 statements.push(format!(
668 "ALTER TABLE \"{}\".\"{}\" RENAME COLUMN \"{}\" TO \"{}\";",
669 schema, hint.table, hint.from, hint.to
670 ));
671 apply_postgres_column_rename(prev, schema, &hint.table, &hint.from, &hint.to);
672 }
673
674 Ok(statements)
675}
676
677fn apply_sqlite_table_rename(ddl: &mut SQLiteDDL, from: &str, to: &str) {
678 let to = to.to_string();
679
680 if let Some(t) = ddl
681 .tables
682 .list_mut()
683 .iter_mut()
684 .find(|t| t.name.as_ref() == from)
685 {
686 t.name = to.clone().into();
687 }
688
689 for c in ddl
690 .columns
691 .list_mut()
692 .iter_mut()
693 .filter(|c| c.table.as_ref() == from)
694 {
695 c.table = to.clone().into();
696 }
697
698 for pk in ddl
699 .pks
700 .list_mut()
701 .iter_mut()
702 .filter(|pk| pk.table.as_ref() == from)
703 {
704 pk.table = to.clone().into();
705 }
706
707 for u in ddl
708 .uniques
709 .list_mut()
710 .iter_mut()
711 .filter(|u| u.table.as_ref() == from)
712 {
713 u.table = to.clone().into();
714 }
715
716 for fk in ddl.fks.list_mut().iter_mut() {
717 if fk.table.as_ref() == from {
718 fk.table = to.clone().into();
719 }
720 if fk.table_to.as_ref() == from {
721 fk.table_to = to.clone().into();
722 }
723 }
724
725 for idx in ddl
726 .indexes
727 .list_mut()
728 .iter_mut()
729 .filter(|i| i.table.as_ref() == from)
730 {
731 idx.table = to.clone().into();
732 }
733
734 for chk in ddl
735 .checks
736 .list_mut()
737 .iter_mut()
738 .filter(|c| c.table.as_ref() == from)
739 {
740 chk.table = to.clone().into();
741 }
742}
743
744fn apply_sqlite_column_rename(ddl: &mut SQLiteDDL, table: &str, from: &str, to: &str) {
745 let to = to.to_string();
746
747 if let Some(c) = ddl
748 .columns
749 .list_mut()
750 .iter_mut()
751 .find(|c| c.table.as_ref() == table && c.name.as_ref() == from)
752 {
753 c.name = to.clone().into();
754 }
755
756 for pk in ddl
757 .pks
758 .list_mut()
759 .iter_mut()
760 .filter(|pk| pk.table.as_ref() == table)
761 {
762 for col in pk.columns.to_mut().iter_mut() {
763 if col.as_ref() == from {
764 *col = to.clone().into();
765 }
766 }
767 }
768
769 for u in ddl
770 .uniques
771 .list_mut()
772 .iter_mut()
773 .filter(|u| u.table.as_ref() == table)
774 {
775 for col in u.columns.to_mut().iter_mut() {
776 if col.as_ref() == from {
777 *col = to.clone().into();
778 }
779 }
780 }
781
782 for fk in ddl.fks.list_mut().iter_mut() {
783 if fk.table.as_ref() == table {
784 for col in fk.columns.to_mut().iter_mut() {
785 if col.as_ref() == from {
786 *col = to.clone().into();
787 }
788 }
789 }
790 if fk.table_to.as_ref() == table {
791 for col in fk.columns_to.to_mut().iter_mut() {
792 if col.as_ref() == from {
793 *col = to.clone().into();
794 }
795 }
796 }
797 }
798
799 for idx in ddl
800 .indexes
801 .list_mut()
802 .iter_mut()
803 .filter(|i| i.table.as_ref() == table)
804 {
805 for col in &mut idx.columns {
806 if !col.is_expression && col.value.as_ref() == from {
807 col.value = to.clone().into();
808 }
809 }
810 }
811}
812
813fn rewrite_cow(value: &mut Cow<'static, str>, from: &str, to: &str) {
814 if value.as_ref() == from {
815 *value = to.to_string().into();
816 }
817}
818
819fn rewrite_optional_cow(value: &mut Option<Cow<'static, str>>, from: &str, to: &str) {
820 if value.as_deref() == Some(from) {
821 *value = Some(to.to_string().into());
822 }
823}
824
825fn rewrite_schema_qualified_value(value: &mut Option<Cow<'static, str>>, from: &str, to: &str) {
826 let Some(current) = value.as_deref() else {
827 return;
828 };
829 let Some(rest) = current
830 .strip_prefix(from)
831 .and_then(|rest| rest.strip_prefix('.'))
832 else {
833 return;
834 };
835 *value = Some(format!("{to}.{rest}").into());
836}
837
838fn apply_postgres_schema_rename(ddl: &mut PostgresDDL, from: &str, to: &str) {
839 for schema in ddl.schemas.list_mut() {
840 rewrite_cow(&mut schema.name, from, to);
841 }
842
843 for table in ddl.tables.list_mut() {
844 rewrite_cow(&mut table.schema, from, to);
845 rewrite_schema_qualified_value(&mut table.inherits, from, to);
846 }
847
848 for column in ddl.columns.list_mut() {
849 rewrite_cow(&mut column.schema, from, to);
850 rewrite_optional_cow(&mut column.type_schema, from, to);
851 if let Some(identity) = &mut column.identity {
852 rewrite_optional_cow(&mut identity.schema, from, to);
853 }
854 }
855
856 for index in ddl.indexes.list_mut() {
857 rewrite_cow(&mut index.schema, from, to);
858 }
859
860 for fk in ddl.fks.list_mut() {
861 rewrite_cow(&mut fk.schema, from, to);
862 rewrite_cow(&mut fk.schema_to, from, to);
863 }
864
865 for pk in ddl.pks.list_mut() {
866 rewrite_cow(&mut pk.schema, from, to);
867 }
868
869 for unique in ddl.uniques.list_mut() {
870 rewrite_cow(&mut unique.schema, from, to);
871 }
872
873 for check in ddl.checks.list_mut() {
874 rewrite_cow(&mut check.schema, from, to);
875 }
876
877 for policy in ddl.policies.list_mut() {
878 rewrite_cow(&mut policy.schema, from, to);
879 }
880
881 for enum_ in ddl.enums.list_mut() {
882 rewrite_cow(&mut enum_.schema, from, to);
883 }
884
885 for sequence in ddl.sequences.list_mut() {
886 rewrite_cow(&mut sequence.schema, from, to);
887 }
888
889 for view in ddl.views.list_mut() {
890 rewrite_cow(&mut view.schema, from, to);
891 }
892}
893
894fn apply_postgres_table_rename(ddl: &mut PostgresDDL, schema: &str, from: &str, to: &str) {
895 let to = to.to_string();
896
897 for table in ddl.tables.list_mut() {
898 if table.schema.as_ref() == schema && table.name.as_ref() == from {
899 table.name = to.clone().into();
900 }
901
902 if table.schema.as_ref() == schema
903 && let Some(inherits) = &mut table.inherits
904 {
905 if inherits.as_ref() == from {
906 *inherits = to.clone().into();
907 } else if inherits.as_ref() == format!("{schema}.{from}") {
908 *inherits = format!("{schema}.{to}").into();
909 }
910 }
911 }
912
913 for c in ddl
914 .columns
915 .list_mut()
916 .iter_mut()
917 .filter(|c| c.schema.as_ref() == schema && c.table.as_ref() == from)
918 {
919 c.table = to.clone().into();
920 }
921
922 for pk in ddl
923 .pks
924 .list_mut()
925 .iter_mut()
926 .filter(|pk| pk.schema.as_ref() == schema && pk.table.as_ref() == from)
927 {
928 pk.table = to.clone().into();
929 }
930
931 for u in ddl
932 .uniques
933 .list_mut()
934 .iter_mut()
935 .filter(|u| u.schema.as_ref() == schema && u.table.as_ref() == from)
936 {
937 u.table = to.clone().into();
938 }
939
940 for fk in ddl.fks.list_mut().iter_mut() {
941 if fk.schema.as_ref() == schema && fk.table.as_ref() == from {
942 fk.table = to.clone().into();
943 }
944 if fk.schema_to.as_ref() == schema && fk.table_to.as_ref() == from {
945 fk.table_to = to.clone().into();
946 }
947 }
948
949 for idx in ddl
950 .indexes
951 .list_mut()
952 .iter_mut()
953 .filter(|i| i.schema.as_ref() == schema && i.table.as_ref() == from)
954 {
955 idx.table = to.clone().into();
956 }
957
958 for chk in ddl
959 .checks
960 .list_mut()
961 .iter_mut()
962 .filter(|c| c.schema.as_ref() == schema && c.table.as_ref() == from)
963 {
964 chk.table = to.clone().into();
965 }
966
967 for policy in ddl
968 .policies
969 .list_mut()
970 .iter_mut()
971 .filter(|p| p.schema.as_ref() == schema && p.table.as_ref() == from)
972 {
973 policy.table = to.clone().into();
974 }
975}
976
977fn apply_postgres_column_rename(
978 ddl: &mut PostgresDDL,
979 schema: &str,
980 table: &str,
981 from: &str,
982 to: &str,
983) {
984 let to = to.to_string();
985
986 for c in ddl.columns.list_mut().iter_mut() {
987 if c.schema.as_ref() == schema && c.table.as_ref() == table && c.name.as_ref() == from {
988 c.name = to.clone().into();
989 }
990 }
991
992 for pk in ddl
993 .pks
994 .list_mut()
995 .iter_mut()
996 .filter(|p| p.schema.as_ref() == schema && p.table.as_ref() == table)
997 {
998 for col in pk.columns.to_mut().iter_mut() {
999 if col.as_ref() == from {
1000 *col = to.clone().into();
1001 }
1002 }
1003 }
1004
1005 for u in ddl
1006 .uniques
1007 .list_mut()
1008 .iter_mut()
1009 .filter(|u| u.schema.as_ref() == schema && u.table.as_ref() == table)
1010 {
1011 for col in u.columns.to_mut().iter_mut() {
1012 if col.as_ref() == from {
1013 *col = to.clone().into();
1014 }
1015 }
1016 }
1017
1018 for fk in ddl.fks.list_mut().iter_mut() {
1019 if fk.schema.as_ref() == schema && fk.table.as_ref() == table {
1020 for col in fk.columns.to_mut().iter_mut() {
1021 if col.as_ref() == from {
1022 *col = to.clone().into();
1023 }
1024 }
1025 }
1026 if fk.schema_to.as_ref() == schema && fk.table_to.as_ref() == table {
1027 for col in fk.columns_to.to_mut().iter_mut() {
1028 if col.as_ref() == from {
1029 *col = to.clone().into();
1030 }
1031 }
1032 }
1033 }
1034
1035 for idx in ddl
1036 .indexes
1037 .list_mut()
1038 .iter_mut()
1039 .filter(|i| i.schema.as_ref() == schema && i.table.as_ref() == table)
1040 {
1041 for col in &mut idx.columns {
1042 if !col.is_expression && col.value.as_ref() == from {
1043 col.value = to.clone().into();
1044 }
1045 }
1046 }
1047}
1048
1049fn valid_rename_name(name: &str) -> bool {
1050 !name.trim().is_empty()
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056 use crate::postgres::PostgresSnapshot;
1057 use crate::postgres::ddl::{
1058 Column as PgColumn, PostgresEntity, Schema as PgSchema, Table as PgTable,
1059 };
1060 use crate::schema::Schema as MigrationSchema;
1061 use crate::sqlite::SQLiteSnapshot;
1062 use crate::sqlite::ddl::{Column, SqliteEntity, Table};
1063
1064 #[derive(Default)]
1065 struct EmptySqliteSchema;
1066
1067 impl MigrationSchema for EmptySqliteSchema {
1068 fn dialect(&self) -> drizzle_types::Dialect {
1069 drizzle_types::Dialect::SQLite
1070 }
1071
1072 fn to_snapshot(&self) -> Snapshot {
1073 Snapshot::empty(drizzle_types::Dialect::SQLite)
1074 }
1075 }
1076
1077 #[test]
1078 fn test_generate_empty_to_empty() {
1079 let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1080 let cur = Snapshot::empty(drizzle_types::Dialect::SQLite);
1081 let migration = diff(&prev, &cur).unwrap();
1082 assert!(migration.statements.is_empty());
1083 }
1084
1085 #[test]
1086 fn test_generate_create_table() {
1087 let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1088
1089 let mut cur_snap = SQLiteSnapshot::new();
1090 cur_snap.add_entity(SqliteEntity::Table(Table::new("users")));
1091 cur_snap.add_entity(SqliteEntity::Column(
1092 Column::new("users", "id", "integer").not_null(),
1093 ));
1094 cur_snap.add_entity(SqliteEntity::Column(
1095 Column::new("users", "name", "text").not_null(),
1096 ));
1097 let cur = Snapshot::Sqlite(cur_snap);
1098
1099 let migration = diff(&prev, &cur).unwrap();
1100 assert!(!migration.statements.is_empty());
1101 assert!(migration.statements[0].contains("CREATE TABLE"));
1102 assert!(migration.statements[0].contains("users"));
1103 }
1104
1105 #[test]
1106 fn test_generate_dialect_mismatch() {
1107 let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1108 let cur = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1109 let result = diff(&prev, &cur);
1110 assert!(matches!(result, Err(MigrationError::DialectMismatch)));
1111 }
1112
1113 #[test]
1114 fn test_generate_postgres_empty() {
1115 let prev = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1116 let cur = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1117 let migration = diff(&prev, &cur).unwrap();
1118 assert!(migration.statements.is_empty());
1119 }
1120
1121 #[test]
1122 fn test_diff_schemas_empty() {
1123 let prev = EmptySqliteSchema;
1124 let cur = EmptySqliteSchema;
1125 let migration = diff_schemas(&prev, &cur).unwrap();
1126 assert!(migration.statements.is_empty());
1127 }
1128
1129 #[test]
1130 fn test_diff_with_sqlite_rename_hints() {
1131 let mut prev_snap = SQLiteSnapshot::new();
1132 prev_snap.add_entity(SqliteEntity::Table(Table::new("users")));
1133 prev_snap.add_entity(SqliteEntity::Column(
1134 Column::new("users", "full_name", "text").not_null(),
1135 ));
1136
1137 let mut cur_snap = SQLiteSnapshot::new();
1138 cur_snap.add_entity(SqliteEntity::Table(Table::new("accounts")));
1139 cur_snap.add_entity(SqliteEntity::Column(
1140 Column::new("accounts", "display_name", "text").not_null(),
1141 ));
1142
1143 let prev = Snapshot::Sqlite(prev_snap);
1144 let cur = Snapshot::Sqlite(cur_snap);
1145
1146 let options = DiffOptions::new()
1147 .rename_table("users", "accounts")
1148 .rename_column("accounts", "full_name", "display_name");
1149
1150 let migration = diff_with(&prev, &cur, &options).unwrap();
1151 assert_eq!(
1152 migration.statements,
1153 vec![
1154 "ALTER TABLE `users` RENAME TO `accounts`;".to_string(),
1155 "ALTER TABLE `accounts` RENAME COLUMN `full_name` TO `display_name`;".to_string(),
1156 ]
1157 );
1158 }
1159
1160 #[test]
1161 fn test_diff_with_sqlite_table_rename_hint_and_add_column() {
1162 let mut prev_snap = SQLiteSnapshot::new();
1163 prev_snap.add_entity(SqliteEntity::Table(Table::new("users")));
1164 prev_snap.add_entity(SqliteEntity::Column(
1165 Column::new("users", "id", "integer").not_null(),
1166 ));
1167
1168 let mut cur_snap = SQLiteSnapshot::new();
1169 cur_snap.add_entity(SqliteEntity::Table(Table::new("accounts")));
1170 cur_snap.add_entity(SqliteEntity::Column(
1171 Column::new("accounts", "id", "integer").not_null(),
1172 ));
1173 cur_snap.add_entity(SqliteEntity::Column(Column::new(
1174 "accounts", "email", "text",
1175 )));
1176
1177 let migration = diff_with(
1178 &Snapshot::Sqlite(prev_snap),
1179 &Snapshot::Sqlite(cur_snap),
1180 &DiffOptions::new().rename_table("users", "accounts"),
1181 )
1182 .unwrap();
1183
1184 assert_eq!(
1185 migration.statements,
1186 vec![
1187 "ALTER TABLE `users` RENAME TO `accounts`;".to_string(),
1188 "ALTER TABLE `accounts` ADD `email` TEXT;".to_string(),
1189 ]
1190 );
1191 }
1192
1193 #[test]
1194 fn test_diff_with_postgres_table_rename_hint_and_add_column() {
1195 let mut prev_snap = PostgresSnapshot::new();
1196 prev_snap.add_entity(PostgresEntity::Schema(PgSchema::new("public")));
1197 prev_snap.add_entity(PostgresEntity::Table(PgTable::new("public", "users")));
1198 prev_snap.add_entity(PostgresEntity::Column(
1199 PgColumn::new("public", "users", "id", "integer").not_null(),
1200 ));
1201
1202 let mut cur_snap = PostgresSnapshot::new();
1203 cur_snap.add_entity(PostgresEntity::Schema(PgSchema::new("public")));
1204 cur_snap.add_entity(PostgresEntity::Table(PgTable::new("public", "accounts")));
1205 cur_snap.add_entity(PostgresEntity::Column(
1206 PgColumn::new("public", "accounts", "id", "integer").not_null(),
1207 ));
1208 cur_snap.add_entity(PostgresEntity::Column(PgColumn::new(
1209 "public", "accounts", "email", "text",
1210 )));
1211
1212 let migration = diff_with(
1213 &Snapshot::Postgres(prev_snap),
1214 &Snapshot::Postgres(cur_snap),
1215 &DiffOptions::new().rename_table("users", "accounts"),
1216 )
1217 .unwrap();
1218
1219 assert_eq!(
1220 migration.statements,
1221 vec![
1222 "ALTER TABLE \"public\".\"users\" RENAME TO \"accounts\";".to_string(),
1223 "ALTER TABLE \"accounts\" ADD COLUMN \"email\" text;".to_string(),
1224 ]
1225 );
1226 }
1227
1228 #[test]
1229 fn test_diff_with_strict_rename_hints_errors() {
1230 let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1231 let cur = Snapshot::empty(drizzle_types::Dialect::SQLite);
1232 let options = DiffOptions::new()
1233 .strict_renames(true)
1234 .rename_table("missing_table", "users");
1235
1236 let result = diff_with(&prev, &cur, &options);
1237 assert!(matches!(result, Err(MigrationError::ConfigError(_))));
1238
1239 let prev = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1240 let cur = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1241 let options = DiffOptions::new()
1242 .strict_renames(true)
1243 .rename_schema("missing_schema", "app");
1244
1245 let result = diff_with(&prev, &cur, &options);
1246 assert!(matches!(result, Err(MigrationError::ConfigError(_))));
1247 }
1248}