1use petgraph::Undirected;
4use petgraph::graph::Graph;
5use petgraph::visit::EdgeRef;
6use regex::Regex;
7use std::collections::{BTreeMap, BTreeSet, HashMap};
8use strsim::{jaro_winkler, levenshtein};
9
10use super::model_registry::ManyToManyMetadata;
11
12#[derive(
14 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
15)]
16pub enum ForeignKeyAction {
17 Restrict,
19 Cascade,
21 SetNull,
23 NoAction,
25 SetDefault,
27}
28
29impl ForeignKeyAction {
30 pub fn to_sql_keyword(&self) -> &'static str {
32 match self {
33 ForeignKeyAction::Restrict => "RESTRICT",
34 ForeignKeyAction::Cascade => "CASCADE",
35 ForeignKeyAction::SetNull => "SET NULL",
36 ForeignKeyAction::NoAction => "NO ACTION",
37 ForeignKeyAction::SetDefault => "SET DEFAULT",
38 }
39 }
40}
41
42impl From<ForeignKeyAction> for reinhardt_query::prelude::ForeignKeyAction {
43 fn from(action: ForeignKeyAction) -> Self {
44 match action {
45 ForeignKeyAction::Restrict => reinhardt_query::prelude::ForeignKeyAction::Restrict,
46 ForeignKeyAction::Cascade => reinhardt_query::prelude::ForeignKeyAction::Cascade,
47 ForeignKeyAction::SetNull => reinhardt_query::prelude::ForeignKeyAction::SetNull,
48 ForeignKeyAction::NoAction => reinhardt_query::prelude::ForeignKeyAction::NoAction,
49 ForeignKeyAction::SetDefault => reinhardt_query::prelude::ForeignKeyAction::SetDefault,
50 }
51 }
52}
53
54impl From<reinhardt_query::prelude::ForeignKeyAction> for ForeignKeyAction {
55 fn from(action: reinhardt_query::prelude::ForeignKeyAction) -> Self {
56 match action {
57 reinhardt_query::prelude::ForeignKeyAction::Restrict => ForeignKeyAction::Restrict,
58 reinhardt_query::prelude::ForeignKeyAction::Cascade => ForeignKeyAction::Cascade,
59 reinhardt_query::prelude::ForeignKeyAction::SetNull => ForeignKeyAction::SetNull,
60 reinhardt_query::prelude::ForeignKeyAction::NoAction => ForeignKeyAction::NoAction,
61 reinhardt_query::prelude::ForeignKeyAction::SetDefault => ForeignKeyAction::SetDefault,
62 _ => ForeignKeyAction::NoAction,
64 }
65 }
66}
67
68pub use crate::naming::to_snake_case;
88
89pub fn to_pascal_case(name: &str) -> String {
106 name.split(['_', '.', '-', ' '])
107 .filter(|word| !word.is_empty())
108 .map(|word| {
109 let mut chars = word.chars();
110 match chars.next() {
111 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
112 None => String::new(),
113 }
114 })
115 .collect()
116}
117
118#[derive(Debug, Clone, PartialEq)]
120pub struct ForeignKeyInfo {
121 pub referenced_table: String,
123 pub referenced_column: String,
125 pub on_delete: ForeignKeyAction,
127 pub on_update: ForeignKeyAction,
129}
130
131#[derive(Debug, Clone)]
133pub struct FieldState {
134 pub name: String,
136 pub field_type: super::FieldType,
138 pub nullable: bool,
140 pub params: std::collections::HashMap<String, String>,
142 pub foreign_key: Option<ForeignKeyInfo>,
144}
145
146impl FieldState {
147 pub fn new(name: impl Into<String>, field_type: super::FieldType, nullable: bool) -> Self {
149 Self {
150 name: name.into(),
151 field_type,
152 nullable,
153 params: std::collections::HashMap::new(),
154 foreign_key: None,
155 }
156 }
157
158 pub fn with_foreign_key(
160 name: impl Into<String>,
161 field_type: super::FieldType,
162 nullable: bool,
163 foreign_key: ForeignKeyInfo,
164 ) -> Self {
165 Self {
166 name: name.into(),
167 field_type,
168 nullable,
169 params: std::collections::HashMap::new(),
170 foreign_key: Some(foreign_key),
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
179pub struct ModelState {
180 pub app_label: String,
182 pub name: String,
184 pub table_name: String,
186 pub fields: std::collections::BTreeMap<String, FieldState>,
188 pub options: std::collections::HashMap<String, String>,
190 pub base_model: Option<String>,
192 pub inheritance_type: Option<String>,
194 pub discriminator_column: Option<String>,
196 pub indexes: Vec<IndexDefinition>,
198 pub constraints: Vec<ConstraintDefinition>,
200 pub many_to_many_fields: Vec<ManyToManyMetadata>,
202}
203
204#[derive(Debug, Clone, PartialEq)]
206pub struct IndexDefinition {
207 pub name: String,
209 pub fields: Vec<String>,
211 pub unique: bool,
213 pub where_clause: Option<String>,
215 pub index_type: Option<super::operations::IndexType>,
217 pub expressions: Option<Vec<String>>,
219 pub concurrently: bool,
221 pub mysql_options: Option<super::operations::AlterTableOptions>,
223 pub operator_class: Option<String>,
225}
226
227impl IndexDefinition {
228 fn create_operation(&self, table: &str) -> super::Operation {
229 super::Operation::CreateIndexRepair {
230 table: table.to_string(),
231 name: Some(self.name.clone()),
232 columns: self.fields.clone(),
233 unique: self.unique,
234 index_type: self.index_type,
235 where_clause: self.where_clause.clone(),
236 concurrently: self.concurrently,
237 expressions: self.expressions.clone(),
238 mysql_options: self.mysql_options,
239 operator_class: self.operator_class.clone(),
240 }
241 }
242
243 fn drop_operation(&self, table: &str) -> super::Operation {
244 super::Operation::DropNamedIndex {
245 table: table.to_string(),
246 name: self.name.clone(),
247 columns: self.fields.clone(),
248 unique: self.unique,
249 index_type: self.index_type,
250 where_clause: self.where_clause.clone(),
251 concurrently: self.concurrently,
252 expressions: self.expressions.clone(),
253 mysql_options: self.mysql_options,
254 operator_class: self.operator_class.clone(),
255 }
256 }
257}
258
259const ADVANCED_INDEX_OPTION_PREFIX: &str = "__reinhardt_advanced_index__:";
260
261fn advanced_index_option_key(name: &str) -> String {
262 format!("{ADVANCED_INDEX_OPTION_PREFIX}{name}")
263}
264
265fn model_index_is_advanced(_model: &ModelState, index: &IndexDefinition) -> bool {
266 index.where_clause.is_some()
267 || index.index_type.is_some()
268 || index.expressions.is_some()
269 || index.operator_class.is_some()
270}
271
272fn model_index_definitions_equivalent(
273 left_model: &ModelState,
274 left: &IndexDefinition,
275 right_model: &ModelState,
276 right: &IndexDefinition,
277) -> bool {
278 index_definitions_equivalent(left, right)
279 && (left_model.table_name != right_model.table_name || left.name == right.name)
280 && model_index_is_advanced(left_model, left) == model_index_is_advanced(right_model, right)
281}
282
283pub(crate) fn default_index_name(table: &str, fields: &[String]) -> String {
285 super::operations::generated_index_name(table, fields, None)
286}
287
288pub(crate) fn index_definitions_equivalent(
290 left: &IndexDefinition,
291 right: &IndexDefinition,
292) -> bool {
293 left.fields == right.fields
294 && left.unique == right.unique
295 && left.where_clause == right.where_clause
296 && left.index_type == right.index_type
297 && left.expressions == right.expressions
298 && left.operator_class == right.operator_class
299}
300
301#[derive(Debug, Clone, PartialEq)]
303pub struct ConstraintDefinition {
304 pub name: String,
306 pub constraint_type: String,
308 pub fields: Vec<String>,
310 pub expression: Option<String>,
312 pub foreign_key_info: Option<ForeignKeyConstraintInfo>,
314}
315
316#[derive(Debug, Clone, PartialEq)]
318pub struct ForeignKeyConstraintInfo {
319 pub referenced_table: String,
321 pub referenced_columns: Vec<String>,
323 pub on_delete: ForeignKeyAction,
325 pub on_update: ForeignKeyAction,
327}
328
329fn is_single_field_unique(c: &ConstraintDefinition) -> bool {
337 c.constraint_type.eq_ignore_ascii_case("unique") && c.fields.len() == 1
338}
339
340fn parse_single_column_unique(constraint_sql: &str) -> Option<&str> {
349 let after_unique = constraint_sql.split(" UNIQUE (").nth(1)?;
353 let close = after_unique.find(')')?;
354 let body = after_unique[..close].trim();
355 if body.contains(',') || body.is_empty() {
356 return None;
357 }
358 Some(
359 body.strip_prefix('"')
360 .and_then(|body| body.strip_suffix('"'))
361 .unwrap_or(body),
362 )
363}
364
365impl ConstraintDefinition {
366 pub fn to_constraint(&self) -> super::operations::Constraint {
368 match self.constraint_type.as_str() {
369 unique if unique.eq_ignore_ascii_case("unique") => {
370 super::operations::Constraint::Unique {
371 name: self.name.clone(),
372 columns: self.fields.clone(),
373 }
374 }
375 "check" => super::operations::Constraint::Check {
376 name: self.name.clone(),
377 expression: self.expression.clone().unwrap_or_default(),
378 },
379 "foreign_key" => {
380 if let Some(fk_info) = &self.foreign_key_info {
381 super::operations::Constraint::ForeignKey {
382 name: self.name.clone(),
383 columns: self.fields.clone(),
384 referenced_table: fk_info.referenced_table.clone(),
385 referenced_columns: fk_info.referenced_columns.clone(),
386 on_delete: fk_info.on_delete,
387 on_update: fk_info.on_update,
388 deferrable: None,
389 }
390 } else {
391 super::operations::Constraint::ForeignKey {
393 name: self.name.clone(),
394 columns: self.fields.clone(),
395 referenced_table: String::new(),
396 referenced_columns: vec!["id".to_string()],
397 on_delete: ForeignKeyAction::Cascade,
398 on_update: ForeignKeyAction::Cascade,
399 deferrable: None,
400 }
401 }
402 }
403 "one_to_one" => {
404 if let Some(fk_info) = &self.foreign_key_info {
405 super::operations::Constraint::OneToOne {
406 name: self.name.clone(),
407 column: self.fields.first().cloned().unwrap_or_default(),
408 referenced_table: fk_info.referenced_table.clone(),
409 referenced_column: fk_info
410 .referenced_columns
411 .first()
412 .cloned()
413 .unwrap_or_else(|| "id".to_string()),
414 on_delete: fk_info.on_delete,
415 on_update: fk_info.on_update,
416 deferrable: None,
417 }
418 } else {
419 super::operations::Constraint::OneToOne {
421 name: self.name.clone(),
422 column: self.fields.first().cloned().unwrap_or_default(),
423 referenced_table: String::new(),
424 referenced_column: "id".to_string(),
425 on_delete: ForeignKeyAction::Cascade,
426 on_update: ForeignKeyAction::Cascade,
427 deferrable: None,
428 }
429 }
430 }
431 _ => {
432 super::operations::Constraint::Check {
434 name: self.name.clone(),
435 expression: self.expression.clone().unwrap_or_default(),
436 }
437 }
438 }
439 }
440}
441
442impl ModelState {
443 pub fn new(app_label: impl Into<String>, name: impl Into<String>) -> Self {
457 let name_str = name.into();
458 let table_name = to_snake_case(&name_str);
460
461 Self {
462 app_label: app_label.into(),
463 name: name_str,
464 table_name,
465 fields: std::collections::BTreeMap::new(),
466 options: std::collections::HashMap::new(),
467 base_model: None,
468 inheritance_type: None,
469 discriminator_column: None,
470 indexes: Vec::new(),
471 constraints: Vec::new(),
472 many_to_many_fields: Vec::new(),
473 }
474 }
475
476 pub fn add_field(&mut self, field: FieldState) {
490 self.fields.insert(field.name.clone(), field);
491 }
492
493 pub fn get_field(&self, name: &str) -> Option<&FieldState> {
509 self.fields.get(name)
510 }
511
512 pub fn has_field(&self, name: &str) -> bool {
527 self.fields.contains_key(name)
528 }
529
530 pub fn rename_field(&mut self, old_name: &str, new_name: String) {
546 if let Some(mut field) = self.fields.remove(old_name) {
547 field.name = new_name.clone();
548 self.fields.insert(new_name, field);
549 }
550 }
551
552 pub fn add_constraint(&mut self, constraint: ConstraintDefinition) {
571 self.constraints.push(constraint);
572 }
573
574 pub fn add_foreign_key_constraint_from_field(&mut self, field_name: &str) {
576 if let Some(field) = self.fields.get(field_name)
577 && let Some(ref fk_info) = field.foreign_key
578 {
579 let constraint = ConstraintDefinition {
580 name: format!("fk_{}_{}", self.table_name, field_name),
581 constraint_type: "foreign_key".to_string(),
582 fields: vec![field_name.to_string()],
583 expression: None,
584 foreign_key_info: Some(ForeignKeyConstraintInfo {
585 referenced_table: fk_info.referenced_table.clone(),
586 referenced_columns: vec![fk_info.referenced_column.clone()],
587 on_delete: fk_info.on_delete,
588 on_update: fk_info.on_update,
589 }),
590 };
591 self.add_constraint(constraint);
592 }
593 }
594}
595
596#[derive(Debug, Clone)]
613pub struct ProjectState {
614 pub models: std::collections::BTreeMap<(String, String), ModelState>,
616}
617
618impl Default for ProjectState {
619 fn default() -> Self {
620 Self::new()
621 }
622}
623
624impl ProjectState {
625 pub fn to_database_schema(&self) -> super::schema_diff::DatabaseSchema {
627 let mut tables = BTreeMap::new();
628
629 for ((app_label, model_name), model_state) in &self.models {
630 let mut columns = BTreeMap::new();
631 for (field_name, field_state) in &model_state.fields {
632 let data_type = field_state.field_type.clone();
636 let nullable = field_state.nullable;
637 let primary_key = field_state
638 .params
639 .get("primary_key")
640 .is_some_and(|s| s == "true");
641 let auto_increment = field_state
642 .params
643 .get("auto_increment")
644 .is_some_and(|s| s == "true");
645 let default = field_state.params.get("default").cloned();
646
647 columns.insert(
648 field_name.clone(),
649 super::schema_diff::ColumnSchema {
650 name: field_name.clone(),
651 data_type,
652 nullable,
653 default,
654 primary_key,
655 auto_increment,
656 },
657 );
658 }
659 let constraints: Vec<super::schema_diff::ConstraintSchema> = model_state
661 .constraints
662 .iter()
663 .map(|c| super::schema_diff::ConstraintSchema {
664 name: c.name.clone(),
665 constraint_type: c.constraint_type.clone(),
666 definition: c.fields.join(", "),
667 foreign_key_info: None,
668 })
669 .collect();
670
671 let indexes: Vec<super::schema_diff::IndexSchema> = model_state
673 .indexes
674 .iter()
675 .map(|idx| super::schema_diff::IndexSchema {
676 name: idx.name.clone(),
677 columns: idx.fields.clone(),
678 unique: idx.unique,
679 })
680 .collect();
681
682 let table_key = format!("{}_{}", app_label, model_name.to_lowercase());
685 tables.insert(
686 table_key,
687 super::schema_diff::TableSchema {
688 name: model_state.table_name.clone(),
689 columns,
690 indexes,
691 constraints,
692 },
693 );
694 }
695
696 super::schema_diff::DatabaseSchema { tables }
697 }
698
699 pub fn to_database_schema_for_app(
714 &self,
715 app_label: &str,
716 ) -> super::schema_diff::DatabaseSchema {
717 let mut tables = BTreeMap::new();
718
719 for ((this_app_label, model_name), model_state) in &self.models {
720 if this_app_label == app_label {
722 let mut columns = BTreeMap::new();
723 for (field_name, field_state) in &model_state.fields {
724 let data_type = field_state.field_type.clone();
725 let nullable = field_state.nullable;
726 let primary_key = field_state
727 .params
728 .get("primary_key")
729 .is_some_and(|s| s == "true");
730 let auto_increment = field_state
731 .params
732 .get("auto_increment")
733 .is_some_and(|s| s == "true");
734 let default = field_state.params.get("default").cloned();
735
736 columns.insert(
737 field_name.clone(),
738 super::schema_diff::ColumnSchema {
739 name: field_name.clone(),
740 data_type,
741 nullable,
742 default,
743 primary_key,
744 auto_increment,
745 },
746 );
747 }
748
749 let constraints: Vec<super::schema_diff::ConstraintSchema> = model_state
751 .constraints
752 .iter()
753 .map(|c| super::schema_diff::ConstraintSchema {
754 name: c.name.clone(),
755 constraint_type: c.constraint_type.clone(),
756 definition: c.fields.join(", "),
757 foreign_key_info: None,
758 })
759 .collect();
760
761 let indexes: Vec<super::schema_diff::IndexSchema> = model_state
763 .indexes
764 .iter()
765 .map(|idx| super::schema_diff::IndexSchema {
766 name: idx.name.clone(),
767 columns: idx.fields.clone(),
768 unique: idx.unique,
769 })
770 .collect();
771
772 let table_key = format!("{}_{}", this_app_label, model_name.to_lowercase());
775 tables.insert(
776 table_key,
777 super::schema_diff::TableSchema {
778 name: model_state.table_name.clone(),
779 columns,
780 indexes,
781 constraints,
782 },
783 );
784 }
785 }
786
787 super::schema_diff::DatabaseSchema { tables }
788 }
789
790 pub fn new() -> Self {
801 Self {
802 models: std::collections::BTreeMap::new(),
803 }
804 }
805
806 pub fn add_model(&mut self, model: ModelState) {
821 let key = (model.app_label.clone(), model.name.clone());
822 self.models.insert(key, model);
823 }
824
825 pub fn get_model(&self, app_label: &str, model_name: &str) -> Option<&ModelState> {
841 self.models
842 .get(&(app_label.to_string(), model_name.to_string()))
843 }
844
845 pub fn get_model_mut(&mut self, app_label: &str, model_name: &str) -> Option<&mut ModelState> {
864 self.models
865 .get_mut(&(app_label.to_string(), model_name.to_string()))
866 }
867
868 fn get_primary_key_type(&self, app_label: &str, model_name: &str) -> super::FieldType {
888 if let Some(model_state) = self.get_model(app_label, model_name) {
890 if let Some((_, id_field)) = model_state
892 .fields
893 .iter()
894 .find(|(name, _)| name.as_str() == "id")
895 {
896 return id_field.field_type.clone();
897 }
898
899 if let Some((_, pk_field)) = model_state
901 .fields
902 .iter()
903 .find(|(_, f)| f.params.get("primary_key").map(String::as_str) == Some("true"))
904 {
905 return pk_field.field_type.clone();
906 }
907 }
908
909 if let Some(model_meta) =
911 super::model_registry::global_registry().get_model(app_label, model_name)
912 {
913 if let Some(id_field) = model_meta.fields.get("id") {
915 return id_field.field_type.clone();
916 }
917
918 for field_meta in model_meta.fields.values() {
920 if field_meta.params.get("primary_key").map(String::as_str) == Some("true") {
921 return field_meta.field_type.clone();
922 }
923 }
924 }
925
926 super::FieldType::Uuid
928 }
929
930 pub fn get_model_by_table_name(
947 &self,
948 app_label: &str,
949 table_name: &str,
950 ) -> Option<&ModelState> {
951 self.models
952 .values()
953 .find(|model| model.app_label == app_label && model.table_name == table_name)
954 }
955
956 pub fn filter_by_app(&self, app_label: &str) -> Self {
977 let mut filtered = Self::new();
978 for ((app, _model_name), model_state) in &self.models {
979 if app == app_label {
980 filtered.add_model(model_state.clone());
981 }
982 }
983 filtered
984 }
985
986 pub fn remove_model(&mut self, app_label: &str, model_name: &str) -> Option<ModelState> {
1001 self.models
1002 .remove(&(app_label.to_string(), model_name.to_string()))
1003 }
1004
1005 pub fn rename_model(&mut self, app_label: &str, old_name: &str, new_name: String) {
1021 if let Some(mut model) = self
1022 .models
1023 .remove(&(app_label.to_string(), old_name.to_string()))
1024 {
1025 model.name = new_name.clone();
1026 self.models.insert((app_label.to_string(), new_name), model);
1027 }
1028 }
1029
1030 pub fn from_global_registry() -> Self {
1043 use super::model_registry::global_registry;
1044
1045 let registry = global_registry();
1046 let models_metadata = registry.get_models();
1047
1048 let mut state = ProjectState::new();
1049 let mut intermediate_tables = Vec::new();
1050
1051 for metadata in &models_metadata {
1053 let model_state = metadata.to_model_state();
1054 state.add_model(model_state);
1055 }
1056
1057 for metadata in &models_metadata {
1059 for m2m in &metadata.many_to_many_fields {
1060 let intermediate_table = state.create_intermediate_table_for_m2m(
1062 &metadata.app_label,
1063 &metadata.model_name,
1064 &metadata.table_name,
1065 m2m,
1066 );
1067 intermediate_tables.push(intermediate_table);
1068 }
1069 }
1070
1071 for table in intermediate_tables {
1073 state.add_model(table);
1074 }
1075
1076 state
1077 }
1078
1079 fn create_intermediate_table_for_m2m(
1102 &self,
1103 source_app_label: &str,
1104 source_model_name: &str,
1105 source_table_name: &str,
1106 m2m: &super::model_registry::ManyToManyMetadata,
1107 ) -> ModelState {
1108 let table_name = m2m.through.clone().unwrap_or_else(|| {
1114 crate::m2m_naming::default_through_table(source_table_name, &m2m.field_name)
1115 });
1116
1117 let model_name = format!("{}{}", source_model_name, to_pascal_case(&m2m.field_name));
1120
1121 let mut model_state = ModelState::new(source_app_label, &model_name);
1122 model_state.table_name = table_name.clone();
1123
1124 let mut id_field = FieldState::new("id".to_string(), super::FieldType::Integer, false);
1126 id_field
1127 .params
1128 .insert("primary_key".to_string(), "true".to_string());
1129 id_field
1130 .params
1131 .insert("auto_increment".to_string(), "true".to_string());
1132 model_state.add_field(id_field);
1133
1134 let source_pk_type = self.get_primary_key_type(source_app_label, source_model_name);
1136 let (target_app, target_model) =
1138 self.resolve_model_reference(&m2m.to_model, source_app_label);
1139
1140 let target_pk_type = self.get_primary_key_type(&target_app, &target_model);
1141
1142 let target_table_name = self
1145 .get_model(&target_app, &target_model)
1146 .map(|m| m.table_name.clone())
1147 .unwrap_or_else(|| format!("{}_{}", target_app, to_snake_case(&target_model)));
1148
1149 let (default_source_col, default_target_col) =
1158 crate::m2m_naming::default_m2m_columns(source_table_name, &target_table_name);
1159 let source_field_name = m2m.source_field.clone().unwrap_or(default_source_col);
1160 let target_field_name = m2m.target_field.clone().unwrap_or(default_target_col);
1161
1162 let mut from_field =
1164 FieldState::new(source_field_name.clone(), source_pk_type.clone(), false);
1165 from_field
1166 .params
1167 .insert("not_null".to_string(), "true".to_string());
1168 from_field.foreign_key = Some(ForeignKeyInfo {
1169 referenced_table: source_table_name.to_string(),
1170 referenced_column: "id".to_string(),
1171 on_delete: ForeignKeyAction::Cascade,
1172 on_update: ForeignKeyAction::Cascade,
1173 });
1174 model_state.add_field(from_field);
1175
1176 let mut to_field = FieldState::new(target_field_name.clone(), target_pk_type, false);
1178 to_field
1179 .params
1180 .insert("not_null".to_string(), "true".to_string());
1181 to_field.foreign_key = Some(ForeignKeyInfo {
1182 referenced_table: target_table_name,
1183 referenced_column: "id".to_string(),
1184 on_delete: ForeignKeyAction::Cascade,
1185 on_update: ForeignKeyAction::Cascade,
1186 });
1187 model_state.add_field(to_field);
1188
1189 model_state.add_foreign_key_constraint_from_field(&source_field_name);
1191 model_state.add_foreign_key_constraint_from_field(&target_field_name);
1192
1193 let unique_constraint = ConstraintDefinition {
1195 name: format!("{}_unique", table_name),
1196 constraint_type: "unique".to_string(),
1197 fields: vec![source_field_name, target_field_name],
1198 expression: None,
1199 foreign_key_info: None,
1200 };
1201 model_state.constraints.push(unique_constraint);
1202
1203 model_state
1204 }
1205
1206 fn resolve_model_reference(&self, reference: &str, current_app: &str) -> (String, String) {
1207 let parts: Vec<&str> = reference.split('.').collect();
1208 match parts.as_slice() {
1209 [app, model] => (app.to_string(), model.to_string()),
1210 [model] => {
1211 let model = model.to_string();
1212 if self.get_model(current_app, &model).is_some() {
1213 (current_app.to_string(), model)
1214 } else {
1215 let app = self
1216 .models
1217 .keys()
1218 .find_map(|(app_label, model_name)| {
1219 (model_name == &model).then(|| app_label.clone())
1220 })
1221 .or_else(|| {
1222 super::model_registry::global_registry()
1223 .get_models()
1224 .iter()
1225 .find(|metadata| metadata.model_name == model)
1226 .map(|metadata| metadata.app_label.clone())
1227 })
1228 .unwrap_or_else(|| current_app.to_string());
1229 (app, model)
1230 }
1231 }
1232 _ => (current_app.to_string(), reference.to_string()),
1233 }
1234 }
1235
1236 pub fn from_migrations(migrations: &[super::migration::Migration]) -> Self {
1252 let mut state = Self::new();
1253 for migration in migrations {
1254 state.apply_migration_operations(&migration.operations, &migration.app_label);
1255 }
1256 state
1257 }
1258
1259 pub fn apply_migration_operations(
1273 &mut self,
1274 operations: &[super::operations::Operation],
1275 app_label: &str,
1276 ) {
1277 use super::operations::Operation;
1278
1279 for op in operations {
1280 match op {
1281 Operation::CreateTable {
1282 name,
1283 columns,
1284 constraints,
1285 ..
1286 } => {
1287 let model_name = Self::table_name_to_model_name(name, app_label);
1291 let mut model = ModelState::new(app_label, model_name);
1292 model.table_name = name.to_string();
1293
1294 for col in columns {
1296 let field = self.column_def_to_field_state(col);
1297 model.add_field(field);
1298 }
1299 for constraint in constraints {
1300 model
1301 .constraints
1302 .push(Self::constraint_to_definition(constraint));
1303 }
1304
1305 self.add_model(model);
1306 }
1307 Operation::CreateIndex {
1308 table,
1309 columns,
1310 unique,
1311 index_type,
1312 where_clause,
1313 concurrently,
1314 expressions,
1315 mysql_options,
1316 operator_class,
1317 } => {
1318 let name = super::operations::generated_index_name(
1319 table,
1320 columns,
1321 expressions.as_deref(),
1322 );
1323 let index = IndexDefinition {
1324 name,
1325 fields: columns.clone(),
1326 unique: *unique,
1327 where_clause: where_clause.clone(),
1328 index_type: *index_type,
1329 expressions: expressions.clone(),
1330 concurrently: *concurrently,
1331 mysql_options: *mysql_options,
1332 operator_class: operator_class.clone(),
1333 };
1334 let is_advanced = where_clause.is_some()
1335 || index_type.is_some()
1336 || expressions.is_some()
1337 || operator_class.is_some();
1338 if let Some(model) = self.find_model_by_table_mut(table)
1339 && !model.indexes.iter().any(|existing| {
1340 index_definitions_equivalent(existing, &index)
1341 && model_index_is_advanced(model, existing) == is_advanced
1342 }) {
1343 model.indexes.push(index.clone());
1344 if is_advanced {
1345 model
1346 .options
1347 .insert(advanced_index_option_key(&index.name), "true".to_string());
1348 }
1349 }
1350 }
1351 Operation::CreateIndexRepair {
1352 table,
1353 name,
1354 columns,
1355 unique,
1356 index_type,
1357 where_clause,
1358 concurrently,
1359 expressions,
1360 mysql_options,
1361 operator_class,
1362 } => {
1363 let name = name.clone().unwrap_or_else(|| {
1364 super::operations::generated_index_name(
1365 table,
1366 columns,
1367 expressions.as_deref(),
1368 )
1369 });
1370 let index = IndexDefinition {
1371 name,
1372 fields: columns.clone(),
1373 unique: *unique,
1374 where_clause: where_clause.clone(),
1375 index_type: *index_type,
1376 expressions: expressions.clone(),
1377 concurrently: *concurrently,
1378 mysql_options: *mysql_options,
1379 operator_class: operator_class.clone(),
1380 };
1381 if let Some(model) = self.find_model_by_table_mut(table)
1382 && !model
1383 .indexes
1384 .iter()
1385 .any(|existing| index_definitions_equivalent(existing, &index))
1386 {
1387 model.indexes.push(index);
1388 }
1389 }
1390 Operation::DropIndex { table, columns } => {
1391 if let Some(model) = self.find_model_by_table_mut(table) {
1392 let generated_name =
1393 super::operations::generated_index_name(table, columns, None);
1394 model.indexes.retain(|index| index.name != generated_name);
1395 model
1396 .options
1397 .remove(&advanced_index_option_key(&generated_name));
1398 }
1399 }
1400 Operation::DropNamedIndex { table, name, .. } => {
1401 if let Some(model) = self.find_model_by_table_mut(table) {
1402 model.indexes.retain(|index| index.name != *name);
1403 model.options.remove(&advanced_index_option_key(name));
1404 }
1405 }
1406 Operation::DropTable { name } => {
1407 let keys_to_remove: Vec<_> = self
1409 .models
1410 .iter()
1411 .filter(|(_, model)| model.table_name == *name)
1412 .map(|(key, _)| key.clone())
1413 .collect();
1414
1415 for key in keys_to_remove {
1416 self.models.remove(&key);
1417 }
1418 }
1419 Operation::AddColumn { table, column, .. } => {
1420 let field = self.column_def_to_field_state(column);
1422 if let Some(model) = self.find_model_by_table_mut(table) {
1423 model.add_field(field);
1424 }
1425 }
1426 Operation::DropColumn { table, column } => {
1427 if let Some(model) = self.find_model_by_table_mut(table) {
1429 let removed_names: Vec<_> = model
1430 .indexes
1431 .iter()
1432 .filter(|index| Self::index_definition_references_column(index, column))
1433 .map(|index| index.name.clone())
1434 .collect();
1435 model.fields.remove(column);
1436 model.indexes.retain(|index| {
1437 !Self::index_definition_references_column(index, column)
1438 });
1439 for name in removed_names {
1440 model.options.remove(&advanced_index_option_key(&name));
1441 }
1442 model.constraints.retain(|constraint| {
1443 !constraint.fields.iter().any(|field| field == column)
1444 });
1445 }
1446 }
1447 Operation::AlterColumn {
1448 table,
1449 column,
1450 new_definition,
1451 ..
1452 } => {
1453 let new_field = self.column_def_to_field_state(new_definition);
1455 let mut updated_field = new_field;
1457 updated_field.name = column.to_string();
1458
1459 if let Some(model) = self.find_model_by_table_mut(table) {
1461 model.fields.insert(column.to_string(), updated_field);
1462 } else {
1463 let model_name = Self::table_name_to_model_name(table, app_label);
1467 let mut model = ModelState::new(app_label, model_name);
1468 model.table_name = table.to_string();
1469 model.add_field(updated_field);
1470 self.add_model(model);
1471 }
1472 }
1473 Operation::RenameTable { old_name, new_name } => {
1474 if let Some(model) = self.find_model_by_table_mut(old_name) {
1476 let advanced_index_renames: Vec<_> = model
1477 .indexes
1478 .iter()
1479 .filter(|index| model_index_is_advanced(model, index))
1480 .map(|index| {
1481 let old_default = default_index_name(old_name, &index.fields);
1482 let old_legacy =
1483 format!("{}_{}_idx", old_name, index.fields.join("_"));
1484 let renamed_index_name =
1485 if index.name == old_default || index.name == old_legacy {
1486 default_index_name(new_name, &index.fields)
1487 } else {
1488 index.name.clone()
1489 };
1490 (index.name.clone(), renamed_index_name)
1491 })
1492 .collect();
1493 for index in &mut model.indexes {
1494 let old_default = default_index_name(old_name, &index.fields);
1495 let old_legacy = format!("{}_{}_idx", old_name, index.fields.join("_"));
1496 if index.name == old_default || index.name == old_legacy {
1497 index.name = default_index_name(new_name, &index.fields);
1498 }
1499 }
1500 for (old_index_name, new_index_name) in advanced_index_renames {
1501 model
1502 .options
1503 .remove(&advanced_index_option_key(&old_index_name));
1504 model.options.insert(
1505 advanced_index_option_key(&new_index_name),
1506 "true".to_string(),
1507 );
1508 }
1509 model.table_name = new_name.to_string();
1510 }
1511 }
1512 Operation::RenameColumn {
1513 table,
1514 old_name,
1515 new_name,
1516 } => {
1517 if let Some(model) = self.find_model_by_table_mut(table) {
1519 let advanced_index_renames: Vec<_> = model
1520 .indexes
1521 .iter()
1522 .filter(|index| model_index_is_advanced(model, index))
1523 .map(|index| {
1524 let old_fields = index.fields.clone();
1525 let old_default = default_index_name(table, &old_fields);
1526 let old_legacy = format!("{}_{}_idx", table, old_fields.join("_"));
1527 let mut new_fields = old_fields.clone();
1528 for field in &mut new_fields {
1529 if field == old_name {
1530 *field = new_name.clone();
1531 }
1532 }
1533 let new_index_name =
1534 if index.name == old_default || index.name == old_legacy {
1535 default_index_name(table, &new_fields)
1536 } else {
1537 index.name.clone()
1538 };
1539 (index.name.clone(), new_index_name)
1540 })
1541 .collect();
1542 model.rename_field(old_name, new_name.to_string());
1543 for index in &mut model.indexes {
1544 if !index.fields.iter().any(|field| field == old_name) {
1545 continue;
1546 }
1547 let old_fields = index.fields.clone();
1548 let old_default = default_index_name(table, &old_fields);
1549 let old_legacy = format!("{}_{}_idx", table, old_fields.join("_"));
1550 for field in &mut index.fields {
1551 if field == old_name {
1552 *field = new_name.to_string();
1553 }
1554 }
1555 if index.name == old_default || index.name == old_legacy {
1556 index.name = default_index_name(table, &index.fields);
1557 }
1558 }
1559 for (old_index_name, new_index_name) in advanced_index_renames {
1560 model
1561 .options
1562 .remove(&advanced_index_option_key(&old_index_name));
1563 model.options.insert(
1564 advanced_index_option_key(&new_index_name),
1565 "true".to_string(),
1566 );
1567 }
1568 for constraint in &mut model.constraints {
1569 for field in &mut constraint.fields {
1570 if field == old_name {
1571 *field = new_name.to_string();
1572 }
1573 }
1574 }
1575 }
1576 }
1577 Operation::AddConstraint {
1578 table,
1579 constraint_sql,
1580 } => {
1581 if let Some(model) = self.find_model_by_table_mut(table)
1582 && let Some(constraint) =
1583 Self::constraint_definition_from_sql(constraint_sql)
1584 && !model.constraints.iter().any(|c| c.name == constraint.name)
1585 {
1586 model.constraints.push(constraint);
1587 }
1588 }
1589 Operation::DropConstraint {
1590 table,
1591 constraint_name,
1592 } => {
1593 if let Some(model) = self.find_model_by_table_mut(table) {
1594 model
1595 .constraints
1596 .retain(|constraint| constraint.name != *constraint_name);
1597 }
1598 }
1599 _ => {
1601 }
1604 }
1605 }
1606 }
1607
1608 fn index_definition_references_column(index: &IndexDefinition, column: &str) -> bool {
1609 index.fields.iter().any(|field| field == column)
1610 || index.expressions.as_deref().is_some_and(|expressions| {
1611 expressions
1612 .iter()
1613 .any(|expression| Self::expression_references_column(expression, column))
1614 }) || index
1615 .where_clause
1616 .as_deref()
1617 .is_some_and(|where_clause| Self::expression_references_column(where_clause, column))
1618 }
1619
1620 fn expression_references_column(expression: &str, column: &str) -> bool {
1621 let mut token = String::new();
1622 let mut in_string = false;
1623 let mut characters = expression.chars().peekable();
1624
1625 while let Some(character) = characters.next() {
1626 if character == '\'' {
1627 in_string = !in_string;
1628 if !in_string {
1629 token.clear();
1630 }
1631 continue;
1632 }
1633 if in_string {
1634 continue;
1635 }
1636 if character.is_ascii_alphanumeric() || character == '_' {
1637 token.push(character);
1638 } else if !token.is_empty() {
1639 let is_function_call = character == '('
1640 || (character.is_ascii_whitespace()
1641 && characters
1642 .clone()
1643 .find(|next| !next.is_ascii_whitespace())
1644 .is_some_and(|next| next == '('));
1645 if !is_function_call && token.eq_ignore_ascii_case(column) {
1646 return true;
1647 }
1648 token.clear();
1649 }
1650 }
1651
1652 !token.is_empty() && token.eq_ignore_ascii_case(column)
1653 }
1654
1655 pub fn find_model_by_table(&self, table_name: &str) -> Option<&ModelState> {
1657 self.models
1658 .values()
1659 .find(|model| model.table_name == table_name)
1660 }
1661
1662 pub fn find_model_by_table_mut(&mut self, table_name: &str) -> Option<&mut ModelState> {
1664 self.models
1665 .values_mut()
1666 .find(|model| model.table_name == table_name)
1667 }
1668
1669 fn table_name_to_model_name(table_name: &str, app_label: &str) -> String {
1678 let prefix = format!("{}_", app_label);
1680 let name_without_prefix = if table_name.starts_with(&prefix) {
1681 &table_name[prefix.len()..]
1682 } else {
1683 table_name
1684 };
1685
1686 name_without_prefix
1688 .split('_')
1689 .map(|word| {
1690 let mut chars = word.chars();
1691 match chars.next() {
1692 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1693 None => String::new(),
1694 }
1695 })
1696 .collect()
1697 }
1698
1699 fn constraint_to_definition(
1700 constraint: &super::operations::Constraint,
1701 ) -> ConstraintDefinition {
1702 match constraint {
1703 super::operations::Constraint::PrimaryKey { name, columns } => ConstraintDefinition {
1704 name: name.clone(),
1705 constraint_type: "primary_key".to_string(),
1706 fields: columns.clone(),
1707 expression: None,
1708 foreign_key_info: None,
1709 },
1710 super::operations::Constraint::ForeignKey {
1711 name,
1712 columns,
1713 referenced_table,
1714 referenced_columns,
1715 on_delete,
1716 on_update,
1717 ..
1718 } => ConstraintDefinition {
1719 name: name.clone(),
1720 constraint_type: "foreign_key".to_string(),
1721 fields: columns.clone(),
1722 expression: None,
1723 foreign_key_info: Some(ForeignKeyConstraintInfo {
1724 referenced_table: referenced_table.clone(),
1725 referenced_columns: referenced_columns.clone(),
1726 on_delete: *on_delete,
1727 on_update: *on_update,
1728 }),
1729 },
1730 super::operations::Constraint::Unique { name, columns } => ConstraintDefinition {
1731 name: name.clone(),
1732 constraint_type: "unique".to_string(),
1733 fields: columns.clone(),
1734 expression: None,
1735 foreign_key_info: None,
1736 },
1737 super::operations::Constraint::Check { name, expression } => ConstraintDefinition {
1738 name: name.clone(),
1739 constraint_type: "check".to_string(),
1740 fields: Vec::new(),
1741 expression: Some(expression.clone()),
1742 foreign_key_info: None,
1743 },
1744 super::operations::Constraint::OneToOne {
1745 name,
1746 column,
1747 referenced_table,
1748 referenced_column,
1749 on_delete,
1750 on_update,
1751 ..
1752 } => ConstraintDefinition {
1753 name: name.clone(),
1754 constraint_type: "one_to_one".to_string(),
1755 fields: vec![column.clone()],
1756 expression: None,
1757 foreign_key_info: Some(ForeignKeyConstraintInfo {
1758 referenced_table: referenced_table.clone(),
1759 referenced_columns: vec![referenced_column.clone()],
1760 on_delete: *on_delete,
1761 on_update: *on_update,
1762 }),
1763 },
1764 super::operations::Constraint::ManyToMany {
1765 name,
1766 source_column,
1767 target_column,
1768 ..
1769 } => ConstraintDefinition {
1770 name: name.clone(),
1771 constraint_type: "many_to_many".to_string(),
1772 fields: vec![source_column.clone(), target_column.clone()],
1773 expression: None,
1774 foreign_key_info: None,
1775 },
1776 super::operations::Constraint::Exclude { name, elements, .. } => ConstraintDefinition {
1777 name: name.clone(),
1778 constraint_type: "exclude".to_string(),
1779 fields: elements.iter().map(|(field, _)| field.clone()).collect(),
1780 expression: None,
1781 foreign_key_info: None,
1782 },
1783 }
1784 }
1785
1786 fn trim_sql_identifier(identifier: &str) -> String {
1787 let trimmed = identifier.trim();
1788 let Some(quote) = trimmed.chars().next() else {
1789 return String::new();
1790 };
1791 let stripped = match quote {
1792 '"' => trimmed
1793 .strip_prefix('"')
1794 .and_then(|value| value.strip_suffix('"')),
1795 '`' => trimmed
1796 .strip_prefix('`')
1797 .and_then(|value| value.strip_suffix('`')),
1798 '\'' => trimmed
1799 .strip_prefix('\'')
1800 .and_then(|value| value.strip_suffix('\'')),
1801 _ => None,
1802 };
1803 if let Some(stripped) = stripped {
1804 stripped.replace(&format!("{quote}{quote}"), "e.to_string())
1805 } else {
1806 trimmed.to_string()
1807 }
1808 }
1809
1810 fn parse_constraint_identifier_list(identifier_list: &str) -> Vec<String> {
1811 let mut identifiers = Vec::new();
1812 let mut current = String::new();
1813 let mut quote = None;
1814 let mut depth = 0usize;
1815 let mut chars = identifier_list.chars().peekable();
1816
1817 while let Some(character) = chars.next() {
1818 if let Some(quote_char) = quote {
1819 current.push(character);
1820 if character == quote_char {
1821 if chars.peek() == Some("e_char) {
1822 current.push(chars.next().expect("peeked quote must exist"));
1823 } else {
1824 quote = None;
1825 }
1826 }
1827 continue;
1828 }
1829
1830 match character {
1831 '\'' | '"' | '`' => {
1832 quote = Some(character);
1833 current.push(character);
1834 }
1835 '(' => {
1836 depth += 1;
1837 current.push(character);
1838 }
1839 ')' => {
1840 depth = depth.saturating_sub(1);
1841 current.push(character);
1842 }
1843 ',' if depth == 0 => {
1844 let identifier = Self::trim_sql_identifier(¤t);
1845 if !identifier.is_empty() {
1846 identifiers.push(identifier);
1847 }
1848 current.clear();
1849 }
1850 _ => current.push(character),
1851 }
1852 }
1853
1854 let identifier = Self::trim_sql_identifier(¤t);
1855 if !identifier.is_empty() {
1856 identifiers.push(identifier);
1857 }
1858 identifiers
1859 }
1860
1861 fn extract_sql_parenthesized_expression(sql: &str, open: usize) -> Option<(&str, usize)> {
1862 if !sql.is_char_boundary(open) || sql[open..].chars().next()? != '(' {
1863 return None;
1864 }
1865
1866 let mut depth = 0usize;
1867 let mut quote = None;
1868 let mut chars = sql[open..].char_indices().peekable();
1869 while let Some((relative_index, character)) = chars.next() {
1870 let index = open + relative_index;
1871 if let Some(quote_char) = quote {
1872 if character == quote_char {
1873 if chars.peek().is_some_and(|(_, next)| *next == quote_char) {
1874 chars.next();
1875 } else {
1876 quote = None;
1877 }
1878 }
1879 continue;
1880 }
1881
1882 match character {
1883 '\'' | '"' | '`' => quote = Some(character),
1884 '(' => depth += 1,
1885 ')' => {
1886 depth = depth.checked_sub(1)?;
1887 if depth == 0 {
1888 return Some((&sql[open + 1..index], index));
1889 }
1890 }
1891 _ => {}
1892 }
1893 }
1894
1895 None
1896 }
1897
1898 fn foreign_key_action_from_clause(clauses: &str, clause: &str) -> Option<ForeignKeyAction> {
1899 let upper_clauses = clauses.to_ascii_uppercase();
1900 let tail = upper_clauses.split_once(clause)?.1.trim_start();
1901 if tail.starts_with("SET NULL") {
1902 Some(ForeignKeyAction::SetNull)
1903 } else if tail.starts_with("SET DEFAULT") {
1904 Some(ForeignKeyAction::SetDefault)
1905 } else if tail.starts_with("NO ACTION") {
1906 Some(ForeignKeyAction::NoAction)
1907 } else if tail.starts_with("CASCADE") {
1908 Some(ForeignKeyAction::Cascade)
1909 } else if tail.starts_with("RESTRICT") {
1910 Some(ForeignKeyAction::Restrict)
1911 } else {
1912 None
1913 }
1914 }
1915
1916 fn constraint_definition_from_sql(constraint_sql: &str) -> Option<ConstraintDefinition> {
1917 let rest = constraint_sql.trim().strip_prefix("CONSTRAINT ")?;
1918 let (name, body) = rest.split_once(' ')?;
1919 let body = body.trim();
1920 let upper_body = body.to_ascii_uppercase();
1921 if upper_body.starts_with("UNIQUE (") {
1922 let open = body.find('(')?;
1923 let (identifier_list, _) = Self::extract_sql_parenthesized_expression(body, open)?;
1924 let fields = Self::parse_constraint_identifier_list(identifier_list);
1925 if fields.is_empty() {
1926 return None;
1927 }
1928 return Some(ConstraintDefinition {
1929 name: name.trim_matches('"').to_string(),
1930 constraint_type: "unique".to_string(),
1931 fields,
1932 expression: None,
1933 foreign_key_info: None,
1934 });
1935 }
1936 if upper_body.starts_with("CHECK (") {
1937 let open = body.find('(')?;
1938 let close = body.rfind(')')?;
1939 return Some(ConstraintDefinition {
1940 name: name.trim_matches('"').to_string(),
1941 constraint_type: "check".to_string(),
1942 fields: Vec::new(),
1943 expression: Some(body[open + 1..close].trim().to_string()),
1944 foreign_key_info: None,
1945 });
1946 }
1947 if upper_body.starts_with("FOREIGN KEY") {
1948 let open = body.find('(')?;
1949 let (identifier_list, close) = Self::extract_sql_parenthesized_expression(body, open)?;
1950 let fields = Self::parse_constraint_identifier_list(identifier_list);
1951 if fields.is_empty() {
1952 return None;
1953 }
1954
1955 let after_fields = body[close + 1..].trim_start();
1956 if !after_fields.to_ascii_uppercase().starts_with("REFERENCES") {
1957 return None;
1958 }
1959 let after_references = after_fields["REFERENCES".len()..].trim_start();
1960 let referenced_open = after_references.find('(')?;
1961 let referenced_table = Self::trim_sql_identifier(&after_references[..referenced_open]);
1962 if referenced_table.is_empty() {
1963 return None;
1964 }
1965
1966 let (referenced_identifier_list, referenced_close) =
1967 Self::extract_sql_parenthesized_expression(after_references, referenced_open)?;
1968 let referenced_columns =
1969 Self::parse_constraint_identifier_list(referenced_identifier_list);
1970 if referenced_columns.is_empty() {
1971 return None;
1972 }
1973
1974 let clauses = &after_references[referenced_close + 1..];
1975 return Some(ConstraintDefinition {
1976 name: name.trim_matches('"').to_string(),
1977 constraint_type: "foreign_key".to_string(),
1978 fields,
1979 expression: None,
1980 foreign_key_info: Some(ForeignKeyConstraintInfo {
1981 referenced_table,
1982 referenced_columns,
1983 on_delete: Self::foreign_key_action_from_clause(clauses, "ON DELETE")
1984 .unwrap_or(ForeignKeyAction::NoAction),
1985 on_update: Self::foreign_key_action_from_clause(clauses, "ON UPDATE")
1986 .unwrap_or(ForeignKeyAction::NoAction),
1987 }),
1988 });
1989 }
1990 None
1991 }
1992
1993 fn column_def_to_field_state(&self, col: &super::operations::ColumnDefinition) -> FieldState {
1995 let mut params = std::collections::HashMap::new();
1996
1997 if col.primary_key {
1998 params.insert("primary_key".to_string(), "true".to_string());
1999 }
2000 if col.auto_increment {
2001 params.insert("auto_increment".to_string(), "true".to_string());
2002 }
2003 if col.unique {
2004 params.insert("unique".to_string(), "true".to_string());
2005 }
2006 if let Some(default) = &col.default {
2007 params.insert("default".to_string(), default.to_string());
2008 }
2009
2010 FieldState {
2011 name: col.name.to_string(),
2012 field_type: col.type_definition.clone(),
2013 nullable: !col.not_null,
2014 params,
2015 foreign_key: None,
2016 }
2017 }
2018}
2019
2020#[non_exhaustive]
2048#[derive(Debug, Clone)]
2049pub struct SimilarityConfig {
2050 model_threshold: f64,
2053 field_threshold: f64,
2056 jaro_winkler_weight: f64,
2059 levenshtein_weight: f64,
2063}
2064
2065impl SimilarityConfig {
2066 pub fn new(model_threshold: f64, field_threshold: f64) -> Result<Self, String> {
2095 Self::with_weights(model_threshold, field_threshold, 0.7, 0.3)
2096 }
2097
2098 pub fn with_weights(
2129 model_threshold: f64,
2130 field_threshold: f64,
2131 jaro_winkler_weight: f64,
2132 levenshtein_weight: f64,
2133 ) -> Result<Self, String> {
2134 if !(0.45..=0.95).contains(&model_threshold) {
2138 return Err(format!(
2139 "model_threshold must be between 0.45 and 0.95, got {}",
2140 model_threshold
2141 ));
2142 }
2143 if !(0.45..=0.95).contains(&field_threshold) {
2144 return Err(format!(
2145 "field_threshold must be between 0.45 and 0.95, got {}",
2146 field_threshold
2147 ));
2148 }
2149
2150 if !(0.0..=1.0).contains(&jaro_winkler_weight) {
2152 return Err(format!(
2153 "jaro_winkler_weight must be between 0.0 and 1.0, got {}",
2154 jaro_winkler_weight
2155 ));
2156 }
2157 if !(0.0..=1.0).contains(&levenshtein_weight) {
2158 return Err(format!(
2159 "levenshtein_weight must be between 0.0 and 1.0, got {}",
2160 levenshtein_weight
2161 ));
2162 }
2163
2164 let weight_sum = jaro_winkler_weight + levenshtein_weight;
2166 if (weight_sum - 1.0).abs() > 0.01 {
2167 return Err(format!(
2168 "jaro_winkler_weight + levenshtein_weight must sum to 1.0, got {} + {} = {}",
2169 jaro_winkler_weight, levenshtein_weight, weight_sum
2170 ));
2171 }
2172
2173 Ok(Self {
2174 model_threshold,
2175 field_threshold,
2176 jaro_winkler_weight,
2177 levenshtein_weight,
2178 })
2179 }
2180
2181 pub fn model_threshold(&self) -> f64 {
2183 self.model_threshold
2184 }
2185
2186 pub fn field_threshold(&self) -> f64 {
2188 self.field_threshold
2189 }
2190}
2191
2192impl Default for SimilarityConfig {
2193 fn default() -> Self {
2200 Self {
2201 model_threshold: 0.7,
2202 field_threshold: 0.8,
2203 jaro_winkler_weight: 0.7,
2204 levenshtein_weight: 0.3,
2205 }
2206 }
2207}
2208
2209pub struct MigrationAutodetector {
2235 from_state: ProjectState,
2236 to_state: ProjectState,
2237 similarity_config: SimilarityConfig,
2238}
2239
2240type MovedModelInfo = (
2243 String,
2244 String,
2245 String,
2246 String,
2247 bool,
2248 Option<String>,
2249 Option<String>,
2250);
2251
2252type ModelMatchResult = ((String, String), (String, String), f64);
2254
2255#[derive(Debug, Clone, Default)]
2257pub struct DetectedChanges {
2258 pub created_models: Vec<(String, String)>,
2260 pub deleted_models: Vec<(String, String)>,
2262 pub added_fields: Vec<(String, String, String)>,
2264 pub removed_fields: Vec<(String, String, String)>,
2266 pub altered_fields: Vec<(String, String, String)>,
2268 pub renamed_models: Vec<(String, String, String)>,
2270 pub moved_models: Vec<MovedModelInfo>,
2272 pub renamed_fields: Vec<(String, String, String, String)>,
2274 pub added_indexes: Vec<(String, String, IndexDefinition)>,
2276 pub removed_indexes: Vec<(String, String, String)>,
2278 pub added_constraints: Vec<(String, String, ConstraintDefinition)>,
2280 pub removed_constraints: Vec<(String, String, String)>,
2282 pub added_composite_primary_keys: Vec<(String, String, ConstraintDefinition)>,
2284 pub removed_composite_primary_keys: Vec<(String, String, String)>,
2286 pub auto_increment_resets: Vec<(String, String, String, i64)>,
2288 pub model_dependencies: std::collections::BTreeMap<(String, String), Vec<(String, String)>>,
2292 pub created_many_to_many: Vec<(String, String, String, ManyToManyMetadata)>,
2295}
2296
2297fn topological_sort_model_keys(
2308 nodes: &[(String, String)],
2309 dependencies: &std::collections::BTreeMap<(String, String), Vec<(String, String)>>,
2310) -> Vec<(String, String)> {
2311 use std::collections::{BTreeMap, BTreeSet};
2312
2313 let node_set: BTreeSet<(String, String)> = nodes.iter().cloned().collect();
2314 let mut in_degree: BTreeMap<(String, String), usize> =
2315 node_set.iter().cloned().map(|node| (node, 0)).collect();
2316 let mut dependents: BTreeMap<(String, String), BTreeSet<(String, String)>> = BTreeMap::new();
2317
2318 for (dependent, deps) in dependencies {
2319 if !node_set.contains(dependent) {
2320 continue;
2321 }
2322 for dependency in deps {
2323 if dependent == dependency || !node_set.contains(dependency) {
2324 continue;
2325 }
2326 *in_degree.entry(dependent.clone()).or_insert(0) += 1;
2327 dependents
2328 .entry(dependency.clone())
2329 .or_default()
2330 .insert(dependent.clone());
2331 }
2332 }
2333
2334 let mut ready: BTreeSet<(String, String)> = in_degree
2335 .iter()
2336 .filter(|(_, degree)| **degree == 0)
2337 .map(|(node, _)| node.clone())
2338 .collect();
2339 let mut ordered = Vec::with_capacity(node_set.len());
2340
2341 while let Some(node) = ready.iter().next().cloned() {
2342 ready.remove(&node);
2343 ordered.push(node.clone());
2344 if let Some(children) = dependents.get(&node) {
2345 for child in children {
2346 if let Some(degree) = in_degree.get_mut(child) {
2347 *degree = degree.saturating_sub(1);
2348 if *degree == 0 {
2349 ready.insert(child.clone());
2350 }
2351 }
2352 }
2353 }
2354 }
2355
2356 if ordered.len() < node_set.len() {
2357 let mut remaining: Vec<(String, String)> = node_set
2358 .into_iter()
2359 .filter(|node| !ordered.contains(node))
2360 .collect();
2361 remaining.sort();
2362 eprintln!(
2363 "⚠️ Warning: Circular dependency detected in models: [{}]",
2364 remaining
2365 .iter()
2366 .map(|(app, name)| format!("{app}.{name}"))
2367 .collect::<Vec<_>>()
2368 .join(", ")
2369 );
2370 eprintln!(
2371 " Falling back to lexicographic order for remaining models. Migration operations may need manual reordering."
2372 );
2373 ordered.extend(remaining);
2374 }
2375
2376 ordered
2377}
2378
2379impl DetectedChanges {
2380 pub fn order_models_by_dependency(&self) -> Vec<(String, String)> {
2419 let mut nodes = self.created_models.clone();
2420 for moved in &self.moved_models {
2421 nodes.push((moved.2.clone(), moved.3.clone()));
2422 }
2423 topological_sort_model_keys(&nodes, &self.model_dependencies)
2424 }
2425
2426 pub fn order_created_models_by_dependency(&self) -> Vec<(String, String)> {
2432 topological_sort_model_keys(&self.created_models, &self.model_dependencies)
2433 }
2434
2435 pub fn check_circular_dependencies(&self) -> Result<(), Vec<(String, String)>> {
2470 use std::collections::HashSet;
2471
2472 let mut visited: HashSet<(String, String)> = HashSet::new();
2473 let mut rec_stack: HashSet<(String, String)> = HashSet::new();
2474 let mut path: Vec<(String, String)> = Vec::new();
2475
2476 fn dfs(
2477 model: &(String, String),
2478 deps: &BTreeMap<(String, String), Vec<(String, String)>>,
2479 visited: &mut HashSet<(String, String)>,
2480 rec_stack: &mut HashSet<(String, String)>,
2481 path: &mut Vec<(String, String)>,
2482 ) -> Option<Vec<(String, String)>> {
2483 visited.insert(model.clone());
2484 rec_stack.insert(model.clone());
2485 path.push(model.clone());
2486
2487 if let Some(dependencies) = deps.get(model) {
2488 for dep in dependencies {
2489 if !visited.contains(dep) {
2490 if let Some(cycle) = dfs(dep, deps, visited, rec_stack, path) {
2491 return Some(cycle);
2492 }
2493 } else if rec_stack.contains(dep) {
2494 let cycle_start = path.iter().position(|m| m == dep).unwrap();
2496 return Some(path[cycle_start..].to_vec());
2497 }
2498 }
2499 }
2500
2501 path.pop();
2502 rec_stack.remove(model);
2503 None
2504 }
2505
2506 for model in self.model_dependencies.keys() {
2507 if !visited.contains(model)
2508 && let Some(cycle) = dfs(
2509 model,
2510 &self.model_dependencies,
2511 &mut visited,
2512 &mut rec_stack,
2513 &mut path,
2514 ) {
2515 return Err(cycle);
2516 }
2517 }
2518
2519 Ok(())
2520 }
2521
2522 pub fn remove_operations(&mut self, refs: &[OperationRef]) {
2562 for op_ref in refs {
2563 match op_ref {
2564 OperationRef::RenamedModel {
2565 app_label,
2566 old_name,
2567 new_name,
2568 } => {
2569 self.renamed_models.retain(|(app, old, new)| {
2570 !(app == app_label && old == old_name && new == new_name)
2571 });
2572 }
2573 OperationRef::MovedModel {
2574 from_app,
2575 to_app,
2576 model_name,
2577 } => {
2578 self.moved_models.retain(|info| {
2580 !(&info.0 == from_app
2581 && &info.2 == to_app && (&info.1 == model_name || &info.3 == model_name))
2582 });
2583 }
2584 OperationRef::AddedField {
2585 app_label,
2586 model_name,
2587 field_name,
2588 } => {
2589 self.added_fields.retain(|(app, model, field)| {
2590 !(app == app_label && model == model_name && field == field_name)
2591 });
2592 }
2593 OperationRef::RenamedField {
2594 app_label,
2595 model_name,
2596 old_name,
2597 new_name,
2598 } => {
2599 self.renamed_fields.retain(|(app, model, old, new)| {
2600 !(app == app_label
2601 && model == model_name
2602 && old == old_name && new == new_name)
2603 });
2604 }
2605 OperationRef::RemovedField {
2606 app_label,
2607 model_name,
2608 field_name,
2609 } => {
2610 self.removed_fields.retain(|(app, model, field)| {
2611 !(app == app_label && model == model_name && field == field_name)
2612 });
2613 }
2614 OperationRef::AlteredField {
2615 app_label,
2616 model_name,
2617 field_name,
2618 } => {
2619 self.altered_fields.retain(|(app, model, field)| {
2620 !(app == app_label && model == model_name && field == field_name)
2621 });
2622 }
2623 OperationRef::CreatedModel {
2624 app_label,
2625 model_name,
2626 } => {
2627 self.created_models
2628 .retain(|(app, model)| !(app == app_label && model == model_name));
2629 }
2630 OperationRef::DeletedModel {
2631 app_label,
2632 model_name,
2633 } => {
2634 self.deleted_models
2635 .retain(|(app, model)| !(app == app_label && model == model_name));
2636 }
2637 }
2638 }
2639 }
2640}
2641
2642#[derive(Debug, Clone)]
2669pub struct ChangeHistoryEntry {
2670 pub timestamp: std::time::SystemTime,
2672 pub change_type: String,
2674 pub app_label: String,
2676 pub model_name: String,
2678 pub field_name: Option<String>,
2680 pub old_value: Option<String>,
2682 pub new_value: Option<String>,
2684}
2685
2686#[derive(Debug, Clone)]
2692pub struct PatternFrequency {
2693 pub pattern: String,
2695 pub frequency: usize,
2697 pub last_seen: std::time::SystemTime,
2699 pub contexts: Vec<String>,
2701}
2702
2703#[derive(Debug, Clone)]
2732pub struct ChangeTracker {
2733 history: Vec<ChangeHistoryEntry>,
2735 patterns: HashMap<String, PatternFrequency>,
2737 max_history_size: usize,
2739}
2740
2741impl ChangeTracker {
2742 pub fn new() -> Self {
2746 Self {
2747 history: Vec::new(),
2748 patterns: HashMap::new(),
2749 max_history_size: 1000,
2750 }
2751 }
2752
2753 pub fn with_capacity(max_size: usize) -> Self {
2755 Self {
2756 history: Vec::with_capacity(max_size),
2757 patterns: HashMap::new(),
2758 max_history_size: max_size,
2759 }
2760 }
2761
2762 pub fn record_model_rename(&mut self, app_label: &str, old_name: &str, new_name: &str) {
2769 let entry = ChangeHistoryEntry {
2770 timestamp: std::time::SystemTime::now(),
2771 change_type: "RenameModel".to_string(),
2772 app_label: app_label.to_string(),
2773 model_name: new_name.to_string(),
2774 field_name: None,
2775 old_value: Some(old_name.to_string()),
2776 new_value: Some(new_name.to_string()),
2777 };
2778
2779 self.add_entry(entry);
2780 self.update_pattern(
2781 &format!("RenameModel:{}->{}", old_name, new_name),
2782 app_label,
2783 );
2784 }
2785
2786 pub fn record_model_move(&mut self, from_app: &str, to_app: &str, model_name: &str) {
2788 let entry = ChangeHistoryEntry {
2789 timestamp: std::time::SystemTime::now(),
2790 change_type: "MoveModel".to_string(),
2791 app_label: to_app.to_string(),
2792 model_name: model_name.to_string(),
2793 field_name: None,
2794 old_value: Some(from_app.to_string()),
2795 new_value: Some(to_app.to_string()),
2796 };
2797
2798 self.add_entry(entry);
2799 self.update_pattern(
2800 &format!("MoveModel:{}->{}:{}", from_app, to_app, model_name),
2801 to_app,
2802 );
2803 }
2804
2805 pub fn record_field_addition(&mut self, app_label: &str, model_name: &str, field_name: &str) {
2807 let entry = ChangeHistoryEntry {
2808 timestamp: std::time::SystemTime::now(),
2809 change_type: "AddField".to_string(),
2810 app_label: app_label.to_string(),
2811 model_name: model_name.to_string(),
2812 field_name: Some(field_name.to_string()),
2813 old_value: None,
2814 new_value: Some(field_name.to_string()),
2815 };
2816
2817 self.add_entry(entry);
2818 self.update_pattern(
2819 &format!("AddField:{}:{}", model_name, field_name),
2820 app_label,
2821 );
2822 }
2823
2824 pub fn record_field_rename(
2826 &mut self,
2827 app_label: &str,
2828 model_name: &str,
2829 old_name: &str,
2830 new_name: &str,
2831 ) {
2832 let entry = ChangeHistoryEntry {
2833 timestamp: std::time::SystemTime::now(),
2834 change_type: "RenameField".to_string(),
2835 app_label: app_label.to_string(),
2836 model_name: model_name.to_string(),
2837 field_name: Some(new_name.to_string()),
2838 old_value: Some(old_name.to_string()),
2839 new_value: Some(new_name.to_string()),
2840 };
2841
2842 self.add_entry(entry);
2843 self.update_pattern(
2844 &format!("RenameField:{}:{}->{}", model_name, old_name, new_name),
2845 app_label,
2846 );
2847 }
2848
2849 fn add_entry(&mut self, entry: ChangeHistoryEntry) {
2851 self.history.push(entry);
2852
2853 if self.history.len() > self.max_history_size {
2855 self.history.remove(0);
2856 }
2857 }
2858
2859 fn update_pattern(&mut self, pattern: &str, context: &str) {
2861 self.patterns
2862 .entry(pattern.to_string())
2863 .and_modify(|pf| {
2864 pf.frequency += 1;
2865 pf.last_seen = std::time::SystemTime::now();
2866 if !pf.contexts.contains(&context.to_string()) {
2867 pf.contexts.push(context.to_string());
2868 }
2869 })
2870 .or_insert(PatternFrequency {
2871 pattern: pattern.to_string(),
2872 frequency: 1,
2873 last_seen: std::time::SystemTime::now(),
2874 contexts: vec![context.to_string()],
2875 });
2876 }
2877
2878 pub fn get_frequent_patterns(&self, min_frequency: usize) -> Vec<PatternFrequency> {
2882 let mut patterns: Vec<_> = self
2883 .patterns
2884 .values()
2885 .filter(|p| p.frequency >= min_frequency)
2886 .cloned()
2887 .collect();
2888
2889 patterns.sort_by_key(|pattern| std::cmp::Reverse(pattern.frequency));
2890 patterns
2891 }
2892
2893 pub fn get_recent_changes(&self, duration: std::time::Duration) -> Vec<&ChangeHistoryEntry> {
2898 let now = std::time::SystemTime::now();
2899 self.history
2900 .iter()
2901 .filter(|entry| {
2902 now.duration_since(entry.timestamp)
2903 .map(|d| d < duration)
2904 .unwrap_or(false)
2905 })
2906 .collect()
2907 }
2908
2909 pub fn analyze_cooccurrence(
2914 &self,
2915 window: std::time::Duration,
2916 ) -> HashMap<(String, String), usize> {
2917 let mut cooccurrences = HashMap::new();
2918
2919 for i in 0..self.history.len() {
2920 for j in (i + 1)..self.history.len() {
2921 if let Ok(diff) = self.history[j]
2922 .timestamp
2923 .duration_since(self.history[i].timestamp)
2924 && diff <= window
2925 {
2926 let pattern1 = format!(
2927 "{}:{}",
2928 self.history[i].change_type, self.history[i].model_name
2929 );
2930 let pattern2 = format!(
2931 "{}:{}",
2932 self.history[j].change_type, self.history[j].model_name
2933 );
2934 let key = if pattern1 < pattern2 {
2935 (pattern1, pattern2)
2936 } else {
2937 (pattern2, pattern1)
2938 };
2939 *cooccurrences.entry(key).or_insert(0) += 1;
2940 }
2941 }
2942 }
2943
2944 cooccurrences
2945 }
2946
2947 pub fn clear(&mut self) {
2949 self.history.clear();
2950 self.patterns.clear();
2951 }
2952
2953 pub fn len(&self) -> usize {
2955 self.history.len()
2956 }
2957
2958 pub fn is_empty(&self) -> bool {
2960 self.history.is_empty()
2961 }
2962}
2963
2964impl Default for ChangeTracker {
2965 fn default() -> Self {
2966 Self::new()
2967 }
2968}
2969
2970#[derive(Debug, Clone)]
2974pub struct PatternMatch {
2975 pub pattern: String,
2977 pub start: usize,
2979 pub end: usize,
2981 pub matched_text: String,
2983}
2984
2985#[derive(Debug, Clone)]
3012pub struct PatternMatcher {
3013 patterns: Vec<String>,
3015 automaton: Option<aho_corasick::AhoCorasick>,
3017}
3018
3019impl PatternMatcher {
3020 pub fn new() -> Self {
3022 Self {
3023 patterns: Vec::new(),
3024 automaton: None,
3025 }
3026 }
3027
3028 pub fn add_pattern(&mut self, pattern: &str) {
3033 self.patterns.push(pattern.to_string());
3034 self.automaton = None;
3036 }
3037
3038 pub fn add_patterns<I, S>(&mut self, patterns: I)
3040 where
3041 I: IntoIterator<Item = S>,
3042 S: AsRef<str>,
3043 {
3044 for pattern in patterns {
3045 self.patterns.push(pattern.as_ref().to_string());
3046 }
3047 self.automaton = None;
3048 }
3049
3050 pub fn build(&mut self) -> Result<(), String> {
3055 if self.patterns.is_empty() {
3056 return Err("No patterns to build automaton".to_string());
3057 }
3058
3059 self.automaton = Some(
3060 aho_corasick::AhoCorasick::new(&self.patterns)
3061 .map_err(|e| format!("Failed to build Aho-Corasick automaton: {}", e))?,
3062 );
3063
3064 Ok(())
3065 }
3066
3067 pub fn find_all(&self, text: &str) -> Vec<PatternMatch> {
3071 let Some(ref automaton) = self.automaton else {
3072 return Vec::new();
3073 };
3074
3075 automaton
3076 .find_iter(text)
3077 .map(|mat| PatternMatch {
3078 pattern: self.patterns[mat.pattern().as_usize()].clone(),
3079 start: mat.start(),
3080 end: mat.end(),
3081 matched_text: text[mat.start()..mat.end()].to_string(),
3082 })
3083 .collect()
3084 }
3085
3086 pub fn contains_any(&self, text: &str) -> bool {
3088 self.automaton
3089 .as_ref()
3090 .map(|ac| ac.is_match(text))
3091 .unwrap_or(false)
3092 }
3093
3094 pub fn find_first(&self, text: &str) -> Option<PatternMatch> {
3096 let automaton = self.automaton.as_ref()?;
3097 let mat = automaton.find(text)?;
3098
3099 Some(PatternMatch {
3100 pattern: self.patterns[mat.pattern().as_usize()].clone(),
3101 start: mat.start(),
3102 end: mat.end(),
3103 matched_text: text[mat.start()..mat.end()].to_string(),
3104 })
3105 }
3106
3107 pub fn replace_all(&self, text: &str, replacements: &HashMap<String, String>) -> String {
3116 let Some(ref automaton) = self.automaton else {
3117 return text.to_string();
3118 };
3119
3120 let mut result = String::new();
3121 let mut last_end = 0;
3122
3123 for mat in automaton.find_iter(text) {
3124 result.push_str(&text[last_end..mat.start()]);
3126
3127 let pattern = &self.patterns[mat.pattern().as_usize()];
3129 if let Some(replacement) = replacements.get(pattern) {
3130 result.push_str(replacement);
3131 } else {
3132 result.push_str(&text[mat.start()..mat.end()]);
3133 }
3134
3135 last_end = mat.end();
3136 }
3137
3138 result.push_str(&text[last_end..]);
3140 result
3141 }
3142
3143 pub fn patterns(&self) -> &[String] {
3145 &self.patterns
3146 }
3147
3148 pub fn clear(&mut self) {
3150 self.patterns.clear();
3151 self.automaton = None;
3152 }
3153
3154 pub fn is_built(&self) -> bool {
3156 self.automaton.is_some()
3157 }
3158}
3159
3160impl Default for PatternMatcher {
3161 fn default() -> Self {
3162 Self::new()
3163 }
3164}
3165
3166#[derive(Debug, Clone, PartialEq)]
3172pub enum RuleCondition {
3173 ModelRename {
3175 from_pattern: String,
3177 to_pattern: String,
3179 },
3180 ModelMove {
3182 app_pattern: String,
3184 },
3185 FieldAddition {
3187 field_name_pattern: String,
3189 },
3190 FieldRename {
3192 from_pattern: String,
3194 to_pattern: String,
3196 },
3197 MultipleModelRenames {
3199 min_count: usize,
3201 },
3202 MultipleFieldAdditions {
3204 model_pattern: String,
3206 min_count: usize,
3208 },
3209}
3210
3211#[derive(Debug, Clone, PartialEq)]
3216pub enum OperationRef {
3217 RenamedModel {
3219 app_label: String,
3221 old_name: String,
3223 new_name: String,
3225 },
3226 MovedModel {
3228 from_app: String,
3230 to_app: String,
3232 model_name: String,
3234 },
3235 AddedField {
3237 app_label: String,
3239 model_name: String,
3241 field_name: String,
3243 },
3244 RenamedField {
3246 app_label: String,
3248 model_name: String,
3250 old_name: String,
3252 new_name: String,
3254 },
3255 RemovedField {
3257 app_label: String,
3259 model_name: String,
3261 field_name: String,
3263 },
3264 AlteredField {
3266 app_label: String,
3268 model_name: String,
3270 field_name: String,
3272 },
3273 CreatedModel {
3275 app_label: String,
3277 model_name: String,
3279 },
3280 DeletedModel {
3282 app_label: String,
3284 model_name: String,
3286 },
3287}
3288
3289#[derive(Debug, Clone, PartialEq)]
3291pub struct InferredIntent {
3292 pub intent_type: String,
3294 pub confidence: f64,
3296 pub description: String,
3298 pub evidence: Vec<String>,
3300 pub related_operations: Vec<OperationRef>,
3305}
3306
3307#[derive(Debug, Clone)]
3309pub struct InferenceRule {
3310 pub name: String,
3312 pub conditions: Vec<RuleCondition>,
3314 pub optional_conditions: Vec<RuleCondition>,
3316 pub intent_type: String,
3318 pub base_confidence: f64,
3320 pub confidence_boost_per_optional: f64,
3322}
3323
3324#[derive(Debug, Clone)]
3361pub struct InferenceEngine {
3362 rules: Vec<InferenceRule>,
3364 change_tracker: ChangeTracker,
3384}
3385
3386impl Default for InferenceEngine {
3387 fn default() -> Self {
3388 Self::new()
3389 }
3390}
3391
3392impl InferenceEngine {
3393 pub fn new() -> Self {
3395 Self {
3396 rules: Vec::new(),
3397 change_tracker: ChangeTracker::new(),
3398 }
3399 }
3400
3401 pub fn add_rule(&mut self, rule: InferenceRule) {
3403 self.rules.push(rule);
3404 }
3405
3406 pub fn add_default_rules(&mut self) {
3408 self.add_rule(InferenceRule {
3410 name: "model_refactoring".to_string(),
3411 conditions: vec![RuleCondition::ModelRename {
3412 from_pattern: ".*".to_string(),
3413 to_pattern: ".*".to_string(),
3414 }],
3415 optional_conditions: vec![RuleCondition::MultipleModelRenames { min_count: 2 }],
3416 intent_type: "Refactoring: Model rename".to_string(),
3417 base_confidence: 0.7,
3418 confidence_boost_per_optional: 0.1,
3419 });
3420
3421 self.add_rule(InferenceRule {
3423 name: "add_timestamp_tracking".to_string(),
3424 conditions: vec![RuleCondition::FieldAddition {
3425 field_name_pattern: "created_at".to_string(),
3426 }],
3427 optional_conditions: vec![RuleCondition::FieldAddition {
3428 field_name_pattern: "updated_at".to_string(),
3429 }],
3430 intent_type: "Add timestamp tracking".to_string(),
3431 base_confidence: 0.8,
3432 confidence_boost_per_optional: 0.15,
3433 });
3434
3435 self.add_rule(InferenceRule {
3437 name: "cross_app_move".to_string(),
3438 conditions: vec![RuleCondition::ModelMove {
3439 app_pattern: ".*".to_string(),
3440 }],
3441 optional_conditions: vec![],
3442 intent_type: "Cross-app model organization".to_string(),
3443 base_confidence: 0.75,
3444 confidence_boost_per_optional: 0.0,
3445 });
3446
3447 self.add_rule(InferenceRule {
3449 name: "field_refactoring".to_string(),
3450 conditions: vec![RuleCondition::FieldRename {
3451 from_pattern: ".*".to_string(),
3452 to_pattern: ".*".to_string(),
3453 }],
3454 optional_conditions: vec![RuleCondition::MultipleFieldAdditions {
3455 model_pattern: ".*".to_string(),
3456 min_count: 2,
3457 }],
3458 intent_type: "Refactoring: Field rename".to_string(),
3459 base_confidence: 0.65,
3460 confidence_boost_per_optional: 0.1,
3461 });
3462
3463 self.add_rule(InferenceRule {
3465 name: "model_normalization".to_string(),
3466 conditions: vec![RuleCondition::MultipleFieldAdditions {
3467 model_pattern: ".*".to_string(),
3468 min_count: 3,
3469 }],
3470 optional_conditions: vec![],
3471 intent_type: "Schema normalization".to_string(),
3472 base_confidence: 0.6,
3473 confidence_boost_per_optional: 0.0,
3474 });
3475 }
3476
3477 fn matches_pattern(value: &str, pattern: &str) -> bool {
3484 if pattern == ".*" {
3486 return true;
3487 }
3488
3489 if value == pattern {
3491 return true;
3492 }
3493
3494 if let Ok(re) = Regex::new(pattern) {
3496 re.is_match(value)
3497 } else {
3498 false
3500 }
3501 }
3502
3503 pub fn rules(&self) -> &[InferenceRule] {
3505 &self.rules
3506 }
3507
3508 pub fn infer_intents(
3510 &self,
3511 model_renames: &[(String, String, String, String)], model_moves: &[(String, String, String, String)], field_additions: &[(String, String, String)], field_renames: &[(String, String, String, String)], ) -> Vec<InferredIntent> {
3516 let mut intents = Vec::new();
3517
3518 for rule in &self.rules {
3519 let mut matches_required = true;
3520 let mut optional_matches = 0;
3521 let mut evidence = Vec::new();
3522
3523 for condition in &rule.conditions {
3525 match condition {
3526 RuleCondition::ModelRename {
3527 from_pattern,
3528 to_pattern,
3529 } => {
3530 if model_renames.is_empty() {
3531 matches_required = false;
3532 break;
3533 }
3534
3535 let mut matched = false;
3537 for (from_app, from_model, to_app, to_model) in model_renames {
3538 let from_name = format!("{}.{}", from_app, from_model);
3539 let to_name = format!("{}.{}", to_app, to_model);
3540
3541 if Self::matches_pattern(&from_name, from_pattern)
3542 && Self::matches_pattern(&to_name, to_pattern)
3543 {
3544 evidence.push(format!(
3545 "Model renamed: {} → {} (pattern: {} → {})",
3546 from_name, to_name, from_pattern, to_pattern
3547 ));
3548 matched = true;
3549 break;
3550 }
3551 }
3552
3553 if !matched {
3554 matches_required = false;
3555 break;
3556 }
3557 }
3558 RuleCondition::ModelMove { app_pattern } => {
3559 if model_moves.is_empty() {
3560 matches_required = false;
3561 break;
3562 }
3563
3564 let mut matched = false;
3566 for (from_app, from_model, to_app, to_model) in model_moves {
3567 if Self::matches_pattern(to_app, app_pattern) {
3568 evidence.push(format!(
3569 "Model moved: {}.{} → {}.{} (app pattern: {})",
3570 from_app, from_model, to_app, to_model, app_pattern
3571 ));
3572 matched = true;
3573 break;
3574 }
3575 }
3576
3577 if !matched {
3578 matches_required = false;
3579 break;
3580 }
3581 }
3582 RuleCondition::FieldAddition { field_name_pattern } => {
3583 let matching_fields: Vec<_> = field_additions
3584 .iter()
3585 .filter(|(_, _, field)| {
3586 Self::matches_pattern(field, field_name_pattern)
3587 })
3588 .collect();
3589
3590 if matching_fields.is_empty() {
3591 matches_required = false;
3592 break;
3593 }
3594 evidence.push(format!(
3595 "Field added: {}.{}.{} (pattern: {})",
3596 matching_fields[0].0,
3597 matching_fields[0].1,
3598 matching_fields[0].2,
3599 field_name_pattern
3600 ));
3601 }
3602 RuleCondition::FieldRename {
3603 from_pattern,
3604 to_pattern,
3605 } => {
3606 if field_renames.is_empty() {
3607 matches_required = false;
3608 break;
3609 }
3610
3611 let mut matched = false;
3613 for (app, model, from_field, to_field) in field_renames {
3614 if Self::matches_pattern(from_field, from_pattern)
3615 && Self::matches_pattern(to_field, to_pattern)
3616 {
3617 evidence.push(format!(
3618 "Field renamed: {}.{}.{} → {} (pattern: {} → {})",
3619 app, model, from_field, to_field, from_pattern, to_pattern
3620 ));
3621 matched = true;
3622 break;
3623 }
3624 }
3625
3626 if !matched {
3627 matches_required = false;
3628 break;
3629 }
3630 }
3631 RuleCondition::MultipleModelRenames { min_count } => {
3632 if model_renames.len() < *min_count {
3633 matches_required = false;
3634 break;
3635 }
3636 evidence.push(format!("Multiple model renames: {}", model_renames.len()));
3637 }
3638 RuleCondition::MultipleFieldAdditions {
3639 model_pattern,
3640 min_count,
3641 } => {
3642 let count = field_additions
3643 .iter()
3644 .filter(|(_, model, _)| Self::matches_pattern(model, model_pattern))
3645 .count();
3646
3647 if count < *min_count {
3648 matches_required = false;
3649 break;
3650 }
3651 evidence.push(format!(
3652 "Multiple field additions: {} (pattern: {}, min: {})",
3653 count, model_pattern, min_count
3654 ));
3655 }
3656 }
3657 }
3658
3659 if !matches_required {
3660 continue;
3661 }
3662
3663 for condition in &rule.optional_conditions {
3665 match condition {
3666 RuleCondition::FieldAddition { field_name_pattern } => {
3667 if field_additions
3668 .iter()
3669 .any(|(_, _, field)| field.contains(field_name_pattern.as_str()))
3670 {
3671 optional_matches += 1;
3672 evidence.push(format!("Optional field added: {}", field_name_pattern));
3673 }
3674 }
3675 RuleCondition::MultipleModelRenames { min_count }
3676 if model_renames.len() >= *min_count =>
3677 {
3678 optional_matches += 1;
3679 evidence.push(format!("Multiple renames: {}", model_renames.len()));
3680 }
3681 _ => {}
3682 }
3683 }
3684
3685 let confidence = rule.base_confidence
3687 + (optional_matches as f64 * rule.confidence_boost_per_optional);
3688 let confidence = confidence.min(1.0);
3689
3690 intents.push(InferredIntent {
3691 intent_type: rule.intent_type.clone(),
3692 confidence,
3693 description: format!("Detected: {}", rule.name),
3694 evidence,
3695 related_operations: Vec::new(),
3696 });
3697 }
3698
3699 intents.sort_by(|a, b| {
3701 b.confidence
3702 .partial_cmp(&a.confidence)
3703 .unwrap_or(std::cmp::Ordering::Equal)
3704 });
3705
3706 intents
3707 }
3708
3709 pub fn infer_from_detected_changes(&self, changes: &DetectedChanges) -> Vec<InferredIntent> {
3719 let model_renames: Vec<(String, String, String, String)> = changes
3721 .renamed_models
3722 .iter()
3723 .map(|(app, old_name, new_name)| {
3724 (app.clone(), old_name.clone(), app.clone(), new_name.clone())
3725 })
3726 .collect();
3727
3728 let model_moves: Vec<(String, String, String, String)> = changes
3730 .moved_models
3731 .iter()
3732 .map(|(from_app, from_model, to_app, to_model, _, _, _)| {
3733 (
3734 from_app.clone(),
3735 from_model.clone(),
3736 to_app.clone(),
3737 to_model.clone(),
3738 )
3739 })
3740 .collect();
3741
3742 let field_additions: Vec<(String, String, String)> = changes
3744 .added_fields
3745 .iter()
3746 .map(|(app, model, field)| (app.clone(), model.clone(), field.clone()))
3747 .collect();
3748
3749 let field_renames: Vec<(String, String, String, String)> = changes
3751 .renamed_fields
3752 .iter()
3753 .map(|(app, model, old_name, new_name)| {
3754 (
3755 app.clone(),
3756 model.clone(),
3757 old_name.clone(),
3758 new_name.clone(),
3759 )
3760 })
3761 .collect();
3762
3763 let mut intents = self.infer_intents(
3765 &model_renames,
3766 &model_moves,
3767 &field_additions,
3768 &field_renames,
3769 );
3770
3771 for intent in &mut intents {
3773 for evidence_str in &intent.evidence {
3776 if evidence_str.starts_with("Model renamed:") {
3778 for (app, old_name, new_name) in &changes.renamed_models {
3779 intent.related_operations.push(OperationRef::RenamedModel {
3780 app_label: app.clone(),
3781 old_name: old_name.clone(),
3782 new_name: new_name.clone(),
3783 });
3784 }
3785 }
3786 else if evidence_str.starts_with("Model moved:") {
3788 for (from_app, _from_model, to_app, to_model, _, _, _) in &changes.moved_models
3789 {
3790 intent.related_operations.push(OperationRef::MovedModel {
3791 from_app: from_app.clone(),
3792 to_app: to_app.clone(),
3793 model_name: to_model.clone(),
3794 });
3795 }
3796 }
3797 else if evidence_str.starts_with("Field added:") {
3799 for (app, model, field) in &changes.added_fields {
3800 intent.related_operations.push(OperationRef::AddedField {
3801 app_label: app.clone(),
3802 model_name: model.clone(),
3803 field_name: field.clone(),
3804 });
3805 }
3806 }
3807 else if evidence_str.starts_with("Field renamed:") {
3809 for (app, model, old_name, new_name) in &changes.renamed_fields {
3810 intent.related_operations.push(OperationRef::RenamedField {
3811 app_label: app.clone(),
3812 model_name: model.clone(),
3813 old_name: old_name.clone(),
3814 new_name: new_name.clone(),
3815 });
3816 }
3817 }
3818 else if evidence_str.starts_with("Multiple model renames:") {
3820 for (app, old_name, new_name) in &changes.renamed_models {
3821 intent.related_operations.push(OperationRef::RenamedModel {
3822 app_label: app.clone(),
3823 old_name: old_name.clone(),
3824 new_name: new_name.clone(),
3825 });
3826 }
3827 }
3828 else if evidence_str.starts_with("Multiple field additions:")
3830 || evidence_str.starts_with("Optional field added:")
3831 {
3832 for (app, model, field) in &changes.added_fields {
3833 intent.related_operations.push(OperationRef::AddedField {
3834 app_label: app.clone(),
3835 model_name: model.clone(),
3836 field_name: field.clone(),
3837 });
3838 }
3839 }
3840 }
3841
3842 intent
3844 .related_operations
3845 .sort_by(|a, b| format!("{:?}", a).cmp(&format!("{:?}", b)));
3846 intent.related_operations.dedup();
3847 }
3848
3849 intents
3850 }
3851
3852 pub fn record_model_rename(&mut self, app_label: &str, old_name: &str, new_name: &str) {
3861 self.change_tracker
3862 .record_model_rename(app_label, old_name, new_name);
3863 }
3864
3865 pub fn record_model_move(&mut self, from_app: &str, to_app: &str, model_name: &str) {
3872 self.change_tracker
3873 .record_model_move(from_app, to_app, model_name);
3874 }
3875
3876 pub fn record_field_addition(&mut self, app_label: &str, model_name: &str, field_name: &str) {
3883 self.change_tracker
3884 .record_field_addition(app_label, model_name, field_name);
3885 }
3886
3887 pub fn record_field_rename(
3895 &mut self,
3896 app_label: &str,
3897 model_name: &str,
3898 old_name: &str,
3899 new_name: &str,
3900 ) {
3901 self.change_tracker
3902 .record_field_rename(app_label, model_name, old_name, new_name);
3903 }
3904
3905 pub fn get_frequent_patterns(&self, min_frequency: usize) -> Vec<PatternFrequency> {
3913 self.change_tracker.get_frequent_patterns(min_frequency)
3914 }
3915
3916 pub fn get_recent_changes(&self, duration: std::time::Duration) -> Vec<&ChangeHistoryEntry> {
3921 self.change_tracker.get_recent_changes(duration)
3922 }
3923
3924 pub fn analyze_cooccurrence(
3932 &self,
3933 window: std::time::Duration,
3934 ) -> HashMap<(String, String), usize> {
3935 self.change_tracker.analyze_cooccurrence(window)
3936 }
3937}
3938
3939pub struct MigrationPrompt {
3952 auto_accept_threshold: f64,
3955
3956 theme: dialoguer::theme::ColorfulTheme,
3958}
3959
3960impl std::fmt::Debug for MigrationPrompt {
3961 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3962 f.debug_struct("MigrationPrompt")
3963 .field("auto_accept_threshold", &self.auto_accept_threshold)
3964 .field("theme", &"ColorfulTheme")
3965 .finish()
3966 }
3967}
3968
3969impl MigrationPrompt {
3970 pub fn new() -> Self {
3972 Self {
3973 auto_accept_threshold: 0.85,
3974 theme: dialoguer::theme::ColorfulTheme::default(),
3975 }
3976 }
3977
3978 pub fn with_threshold(threshold: f64) -> Self {
3980 Self {
3981 auto_accept_threshold: threshold,
3982 theme: dialoguer::theme::ColorfulTheme::default(),
3983 }
3984 }
3985
3986 pub fn auto_accept_threshold(&self) -> f64 {
3988 self.auto_accept_threshold
3989 }
3990
3991 pub fn confirm_intent(
3995 &self,
3996 intent: &InferredIntent,
3997 ) -> Result<bool, Box<dyn std::error::Error>> {
3998 if intent.confidence >= self.auto_accept_threshold {
4000 println!(
4001 "✓ Auto-accepting (confidence: {:.1}%): {}",
4002 intent.confidence * 100.0,
4003 intent.intent_type
4004 );
4005 return Ok(true);
4006 }
4007
4008 let message = format!(
4010 "Detected: {} (confidence: {:.1}%)\nDetails: {}\n\nAccept this change?",
4011 intent.intent_type,
4012 intent.confidence * 100.0,
4013 intent.description
4014 );
4015
4016 if !intent.evidence.is_empty() {
4018 println!("\nEvidence:");
4019 for evidence in &intent.evidence {
4020 println!(" • {}", evidence);
4021 }
4022 }
4023
4024 dialoguer::Confirm::with_theme(&self.theme)
4026 .with_prompt(message)
4027 .default(true)
4028 .interact()
4029 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
4030 }
4031
4032 pub fn select_intent(
4036 &self,
4037 alternatives: &[InferredIntent],
4038 prompt: &str,
4039 ) -> Result<Option<usize>, Box<dyn std::error::Error>> {
4040 if alternatives.is_empty() {
4041 return Ok(None);
4042 }
4043
4044 if alternatives.len() == 1 {
4046 let confirmed = self.confirm_intent(&alternatives[0])?;
4047 return Ok(if confirmed { Some(0) } else { None });
4048 }
4049
4050 let items: Vec<String> = alternatives
4052 .iter()
4053 .map(|intent| {
4054 format!(
4055 "{} (confidence: {:.1}%) - {}",
4056 intent.intent_type,
4057 intent.confidence * 100.0,
4058 intent.description
4059 )
4060 })
4061 .collect();
4062
4063 println!("\n{}", prompt);
4065 println!("Multiple possibilities detected:\n");
4066
4067 let mut items_with_none = items.clone();
4069 items_with_none.push("None of the above / Skip".to_string());
4070
4071 let selection = dialoguer::Select::with_theme(&self.theme)
4073 .items(&items_with_none)
4074 .default(0)
4075 .interact()
4076 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
4077
4078 if selection >= items.len() {
4080 Ok(None)
4081 } else {
4082 Ok(Some(selection))
4083 }
4084 }
4085
4086 pub fn multi_select_intents(
4090 &self,
4091 alternatives: &[InferredIntent],
4092 prompt: &str,
4093 ) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
4094 if alternatives.is_empty() {
4095 return Ok(Vec::new());
4096 }
4097
4098 let items: Vec<String> = alternatives
4100 .iter()
4101 .map(|intent| {
4102 format!(
4103 "{} (confidence: {:.1}%) - {}",
4104 intent.intent_type,
4105 intent.confidence * 100.0,
4106 intent.description
4107 )
4108 })
4109 .collect();
4110
4111 println!("\n{}", prompt);
4113 println!("Select all that apply:\n");
4114
4115 let selections = dialoguer::MultiSelect::with_theme(&self.theme)
4117 .items(&items)
4118 .interact()
4119 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
4120
4121 Ok(selections)
4122 }
4123
4124 pub fn confirm_model_rename(
4126 &self,
4127 from_app: &str,
4128 from_model: &str,
4129 to_app: &str,
4130 to_model: &str,
4131 confidence: f64,
4132 ) -> Result<bool, Box<dyn std::error::Error>> {
4133 if confidence >= self.auto_accept_threshold {
4135 println!(
4136 "✓ Auto-accepting model rename (confidence: {:.1}%): {}.{} → {}.{}",
4137 confidence * 100.0,
4138 from_app,
4139 from_model,
4140 to_app,
4141 to_model
4142 );
4143 return Ok(true);
4144 }
4145
4146 let message = format!(
4147 "Rename model from {}.{} to {}.{}?\n(confidence: {:.1}%)",
4148 from_app,
4149 from_model,
4150 to_app,
4151 to_model,
4152 confidence * 100.0
4153 );
4154
4155 dialoguer::Confirm::with_theme(&self.theme)
4156 .with_prompt(message)
4157 .default(true)
4158 .interact()
4159 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
4160 }
4161
4162 pub fn confirm_field_rename(
4164 &self,
4165 model: &str,
4166 from_field: &str,
4167 to_field: &str,
4168 confidence: f64,
4169 ) -> Result<bool, Box<dyn std::error::Error>> {
4170 if confidence >= self.auto_accept_threshold {
4172 println!(
4173 "✓ Auto-accepting field rename (confidence: {:.1}%): {}.{} → {}.{}",
4174 confidence * 100.0,
4175 model,
4176 from_field,
4177 model,
4178 to_field
4179 );
4180 return Ok(true);
4181 }
4182
4183 let message = format!(
4184 "Rename field in model {}:\n {} → {}?\n(confidence: {:.1}%)",
4185 model,
4186 from_field,
4187 to_field,
4188 confidence * 100.0
4189 );
4190
4191 dialoguer::Confirm::with_theme(&self.theme)
4192 .with_prompt(message)
4193 .default(true)
4194 .interact()
4195 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
4196 }
4197
4198 pub fn with_progress<F, T>(
4200 &self,
4201 message: &str,
4202 total: u64,
4203 operation: F,
4204 ) -> Result<T, Box<dyn std::error::Error>>
4205 where
4206 F: FnOnce(&indicatif::ProgressBar) -> Result<T, Box<dyn std::error::Error>>,
4207 {
4208 let pb = indicatif::ProgressBar::new(total);
4209 pb.set_style(
4210 indicatif::ProgressStyle::default_bar()
4211 .template("{msg} [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
4212 .expect("Failed to create progress bar template")
4213 .progress_chars("#>-"),
4214 );
4215 pb.set_message(message.to_string());
4216
4217 let result = operation(&pb)?;
4218
4219 pb.finish_with_message("Done");
4220 Ok(result)
4221 }
4222}
4223
4224impl Default for MigrationPrompt {
4225 fn default() -> Self {
4226 Self::new()
4227 }
4228}
4229
4230pub trait InteractiveAutodetector {
4232 fn detect_changes_interactive(&self) -> Result<DetectedChanges, Box<dyn std::error::Error>>;
4234
4235 fn apply_intents_interactive(
4237 &self,
4238 intents: Vec<InferredIntent>,
4239 changes: &mut DetectedChanges,
4240 ) -> Result<(), Box<dyn std::error::Error>>;
4241}
4242
4243impl InteractiveAutodetector for MigrationAutodetector {
4244 fn detect_changes_interactive(&self) -> Result<DetectedChanges, Box<dyn std::error::Error>> {
4245 let prompt = MigrationPrompt::new();
4246 let mut changes = self.detect_changes();
4247
4248 let mut engine = InferenceEngine::new();
4250 engine.add_default_rules();
4251
4252 let intents = engine.infer_from_detected_changes(&changes);
4254
4255 let ambiguous_intents: Vec<_> = intents
4257 .into_iter()
4258 .filter(|intent| intent.confidence < prompt.auto_accept_threshold)
4259 .collect();
4260
4261 if !ambiguous_intents.is_empty() {
4263 println!(
4264 "\n⚠️ Found {} ambiguous change(s) requiring confirmation:",
4265 ambiguous_intents.len()
4266 );
4267
4268 for intent in &ambiguous_intents {
4269 let confirmed = prompt.confirm_intent(intent)?;
4270
4271 if !confirmed {
4272 println!("✗ Skipped: {}", intent.description);
4273 if !intent.related_operations.is_empty() {
4276 changes.remove_operations(&intent.related_operations);
4277 println!(
4278 " → Removed {} related operation(s) from migration",
4279 intent.related_operations.len()
4280 );
4281 }
4282 }
4283 }
4284 }
4285
4286 self.detect_model_dependencies(&mut changes);
4288
4289 if let Err(cycle) = changes.check_circular_dependencies() {
4291 println!("\n⚠️ Warning: Circular dependency detected: {:?}", cycle);
4292
4293 let should_continue = dialoguer::Confirm::new()
4294 .with_prompt("Continue anyway? (may require manual intervention)")
4295 .default(false)
4296 .interact()?;
4297
4298 if !should_continue {
4299 return Err("Aborted due to circular dependency".into());
4300 }
4301 }
4302
4303 Ok(changes)
4304 }
4305
4306 fn apply_intents_interactive(
4307 &self,
4308 intents: Vec<InferredIntent>,
4309 _changes: &mut DetectedChanges,
4310 ) -> Result<(), Box<dyn std::error::Error>> {
4311 let prompt = MigrationPrompt::new();
4312
4313 let mut high_confidence = Vec::new();
4315 let mut medium_confidence = Vec::new();
4316 let mut low_confidence = Vec::new();
4317
4318 for intent in intents {
4319 if intent.confidence >= 0.85 {
4320 high_confidence.push(intent);
4321 } else if intent.confidence >= 0.65 {
4322 medium_confidence.push(intent);
4323 } else {
4324 low_confidence.push(intent);
4325 }
4326 }
4327
4328 println!(
4330 "\n✓ Auto-applying {} high-confidence change(s):",
4331 high_confidence.len()
4332 );
4333 for intent in &high_confidence {
4334 println!(
4335 " • {} (confidence: {:.1}%)",
4336 intent.description,
4337 intent.confidence * 100.0
4338 );
4339 }
4340
4341 if !medium_confidence.is_empty() {
4343 println!(
4344 "\n⚠️ Review {} medium-confidence change(s):",
4345 medium_confidence.len()
4346 );
4347
4348 for intent in &medium_confidence {
4349 let confirmed = prompt.confirm_intent(intent)?;
4350 if confirmed {
4351 println!(" ✓ Accepted: {}", intent.description);
4352 } else {
4353 println!(" ✗ Rejected: {}", intent.description);
4354 }
4355 }
4356 }
4357
4358 if !low_confidence.is_empty() {
4360 let selections = prompt.multi_select_intents(
4361 &low_confidence,
4362 "⚠️ Select low-confidence changes to apply:",
4363 )?;
4364
4365 for idx in selections {
4366 println!(" ✓ Accepted: {}", low_confidence[idx].description);
4367 }
4368 }
4369
4370 Ok(())
4371 }
4372}
4373
4374impl MigrationAutodetector {
4375 pub fn new(from_state: ProjectState, to_state: ProjectState) -> Self {
4388 Self {
4389 from_state,
4390 to_state,
4391 similarity_config: SimilarityConfig::default(),
4392 }
4393 }
4394
4395 pub fn with_config(
4409 from_state: ProjectState,
4410 to_state: ProjectState,
4411 similarity_config: SimilarityConfig,
4412 ) -> Self {
4413 Self {
4414 from_state,
4415 to_state,
4416 similarity_config,
4417 }
4418 }
4419
4420 pub fn detect_changes(&self) -> DetectedChanges {
4442 self.detect_changes_internal(false)
4443 .expect("non-strict autodetection must not fail")
4444 }
4445
4446 pub fn try_detect_changes(&self) -> super::Result<DetectedChanges> {
4453 self.detect_changes_internal(true)
4454 }
4455
4456 fn detect_changes_internal(
4457 &self,
4458 strict_rename_ambiguity: bool,
4459 ) -> super::Result<DetectedChanges> {
4460 let mut changes = DetectedChanges::default();
4461
4462 self.detect_created_models(&mut changes);
4464 self.detect_deleted_models(&mut changes);
4465 self.detect_renamed_models(&mut changes);
4466
4467 self.detect_added_fields(&mut changes);
4469 self.detect_removed_fields(&mut changes);
4470 self.detect_altered_fields(&mut changes);
4471 self.detect_renamed_fields(&mut changes, strict_rename_ambiguity)?;
4472
4473 self.detect_added_indexes(&mut changes);
4475 self.detect_removed_indexes(&mut changes);
4476 self.detect_added_constraints(&mut changes);
4477 self.detect_removed_constraints(&mut changes);
4478 self.detect_composite_pk_changes(&mut changes);
4479 self.detect_auto_increment_resets(&mut changes);
4480
4481 self.detect_created_many_to_many(&mut changes);
4483
4484 self.detect_model_dependencies(&mut changes);
4486
4487 let created_set: std::collections::BTreeSet<_> =
4492 changes.created_models.iter().cloned().collect();
4493 changes.created_models = changes
4494 .order_created_models_by_dependency()
4495 .into_iter()
4496 .filter(|model| created_set.contains(model))
4497 .collect();
4498
4499 changes.deleted_models.sort();
4501 changes.added_fields.sort();
4502 changes.removed_fields.sort();
4503 changes.altered_fields.sort();
4504 changes.renamed_models.sort();
4505 changes.renamed_fields.sort();
4506
4507 changes
4509 .added_indexes
4510 .sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
4511 changes.removed_indexes.sort();
4512 changes
4513 .added_constraints
4514 .sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
4515 changes.removed_constraints.sort();
4516 changes
4517 .added_composite_primary_keys
4518 .sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
4519 changes.removed_composite_primary_keys.sort();
4520 changes.auto_increment_resets.sort();
4521 changes
4522 .created_many_to_many
4523 .sort_by(|a, b| (&a.0, &a.1, &a.2).cmp(&(&b.0, &b.1, &b.2)));
4524
4525 Ok(changes)
4526 }
4527
4528 fn detect_created_models(&self, changes: &mut DetectedChanges) {
4532 for ((app_label, model_name), to_model) in &self.to_state.models {
4533 if self
4535 .from_state
4536 .get_model_by_table_name(app_label, &to_model.table_name)
4537 .is_none()
4538 {
4539 changes
4540 .created_models
4541 .push((app_label.clone(), model_name.clone()));
4542 }
4543 }
4544 }
4545
4546 fn detect_deleted_models(&self, changes: &mut DetectedChanges) {
4550 for ((app_label, model_name), from_model) in &self.from_state.models {
4551 if self
4553 .to_state
4554 .get_model_by_table_name(app_label, &from_model.table_name)
4555 .is_none()
4556 {
4557 changes
4558 .deleted_models
4559 .push((app_label.clone(), model_name.clone()));
4560 }
4561 }
4562 }
4563
4564 fn detect_added_fields(&self, changes: &mut DetectedChanges) {
4568 for ((app_label, model_name), to_model) in &self.to_state.models {
4569 if let Some(from_model) =
4573 self.matching_from_model_for_to_model(app_label, model_name, to_model, changes)
4574 {
4575 for field_name in to_model.fields.keys() {
4576 if !from_model.fields.contains_key(field_name) {
4577 changes.added_fields.push((
4578 app_label.clone(),
4579 model_name.clone(),
4580 field_name.clone(),
4581 ));
4582 }
4583 }
4584 }
4585 }
4586 }
4587
4588 fn detect_removed_fields(&self, changes: &mut DetectedChanges) {
4592 for ((app_label, model_name), from_model) in &self.from_state.models {
4593 if let Some(to_model) =
4597 self.matching_to_model_for_from_model(app_label, model_name, from_model, changes)
4598 {
4599 for field_name in from_model.fields.keys() {
4600 if !to_model.fields.contains_key(field_name) {
4601 changes.removed_fields.push((
4602 app_label.clone(),
4603 model_name.clone(),
4604 field_name.clone(),
4605 ));
4606 }
4607 }
4608 }
4609 }
4610 }
4611
4612 fn detect_altered_fields(&self, changes: &mut DetectedChanges) {
4616 for ((app_label, model_name), to_model) in &self.to_state.models {
4617 if let Some(from_model) =
4621 self.matching_from_model_for_to_model(app_label, model_name, to_model, changes)
4622 {
4623 for (field_name, to_field) in &to_model.fields {
4624 if let Some(from_field) = from_model.fields.get(field_name) {
4625 if self.has_field_changed_in_model_context(
4627 field_name, from_model, to_model, from_field, to_field,
4628 ) {
4629 changes.altered_fields.push((
4630 app_label.clone(),
4631 model_name.clone(),
4632 field_name.clone(),
4633 ));
4634 }
4635 }
4636 }
4637 }
4638 }
4639 }
4640
4641 fn matching_from_model_for_to_model<'a>(
4642 &'a self,
4643 app_label: &str,
4644 to_model_name: &str,
4645 to_model: &ModelState,
4646 changes: &DetectedChanges,
4647 ) -> Option<&'a ModelState> {
4648 self.from_state
4649 .get_model_by_table_name(app_label, &to_model.table_name)
4650 .or_else(|| {
4651 changes
4652 .renamed_models
4653 .iter()
4654 .find(|(app, _old_name, new_name)| {
4655 app == app_label && new_name == to_model_name
4656 })
4657 .and_then(|(_app, old_name, _new_name)| {
4658 self.from_state.get_model(app_label, old_name)
4659 })
4660 })
4661 .or_else(|| {
4662 changes
4663 .moved_models
4664 .iter()
4665 .find(|(_from_app, _from_model, to_app, to_model, _, _, _)| {
4666 to_app == app_label && to_model == to_model_name
4667 })
4668 .and_then(|(from_app, from_model, _to_app, _to_model, _, _, _)| {
4669 self.from_state.get_model(from_app, from_model)
4670 })
4671 })
4672 }
4673
4674 fn matching_to_model_for_from_model<'a>(
4675 &'a self,
4676 app_label: &str,
4677 from_model_name: &str,
4678 from_model: &ModelState,
4679 changes: &DetectedChanges,
4680 ) -> Option<&'a ModelState> {
4681 self.to_state
4682 .get_model_by_table_name(app_label, &from_model.table_name)
4683 .or_else(|| {
4684 changes
4685 .renamed_models
4686 .iter()
4687 .find(|(app, old_name, _new_name)| {
4688 app == app_label && old_name == from_model_name
4689 })
4690 .and_then(|(_app, _old_name, new_name)| {
4691 self.to_state.get_model(app_label, new_name)
4692 })
4693 })
4694 .or_else(|| {
4695 changes
4696 .moved_models
4697 .iter()
4698 .find(|(from_app, from_model, _to_app, _to_model, _, _, _)| {
4699 from_app == app_label && from_model == from_model_name
4700 })
4701 .and_then(|(_from_app, _from_model, to_app, to_model, _, _, _)| {
4702 self.to_state.get_model(to_app, to_model)
4703 })
4704 })
4705 }
4706
4707 fn has_field_changed_in_model_context(
4708 &self,
4709 field_name: &str,
4710 from_model: &ModelState,
4711 to_model: &ModelState,
4712 from_field: &FieldState,
4713 to_field: &FieldState,
4714 ) -> bool {
4715 let from_constraint_managed =
4716 Self::single_field_unique_constraint_present(from_model, field_name);
4717 let to_constraint_managed =
4718 Self::single_field_unique_constraint_present(to_model, field_name);
4719 let constraint_managed = from_constraint_managed || to_constraint_managed;
4720 let from_inline_unique = Self::field_has_inline_unique(from_model, field_name);
4721 let to_inline_unique = Self::field_has_inline_unique(to_model, field_name);
4722 let from_unique = Some(if constraint_managed {
4723 from_inline_unique && !to_constraint_managed
4724 } else {
4725 Self::single_field_unique_column_already_present(from_model, field_name)
4726 });
4727 let to_unique = Some(if constraint_managed {
4728 to_inline_unique && !from_constraint_managed
4729 } else {
4730 Self::single_field_unique_column_already_present(to_model, field_name)
4731 });
4732 self.has_field_changed_with_unique(field_name, from_field, to_field, from_unique, to_unique)
4733 }
4734
4735 fn field_has_inline_unique(model: &ModelState, field_name: &str) -> bool {
4736 model
4737 .fields
4738 .get(field_name)
4739 .and_then(|field| field.params.get("unique"))
4740 .map(String::as_str)
4741 == Some("true")
4742 }
4743
4744 fn has_field_changed_with_unique(
4745 &self,
4746 field_name: &str,
4747 from_field: &FieldState,
4748 to_field: &FieldState,
4749 from_unique: Option<bool>,
4750 to_unique: Option<bool>,
4751 ) -> bool {
4752 let mut from_def = super::ColumnDefinition::from_field_state(field_name, from_field);
4755 let mut to_def = super::ColumnDefinition::from_field_state(field_name, to_field);
4756 from_def.auto_increment =
4757 Self::canonical_auto_increment(&from_def.type_definition, from_def.auto_increment);
4758 to_def.auto_increment =
4759 Self::canonical_auto_increment(&to_def.type_definition, to_def.auto_increment);
4760 if let Some(unique) = from_unique {
4761 from_def.unique = unique;
4762 }
4763 if let Some(unique) = to_unique {
4764 to_def.unique = unique;
4765 }
4766 from_def.type_definition != to_def.type_definition
4767 || from_def.not_null != to_def.not_null
4768 || from_def.primary_key != to_def.primary_key
4769 || from_def.auto_increment != to_def.auto_increment
4770 || from_def.unique != to_def.unique
4771 || from_def.default != to_def.default
4772 }
4773
4774 fn canonical_auto_increment(field_type: &super::FieldType, auto_increment: bool) -> bool {
4775 auto_increment
4776 && matches!(
4777 field_type,
4778 super::FieldType::BigInteger
4779 | super::FieldType::Integer
4780 | super::FieldType::SmallInteger
4781 | super::FieldType::TinyInt
4782 | super::FieldType::MediumInt
4783 )
4784 }
4785
4786 fn detect_renamed_models(&self, changes: &mut DetectedChanges) {
4832 let deleted: Vec<_> = self
4834 .from_state
4835 .models
4836 .keys()
4837 .filter(|k| !self.to_state.models.contains_key(k))
4838 .collect();
4839
4840 let created: Vec<_> = self
4841 .to_state
4842 .models
4843 .keys()
4844 .filter(|k| !self.from_state.models.contains_key(k))
4845 .collect();
4846
4847 let matches = self.find_optimal_model_matches(&deleted, &created);
4850
4851 for (deleted_key, created_key, _similarity) in matches {
4852 if deleted_key.0 == created_key.0 {
4854 let app_label = deleted_key.0.clone();
4855 let old_model_name = deleted_key.1.clone();
4856 let new_model_name = created_key.1.clone();
4857 let old_table = self
4860 .from_state
4861 .get_model(&app_label, &old_model_name)
4862 .map(|m| m.table_name.as_str());
4863 let new_table = self
4864 .to_state
4865 .get_model(&app_label, &new_model_name)
4866 .map(|m| m.table_name.as_str());
4867
4868 if old_table != new_table {
4869 changes.renamed_models.push((
4870 app_label.clone(),
4871 old_model_name.clone(),
4872 new_model_name.clone(),
4873 ));
4874 changes
4875 .created_models
4876 .retain(|(app, model)| !(app == &app_label && model == &new_model_name));
4877 changes
4878 .deleted_models
4879 .retain(|(app, model)| !(app == &app_label && model == &old_model_name));
4880 }
4881 } else {
4882 let from_app = deleted_key.0.clone();
4883 let to_app = created_key.0.clone();
4884 let model_name = created_key.1.clone();
4885 let deleted_model_name = deleted_key.1.clone();
4886 let old_table = self
4889 .from_state
4890 .get_model(&from_app, &deleted_model_name)
4891 .map(|model| model.table_name.clone())
4892 .unwrap_or_else(|| {
4893 format!("{}_{}", from_app, deleted_model_name.to_lowercase())
4894 });
4895 let new_table = self
4896 .to_state
4897 .get_model(&to_app, &model_name)
4898 .map(|model| model.table_name.clone())
4899 .unwrap_or_else(|| format!("{}_{}", to_app, model_name.to_lowercase()));
4900 let rename_table = old_table != new_table;
4901
4902 changes.moved_models.push((
4903 from_app.clone(),
4904 deleted_model_name.clone(),
4905 to_app.clone(),
4906 model_name.clone(),
4907 rename_table,
4908 if rename_table { Some(old_table) } else { None },
4909 if rename_table { Some(new_table) } else { None },
4910 ));
4911 changes
4912 .created_models
4913 .retain(|(app, model)| !(app == &to_app && model == &model_name));
4914 changes
4915 .deleted_models
4916 .retain(|(app, model)| !(app == &from_app && model == &deleted_model_name));
4917 }
4918 }
4919 }
4920
4921 fn detect_renamed_fields(
4962 &self,
4963 changes: &mut DetectedChanges,
4964 strict_rename_ambiguity: bool,
4965 ) -> super::Result<()> {
4966 let mut confirmed_renames = Vec::new();
4967 let mut ambiguous_groups = Vec::new();
4968
4969 for ((app_label, model_name), to_model) in &self.to_state.models {
4970 let Some(from_model) =
4971 self.matching_from_model_for_to_model(app_label, model_name, to_model, changes)
4972 else {
4973 continue;
4974 };
4975
4976 let removed_fields: Vec<_> = from_model
4977 .fields
4978 .iter()
4979 .filter(|(name, _)| !to_model.fields.contains_key(*name))
4980 .collect();
4981 let added_fields: Vec<_> = to_model
4982 .fields
4983 .iter()
4984 .filter(|(name, _)| !from_model.fields.contains_key(*name))
4985 .collect();
4986
4987 if removed_fields.is_empty() || added_fields.is_empty() {
4988 continue;
4989 }
4990
4991 let mut old_to_new: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
4992 let mut new_to_old: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
4993
4994 for (removed_name, removed_field) in &removed_fields {
4995 for (added_name, added_field) in &added_fields {
4996 if Self::field_definitions_match_for_rename(
4997 removed_name,
4998 removed_field,
4999 added_name,
5000 added_field,
5001 Self::single_field_unique_column_already_present(from_model, removed_name),
5002 Self::single_field_unique_column_already_present(to_model, added_name),
5003 ) {
5004 old_to_new
5005 .entry((*removed_name).clone())
5006 .or_default()
5007 .insert((*added_name).clone());
5008 new_to_old
5009 .entry((*added_name).clone())
5010 .or_default()
5011 .insert((*removed_name).clone());
5012 }
5013 }
5014 }
5015
5016 if old_to_new.is_empty() {
5017 continue;
5018 }
5019
5020 for (old_name, new_names) in &old_to_new {
5021 if new_names.len() == 1 {
5022 let new_name = new_names.iter().next().expect("one candidate");
5023 if new_to_old
5024 .get(new_name)
5025 .is_some_and(|old_names| old_names.len() == 1)
5026 {
5027 confirmed_renames.push((
5028 app_label.clone(),
5029 model_name.clone(),
5030 from_model.name.clone(),
5031 to_model.table_name.clone(),
5032 old_name.clone(),
5033 new_name.clone(),
5034 ));
5035 continue;
5036 }
5037 }
5038
5039 ambiguous_groups.push(format!(
5040 "{}.{} (table {}): old [{}] -> new [{}]",
5041 app_label,
5042 model_name,
5043 to_model.table_name,
5044 old_name,
5045 new_names.iter().cloned().collect::<Vec<_>>().join(", ")
5046 ));
5047 }
5048
5049 for (new_name, old_names) in &new_to_old {
5050 if old_names.len() > 1 {
5051 ambiguous_groups.push(format!(
5052 "{}.{} (table {}): old [{}] -> new [{}]",
5053 app_label,
5054 model_name,
5055 to_model.table_name,
5056 old_names.iter().cloned().collect::<Vec<_>>().join(", "),
5057 new_name
5058 ));
5059 }
5060 }
5061 }
5062
5063 ambiguous_groups.sort();
5064 ambiguous_groups.dedup();
5065 if strict_rename_ambiguity && !ambiguous_groups.is_empty() {
5066 return Err(super::MigrationError::InvalidMigration(format!(
5067 "Ambiguous field rename candidates detected. \
5068 Reinhardt will not emit destructive AddColumn + DropColumn operations for \
5069 rename-like changes. Split the change or make the rename intent explicit. \
5070 Candidates: {}",
5071 ambiguous_groups.join("; ")
5072 )));
5073 }
5074
5075 for (app_label, model_name, from_model_name, table_name, old_name, new_name) in
5076 confirmed_renames
5077 {
5078 changes.renamed_fields.push((
5079 app_label.clone(),
5080 model_name.clone(),
5081 old_name.clone(),
5082 new_name.clone(),
5083 ));
5084 changes.added_fields.retain(|(app, model, field)| {
5085 !(app == &app_label && model == &model_name && field == &new_name)
5086 });
5087 changes.removed_fields.retain(|(app, model, field)| {
5088 !(app == &app_label
5089 && field == &old_name
5090 && (model == &from_model_name
5091 || self
5092 .from_state
5093 .get_model(app, model)
5094 .is_some_and(|from_model| from_model.table_name == table_name)))
5095 });
5096 changes.altered_fields.retain(|(app, model, field)| {
5097 !(app == &app_label && model == &model_name && field == &new_name)
5098 });
5099 }
5100
5101 Ok(())
5102 }
5103
5104 fn field_definitions_match_for_rename(
5105 from_name: &str,
5106 from_field: &FieldState,
5107 to_name: &str,
5108 to_field: &FieldState,
5109 from_unique: bool,
5110 to_unique: bool,
5111 ) -> bool {
5112 if from_field.field_type != to_field.field_type
5113 || from_field.nullable != to_field.nullable
5114 || from_field.foreign_key != to_field.foreign_key
5115 {
5116 return false;
5117 }
5118
5119 let mut from_def = super::ColumnDefinition::from_field_state(from_name, from_field);
5120 let mut to_def = super::ColumnDefinition::from_field_state(to_name, to_field);
5121 from_def.name = "__renamed_field__".to_string();
5122 to_def.name = "__renamed_field__".to_string();
5123 from_def.unique = from_unique;
5124 to_def.unique = to_unique;
5125 from_def == to_def
5126 }
5127
5128 fn calculate_model_similarity(&self, from_model: &ModelState, to_model: &ModelState) -> f64 {
5163 if from_model.fields.is_empty() && to_model.fields.is_empty() {
5164 return 1.0;
5165 }
5166
5167 if from_model.fields.is_empty() || to_model.fields.is_empty() {
5168 return 0.0;
5169 }
5170
5171 let mut total_similarity = 0.0;
5172 let total_fields = from_model.fields.len().max(to_model.fields.len());
5173
5174 let mut matched_to_fields = std::collections::HashSet::new();
5176
5177 for (from_field_name, from_field) in &from_model.fields {
5178 let mut best_match_score = 0.0;
5179 let mut best_match_name = None;
5180
5181 for (to_field_name, to_field) in &to_model.fields {
5183 if matched_to_fields.contains(to_field_name) {
5184 continue;
5185 }
5186
5187 let similarity = self.calculate_field_similarity(
5188 from_field_name,
5189 to_field_name,
5190 from_field,
5191 to_field,
5192 );
5193
5194 if similarity > best_match_score {
5195 best_match_score = similarity;
5196 best_match_name = Some(to_field_name.clone());
5197 }
5198 }
5199
5200 if let Some(matched_name) = best_match_name {
5201 matched_to_fields.insert(matched_name);
5202 total_similarity += best_match_score;
5203 }
5204 }
5205
5206 total_similarity / total_fields as f64
5207 }
5208
5209 fn calculate_field_similarity(
5241 &self,
5242 from_field_name: &str,
5243 to_field_name: &str,
5244 from_field: &FieldState,
5245 to_field: &FieldState,
5246 ) -> f64 {
5247 if from_field.field_type != to_field.field_type {
5249 return 0.0;
5250 }
5251
5252 let jaro_winkler_sim = jaro_winkler(from_field_name, to_field_name);
5254
5255 let lev_distance = levenshtein(from_field_name, to_field_name);
5257 let max_len = from_field_name.len().max(to_field_name.len()) as f64;
5258 let levenshtein_sim = if max_len > 0.0 {
5259 1.0 - (lev_distance as f64 / max_len)
5260 } else {
5261 1.0 };
5263
5264 let name_similarity = self.similarity_config.jaro_winkler_weight * jaro_winkler_sim
5266 + self.similarity_config.levenshtein_weight * levenshtein_sim;
5267
5268 let nullable_boost = if from_field.nullable == to_field.nullable {
5270 0.1
5271 } else {
5272 0.0
5273 };
5274
5275 (name_similarity + nullable_boost).min(1.0)
5276 }
5277
5278 fn find_optimal_model_matches(
5312 &self,
5313 deleted: &[&(String, String)],
5314 created: &[&(String, String)],
5315 ) -> Vec<ModelMatchResult> {
5316 let mut graph = Graph::<(), f64, Undirected>::new_undirected();
5317 let mut deleted_nodes = Vec::new();
5318 let mut created_nodes = Vec::new();
5319
5320 for _ in deleted {
5322 deleted_nodes.push(graph.add_node(()));
5323 }
5324
5325 for _ in created {
5327 created_nodes.push(graph.add_node(()));
5328 }
5329
5330 for (i, deleted_key) in deleted.iter().enumerate() {
5332 if let Some(from_model) = self.from_state.models.get(*deleted_key) {
5333 for (j, created_key) in created.iter().enumerate() {
5334 if let Some(to_model) = self.to_state.models.get(*created_key) {
5335 let similarity = self.calculate_model_similarity(from_model, to_model);
5336
5337 if similarity >= self.similarity_config.model_threshold() {
5339 graph.add_edge(deleted_nodes[i], created_nodes[j], similarity);
5340 }
5341 }
5342 }
5343 }
5344 }
5345
5346 let mut matches = Vec::new();
5349 let mut used_deleted = std::collections::HashSet::new();
5350 let mut used_created = std::collections::HashSet::new();
5351
5352 let mut weighted_edges: Vec<_> = graph
5354 .edge_references()
5355 .map(|e| (e.source(), e.target(), *e.weight()))
5356 .collect();
5357 weighted_edges.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
5358
5359 for (source, target, weight) in weighted_edges {
5361 let source_idx = deleted_nodes.iter().position(|&n| n == source);
5362 let target_idx = created_nodes.iter().position(|&n| n == target);
5363
5364 if let (Some(i), Some(j)) = (source_idx, target_idx)
5365 && !used_deleted.contains(&i)
5366 && !used_created.contains(&j)
5367 {
5368 matches.push((deleted[i].clone(), created[j].clone(), weight));
5369 used_deleted.insert(i);
5370 used_created.insert(j);
5371 }
5372 }
5373
5374 matches
5375 }
5376
5377 fn detect_added_indexes(&self, changes: &mut DetectedChanges) {
5382 for ((app_label, model_name), to_model) in &self.to_state.models {
5383 if let Some(from_model) =
5384 self.matching_from_model_for_to_model(app_label, model_name, to_model, changes)
5385 {
5386 for to_index in &to_model.indexes {
5387 if !from_model.indexes.iter().any(|idx| {
5389 model_index_definitions_equivalent(from_model, idx, to_model, to_index)
5390 }) {
5391 changes.added_indexes.push((
5392 app_label.clone(),
5393 model_name.clone(),
5394 to_index.clone(),
5395 ));
5396 }
5397 }
5398 }
5399 }
5400 }
5401
5402 fn detect_removed_indexes(&self, changes: &mut DetectedChanges) {
5407 for ((app_label, model_name), from_model) in &self.from_state.models {
5408 if let Some(to_model) =
5409 self.matching_to_model_for_from_model(app_label, model_name, from_model, changes)
5410 {
5411 for from_index in &from_model.indexes {
5412 if !to_model.indexes.iter().any(|idx| {
5414 model_index_definitions_equivalent(from_model, from_index, to_model, idx)
5415 }) {
5416 changes.removed_indexes.push((
5417 app_label.clone(),
5418 model_name.clone(),
5419 from_index.name.clone(),
5420 ));
5421 }
5422 }
5423 }
5424 }
5425 }
5426
5427 fn detect_added_constraints(&self, changes: &mut DetectedChanges) {
5452 for ((app_label, model_name), to_model) in &self.to_state.models {
5453 if let Some(from_model) =
5454 self.matching_from_model_for_to_model(app_label, model_name, to_model, changes)
5455 {
5456 for to_constraint in &to_model.constraints {
5457 if from_model.constraints.iter().any(|c| {
5458 c.name == to_constraint.name
5459 && Self::constraint_definitions_match(c, to_constraint)
5460 }) {
5461 continue;
5462 }
5463 if Self::single_field_unique_already_present(to_constraint, from_model) {
5464 continue;
5465 }
5466 if Self::added_single_field_unique_preserved_by_rename(
5467 changes,
5468 app_label,
5469 model_name,
5470 to_constraint,
5471 from_model,
5472 ) {
5473 continue;
5474 }
5475 changes.added_constraints.push((
5476 app_label.clone(),
5477 model_name.clone(),
5478 to_constraint.clone(),
5479 ));
5480 }
5481 }
5482 }
5483 }
5484
5485 fn detect_removed_constraints(&self, changes: &mut DetectedChanges) {
5501 for ((app_label, model_name), from_model) in &self.from_state.models {
5502 if let Some(to_model) =
5503 self.matching_to_model_for_from_model(app_label, model_name, from_model, changes)
5504 {
5505 for from_constraint in &from_model.constraints {
5506 if to_model.constraints.iter().any(|c| {
5507 c.name == from_constraint.name
5508 && Self::constraint_definitions_match(c, from_constraint)
5509 }) {
5510 continue;
5511 }
5512 if Self::single_field_unique_already_present(from_constraint, to_model) {
5513 continue;
5514 }
5515 if Self::removed_single_field_unique_preserved_by_rename(
5516 changes,
5517 app_label,
5518 model_name,
5519 to_model,
5520 from_constraint,
5521 ) {
5522 continue;
5523 }
5524 changes.removed_constraints.push((
5525 app_label.clone(),
5526 model_name.clone(),
5527 from_constraint.name.clone(),
5528 ));
5529 }
5530 }
5531 }
5532 }
5533
5534 fn constraint_definitions_match(
5535 left: &ConstraintDefinition,
5536 right: &ConstraintDefinition,
5537 ) -> bool {
5538 left.constraint_type
5539 .eq_ignore_ascii_case(&right.constraint_type)
5540 && left.fields == right.fields
5541 && left.expression == right.expression
5542 && left.foreign_key_info == right.foreign_key_info
5543 }
5544
5545 fn renamed_single_field_unique_constraints(
5546 from_model: &ModelState,
5547 to_model: &ModelState,
5548 ) -> Vec<(ConstraintDefinition, ConstraintDefinition)> {
5549 from_model
5550 .constraints
5551 .iter()
5552 .filter(|from_constraint| is_single_field_unique(from_constraint))
5553 .filter_map(|from_constraint| {
5554 to_model
5555 .constraints
5556 .iter()
5557 .find(|to_constraint| {
5558 to_constraint.name != from_constraint.name
5559 && is_single_field_unique(to_constraint)
5560 && Self::constraint_definitions_match(from_constraint, to_constraint)
5561 })
5562 .map(|to_constraint| (from_constraint.clone(), to_constraint.clone()))
5563 })
5564 .collect()
5565 }
5566
5567 fn single_field_unique_already_present(
5578 candidate: &ConstraintDefinition,
5579 other_side: &ModelState,
5580 ) -> bool {
5581 if !is_single_field_unique(candidate) {
5582 return false;
5583 }
5584 let column = &candidate.fields[0];
5585 Self::single_field_unique_column_already_present(other_side, column)
5586 }
5587
5588 fn single_field_unique_column_already_present(model: &ModelState, column: &str) -> bool {
5589 if Self::single_field_unique_constraint_present(model, column) {
5590 return true;
5591 }
5592 model
5593 .fields
5594 .get(column)
5595 .and_then(|f| f.params.get("unique"))
5596 .map(String::as_str)
5597 == Some("true")
5598 }
5599
5600 fn single_field_unique_constraint_present(model: &ModelState, column: &str) -> bool {
5601 model
5602 .constraints
5603 .iter()
5604 .any(|constraint| is_single_field_unique(constraint) && constraint.fields[0] == column)
5605 }
5606
5607 fn added_single_field_unique_preserved_by_rename(
5608 changes: &DetectedChanges,
5609 app_label: &str,
5610 model_name: &str,
5611 to_constraint: &ConstraintDefinition,
5612 from_model: &ModelState,
5613 ) -> bool {
5614 if !is_single_field_unique(to_constraint) {
5615 return false;
5616 }
5617 let new_column = &to_constraint.fields[0];
5618 changes.renamed_fields.iter().any(|(app, model, old, new)| {
5619 app == app_label
5620 && model == model_name
5621 && new == new_column
5622 && !Self::single_field_unique_constraint_present(from_model, old)
5623 && Self::single_field_unique_column_already_present(from_model, old)
5624 })
5625 }
5626
5627 fn removed_single_field_unique_preserved_by_rename(
5628 changes: &DetectedChanges,
5629 app_label: &str,
5630 from_model_name: &str,
5631 to_model: &ModelState,
5632 from_constraint: &ConstraintDefinition,
5633 ) -> bool {
5634 if !is_single_field_unique(from_constraint) {
5635 return false;
5636 }
5637 let old_column = &from_constraint.fields[0];
5638 changes.renamed_fields.iter().any(|(app, model, old, new)| {
5639 app == app_label
5640 && (model == from_model_name || model == &to_model.name)
5641 && old == old_column
5642 && !Self::single_field_unique_constraint_present(to_model, new)
5643 && Self::single_field_unique_column_already_present(to_model, new)
5644 })
5645 }
5646
5647 fn dedup_redundant_unique_add_constraints(
5675 by_app: &mut std::collections::BTreeMap<String, Vec<super::Operation>>,
5676 ) {
5677 use std::collections::HashSet;
5678
5679 for operations in by_app.values_mut() {
5680 let mut covered: HashSet<(String, String)> = HashSet::new();
5682 let mut keep = Vec::with_capacity(operations.len());
5683 for op in operations.drain(..) {
5684 match &op {
5685 super::Operation::CreateTable {
5686 name,
5687 columns,
5688 constraints,
5689 ..
5690 } => {
5691 for col in columns {
5692 if col.unique {
5693 covered.insert((name.clone(), col.name.clone()));
5694 }
5695 }
5696 for c in constraints {
5697 if let super::operations::Constraint::Unique { columns, .. } = c
5698 && columns.len() == 1
5699 {
5700 covered.insert((name.clone(), columns[0].clone()));
5701 }
5702 }
5703 keep.push(op);
5704 }
5705 super::Operation::AddColumn { table, column, .. } => {
5706 if column.unique {
5707 covered.insert((table.clone(), column.name.clone()));
5708 }
5709 keep.push(op);
5710 }
5711 super::Operation::AddConstraint {
5712 table,
5713 constraint_sql,
5714 } => {
5715 if let Some(col) = parse_single_column_unique(constraint_sql) {
5716 let key = (table.clone(), col.to_string());
5717 if covered.contains(&key) {
5718 continue;
5720 }
5721 covered.insert(key);
5722 }
5723 keep.push(op);
5724 }
5725 _ => keep.push(op),
5726 }
5727 }
5728 *operations = keep;
5729 }
5730 }
5731
5732 fn detect_composite_pk_changes(&self, changes: &mut DetectedChanges) {
5742 for ((app_label, model_name), to_model) in &self.to_state.models {
5743 let from_model = self
5744 .from_state
5745 .get_model_by_table_name(app_label, &to_model.table_name);
5746 for constraint in &to_model.constraints {
5747 if constraint.constraint_type != "primary_key" || constraint.fields.len() < 2 {
5748 continue;
5749 }
5750 let from_pk = from_model
5751 .and_then(|m| m.constraints.iter().find(|c| c.name == constraint.name));
5752 match from_pk {
5753 Some(existing) if existing.fields == constraint.fields => {
5754 }
5756 Some(_) => {
5757 changes.removed_composite_primary_keys.push((
5759 app_label.clone(),
5760 model_name.clone(),
5761 constraint.name.clone(),
5762 ));
5763 changes.added_composite_primary_keys.push((
5764 app_label.clone(),
5765 model_name.clone(),
5766 constraint.clone(),
5767 ));
5768 }
5769 None => {
5770 changes.added_composite_primary_keys.push((
5772 app_label.clone(),
5773 model_name.clone(),
5774 constraint.clone(),
5775 ));
5776 }
5777 }
5778 }
5779 }
5780 }
5781
5782 fn detect_auto_increment_resets(&self, changes: &mut DetectedChanges) {
5787 for ((app_label, model_name), to_model) in &self.to_state.models {
5788 let Some(value_str) = to_model.options.get("sequence_reset") else {
5789 continue;
5790 };
5791 let from_value = self
5792 .from_state
5793 .get_model(app_label, model_name)
5794 .and_then(|m| m.options.get("sequence_reset"))
5795 .map(String::as_str);
5796 if from_value == Some(value_str.as_str()) {
5797 continue;
5798 }
5799 let Ok(value) = value_str.parse::<i64>() else {
5800 eprintln!(
5801 "Invalid sequence_reset value for {}.{}: {:?}. Expected an integer.",
5802 app_label, model_name, value_str
5803 );
5804 continue;
5805 };
5806 let Some(column) = to_model
5807 .fields
5808 .iter()
5809 .find(|(_, f)| f.params.get("auto_increment").is_some_and(|v| v == "true"))
5810 .map(|(name, _)| name.clone())
5811 else {
5812 continue;
5813 };
5814 changes.auto_increment_resets.push((
5815 app_label.clone(),
5816 model_name.clone(),
5817 column,
5818 value,
5819 ));
5820 }
5821 }
5822
5823 fn generate_intermediate_table(
5841 &self,
5842 app_label: &str,
5843 model_name: &str,
5844 field_name: &str,
5845 to_model: &str,
5846 through_table: &Option<String>,
5847 ) -> Option<super::Operation> {
5848 let source_table = self
5852 .to_state
5853 .get_model(app_label, model_name)
5854 .map(|m| m.table_name.clone())
5855 .unwrap_or_else(|| {
5856 format!("{}_{}", to_snake_case(app_label), to_snake_case(model_name))
5857 });
5858
5859 let (target_app, target_model) = self.parse_model_reference(to_model, app_label)?;
5861 let target_table = self
5862 .to_state
5863 .get_model(&target_app, &target_model)
5864 .map(|m| m.table_name.clone())
5865 .or_else(|| {
5866 super::model_registry::global_registry()
5867 .get_models()
5868 .iter()
5869 .find(|m| m.app_label == target_app && m.model_name == target_model)
5870 .map(|m| m.table_name.clone())
5871 })
5872 .unwrap_or_else(|| format!("{}_{}", target_app, to_snake_case(&target_model)));
5873
5874 let table_name = if let Some(custom_name) = through_table {
5880 custom_name.clone()
5881 } else {
5882 format!(
5883 "{}_{}",
5884 source_table.to_lowercase(),
5885 to_snake_case(field_name)
5886 )
5887 };
5888
5889 let source_table_lower = source_table.to_lowercase();
5895 let target_table_lower = target_table.to_lowercase();
5896 let (source_column, target_column) = if source_table_lower == target_table_lower {
5897 (
5898 format!("from_{}_id", source_table_lower),
5899 format!("to_{}_id", target_table_lower),
5900 )
5901 } else {
5902 (
5903 format!("{}_id", source_table_lower),
5904 format!("{}_id", target_table_lower),
5905 )
5906 };
5907
5908 let source_pk_type = self.to_state.get_primary_key_type(app_label, model_name);
5913 let target_pk_type = self
5914 .to_state
5915 .get_primary_key_type(&target_app, &target_model);
5916
5917 let columns = vec![
5919 super::ColumnDefinition {
5921 name: "id".to_string(),
5922 type_definition: super::FieldType::BigInteger,
5923 not_null: true,
5924 unique: false,
5925 primary_key: true,
5926 auto_increment: true,
5927 default: None,
5928 },
5929 super::ColumnDefinition {
5931 name: source_column.clone(),
5932 type_definition: source_pk_type,
5933 not_null: true,
5934 unique: false,
5935 primary_key: false,
5936 auto_increment: false,
5937 default: None,
5938 },
5939 super::ColumnDefinition {
5941 name: target_column.clone(),
5942 type_definition: target_pk_type,
5943 not_null: true,
5944 unique: false,
5945 primary_key: false,
5946 auto_increment: false,
5947 default: None,
5948 },
5949 ];
5950
5951 let constraints = vec![
5953 super::Constraint::ForeignKey {
5955 name: format!("fk_{}_{}", table_name, source_column),
5956 columns: vec![source_column.clone()],
5957 referenced_table: source_table.clone(),
5958 referenced_columns: vec!["id".to_string()],
5959 on_delete: super::ForeignKeyAction::Cascade,
5960 on_update: super::ForeignKeyAction::Cascade,
5961 deferrable: None,
5962 },
5963 super::Constraint::ForeignKey {
5965 name: format!("fk_{}_{}", table_name, target_column),
5966 columns: vec![target_column.clone()],
5967 referenced_table: target_table.clone(),
5968 referenced_columns: vec!["id".to_string()],
5969 on_delete: super::ForeignKeyAction::Cascade,
5970 on_update: super::ForeignKeyAction::Cascade,
5971 deferrable: None,
5972 },
5973 super::Constraint::Unique {
5975 name: format!(
5976 "uq_{}_{}_{}",
5977 table_name,
5978 source_column.replace("_id", ""),
5979 target_column.replace("_id", "")
5980 ),
5981 columns: vec![source_column, target_column],
5982 },
5983 ];
5984
5985 Some(super::Operation::CreateTable {
5986 name: table_name,
5987 columns,
5988 constraints,
5989 without_rowid: None,
5990 interleave_in_parent: None,
5991 partition: None,
5992 })
5993 }
5994
5995 fn sort_operations_by_dependency(
6042 &self,
6043 mut operations: Vec<super::Operation>,
6044 ) -> Vec<super::Operation> {
6045 let mut sorted = Vec::new();
6046
6047 let create_tables: Vec<_> = operations
6049 .iter()
6050 .filter(|op| matches!(op, super::Operation::CreateTable { .. }))
6051 .cloned()
6052 .collect();
6053 operations.retain(|op| !matches!(op, super::Operation::CreateTable { .. }));
6054
6055 let field_ops: Vec<_> = operations
6057 .iter()
6058 .filter(|op| {
6059 matches!(
6060 op,
6061 super::Operation::AddColumn { .. } | super::Operation::AlterColumn { .. }
6062 )
6063 })
6064 .cloned()
6065 .collect();
6066 operations.retain(|op| {
6067 !matches!(
6068 op,
6069 super::Operation::AddColumn { .. } | super::Operation::AlterColumn { .. }
6070 )
6071 });
6072
6073 sorted.extend(Self::topological_sort_create_tables(create_tables));
6077 sorted.extend(field_ops);
6078 sorted.extend(operations); sorted
6081 }
6082
6083 fn operation_targets_table(operation: &super::Operation, table_name: &str) -> bool {
6084 match operation {
6085 super::Operation::AddColumn { table, .. }
6086 | super::Operation::AlterColumn { table, .. }
6087 | super::Operation::RenameColumn { table, .. }
6088 | super::Operation::AddConstraint { table, .. }
6089 | super::Operation::DropConstraint { table, .. }
6090 | super::Operation::CreateIndex { table, .. }
6091 | super::Operation::CreateIndexRepair { table, .. }
6092 | super::Operation::DropIndex { table, .. }
6093 | super::Operation::DropNamedIndex { table, .. }
6094 | super::Operation::CreateCompositePrimaryKey { table, .. }
6095 | super::Operation::SetAutoIncrementValue { table, .. } => table == table_name,
6096 super::Operation::CreateTable { name, .. } | super::Operation::DropTable { name } => {
6097 name == table_name
6098 }
6099 super::Operation::RenameTable { old_name, new_name } => {
6100 old_name == table_name || new_name == table_name
6101 }
6102 _ => false,
6103 }
6104 }
6105
6106 fn constraint_references_table(constraint: &super::Constraint, table_name: &str) -> bool {
6107 match constraint {
6108 super::Constraint::ForeignKey {
6109 referenced_table, ..
6110 }
6111 | super::Constraint::OneToOne {
6112 referenced_table, ..
6113 } => referenced_table == table_name,
6114 super::Constraint::ManyToMany {
6115 target_table,
6116 through_table,
6117 ..
6118 } => target_table == table_name || through_table == table_name,
6119 super::Constraint::PrimaryKey { .. }
6120 | super::Constraint::Unique { .. }
6121 | super::Constraint::Check { .. }
6122 | super::Constraint::Exclude { .. } => false,
6123 }
6124 }
6125
6126 fn reference_tail_starts_with_table(tail: &str, table_name: &str) -> bool {
6127 let tail = tail.trim_start();
6128 let Some(first_char) = tail.chars().next() else {
6129 return false;
6130 };
6131
6132 let (referenced_table, rest) = if first_char == '"' {
6133 let Some(end_quote) = tail[1..].find('"') else {
6134 return false;
6135 };
6136 (&tail[1..=end_quote], &tail[end_quote + 2..])
6137 } else {
6138 let end = tail
6139 .find(|ch: char| ch == '(' || ch.is_whitespace())
6140 .unwrap_or(tail.len());
6141 (&tail[..end], &tail[end..])
6142 };
6143
6144 referenced_table == table_name
6145 && (rest.is_empty()
6146 || rest.starts_with('(')
6147 || rest.chars().next().is_some_and(char::is_whitespace))
6148 }
6149
6150 fn constraint_sql_references_table(constraint_sql: &str, table_name: &str) -> bool {
6151 let mut rest = constraint_sql;
6152 while let Some(index) = rest.find("REFERENCES ") {
6153 let tail = &rest[index + "REFERENCES ".len()..];
6154 if Self::reference_tail_starts_with_table(tail, table_name) {
6155 return true;
6156 }
6157 rest = tail;
6158 }
6159 false
6160 }
6161
6162 fn operation_references_table(operation: &super::Operation, table_name: &str) -> bool {
6163 match operation {
6164 super::Operation::CreateTable { constraints, .. } => constraints
6165 .iter()
6166 .any(|constraint| Self::constraint_references_table(constraint, table_name)),
6167 super::Operation::AddConstraint { constraint_sql, .. } => {
6168 Self::constraint_sql_references_table(constraint_sql, table_name)
6169 }
6170 _ => false,
6171 }
6172 }
6173
6174 fn operation_needs_table_after_rename(
6175 operation: &super::Operation,
6176 new_table_name: &str,
6177 ) -> bool {
6178 Self::operation_targets_table(operation, new_table_name)
6179 || Self::operation_references_table(operation, new_table_name)
6180 }
6181
6182 fn table_rename_names(operation: &super::Operation) -> Option<(String, String)> {
6183 match operation {
6184 super::Operation::RenameTable { old_name, new_name } => {
6185 Some((old_name.clone(), new_name.clone()))
6186 }
6187 super::Operation::MoveModel {
6188 rename_table: true,
6189 old_table_name: Some(old_name),
6190 new_table_name: Some(new_name),
6191 ..
6192 } => Some((old_name.clone(), new_name.clone())),
6193 _ => None,
6194 }
6195 }
6196
6197 fn order_renamed_table_operations(operations: &mut Vec<super::Operation>) {
6198 let mut index = 0;
6199 while index < operations.len() {
6200 let (old_name, new_name) = match Self::table_rename_names(&operations[index]) {
6201 Some(names) => names,
6202 _ => {
6203 index += 1;
6204 continue;
6205 }
6206 };
6207
6208 let rename_operation = operations.remove(index);
6209 let mut before_rename = Vec::new();
6210 let mut after_rename = Vec::new();
6211
6212 for (candidate_index, operation) in std::mem::take(operations).into_iter().enumerate() {
6213 if Self::operation_targets_table(&operation, &old_name) {
6214 before_rename.push(operation);
6215 } else if Self::operation_needs_table_after_rename(&operation, &new_name) {
6216 after_rename.push(operation);
6217 } else if candidate_index < index {
6218 before_rename.push(operation);
6219 } else {
6220 after_rename.push(operation);
6221 }
6222 }
6223
6224 let next_index = before_rename.len() + 1;
6225 before_rename.push(rename_operation);
6226 before_rename.append(&mut after_rename);
6227 *operations = before_rename;
6228 index = next_index;
6229 }
6230 }
6231
6232 fn operation_index_references_column(
6233 operation: &super::Operation,
6234 table_name: &str,
6235 column: &str,
6236 ) -> bool {
6237 match operation {
6238 super::Operation::CreateIndex {
6239 table,
6240 columns,
6241 expressions,
6242 where_clause,
6243 ..
6244 }
6245 | super::Operation::CreateIndexRepair {
6246 table,
6247 columns,
6248 expressions,
6249 where_clause,
6250 ..
6251 }
6252 | super::Operation::DropNamedIndex {
6253 table,
6254 columns,
6255 expressions,
6256 where_clause,
6257 ..
6258 } => {
6259 table == table_name
6260 && (columns.iter().any(|field| field == column)
6261 || expressions.as_deref().is_some_and(|expressions| {
6262 expressions.iter().any(|expression| {
6263 ProjectState::expression_references_column(expression, column)
6264 })
6265 }) || where_clause.as_deref().is_some_and(|where_clause| {
6266 ProjectState::expression_references_column(where_clause, column)
6267 }))
6268 }
6269 super::Operation::DropIndex { table, columns } => {
6270 table == table_name && columns.iter().any(|field| field == column)
6271 }
6272 _ => false,
6273 }
6274 }
6275
6276 fn order_renamed_column_operations(operations: &mut Vec<super::Operation>) {
6277 let mut index = 0;
6278 while index < operations.len() {
6279 let (table, old_name, new_name) = match &operations[index] {
6280 super::Operation::RenameColumn {
6281 table,
6282 old_name,
6283 new_name,
6284 } => (table.clone(), old_name.clone(), new_name.clone()),
6285 _ => {
6286 index += 1;
6287 continue;
6288 }
6289 };
6290
6291 let mut remaining = std::mem::take(operations);
6292 let rename_operation = remaining.remove(index);
6293 let prefix = remaining.drain(..index).collect::<Vec<_>>();
6294 let mut before_rename = Vec::new();
6295 let mut after_rename = Vec::new();
6296 let mut prefix_without_recreated_indexes = Vec::new();
6297 for operation in prefix {
6298 match &operation {
6299 super::Operation::CreateIndex { .. }
6300 | super::Operation::CreateIndexRepair { .. }
6301 if Self::operation_index_references_column(
6302 &operation, &table, &new_name,
6303 ) =>
6304 {
6305 after_rename.push(operation);
6306 }
6307 _ => prefix_without_recreated_indexes.push(operation),
6308 }
6309 }
6310 for operation in remaining {
6311 match &operation {
6312 super::Operation::DropIndex { .. }
6313 | super::Operation::DropNamedIndex { .. }
6314 if Self::operation_index_references_column(
6315 &operation, &table, &old_name,
6316 ) =>
6317 {
6318 before_rename.push(operation);
6319 }
6320 super::Operation::CreateIndex { .. }
6321 | super::Operation::CreateIndexRepair { .. }
6322 if Self::operation_index_references_column(
6323 &operation, &table, &new_name,
6324 ) =>
6325 {
6326 after_rename.push(operation);
6327 }
6328 _ => after_rename.push(operation),
6329 }
6330 }
6331
6332 let rename_position = prefix_without_recreated_indexes.len() + before_rename.len();
6333 let mut reordered = prefix_without_recreated_indexes;
6334 reordered.extend(before_rename);
6335 reordered.push(rename_operation);
6336 reordered.extend(after_rename);
6337 *operations = reordered;
6338 index = rename_position + 1;
6339 }
6340 }
6341
6342 pub fn generate_operations(&self) -> Vec<super::Operation> {
6344 let changes = self.detect_changes();
6345 self.generate_operations_from_changes(&changes)
6346 }
6347
6348 pub fn try_generate_operations(&self) -> super::Result<Vec<super::Operation>> {
6350 let changes = self.try_detect_changes()?;
6351 Ok(self.generate_operations_from_changes(&changes))
6352 }
6353
6354 fn generate_operations_from_changes(&self, changes: &DetectedChanges) -> Vec<super::Operation> {
6355 let mut by_app: BTreeMap<String, Vec<super::Operation>> = BTreeMap::new();
6356
6357 self.emit_shared_per_app_operations(changes, &mut by_app);
6362
6363 for (app_label, model_name) in &changes.created_models {
6369 if let Some(model) = self.to_state.get_model(app_label, model_name) {
6370 for (field_name, field_state) in &model.fields {
6371 if let super::FieldType::ManyToMany { to, through } = &field_state.field_type
6372 && let Some(operation) = self.generate_intermediate_table(
6373 app_label, model_name, field_name, to, through,
6374 ) {
6375 by_app.entry(app_label.clone()).or_default().push(operation);
6376 }
6377 }
6378 }
6379 }
6380 for (app_label, model_name, field_name) in &changes.added_fields {
6381 if let Some(model) = self.to_state.get_model(app_label, model_name)
6382 && let Some(field) = model.get_field(field_name)
6383 && let super::FieldType::ManyToMany { to, through } = &field.field_type
6384 && let Some(operation) =
6385 self.generate_intermediate_table(app_label, model_name, field_name, to, through)
6386 {
6387 by_app.entry(app_label.clone()).or_default().push(operation);
6388 }
6389 }
6390
6391 Self::dedup_redundant_unique_add_constraints(&mut by_app);
6403 for operations in by_app.values_mut() {
6404 Self::order_renamed_column_operations(operations);
6405 }
6406
6407 let operations: Vec<super::Operation> = by_app.into_values().flatten().collect();
6409 self.sort_operations_by_dependency(operations)
6410 }
6411
6412 fn emit_shared_per_app_operations(
6427 &self,
6428 changes: &DetectedChanges,
6429 by_app: &mut std::collections::BTreeMap<String, Vec<super::Operation>>,
6430 ) {
6431 for (app_label, model_name) in &changes.created_models {
6433 if let Some(model) = self.to_state.get_model(app_label, model_name) {
6434 let mut columns = Vec::new();
6435 for (field_name, field_state) in &model.fields {
6436 columns.push(super::ColumnDefinition::from_field_state(
6437 field_name.clone(),
6438 field_state,
6439 ));
6440 }
6441
6442 let constraints: Vec<super::operations::Constraint> = model
6443 .constraints
6444 .iter()
6445 .map(|c| c.to_constraint())
6446 .collect();
6447
6448 by_app
6449 .entry(app_label.clone())
6450 .or_default()
6451 .push(super::Operation::CreateTable {
6452 name: model.table_name.clone(),
6453 columns,
6454 constraints,
6455 without_rowid: None,
6456 interleave_in_parent: None,
6457 partition: None,
6458 });
6459
6460 for index in &model.indexes {
6461 by_app
6462 .entry(app_label.clone())
6463 .or_default()
6464 .push(index.create_operation(&model.table_name));
6465 }
6466 }
6467 }
6468
6469 for (app_label, model_name, field_name) in &changes.added_fields {
6477 if let Some(model) = self.to_state.get_model(app_label, model_name)
6478 && let Some(field) = model.get_field(field_name)
6479 {
6480 by_app
6481 .entry(app_label.clone())
6482 .or_default()
6483 .push(super::Operation::AddColumn {
6484 table: model.table_name.clone(),
6485 column: super::ColumnDefinition::from_field_state(
6486 field_name.clone(),
6487 field,
6488 ),
6489 mysql_options: None,
6490 });
6491 }
6492 }
6493
6494 for (app_label, model_name, old_name, new_name) in &changes.renamed_fields {
6496 if let Some(model) = self.to_state.get_model(app_label, model_name) {
6497 by_app
6498 .entry(app_label.clone())
6499 .or_default()
6500 .push(super::Operation::RenameColumn {
6501 table: model.table_name.clone(),
6502 old_name: old_name.clone(),
6503 new_name: new_name.clone(),
6504 });
6505 }
6506 }
6507
6508 for (app_label, model_name, field_name) in &changes.altered_fields {
6510 if let Some(model) = self.to_state.get_model(app_label, model_name)
6511 && let Some(field) = model.get_field(field_name)
6512 {
6513 let old_definition = self
6514 .from_state
6515 .get_model(app_label, model_name)
6516 .and_then(|from_model| from_model.get_field(field_name))
6517 .map(|from_field| {
6518 super::ColumnDefinition::from_field_state(field_name.clone(), from_field)
6519 });
6520 by_app
6521 .entry(app_label.clone())
6522 .or_default()
6523 .push(super::Operation::AlterColumn {
6524 table: model.table_name.clone(),
6525 old_definition,
6526 column: field_name.clone(),
6527 new_definition: super::ColumnDefinition::from_field_state(
6528 field_name.clone(),
6529 field,
6530 ),
6531 mysql_options: None,
6532 });
6533 }
6534 }
6535
6536 for (app_label, model_name, constraint_name) in &changes.removed_constraints {
6542 let Some(from_model) = self.from_state.get_model(app_label, model_name) else {
6543 continue;
6544 };
6545 let is_composite_pk = from_model
6546 .constraints
6547 .iter()
6548 .find(|c| &c.name == constraint_name)
6549 .is_some_and(|c| c.constraint_type == "primary_key" && c.fields.len() >= 2);
6550 if is_composite_pk {
6551 continue;
6552 }
6553 by_app
6554 .entry(app_label.clone())
6555 .or_default()
6556 .push(super::Operation::DropConstraint {
6557 table: from_model.table_name.clone(),
6558 constraint_name: constraint_name.clone(),
6559 });
6560 }
6561
6562 for (app_label, model_name, index_name) in &changes.removed_indexes {
6565 let Some(model) = self.from_state.get_model(app_label, model_name) else {
6566 continue;
6567 };
6568 let Some(index) = model.indexes.iter().find(|index| &index.name == index_name) else {
6569 continue;
6570 };
6571 by_app
6572 .entry(app_label.clone())
6573 .or_default()
6574 .push(index.drop_operation(&model.table_name));
6575 }
6576
6577 for (app_label, model_name, index) in &changes.added_indexes {
6579 if let Some(model) = self.to_state.get_model(app_label, model_name) {
6580 by_app
6581 .entry(app_label.clone())
6582 .or_default()
6583 .push(index.create_operation(&model.table_name));
6584 }
6585 }
6586
6587 for (app_label, model_name, field_name) in &changes.removed_fields {
6589 if let Some(model) = self.from_state.get_model(app_label, model_name) {
6590 by_app
6591 .entry(app_label.clone())
6592 .or_default()
6593 .push(super::Operation::DropColumn {
6594 table: model.table_name.clone(),
6595 column: field_name.clone(),
6596 });
6597 }
6598 }
6599
6600 for (app_label, model_name) in &changes.deleted_models {
6602 if let Some(model) = self.from_state.get_model(app_label, model_name) {
6603 by_app
6604 .entry(app_label.clone())
6605 .or_default()
6606 .push(super::Operation::DropTable {
6607 name: model.table_name.clone(),
6608 });
6609 }
6610 }
6611
6612 for (app_label, model_name, constraint_name) in &changes.removed_composite_primary_keys {
6614 if let Some(model) = self.from_state.get_model(app_label, model_name) {
6615 by_app.entry(app_label.clone()).or_default().push(
6616 super::Operation::DropConstraint {
6617 table: model.table_name.clone(),
6618 constraint_name: constraint_name.clone(),
6619 },
6620 );
6621 }
6622 }
6623
6624 for (app_label, model_name, constraint) in &changes.added_composite_primary_keys {
6626 if let Some(model) = self.to_state.get_model(app_label, model_name) {
6627 by_app.entry(app_label.clone()).or_default().push(
6628 super::Operation::CreateCompositePrimaryKey {
6629 table: model.table_name.clone(),
6630 columns: constraint.fields.clone(),
6631 constraint_name: Some(constraint.name.clone()),
6632 },
6633 );
6634 }
6635 }
6636
6637 for (app_label, model_name, constraint) in &changes.added_constraints {
6651 if constraint.constraint_type == "primary_key" && constraint.fields.len() >= 2 {
6652 continue;
6653 }
6654 let Some(to_model) = self.to_state.get_model(app_label, model_name) else {
6655 continue;
6656 };
6657 let constraint_sql = constraint.to_constraint().to_string();
6658 by_app
6659 .entry(app_label.clone())
6660 .or_default()
6661 .push(super::Operation::AddConstraint {
6662 table: to_model.table_name.clone(),
6663 constraint_sql,
6664 });
6665 }
6666
6667 for (app_label, model_name, column, value) in &changes.auto_increment_resets {
6669 if let Some(model) = self.to_state.get_model(app_label, model_name) {
6670 by_app.entry(app_label.clone()).or_default().push(
6671 super::Operation::SetAutoIncrementValue {
6672 table: model.table_name.clone(),
6673 column: column.clone(),
6674 value: *value,
6675 },
6676 );
6677 }
6678 }
6679 }
6680
6681 pub fn generate_migrations(&self) -> Vec<super::Migration> {
6723 let changes = self.detect_changes();
6724 self.generate_migrations_from_changes(&changes)
6725 }
6726
6727 pub fn try_generate_migrations(&self) -> super::Result<Vec<super::Migration>> {
6729 let changes = self.try_detect_changes()?;
6730 Ok(self.generate_migrations_from_changes(&changes))
6731 }
6732
6733 fn generate_migrations_from_changes(&self, changes: &DetectedChanges) -> Vec<super::Migration> {
6734 let mut migrations_by_app: BTreeMap<String, Vec<super::Operation>> = BTreeMap::new();
6735
6736 self.emit_shared_per_app_operations(changes, &mut migrations_by_app);
6741
6742 for (app_label, model_name, through_table, m2m) in &changes.created_many_to_many {
6744 let source_table = self
6749 .to_state
6750 .get_model(app_label, model_name)
6751 .map(|m| m.table_name.clone())
6752 .unwrap_or_else(|| format!("{}_{}", app_label, model_name.to_lowercase()));
6753
6754 let (parsed_target_app, parsed_target_model) =
6762 self.resolve_model_reference(&m2m.to_model, app_label);
6763
6764 let target_table = self
6772 .to_state
6773 .get_model(&parsed_target_app, &parsed_target_model)
6774 .map(|model| model.table_name.clone())
6775 .or_else(|| {
6776 super::model_registry::global_registry()
6777 .get_models()
6778 .iter()
6779 .find(|m| {
6780 m.app_label == parsed_target_app && m.model_name == parsed_target_model
6781 })
6782 .map(|m| m.table_name.clone())
6783 })
6784 .unwrap_or_else(|| {
6785 format!(
6786 "{}_{}",
6787 parsed_target_app,
6788 parsed_target_model.to_lowercase()
6789 )
6790 });
6791
6792 let (default_source_col, default_target_col) =
6798 crate::m2m_naming::default_m2m_columns(&source_table, &target_table);
6799 let source_column = m2m.source_field.clone().unwrap_or(default_source_col);
6800 let target_column = m2m.target_field.clone().unwrap_or(default_target_col);
6801
6802 let source_pk_type = self.to_state.get_primary_key_type(app_label, model_name);
6804
6805 let target_pk_type = self
6809 .to_state
6810 .get_primary_key_type(&parsed_target_app, &parsed_target_model);
6811
6812 let columns = vec![
6814 super::ColumnDefinition {
6815 name: "id".to_string(),
6816 type_definition: super::FieldType::Integer,
6817 not_null: true,
6818 unique: false,
6819 primary_key: true,
6820 auto_increment: true,
6821 default: None,
6822 },
6823 super::ColumnDefinition {
6824 name: source_column.clone(),
6825 type_definition: source_pk_type.clone(),
6826 not_null: true,
6827 unique: false,
6828 primary_key: false,
6829 auto_increment: false,
6830 default: None,
6831 },
6832 super::ColumnDefinition {
6833 name: target_column.clone(),
6834 type_definition: target_pk_type,
6835 not_null: true,
6836 unique: false,
6837 primary_key: false,
6838 auto_increment: false,
6839 default: None,
6840 },
6841 ];
6842
6843 let constraints = vec![
6845 super::operations::Constraint::ForeignKey {
6846 name: format!("fk_{}_{}", through_table, source_column),
6847 columns: vec![source_column.clone()],
6848 referenced_table: source_table.clone(),
6849 referenced_columns: vec!["id".to_string()],
6850 on_delete: ForeignKeyAction::Cascade,
6851 on_update: ForeignKeyAction::Cascade,
6852 deferrable: None,
6853 },
6854 super::operations::Constraint::ForeignKey {
6855 name: format!("fk_{}_{}", through_table, target_column),
6856 columns: vec![target_column.clone()],
6857 referenced_table: target_table,
6858 referenced_columns: vec!["id".to_string()],
6859 on_delete: ForeignKeyAction::Cascade,
6860 on_update: ForeignKeyAction::Cascade,
6861 deferrable: None,
6862 },
6863 super::operations::Constraint::Unique {
6865 name: format!("{}_unique", through_table),
6866 columns: vec![source_column, target_column],
6867 },
6868 ];
6869
6870 migrations_by_app
6871 .entry(app_label.clone())
6872 .or_default()
6873 .push(super::Operation::CreateTable {
6874 name: through_table.clone(),
6875 columns,
6876 constraints,
6877 without_rowid: None,
6878 interleave_in_parent: None,
6879 partition: None,
6880 });
6881 }
6882
6883 for (app_label, old_name, new_name) in &changes.renamed_models {
6885 if let Some(model) = self.to_state.get_model(app_label, new_name) {
6886 let Some(old_model) = self.from_state.get_model(app_label, old_name) else {
6888 continue;
6889 };
6890 let old_table_name = old_model.table_name.clone();
6891
6892 if old_table_name != model.table_name {
6894 let renamed_constraints =
6895 Self::renamed_single_field_unique_constraints(old_model, model);
6896 let renamed_indexes = old_model
6897 .indexes
6898 .iter()
6899 .filter_map(|old_index| {
6900 model
6901 .indexes
6902 .iter()
6903 .find(|new_index| {
6904 model_index_definitions_equivalent(
6905 old_model, old_index, model, new_index,
6906 )
6907 })
6908 .filter(|new_index| old_index.name != new_index.name)
6909 .map(|new_index| (old_index.clone(), new_index.clone()))
6910 })
6911 .collect::<Vec<_>>();
6912 let operations = migrations_by_app.entry(app_label.clone()).or_default();
6913 for (old_constraint, _) in &renamed_constraints {
6914 operations.push(super::Operation::DropConstraint {
6915 table: old_table_name.clone(),
6916 constraint_name: old_constraint.name.clone(),
6917 });
6918 }
6919 for (old_index, _) in &renamed_indexes {
6920 operations.push(old_index.drop_operation(&old_table_name));
6921 }
6922 operations.push(super::Operation::RenameTable {
6923 old_name: old_table_name,
6924 new_name: model.table_name.clone(),
6925 });
6926 for (_, new_constraint) in renamed_constraints {
6927 operations.push(super::Operation::AddConstraint {
6928 table: model.table_name.clone(),
6929 constraint_sql: new_constraint.to_constraint().to_string(),
6930 });
6931 }
6932 for (_, new_index) in renamed_indexes {
6933 operations.push(new_index.create_operation(&model.table_name));
6934 }
6935 }
6936 }
6937 }
6938
6939 for (
6942 from_app,
6943 from_model_name,
6944 to_app,
6945 to_model_name,
6946 rename_table,
6947 old_table,
6948 new_table,
6949 ) in &changes.moved_models
6950 {
6951 let old_table_name = old_table.clone().unwrap_or_else(|| {
6953 self.from_state
6954 .get_model(from_app, from_model_name)
6955 .map(|m| m.table_name.clone())
6956 .unwrap_or_else(|| format!("{}_{}", from_app, from_model_name.to_lowercase()))
6957 });
6958
6959 let new_table_name = new_table.clone().unwrap_or_else(|| {
6960 self.to_state
6961 .get_model(to_app, to_model_name)
6962 .map(|m| m.table_name.clone())
6963 .unwrap_or_else(|| format!("{}_{}", to_app, to_model_name.to_lowercase()))
6964 });
6965
6966 let (renamed_constraints, renamed_indexes) = if *rename_table {
6967 match (
6968 self.from_state.get_model(from_app, from_model_name),
6969 self.to_state.get_model(to_app, to_model_name),
6970 ) {
6971 (Some(old_model), Some(new_model)) => (
6972 Self::renamed_single_field_unique_constraints(old_model, new_model),
6973 old_model
6974 .indexes
6975 .iter()
6976 .filter_map(|old_index| {
6977 new_model
6978 .indexes
6979 .iter()
6980 .find(|new_index| {
6981 model_index_definitions_equivalent(
6982 old_model, old_index, new_model, new_index,
6983 )
6984 })
6985 .filter(|new_index| old_index.name != new_index.name)
6986 .map(|new_index| (old_index.clone(), new_index.clone()))
6987 })
6988 .collect::<Vec<_>>(),
6989 ),
6990 _ => (Vec::new(), Vec::new()),
6991 }
6992 } else {
6993 (Vec::new(), Vec::new())
6994 };
6995 let operations = migrations_by_app.entry(to_app.clone()).or_default();
6996 for (old_constraint, _) in &renamed_constraints {
6997 operations.push(super::Operation::DropConstraint {
6998 table: old_table_name.clone(),
6999 constraint_name: old_constraint.name.clone(),
7000 });
7001 }
7002 for (old_index, _) in &renamed_indexes {
7003 operations.push(old_index.drop_operation(&old_table_name));
7004 }
7005 operations.push(super::Operation::MoveModel {
7007 model_name: from_model_name.clone(),
7008 from_app: from_app.clone(),
7009 to_app: to_app.clone(),
7010 rename_table: *rename_table,
7011 old_table_name: if *rename_table {
7012 Some(old_table_name)
7013 } else {
7014 None
7015 },
7016 new_table_name: if *rename_table {
7017 Some(new_table_name.clone())
7018 } else {
7019 None
7020 },
7021 });
7022 for (_, new_constraint) in renamed_constraints {
7023 operations.push(super::Operation::AddConstraint {
7024 table: new_table_name.clone(),
7025 constraint_sql: new_constraint.to_constraint().to_string(),
7026 });
7027 }
7028 for (_, new_index) in renamed_indexes {
7029 operations.push(new_index.create_operation(&new_table_name));
7030 }
7031 }
7032
7033 Self::dedup_redundant_unique_add_constraints(&mut migrations_by_app);
7040 for operations in migrations_by_app.values_mut() {
7041 Self::order_create_tables_by_foreign_keys(operations);
7042 Self::order_renamed_table_operations(operations);
7043 Self::order_renamed_column_operations(operations);
7044 }
7045
7046 let mut migrations = Vec::new();
7048 for (app_label, operations) in migrations_by_app {
7049 let migration_name = "autodetected".to_string();
7052
7053 let mut migration = super::Migration::new(&migration_name, &app_label);
7054 for operation in operations {
7055 migration = migration.add_operation(operation);
7056 }
7057 migrations.push(migration);
7058 }
7059
7060 migrations
7061 }
7062
7063 fn detect_created_many_to_many(&self, changes: &mut DetectedChanges) {
7106 for ((app_label, model_name), model_state) in &self.to_state.models {
7107 for m2m in &model_state.many_to_many_fields {
7108 let through_table = m2m.through.clone().unwrap_or_else(|| {
7125 crate::m2m_naming::default_through_table(
7126 &model_state.table_name,
7127 &m2m.field_name,
7128 )
7129 });
7130
7131 let exists_in_from = self
7141 .from_state
7142 .find_model_by_table(&through_table)
7143 .is_some();
7144 let exists_in_to = self.to_state.find_model_by_table(&through_table).is_some();
7145
7146 if !exists_in_from && !exists_in_to {
7147 changes.created_many_to_many.push((
7149 app_label.clone(),
7150 model_name.clone(),
7151 through_table.clone(),
7152 m2m.clone(),
7153 ));
7154
7155 let target_app = self
7158 .find_model_app(&m2m.to_model)
7159 .unwrap_or_else(|| app_label.clone());
7160
7161 changes
7162 .model_dependencies
7163 .entry((app_label.clone(), through_table))
7164 .or_default()
7165 .extend(vec![
7166 (app_label.clone(), model_name.clone()),
7167 (target_app, m2m.to_model.clone()),
7168 ]);
7169 }
7170 }
7171 }
7172 }
7173
7174 fn find_model_app(&self, model_name: &str) -> Option<String> {
7179 for (app_label, name) in self.to_state.models.keys() {
7181 if name == model_name {
7182 return Some(app_label.clone());
7183 }
7184 }
7185
7186 for model_meta in super::model_registry::global_registry().get_models() {
7189 if model_meta.model_name == model_name {
7190 return Some(model_meta.app_label.clone());
7191 }
7192 }
7193
7194 None
7195 }
7196
7197 fn detect_model_dependencies(&self, changes: &mut DetectedChanges) {
7245 use std::collections::BTreeSet;
7246
7247 for ((app_label, model_name), model) in &self.to_state.models {
7248 let current = (app_label.clone(), model_name.clone());
7249 let mut dependencies = Vec::new();
7250 let mut seen = BTreeSet::new();
7251
7252 for field in model.fields.values() {
7253 match &field.field_type {
7254 super::FieldType::ForeignKey { to_table, .. } => {
7255 self.record_table_dependency(
7256 to_table,
7257 ¤t,
7258 &mut dependencies,
7259 &mut seen,
7260 );
7261 }
7262 super::FieldType::OneToOne { to, .. } => {
7263 if let Some(dep) = self.parse_model_reference(to, app_label) {
7264 self.record_model_dependency(
7265 dep,
7266 ¤t,
7267 &mut dependencies,
7268 &mut seen,
7269 );
7270 }
7271 }
7272 super::FieldType::ManyToMany { to, .. } => {
7273 if let Some(dep) = self.parse_model_reference(to, app_label) {
7274 self.record_model_dependency(
7275 dep,
7276 ¤t,
7277 &mut dependencies,
7278 &mut seen,
7279 );
7280 }
7281 }
7282 super::FieldType::Custom(s) => {
7283 if let Some(dep) = self.extract_related_model(s, app_label) {
7284 self.record_model_dependency(
7285 dep,
7286 ¤t,
7287 &mut dependencies,
7288 &mut seen,
7289 );
7290 }
7291 }
7292 _ => {}
7293 }
7294
7295 if let Some(fk) = &field.foreign_key {
7296 self.record_table_dependency(
7297 &fk.referenced_table,
7298 ¤t,
7299 &mut dependencies,
7300 &mut seen,
7301 );
7302 }
7303 }
7304
7305 for constraint in &model.constraints {
7306 if (constraint
7307 .constraint_type
7308 .eq_ignore_ascii_case("foreign_key")
7309 || constraint
7310 .constraint_type
7311 .eq_ignore_ascii_case("one_to_one"))
7312 && let Some(fk_info) = &constraint.foreign_key_info
7313 {
7314 self.record_table_dependency(
7315 &fk_info.referenced_table,
7316 ¤t,
7317 &mut dependencies,
7318 &mut seen,
7319 );
7320 }
7321 }
7322
7323 if !dependencies.is_empty() {
7324 changes.model_dependencies.insert(current, dependencies);
7325 }
7326 }
7327 }
7328
7329 fn record_table_dependency(
7330 &self,
7331 table_name: &str,
7332 current: &(String, String),
7333 dependencies: &mut Vec<(String, String)>,
7334 seen: &mut std::collections::BTreeSet<(String, String)>,
7335 ) {
7336 if let Some(dep) = self.resolve_model_by_table(table_name) {
7337 self.record_model_dependency(dep, current, dependencies, seen);
7338 }
7339 }
7340
7341 fn record_model_dependency(
7342 &self,
7343 dep: (String, String),
7344 current: &(String, String),
7345 dependencies: &mut Vec<(String, String)>,
7346 seen: &mut std::collections::BTreeSet<(String, String)>,
7347 ) {
7348 if dep != *current && seen.insert(dep.clone()) {
7349 dependencies.push(dep);
7350 }
7351 }
7352
7353 fn resolve_model_by_table(&self, table_name: &str) -> Option<(String, String)> {
7354 if let Some(model) = self.to_state.find_model_by_table(table_name) {
7355 return Some((model.app_label.clone(), model.name.clone()));
7356 }
7357 if let Some(model) = self.from_state.find_model_by_table(table_name) {
7358 return Some((model.app_label.clone(), model.name.clone()));
7359 }
7360 self.find_model_by_table_name(table_name)
7361 }
7362
7363 fn extract_related_model(
7379 &self,
7380 field_type: &str,
7381 current_app: &str,
7382 ) -> Option<(String, String)> {
7383 if let Some(inner) = field_type
7385 .strip_prefix("ForeignKey(")
7386 .and_then(|s| s.strip_suffix(")"))
7387 {
7388 return self.parse_model_reference(inner, current_app);
7389 }
7390
7391 if let Some(inner) = field_type
7393 .strip_prefix("ManyToManyField(")
7394 .and_then(|s| s.strip_suffix(")"))
7395 {
7396 return self.parse_model_reference(inner, current_app);
7397 }
7398
7399 if let Some(inner) = field_type
7401 .strip_prefix("OneToOneField(")
7402 .and_then(|s| s.strip_suffix(")"))
7403 {
7404 return self.parse_model_reference(inner, current_app);
7405 }
7406
7407 None
7408 }
7409
7410 fn parse_model_reference(
7424 &self,
7425 reference: &str,
7426 current_app: &str,
7427 ) -> Option<(String, String)> {
7428 let parts: Vec<&str> = reference.split('.').collect();
7429 match parts.as_slice() {
7430 [app, model] => Some((app.to_string(), model.to_string())),
7432 [model] => {
7434 Some((current_app.to_string(), model.to_string()))
7436 }
7437 _ => None,
7439 }
7440 }
7441
7442 fn resolve_model_reference(&self, reference: &str, current_app: &str) -> (String, String) {
7443 let parts: Vec<&str> = reference.split('.').collect();
7444 match parts.as_slice() {
7445 [app, model] => (app.to_string(), model.to_string()),
7446 [model] => {
7447 let model = model.to_string();
7448 if self.to_state.get_model(current_app, &model).is_some() {
7449 (current_app.to_string(), model)
7450 } else {
7451 (
7452 self.find_model_app(&model)
7453 .unwrap_or_else(|| current_app.to_string()),
7454 model,
7455 )
7456 }
7457 }
7458 _ => (current_app.to_string(), reference.to_string()),
7459 }
7460 }
7461
7462 pub fn foreign_key_provider_apps(
7467 to_state: &ProjectState,
7468 operations: &[super::Operation],
7469 current_app: &str,
7470 ) -> Vec<String> {
7471 use std::collections::BTreeSet;
7472
7473 let mut apps = BTreeSet::new();
7474 for operation in operations {
7475 for table in Self::referenced_tables_in_operation(operation) {
7476 if let Some(model) = to_state.find_model_by_table(&table)
7477 && model.app_label != current_app
7478 {
7479 apps.insert(model.app_label.clone());
7480 }
7481 }
7482 if let super::Operation::MoveModel { from_app, .. } = operation
7483 && from_app != current_app
7484 {
7485 apps.insert(from_app.clone());
7486 }
7487 }
7488 apps.into_iter().collect()
7489 }
7490
7491 fn referenced_tables_in_operation(operation: &super::Operation) -> Vec<String> {
7492 match operation {
7493 super::Operation::CreateTable { constraints, .. } => {
7494 Self::referenced_tables_from_constraints(constraints)
7495 }
7496 super::Operation::AddConstraint { constraint_sql, .. } => {
7497 Self::referenced_tables_from_constraint_sql(constraint_sql)
7498 }
7499 _ => Vec::new(),
7500 }
7501 }
7502
7503 fn referenced_tables_from_constraints(
7504 constraints: &[super::operations::Constraint],
7505 ) -> Vec<String> {
7506 let mut tables = Vec::new();
7507 for constraint in constraints {
7508 match constraint {
7509 super::operations::Constraint::ForeignKey {
7510 referenced_table, ..
7511 }
7512 | super::operations::Constraint::OneToOne {
7513 referenced_table, ..
7514 } => {
7515 tables.push(referenced_table.clone());
7516 }
7517 super::operations::Constraint::ManyToMany { target_table, .. } => {
7518 tables.push(target_table.clone());
7519 }
7520 _ => {}
7521 }
7522 }
7523 tables
7524 }
7525
7526 fn referenced_tables_from_constraint_sql(constraint_sql: &str) -> Vec<String> {
7527 let mut tables = Vec::new();
7528 let mut rest = constraint_sql;
7529 while let Some(index) = rest.find("REFERENCES ") {
7530 let tail = rest[index + "REFERENCES ".len()..].trim_start();
7531 if tail.is_empty() {
7532 break;
7533 }
7534 let (table, remaining) = if let Some(stripped) = tail.strip_prefix('"') {
7535 match stripped.find('"') {
7536 Some(end) => (&stripped[..end], &stripped[end + 1..]),
7537 None => break,
7538 }
7539 } else {
7540 let end = tail
7541 .find(|ch: char| ch == '(' || ch.is_whitespace())
7542 .unwrap_or(tail.len());
7543 (&tail[..end], &tail[end..])
7544 };
7545 if !table.is_empty() {
7546 tables.push(table.to_string());
7547 }
7548 rest = remaining;
7549 }
7550 tables
7551 }
7552
7553 fn order_create_tables_by_foreign_keys(operations: &mut [super::Operation]) {
7554 let create_indices: Vec<usize> = operations
7555 .iter()
7556 .enumerate()
7557 .filter(|(_, op)| matches!(op, super::Operation::CreateTable { .. }))
7558 .map(|(i, _)| i)
7559 .collect();
7560 if create_indices.len() <= 1 {
7561 return;
7562 }
7563 let create_ops: Vec<super::Operation> = create_indices
7564 .iter()
7565 .map(|&i| operations[i].clone())
7566 .collect();
7567 let sorted = Self::topological_sort_create_tables(create_ops);
7568 for (slot, op) in create_indices.into_iter().zip(sorted) {
7569 operations[slot] = op;
7570 }
7571 }
7572
7573 fn topological_sort_create_tables(
7574 create_tables: Vec<super::Operation>,
7575 ) -> Vec<super::Operation> {
7576 use std::collections::{BTreeMap, BTreeSet};
7577
7578 if create_tables.len() <= 1 {
7579 return create_tables;
7580 }
7581
7582 let names: Vec<String> = create_tables
7583 .iter()
7584 .filter_map(|op| match op {
7585 super::Operation::CreateTable { name, .. } => Some(name.clone()),
7586 _ => None,
7587 })
7588 .collect();
7589 let name_set: BTreeSet<String> = names.iter().cloned().collect();
7590 let mut in_degree: BTreeMap<String, usize> =
7591 names.iter().cloned().map(|name| (name, 0)).collect();
7592 let mut dependents: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
7593
7594 for op in &create_tables {
7595 let super::Operation::CreateTable {
7596 name, constraints, ..
7597 } = op
7598 else {
7599 continue;
7600 };
7601 for referenced in Self::referenced_tables_from_constraints(constraints) {
7602 if referenced == *name || !name_set.contains(&referenced) {
7603 continue;
7604 }
7605 *in_degree.entry(name.clone()).or_insert(0) += 1;
7606 dependents
7607 .entry(referenced)
7608 .or_default()
7609 .insert(name.clone());
7610 }
7611 }
7612
7613 let mut ready: BTreeSet<String> = in_degree
7614 .iter()
7615 .filter(|(_, degree)| **degree == 0)
7616 .map(|(name, _)| name.clone())
7617 .collect();
7618 let mut ordered_names = Vec::with_capacity(names.len());
7619 while let Some(name) = ready.iter().next().cloned() {
7620 ready.remove(&name);
7621 ordered_names.push(name.clone());
7622 if let Some(children) = dependents.get(&name) {
7623 for child in children {
7624 if let Some(degree) = in_degree.get_mut(child) {
7625 *degree = degree.saturating_sub(1);
7626 if *degree == 0 {
7627 ready.insert(child.clone());
7628 }
7629 }
7630 }
7631 }
7632 }
7633
7634 if ordered_names.len() < names.len() {
7635 let mut remaining: Vec<String> = names
7636 .iter()
7637 .filter(|name| !ordered_names.contains(name))
7638 .cloned()
7639 .collect();
7640 remaining.sort();
7641 eprintln!(
7642 "⚠️ Warning: Circular foreign-key dependency detected among CreateTable operations: [{}]",
7643 remaining.join(", ")
7644 );
7645 ordered_names.extend(remaining);
7646 }
7647
7648 let mut by_name: BTreeMap<String, super::Operation> = BTreeMap::new();
7649 let mut extras = Vec::new();
7650 for op in create_tables {
7651 match &op {
7652 super::Operation::CreateTable { name, .. } => {
7653 by_name.insert(name.clone(), op);
7654 }
7655 _ => extras.push(op),
7656 }
7657 }
7658
7659 let mut sorted: Vec<super::Operation> = ordered_names
7660 .into_iter()
7661 .filter_map(|name| by_name.remove(&name))
7662 .collect();
7663 sorted.extend(by_name.into_values());
7664 sorted.extend(extras);
7665 sorted
7666 }
7667
7668 fn find_model_by_table_name(&self, table_name: &str) -> Option<(String, String)> {
7674 if let Some(model) = self.to_state.find_model_by_table(table_name) {
7675 return Some((model.app_label.clone(), model.name.clone()));
7676 }
7677 if let Some(model) = self.from_state.find_model_by_table(table_name) {
7678 return Some((model.app_label.clone(), model.name.clone()));
7679 }
7680
7681 for (app_label, model_name) in self.to_state.models.keys() {
7683 let django_table = format!("{}_{}", app_label, model_name.to_lowercase());
7685 if django_table == table_name {
7686 return Some((app_label.clone(), model_name.clone()));
7687 }
7688
7689 if model_name.to_lowercase() == table_name {
7691 return Some((app_label.clone(), model_name.clone()));
7692 }
7693 }
7694
7695 for (app_label, model_name) in self.from_state.models.keys() {
7697 let django_table = format!("{}_{}", app_label, model_name.to_lowercase());
7698 if django_table == table_name {
7699 return Some((app_label.clone(), model_name.clone()));
7700 }
7701
7702 if model_name.to_lowercase() == table_name {
7703 return Some((app_label.clone(), model_name.clone()));
7704 }
7705 }
7706
7707 None
7708 }
7709}
7710
7711impl ModelState {
7712 pub fn remove_field(&mut self, name: &str) {
7728 self.fields.remove(name);
7729 }
7730
7731 pub fn alter_field(&mut self, name: &str, new_field: FieldState) {
7750 self.fields.insert(name.to_string(), new_field);
7751 }
7752}
7753
7754#[cfg(test)]
7755mod tests {
7756 use super::*;
7757 use crate::migrations::FieldType;
7758 use rstest::rstest;
7759
7760 fn build_project_state(models: Vec<((String, String), ModelState)>) -> ProjectState {
7762 let mut state = ProjectState::new();
7763 for (key, model) in models {
7764 state.models.insert(key, model);
7765 }
7766 state
7767 }
7768
7769 fn build_model_state(
7771 app_label: &str,
7772 name: &str,
7773 fields: Vec<FieldState>,
7774 indexes: Vec<IndexDefinition>,
7775 constraints: Vec<ConstraintDefinition>,
7776 ) -> ModelState {
7777 let mut field_map = std::collections::BTreeMap::new();
7778 for f in fields {
7779 field_map.insert(f.name.clone(), f);
7780 }
7781 ModelState {
7782 app_label: app_label.to_string(),
7783 name: name.to_string(),
7784 table_name: format!("{}_{}", app_label, name.to_lowercase()),
7785 fields: field_map,
7786 options: std::collections::HashMap::new(),
7787 base_model: None,
7788 inheritance_type: None,
7789 discriminator_column: None,
7790 indexes,
7791 constraints,
7792 many_to_many_fields: Vec::new(),
7793 }
7794 }
7795
7796 #[rstest]
7797 fn apply_migration_operations_replays_foreign_key_add_constraint() {
7798 let create_posts = super::super::Operation::CreateTable {
7800 name: "blog_posts".to_string(),
7801 columns: vec![
7802 super::super::ColumnDefinition {
7803 name: "id".to_string(),
7804 type_definition: super::super::FieldType::BigInteger,
7805 not_null: true,
7806 unique: false,
7807 primary_key: true,
7808 auto_increment: true,
7809 default: None,
7810 },
7811 super::super::ColumnDefinition {
7812 name: "user_id".to_string(),
7813 type_definition: super::super::FieldType::BigInteger,
7814 not_null: true,
7815 unique: false,
7816 primary_key: false,
7817 auto_increment: false,
7818 default: None,
7819 },
7820 ],
7821 constraints: vec![],
7822 without_rowid: None,
7823 interleave_in_parent: None,
7824 partition: None,
7825 };
7826 let add_user_fk = super::super::Operation::AddConstraint {
7827 table: "blog_posts".to_string(),
7828 constraint_sql: "CONSTRAINT blog_posts_user_id_fk FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE ON UPDATE NO ACTION".to_string(),
7829 };
7830 let mut state = ProjectState::new();
7831
7832 state.apply_migration_operations(&[create_posts, add_user_fk], "blog");
7834
7835 let model = state
7837 .find_model_by_table("blog_posts")
7838 .expect("blog_posts model should be reconstructed");
7839 let constraint = model
7840 .constraints
7841 .iter()
7842 .find(|constraint| constraint.name == "blog_posts_user_id_fk")
7843 .expect("foreign key constraint should be reconstructed");
7844 assert_eq!(constraint.constraint_type, "foreign_key");
7845 assert_eq!(constraint.fields, vec!["user_id".to_string()]);
7846 let fk_info = constraint
7847 .foreign_key_info
7848 .as_ref()
7849 .expect("foreign key metadata should be reconstructed");
7850 assert_eq!(fk_info.referenced_table, "auth_users");
7851 assert_eq!(fk_info.referenced_columns, vec!["id".to_string()]);
7852 assert_eq!(fk_info.on_delete, ForeignKeyAction::Cascade);
7853 assert_eq!(fk_info.on_update, ForeignKeyAction::NoAction);
7854 }
7855
7856 #[test]
7857 fn generated_indexes_are_replayed_without_second_migration() {
7858 let mut target = ProjectState::new();
7860 let mut post = ModelState::new("blog", "Post");
7861 post.table_name = "blog_posts".to_string();
7862 post.add_field(FieldState::new("id", FieldType::Integer, false));
7863 post.add_field(FieldState::new("author_id", FieldType::Uuid, false));
7864 post.indexes.push(IndexDefinition {
7865 name: "idx_blog_posts_author_id".to_string(),
7866 fields: vec!["author_id".to_string()],
7867 unique: false,
7868 where_clause: None,
7869 index_type: None,
7870 expressions: None,
7871 concurrently: false,
7872 mysql_options: None,
7873 operator_class: None,
7874 });
7875 target.add_model(post);
7876
7877 let operations =
7879 MigrationAutodetector::new(ProjectState::new(), target.clone()).generate_operations();
7880 let mut replayed = ProjectState::new();
7881 replayed.apply_migration_operations(&operations, "blog");
7882 let second_run = MigrationAutodetector::new(replayed.clone(), target).generate_operations();
7883
7884 assert_eq!(
7886 operations
7887 .iter()
7888 .filter(|operation| {
7889 matches!(
7890 operation,
7891 super::super::Operation::CreateIndex { .. }
7892 | super::super::Operation::CreateIndexRepair { .. }
7893 )
7894 })
7895 .count(),
7896 1
7897 );
7898 assert_eq!(
7899 replayed
7900 .find_model_by_table("blog_posts")
7901 .unwrap()
7902 .indexes
7903 .len(),
7904 1
7905 );
7906 assert!(
7907 second_run.is_empty(),
7908 "unexpected second migration: {second_run:?}"
7909 );
7910 }
7911
7912 #[test]
7913 fn replays_advanced_indexes_for_replacement_and_removal() {
7914 let create_advanced_index = super::super::Operation::CreateIndex {
7916 table: "blog_posts".to_string(),
7917 columns: vec!["slug".to_string()],
7918 unique: false,
7919 index_type: None,
7920 where_clause: Some("published = TRUE".to_string()),
7921 concurrently: false,
7922 expressions: None,
7923 mysql_options: None,
7924 operator_class: None,
7925 };
7926 let mut replayed = ProjectState::new();
7927 let mut old_model = ModelState::new("blog", "Post");
7928 old_model.table_name = "blog_posts".to_string();
7929 old_model.add_field(FieldState::new("slug", FieldType::VarChar(255), false));
7930 replayed.add_model(old_model);
7931 replayed.apply_migration_operations(&[create_advanced_index], "blog");
7932 let mut replacement_model = ModelState::new("blog", "Post");
7933 replacement_model.table_name = "blog_posts".to_string();
7934 replacement_model.add_field(FieldState::new("slug", FieldType::VarChar(255), false));
7935 replacement_model.indexes.push(IndexDefinition {
7936 name: "idx_blog_posts_slug".to_string(),
7937 fields: vec!["slug".to_string()],
7938 unique: false,
7939 where_clause: None,
7940 index_type: None,
7941 expressions: None,
7942 concurrently: false,
7943 mysql_options: None,
7944 operator_class: None,
7945 });
7946 let mut replacement_target = ProjectState::new();
7947 replacement_target.add_model(replacement_model);
7948
7949 let replacement_operations =
7951 MigrationAutodetector::new(replayed.clone(), replacement_target).generate_operations();
7952 let mut removal_target = ProjectState::new();
7953 let mut removal_model = ModelState::new("blog", "Post");
7954 removal_model.table_name = "blog_posts".to_string();
7955 removal_model.add_field(FieldState::new("slug", FieldType::VarChar(255), false));
7956 removal_target.add_model(removal_model);
7957 let removal_operations =
7958 MigrationAutodetector::new(replayed, removal_target).generate_operations();
7959
7960 let drop_position = replacement_operations
7962 .iter()
7963 .position(|operation| {
7964 matches!(operation, super::super::Operation::DropNamedIndex { .. })
7965 })
7966 .expect("advanced index replacement should drop the old index");
7967 let create_position = replacement_operations
7968 .iter()
7969 .position(|operation| {
7970 matches!(
7971 operation,
7972 super::super::Operation::CreateIndex { .. }
7973 | super::super::Operation::CreateIndexRepair { .. }
7974 )
7975 })
7976 .expect("advanced index replacement should create the ordinary index");
7977 assert!(drop_position < create_position);
7978 assert_eq!(
7979 removal_operations
7980 .iter()
7981 .filter(|operation| {
7982 matches!(operation, super::super::Operation::DropNamedIndex { .. })
7983 })
7984 .count(),
7985 1
7986 );
7987 }
7988
7989 #[test]
7990 fn replays_expression_index_name_and_definition_for_removal() {
7991 let create_expression_index = super::super::Operation::CreateIndex {
7993 table: "blog_posts".to_string(),
7994 columns: vec!["slug".to_string()],
7995 unique: true,
7996 index_type: Some(super::super::operations::IndexType::BTree),
7997 where_clause: Some("published = TRUE".to_string()),
7998 concurrently: false,
7999 expressions: Some(vec!["LOWER(slug)".to_string()]),
8000 mysql_options: None,
8001 operator_class: None,
8002 };
8003 let mut replayed = ProjectState::new();
8004 let mut old_model = ModelState::new("blog", "Post");
8005 old_model.table_name = "blog_posts".to_string();
8006 old_model.add_field(FieldState::new("slug", FieldType::VarChar(255), false));
8007 replayed.add_model(old_model);
8008 replayed.apply_migration_operations(&[create_expression_index], "blog");
8009 let replayed_index = &replayed
8010 .find_model_by_table("blog_posts")
8011 .expect("replayed model")
8012 .indexes[0];
8013 assert_eq!(replayed_index.name, "idx_blog_posts_expr");
8014 assert_eq!(
8015 replayed_index.expressions,
8016 Some(vec!["LOWER(slug)".to_string()])
8017 );
8018
8019 let mut removal_target = ProjectState::new();
8020 let mut target_model = ModelState::new("blog", "Post");
8021 target_model.table_name = "blog_posts".to_string();
8022 target_model.add_field(FieldState::new("slug", FieldType::VarChar(255), false));
8023 removal_target.add_model(target_model);
8024
8025 let removal_operations =
8027 MigrationAutodetector::new(replayed, removal_target).generate_operations();
8028
8029 assert!(matches!(
8031 removal_operations.as_slice(),
8032 [super::super::Operation::DropNamedIndex {
8033 name,
8034 unique: true,
8035 where_clause: Some(predicate),
8036 expressions: Some(expressions),
8037 ..
8038 }] if name == "idx_blog_posts_expr"
8039 && predicate == "published = TRUE"
8040 && expressions == &["LOWER(slug)".to_string()]
8041 ));
8042 }
8043
8044 #[test]
8045 fn detects_same_table_index_name_changes() {
8046 let index = |name: &str| IndexDefinition {
8048 name: name.to_string(),
8049 fields: vec!["email".to_string()],
8050 unique: false,
8051 where_clause: None,
8052 index_type: None,
8053 expressions: None,
8054 concurrently: false,
8055 mysql_options: None,
8056 operator_class: None,
8057 };
8058 let from_model = build_model_state(
8059 "blog",
8060 "Post",
8061 vec![FieldState::new("email", FieldType::VarChar(255), false)],
8062 vec![index("old_email_idx")],
8063 Vec::new(),
8064 );
8065 let to_model = build_model_state(
8066 "blog",
8067 "Post",
8068 vec![FieldState::new("email", FieldType::VarChar(255), false)],
8069 vec![index("new_email_idx")],
8070 Vec::new(),
8071 );
8072 let detector = MigrationAutodetector::new(
8073 build_project_state(vec![(("blog".to_string(), "Post".to_string()), from_model)]),
8074 build_project_state(vec![(("blog".to_string(), "Post".to_string()), to_model)]),
8075 );
8076
8077 let operations = detector.generate_operations();
8079
8080 assert!(matches!(
8082 operations.as_slice(),
8083 [
8084 super::super::Operation::DropNamedIndex { name: old, .. },
8085 super::super::Operation::CreateIndexRepair { name: Some(new), .. },
8086 ] if old == "old_email_idx" && new == "new_email_idx"
8087 ));
8088 }
8089
8090 #[test]
8091 fn replays_drop_index_only_removes_generated_name() {
8092 let mut model = ModelState::new("blog", "Post");
8094 model.table_name = "blog_posts".to_string();
8095 model.add_field(FieldState::new("email", FieldType::VarChar(255), false));
8096 model.indexes = vec![
8097 IndexDefinition {
8098 name: "idx_blog_posts_email".to_string(),
8099 fields: vec!["email".to_string()],
8100 unique: false,
8101 where_clause: None,
8102 index_type: None,
8103 expressions: None,
8104 concurrently: false,
8105 mysql_options: None,
8106 operator_class: None,
8107 },
8108 IndexDefinition {
8109 name: "custom_email_idx".to_string(),
8110 fields: vec!["email".to_string()],
8111 unique: true,
8112 where_clause: None,
8113 index_type: None,
8114 expressions: None,
8115 concurrently: false,
8116 mysql_options: None,
8117 operator_class: None,
8118 },
8119 ];
8120 let mut state = ProjectState::new();
8121 state.add_model(model);
8122 let drop = super::super::Operation::DropIndex {
8123 table: "blog_posts".to_string(),
8124 columns: vec!["email".to_string()],
8125 };
8126
8127 state.apply_migration_operations(&[drop], "blog");
8129
8130 let indexes = &state
8132 .find_model_by_table("blog_posts")
8133 .expect("replayed model")
8134 .indexes;
8135 assert_eq!(indexes.len(), 1);
8136 assert_eq!(indexes[0].name, "custom_email_idx");
8137 }
8138
8139 #[test]
8140 fn replays_drop_column_predicate_and_ignores_function_name() {
8141 assert!(!ProjectState::expression_references_column(
8143 "LOWER(email)",
8144 "lower"
8145 ));
8146 assert!(ProjectState::expression_references_column(
8147 "LOWER(email)",
8148 "email"
8149 ));
8150 let mut model = ModelState::new("blog", "Post");
8151 model.table_name = "blog_posts".to_string();
8152 model.add_field(FieldState::new("email", FieldType::VarChar(255), false));
8153 model.add_field(FieldState::new("active", FieldType::Boolean, false));
8154 let mut state = ProjectState::new();
8155 state.add_model(model);
8156 let create = super::super::Operation::CreateIndexRepair {
8157 table: "blog_posts".to_string(),
8158 name: Some("active_email_idx".to_string()),
8159 columns: vec!["email".to_string()],
8160 unique: false,
8161 index_type: None,
8162 where_clause: Some("active = TRUE".to_string()),
8163 concurrently: false,
8164 expressions: Some(vec!["LOWER(email)".to_string()]),
8165 mysql_options: None,
8166 operator_class: None,
8167 };
8168
8169 state.apply_migration_operations(&[create], "blog");
8171 state.apply_migration_operations(
8172 &[super::super::Operation::DropColumn {
8173 table: "blog_posts".to_string(),
8174 column: "active".to_string(),
8175 }],
8176 "blog",
8177 );
8178
8179 assert!(
8181 state
8182 .find_model_by_table("blog_posts")
8183 .expect("replayed model")
8184 .indexes
8185 .is_empty()
8186 );
8187 }
8188
8189 #[test]
8190 fn generate_migrations_recreates_generated_indexes_around_cross_app_move() {
8191 let old_index = IndexDefinition {
8193 name: "idx_legacy_user_email".to_string(),
8194 fields: vec!["email".to_string()],
8195 unique: true,
8196 where_clause: None,
8197 index_type: None,
8198 expressions: None,
8199 concurrently: false,
8200 mysql_options: None,
8201 operator_class: None,
8202 };
8203 let new_index = IndexDefinition {
8204 name: "idx_accounts_user_email".to_string(),
8205 ..old_index.clone()
8206 };
8207 let old_model = build_model_state(
8208 "legacy",
8209 "User",
8210 vec![
8211 FieldState::new("id", FieldType::Integer, false),
8212 FieldState::new("email", FieldType::VarChar(255), false),
8213 ],
8214 vec![old_index],
8215 Vec::new(),
8216 );
8217 let new_model = build_model_state(
8218 "accounts",
8219 "User",
8220 vec![
8221 FieldState::new("id", FieldType::Integer, false),
8222 FieldState::new("email", FieldType::VarChar(255), false),
8223 ],
8224 vec![new_index],
8225 Vec::new(),
8226 );
8227 let detector = MigrationAutodetector::new(
8228 build_project_state(vec![(
8229 ("legacy".to_string(), "User".to_string()),
8230 old_model,
8231 )]),
8232 build_project_state(vec![(
8233 ("accounts".to_string(), "User".to_string()),
8234 new_model,
8235 )]),
8236 );
8237
8238 let migrations = detector
8240 .try_generate_migrations()
8241 .expect("cross-app move should generate a migration");
8242
8243 let operations = &migrations[0].operations;
8245 assert!(matches!(
8246 operations.as_slice(),
8247 [
8248 super::super::Operation::DropNamedIndex { name, .. },
8249 super::super::Operation::MoveModel { .. },
8250 create,
8251 ] if name == "idx_legacy_user_email"
8252 && matches!(
8253 create,
8254 super::super::Operation::CreateIndex {
8255 table,
8256 columns,
8257 unique: true,
8258 ..
8259 } | super::super::Operation::CreateIndexRepair {
8260 table,
8261 columns,
8262 unique: true,
8263 ..
8264 }
8265 if table == "accounts_user" && columns == &["email".to_string()]
8266 )
8267 ));
8268 }
8269
8270 #[test]
8271 fn reorders_index_recreation_around_column_rename() {
8272 let create_index = |columns: &[&str]| super::super::Operation::CreateIndex {
8274 table: "blog_posts".to_string(),
8275 columns: columns.iter().map(|column| (*column).to_string()).collect(),
8276 unique: false,
8277 index_type: None,
8278 where_clause: None,
8279 concurrently: false,
8280 expressions: None,
8281 mysql_options: None,
8282 operator_class: None,
8283 };
8284 let mut operations = vec![
8285 create_index(&["slug_new"]),
8286 super::super::Operation::RenameColumn {
8287 table: "blog_posts".to_string(),
8288 old_name: "slug".to_string(),
8289 new_name: "slug_new".to_string(),
8290 },
8291 super::super::Operation::DropIndex {
8292 table: "blog_posts".to_string(),
8293 columns: vec!["slug".to_string()],
8294 },
8295 ];
8296
8297 MigrationAutodetector::order_renamed_column_operations(&mut operations);
8299
8300 assert!(matches!(
8302 &operations[..],
8303 [
8304 super::super::Operation::DropIndex { .. },
8305 super::super::Operation::RenameColumn { .. },
8306 super::super::Operation::CreateIndex { .. },
8307 ]
8308 ));
8309 }
8310
8311 #[test]
8312 fn reorders_predicate_index_drop_before_column_rename() {
8313 let mut operations = vec![
8315 super::super::Operation::RenameColumn {
8316 table: "blog_posts".to_string(),
8317 old_name: "active_old".to_string(),
8318 new_name: "active_new".to_string(),
8319 },
8320 super::super::Operation::DropNamedIndex {
8321 table: "blog_posts".to_string(),
8322 name: "active_email_idx".to_string(),
8323 columns: vec!["email".to_string()],
8324 unique: false,
8325 index_type: None,
8326 where_clause: Some("active_old = TRUE".to_string()),
8327 concurrently: false,
8328 expressions: None,
8329 mysql_options: None,
8330 operator_class: None,
8331 },
8332 ];
8333
8334 MigrationAutodetector::order_renamed_column_operations(&mut operations);
8336
8337 assert!(matches!(
8339 &operations[..],
8340 [
8341 super::super::Operation::DropNamedIndex { .. },
8342 super::super::Operation::RenameColumn { .. },
8343 ]
8344 ));
8345 }
8346
8347 #[test]
8348 fn drops_replaced_index_before_creating_new_definition() {
8349 let old_model = build_model_state(
8351 "blog",
8352 "Post",
8353 vec![FieldState::new("email", FieldType::VarChar(255), false)],
8354 vec![IndexDefinition {
8355 name: "idx_blog_post_email".to_string(),
8356 fields: vec!["email".to_string()],
8357 unique: false,
8358 where_clause: None,
8359 index_type: None,
8360 expressions: None,
8361 concurrently: false,
8362 mysql_options: None,
8363 operator_class: None,
8364 }],
8365 Vec::new(),
8366 );
8367 let new_model = build_model_state(
8368 "blog",
8369 "Post",
8370 vec![FieldState::new("email", FieldType::VarChar(255), false)],
8371 vec![IndexDefinition {
8372 name: "idx_blog_post_email".to_string(),
8373 fields: vec!["email".to_string()],
8374 unique: true,
8375 where_clause: None,
8376 index_type: None,
8377 expressions: None,
8378 concurrently: false,
8379 mysql_options: None,
8380 operator_class: None,
8381 }],
8382 Vec::new(),
8383 );
8384 let from_state =
8385 build_project_state(vec![(("blog".to_string(), "Post".to_string()), old_model)]);
8386 let to_state =
8387 build_project_state(vec![(("blog".to_string(), "Post".to_string()), new_model)]);
8388
8389 let operations = MigrationAutodetector::new(from_state, to_state).generate_operations();
8391
8392 let drop_position = operations
8394 .iter()
8395 .position(|operation| {
8396 matches!(operation, super::super::Operation::DropNamedIndex { .. })
8397 })
8398 .expect("replacing an index should drop the previous definition");
8399 let create_position = operations
8400 .iter()
8401 .position(|operation| {
8402 matches!(
8403 operation,
8404 super::super::Operation::CreateIndex { .. }
8405 | super::super::Operation::CreateIndexRepair { .. }
8406 )
8407 })
8408 .expect("replacing an index should create the new definition");
8409 assert!(drop_position < create_position);
8410 }
8411
8412 #[rstest]
8413 fn apply_migration_operations_replays_omitted_foreign_key_actions_as_no_action() {
8414 let create_posts = super::super::Operation::CreateTable {
8416 name: "blog_posts".to_string(),
8417 columns: vec![
8418 super::super::ColumnDefinition {
8419 name: "id".to_string(),
8420 type_definition: super::super::FieldType::BigInteger,
8421 not_null: true,
8422 unique: false,
8423 primary_key: true,
8424 auto_increment: true,
8425 default: None,
8426 },
8427 super::super::ColumnDefinition {
8428 name: "user_id".to_string(),
8429 type_definition: super::super::FieldType::BigInteger,
8430 not_null: true,
8431 unique: false,
8432 primary_key: false,
8433 auto_increment: false,
8434 default: None,
8435 },
8436 ],
8437 constraints: vec![],
8438 without_rowid: None,
8439 interleave_in_parent: None,
8440 partition: None,
8441 };
8442 let add_user_fk = super::super::Operation::AddConstraint {
8443 table: "blog_posts".to_string(),
8444 constraint_sql:
8445 "CONSTRAINT blog_posts_user_id_fk FOREIGN KEY (user_id) REFERENCES auth_users(id)"
8446 .to_string(),
8447 };
8448 let mut state = ProjectState::new();
8449
8450 state.apply_migration_operations(&[create_posts, add_user_fk], "blog");
8452
8453 let model = state
8455 .find_model_by_table("blog_posts")
8456 .expect("blog_posts model should be reconstructed");
8457 let fk_info = model
8458 .constraints
8459 .iter()
8460 .find(|constraint| constraint.name == "blog_posts_user_id_fk")
8461 .and_then(|constraint| constraint.foreign_key_info.as_ref())
8462 .expect("foreign key metadata should be reconstructed");
8463 assert_eq!(fk_info.on_delete, ForeignKeyAction::NoAction);
8464 assert_eq!(fk_info.on_update, ForeignKeyAction::NoAction);
8465 }
8466
8467 #[rstest]
8468 fn generate_operations_emits_rename_column_for_unambiguous_field_rename() {
8469 let from_model = build_model_state(
8470 "deployments",
8471 "Deployment",
8472 vec![
8473 FieldState::new("id", super::super::FieldType::Integer, false),
8474 FieldState::new("app_name", super::super::FieldType::VarChar(255), false),
8475 ],
8476 Vec::new(),
8477 Vec::new(),
8478 );
8479 let to_model = build_model_state(
8480 "deployments",
8481 "Deployment",
8482 vec![
8483 FieldState::new("id", super::super::FieldType::Integer, false),
8484 FieldState::new("project_name", super::super::FieldType::VarChar(255), false),
8485 ],
8486 Vec::new(),
8487 Vec::new(),
8488 );
8489 let detector = MigrationAutodetector::new(
8490 build_project_state(vec![(
8491 ("deployments".to_string(), "Deployment".to_string()),
8492 from_model,
8493 )]),
8494 build_project_state(vec![(
8495 ("deployments".to_string(), "Deployment".to_string()),
8496 to_model,
8497 )]),
8498 );
8499
8500 let operations = detector
8501 .try_generate_operations()
8502 .expect("unambiguous rename should generate operations");
8503
8504 assert_eq!(operations.len(), 1, "unexpected operations: {operations:?}");
8505 assert!(matches!(
8506 &operations[0],
8507 super::super::Operation::RenameColumn {
8508 table,
8509 old_name,
8510 new_name
8511 } if table == "deployments_deployment"
8512 && old_name == "app_name"
8513 && new_name == "project_name"
8514 ));
8515 }
8516
8517 #[rstest]
8518 fn generate_operations_renames_unique_column_with_constraint_name_change() {
8519 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
8520 let old_slug_field =
8521 FieldState::new("old_slug", super::super::FieldType::VarChar(255), false);
8522 let new_slug_field = FieldState::new("slug", super::super::FieldType::VarChar(255), false);
8523 let from_unique = ConstraintDefinition {
8524 name: "deployments_deployment_old_slug_uniq".to_string(),
8525 constraint_type: "unique".to_string(),
8526 fields: vec!["old_slug".to_string()],
8527 expression: None,
8528 foreign_key_info: None,
8529 };
8530 let to_unique = ConstraintDefinition {
8531 name: "deployments_deployment_slug_uniq".to_string(),
8532 constraint_type: "unique".to_string(),
8533 fields: vec!["slug".to_string()],
8534 expression: None,
8535 foreign_key_info: None,
8536 };
8537 let from_model = build_model_state(
8538 "deployments",
8539 "Deployment",
8540 vec![id_field.clone(), old_slug_field],
8541 Vec::new(),
8542 vec![from_unique],
8543 );
8544 let to_model = build_model_state(
8545 "deployments",
8546 "Deployment",
8547 vec![id_field, new_slug_field],
8548 Vec::new(),
8549 vec![to_unique],
8550 );
8551 let detector = MigrationAutodetector::new(
8552 build_project_state(vec![(
8553 ("deployments".to_string(), "Deployment".to_string()),
8554 from_model,
8555 )]),
8556 build_project_state(vec![(
8557 ("deployments".to_string(), "Deployment".to_string()),
8558 to_model,
8559 )]),
8560 );
8561
8562 let operations = detector
8563 .try_generate_operations()
8564 .expect("unique column rename should generate operations");
8565
8566 assert_eq!(operations.len(), 3, "unexpected operations: {operations:?}");
8567 assert!(matches!(
8568 &operations[0],
8569 super::super::Operation::RenameColumn {
8570 table,
8571 old_name,
8572 new_name
8573 } if table == "deployments_deployment"
8574 && old_name == "old_slug"
8575 && new_name == "slug"
8576 ));
8577 assert!(matches!(
8578 &operations[1],
8579 super::super::Operation::DropConstraint { constraint_name, .. }
8580 if constraint_name == "deployments_deployment_old_slug_uniq"
8581 ));
8582 assert!(matches!(
8583 &operations[2],
8584 super::super::Operation::AddConstraint { constraint_sql, .. }
8585 if constraint_sql == "CONSTRAINT deployments_deployment_slug_uniq UNIQUE (slug)"
8586 ));
8587 }
8588
8589 #[rstest]
8590 fn generate_operations_renames_unique_column_from_inline_to_model_constraint() {
8591 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
8593 let mut old_email_field =
8594 FieldState::new("old_email", super::super::FieldType::VarChar(255), false);
8595 old_email_field
8596 .params
8597 .insert("unique".to_string(), "true".to_string());
8598 let new_email_field =
8599 FieldState::new("email", super::super::FieldType::VarChar(255), false);
8600 let new_unique = ConstraintDefinition {
8601 name: "accounts_account_email_uniq".to_string(),
8602 constraint_type: "unique".to_string(),
8603 fields: vec!["email".to_string()],
8604 expression: None,
8605 foreign_key_info: None,
8606 };
8607 let from_model = build_model_state(
8608 "accounts",
8609 "Account",
8610 vec![id_field.clone(), old_email_field],
8611 Vec::new(),
8612 Vec::new(),
8613 );
8614 let to_model = build_model_state(
8615 "accounts",
8616 "Account",
8617 vec![id_field, new_email_field],
8618 Vec::new(),
8619 vec![new_unique],
8620 );
8621 let detector = MigrationAutodetector::new(
8622 build_project_state(vec![(
8623 ("accounts".to_string(), "Account".to_string()),
8624 from_model,
8625 )]),
8626 build_project_state(vec![(
8627 ("accounts".to_string(), "Account".to_string()),
8628 to_model,
8629 )]),
8630 );
8631
8632 let operations = detector
8634 .try_generate_operations()
8635 .expect("unique field rename should generate operations");
8636
8637 assert_eq!(operations.len(), 1, "unexpected operations: {operations:?}");
8639 assert!(matches!(
8640 &operations[0],
8641 super::super::Operation::RenameColumn {
8642 table,
8643 old_name,
8644 new_name
8645 } if table == "accounts_account"
8646 && old_name == "old_email"
8647 && new_name == "email"
8648 ));
8649 }
8650
8651 #[rstest]
8652 fn generate_operations_detects_field_rename_from_offline_table_keyed_state() {
8653 let mut from_model = build_model_state(
8654 "deployments",
8655 "DeploymentsDeployment",
8656 vec![
8657 FieldState::new("id", super::super::FieldType::Integer, false),
8658 FieldState::new("reinhardt_app_yaml", super::super::FieldType::Text, false),
8659 ],
8660 Vec::new(),
8661 Vec::new(),
8662 );
8663 from_model.table_name = "deployments_deployment".to_string();
8664 let to_model = build_model_state(
8665 "deployments",
8666 "Deployment",
8667 vec![
8668 FieldState::new("id", super::super::FieldType::Integer, false),
8669 FieldState::new("project_yaml", super::super::FieldType::Text, false),
8670 ],
8671 Vec::new(),
8672 Vec::new(),
8673 );
8674 let detector = MigrationAutodetector::new(
8675 build_project_state(vec![(
8676 (
8677 "deployments".to_string(),
8678 "DeploymentsDeployment".to_string(),
8679 ),
8680 from_model,
8681 )]),
8682 build_project_state(vec![(
8683 ("deployments".to_string(), "Deployment".to_string()),
8684 to_model,
8685 )]),
8686 );
8687
8688 let migrations = detector
8689 .try_generate_migrations()
8690 .expect("table-name matched state should detect rename");
8691 let operations: Vec<_> = migrations
8692 .iter()
8693 .flat_map(|migration| migration.operations.iter())
8694 .collect();
8695
8696 assert_eq!(operations.len(), 1, "unexpected operations: {operations:?}");
8697 assert!(matches!(
8698 operations[0],
8699 super::super::Operation::RenameColumn {
8700 table,
8701 old_name,
8702 new_name
8703 } if table == "deployments_deployment"
8704 && old_name == "reinhardt_app_yaml"
8705 && new_name == "project_yaml"
8706 ));
8707 }
8708
8709 #[rstest]
8710 fn generate_migrations_renames_field_with_renamed_model() {
8711 let from_model = build_model_state(
8712 "deployments",
8713 "Deployment",
8714 vec![
8715 FieldState::new("id", super::super::FieldType::Integer, false),
8716 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8717 FieldState::new("app_name", super::super::FieldType::VarChar(255), false),
8718 ],
8719 Vec::new(),
8720 Vec::new(),
8721 );
8722 let to_model = build_model_state(
8723 "deployments",
8724 "Project",
8725 vec![
8726 FieldState::new("id", super::super::FieldType::Integer, false),
8727 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8728 FieldState::new("project_name", super::super::FieldType::VarChar(255), false),
8729 ],
8730 Vec::new(),
8731 Vec::new(),
8732 );
8733 let detector = MigrationAutodetector::new(
8734 build_project_state(vec![(
8735 ("deployments".to_string(), "Deployment".to_string()),
8736 from_model,
8737 )]),
8738 build_project_state(vec![(
8739 ("deployments".to_string(), "Project".to_string()),
8740 to_model,
8741 )]),
8742 );
8743
8744 let migrations = detector
8745 .try_generate_migrations()
8746 .expect("model and field rename should generate migrations");
8747 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
8748 let operations = &migrations[0].operations;
8749
8750 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
8751 assert!(
8752 matches!(
8753 &operations[0],
8754 super::super::Operation::RenameTable { old_name, new_name }
8755 if old_name == "deployments_deployment"
8756 && new_name == "deployments_project"
8757 ),
8758 "RenameTable must precede new-table field operations: {operations:?}"
8759 );
8760 assert!(matches!(
8761 &operations[1],
8762 super::super::Operation::RenameColumn {
8763 table,
8764 old_name,
8765 new_name
8766 } if table == "deployments_project"
8767 && old_name == "app_name"
8768 && new_name == "project_name"
8769 ));
8770 assert!(
8771 operations.iter().all(|operation| {
8772 !matches!(
8773 operation,
8774 super::super::Operation::AddColumn { .. }
8775 | super::super::Operation::DropColumn { .. }
8776 )
8777 }),
8778 "field rename on a renamed model must not degrade to AddColumn/DropColumn: {operations:?}"
8779 );
8780 }
8781
8782 #[rstest]
8783 fn generate_migrations_adds_field_after_renaming_model_table() {
8784 let from_model = build_model_state(
8785 "deployments",
8786 "Deployment",
8787 vec![
8788 FieldState::new("id", super::super::FieldType::Integer, false),
8789 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8790 FieldState::new("updated_at", super::super::FieldType::DateTime, false),
8791 ],
8792 Vec::new(),
8793 Vec::new(),
8794 );
8795 let to_model = build_model_state(
8796 "deployments",
8797 "Project",
8798 vec![
8799 FieldState::new("id", super::super::FieldType::Integer, false),
8800 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8801 FieldState::new("updated_at", super::super::FieldType::DateTime, false),
8802 FieldState::new("project_name", super::super::FieldType::VarChar(255), true),
8803 ],
8804 Vec::new(),
8805 Vec::new(),
8806 );
8807 let detector = MigrationAutodetector::new(
8808 build_project_state(vec![(
8809 ("deployments".to_string(), "Deployment".to_string()),
8810 from_model,
8811 )]),
8812 build_project_state(vec![(
8813 ("deployments".to_string(), "Project".to_string()),
8814 to_model,
8815 )]),
8816 );
8817
8818 let migrations = detector
8819 .try_generate_migrations()
8820 .expect("model rename with added field should generate migrations");
8821 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
8822 let operations = &migrations[0].operations;
8823
8824 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
8825 assert!(
8826 matches!(
8827 &operations[0],
8828 super::super::Operation::RenameTable { old_name, new_name }
8829 if old_name == "deployments_deployment"
8830 && new_name == "deployments_project"
8831 ),
8832 "RenameTable must precede new-table field operations: {operations:?}"
8833 );
8834 assert!(matches!(
8835 &operations[1],
8836 super::super::Operation::AddColumn { table, column, .. }
8837 if table == "deployments_project" && column.name == "project_name"
8838 ));
8839 }
8840
8841 #[rstest]
8842 fn generate_migrations_keeps_old_table_drop_before_renaming_model_table() {
8843 let from_model = build_model_state(
8844 "deployments",
8845 "Deployment",
8846 vec![
8847 FieldState::new("id", super::super::FieldType::Integer, false),
8848 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8849 FieldState::new("updated_at", super::super::FieldType::DateTime, false),
8850 FieldState::new("legacy_payload", super::super::FieldType::Text, true),
8851 ],
8852 Vec::new(),
8853 Vec::new(),
8854 );
8855 let to_model = build_model_state(
8856 "deployments",
8857 "Project",
8858 vec![
8859 FieldState::new("id", super::super::FieldType::Integer, false),
8860 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8861 FieldState::new("updated_at", super::super::FieldType::DateTime, false),
8862 FieldState::new("retry_count", super::super::FieldType::Integer, false),
8863 ],
8864 Vec::new(),
8865 Vec::new(),
8866 );
8867 let detector = MigrationAutodetector::new(
8868 build_project_state(vec![(
8869 ("deployments".to_string(), "Deployment".to_string()),
8870 from_model,
8871 )]),
8872 build_project_state(vec![(
8873 ("deployments".to_string(), "Project".to_string()),
8874 to_model,
8875 )]),
8876 );
8877
8878 let migrations = detector
8879 .try_generate_migrations()
8880 .expect("model rename with old-table drop should generate migrations");
8881 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
8882 let operations = &migrations[0].operations;
8883
8884 assert_eq!(operations.len(), 3, "unexpected operations: {operations:?}");
8885 assert!(matches!(
8886 &operations[0],
8887 super::super::Operation::DropColumn { table, column }
8888 if table == "deployments_deployment" && column == "legacy_payload"
8889 ));
8890 assert!(matches!(
8891 &operations[1],
8892 super::super::Operation::RenameTable { old_name, new_name }
8893 if old_name == "deployments_deployment" && new_name == "deployments_project"
8894 ));
8895 assert!(matches!(
8896 &operations[2],
8897 super::super::Operation::AddColumn { table, column, .. }
8898 if table == "deployments_project" && column.name == "retry_count"
8899 ));
8900 }
8901
8902 #[rstest]
8903 fn generate_migrations_adds_constraint_after_renaming_model_table() {
8904 let from_model = build_model_state(
8905 "deployments",
8906 "Deployment",
8907 vec![
8908 FieldState::new("id", super::super::FieldType::Integer, false),
8909 FieldState::new("project_name", super::super::FieldType::VarChar(255), false),
8910 ],
8911 Vec::new(),
8912 Vec::new(),
8913 );
8914 let to_model = build_model_state(
8915 "deployments",
8916 "Project",
8917 vec![
8918 FieldState::new("id", super::super::FieldType::Integer, false),
8919 FieldState::new("project_name", super::super::FieldType::VarChar(255), false),
8920 ],
8921 Vec::new(),
8922 vec![ConstraintDefinition {
8923 name: "deployments_project_project_name_not_empty".to_string(),
8924 constraint_type: "check".to_string(),
8925 fields: vec!["project_name".to_string()],
8926 expression: Some("project_name <> ''".to_string()),
8927 foreign_key_info: None,
8928 }],
8929 );
8930 let detector = MigrationAutodetector::new(
8931 build_project_state(vec![(
8932 ("deployments".to_string(), "Deployment".to_string()),
8933 from_model,
8934 )]),
8935 build_project_state(vec![(
8936 ("deployments".to_string(), "Project".to_string()),
8937 to_model,
8938 )]),
8939 );
8940
8941 let migrations = detector
8942 .try_generate_migrations()
8943 .expect("model rename with added constraint should generate migrations");
8944 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
8945 let operations = &migrations[0].operations;
8946
8947 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
8948 assert!(matches!(
8949 &operations[0],
8950 super::super::Operation::RenameTable { old_name, new_name }
8951 if old_name == "deployments_deployment" && new_name == "deployments_project"
8952 ));
8953 assert!(matches!(
8954 &operations[1],
8955 super::super::Operation::AddConstraint {
8956 table,
8957 constraint_sql
8958 } if table == "deployments_project"
8959 && constraint_sql.contains("deployments_project_project_name_not_empty")
8960 && constraint_sql.contains("CHECK")
8961 && constraint_sql.contains("project_name")
8962 ));
8963 }
8964
8965 #[rstest]
8966 fn generate_migrations_preserves_field_changes_for_cross_app_move() {
8967 let from_model = build_model_state(
8968 "legacy",
8969 "Deployment",
8970 vec![
8971 FieldState::new("id", super::super::FieldType::Integer, false),
8972 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8973 FieldState::new("updated_at", super::super::FieldType::DateTime, false),
8974 FieldState::new("status", super::super::FieldType::VarChar(32), false),
8975 ],
8976 Vec::new(),
8977 Vec::new(),
8978 );
8979 let to_model = build_model_state(
8980 "deployments",
8981 "Project",
8982 vec![
8983 FieldState::new("id", super::super::FieldType::Integer, false),
8984 FieldState::new("created_at", super::super::FieldType::DateTime, false),
8985 FieldState::new("updated_at", super::super::FieldType::DateTime, false),
8986 FieldState::new("status", super::super::FieldType::VarChar(32), false),
8987 FieldState::new("project_name", super::super::FieldType::VarChar(255), true),
8988 ],
8989 Vec::new(),
8990 Vec::new(),
8991 );
8992 let detector = MigrationAutodetector::new(
8993 build_project_state(vec![(
8994 ("legacy".to_string(), "Deployment".to_string()),
8995 from_model,
8996 )]),
8997 build_project_state(vec![(
8998 ("deployments".to_string(), "Project".to_string()),
8999 to_model,
9000 )]),
9001 );
9002
9003 let migrations = detector
9004 .try_generate_migrations()
9005 .expect("cross-app model move with added field should generate migrations");
9006 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
9007 assert_eq!(migrations[0].app_label, "deployments");
9008 let operations = &migrations[0].operations;
9009
9010 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
9011 assert!(matches!(
9012 &operations[0],
9013 super::super::Operation::MoveModel {
9014 model_name,
9015 from_app,
9016 to_app,
9017 rename_table: true,
9018 old_table_name: Some(old_table),
9019 new_table_name: Some(new_table)
9020 } if model_name == "Deployment"
9021 && from_app == "legacy"
9022 && to_app == "deployments"
9023 && old_table == "legacy_deployment"
9024 && new_table == "deployments_project"
9025 ));
9026 assert!(matches!(
9027 &operations[1],
9028 super::super::Operation::AddColumn { table, column, .. }
9029 if table == "deployments_project" && column.name == "project_name"
9030 ));
9031 }
9032
9033 #[rstest]
9034 fn generate_migrations_orders_referenced_table_constraint_after_rename() {
9035 let from_account = build_model_state(
9036 "crm",
9037 "User",
9038 vec![
9039 FieldState::new("id", super::super::FieldType::Integer, false),
9040 FieldState::new("email", super::super::FieldType::VarChar(255), false),
9041 ],
9042 Vec::new(),
9043 Vec::new(),
9044 );
9045 let from_profile = build_model_state(
9046 "crm",
9047 "Profile",
9048 vec![
9049 FieldState::new("id", super::super::FieldType::Integer, false),
9050 FieldState::new("account_id", super::super::FieldType::Integer, false),
9051 ],
9052 Vec::new(),
9053 Vec::new(),
9054 );
9055 let to_account = build_model_state(
9056 "crm",
9057 "Account",
9058 vec![
9059 FieldState::new("id", super::super::FieldType::Integer, false),
9060 FieldState::new("email", super::super::FieldType::VarChar(255), false),
9061 ],
9062 Vec::new(),
9063 Vec::new(),
9064 );
9065 let to_profile = build_model_state(
9066 "crm",
9067 "Profile",
9068 vec![
9069 FieldState::new("id", super::super::FieldType::Integer, false),
9070 FieldState::new("account_id", super::super::FieldType::Integer, false),
9071 ],
9072 Vec::new(),
9073 vec![ConstraintDefinition {
9074 name: "crm_profile_account_id_fk".to_string(),
9075 constraint_type: "foreign_key".to_string(),
9076 fields: vec!["account_id".to_string()],
9077 expression: None,
9078 foreign_key_info: Some(ForeignKeyConstraintInfo {
9079 referenced_table: "crm_account".to_string(),
9080 referenced_columns: vec!["id".to_string()],
9081 on_delete: ForeignKeyAction::Cascade,
9082 on_update: ForeignKeyAction::Cascade,
9083 }),
9084 }],
9085 );
9086 let detector = MigrationAutodetector::new(
9087 build_project_state(vec![
9088 (("crm".to_string(), "User".to_string()), from_account),
9089 (("crm".to_string(), "Profile".to_string()), from_profile),
9090 ]),
9091 build_project_state(vec![
9092 (("crm".to_string(), "Account".to_string()), to_account),
9093 (("crm".to_string(), "Profile".to_string()), to_profile),
9094 ]),
9095 );
9096
9097 let migrations = detector
9098 .try_generate_migrations()
9099 .expect("referenced table rename with added FK should generate migrations");
9100 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
9101 let operations = &migrations[0].operations;
9102
9103 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
9104 assert!(matches!(
9105 &operations[0],
9106 super::super::Operation::RenameTable { old_name, new_name }
9107 if old_name == "crm_user" && new_name == "crm_account"
9108 ));
9109 assert!(matches!(
9110 &operations[1],
9111 super::super::Operation::AddConstraint {
9112 table,
9113 constraint_sql
9114 } if table == "crm_profile"
9115 && constraint_sql.contains("crm_profile_account_id_fk")
9116 && constraint_sql.contains("REFERENCES crm_account")
9117 ));
9118 }
9119
9120 #[rstest]
9121 fn try_generate_operations_rejects_ambiguous_field_rename_candidates() {
9122 let from_model = build_model_state(
9123 "projects",
9124 "Project",
9125 vec![
9126 FieldState::new("old_code", super::super::FieldType::VarChar(255), false),
9127 FieldState::new("legacy_code", super::super::FieldType::VarChar(255), false),
9128 ],
9129 Vec::new(),
9130 Vec::new(),
9131 );
9132 let to_model = build_model_state(
9133 "projects",
9134 "Project",
9135 vec![FieldState::new(
9136 "project_code",
9137 super::super::FieldType::VarChar(255),
9138 false,
9139 )],
9140 Vec::new(),
9141 Vec::new(),
9142 );
9143 let detector = MigrationAutodetector::new(
9144 build_project_state(vec![(
9145 ("projects".to_string(), "Project".to_string()),
9146 from_model,
9147 )]),
9148 build_project_state(vec![(
9149 ("projects".to_string(), "Project".to_string()),
9150 to_model,
9151 )]),
9152 );
9153
9154 let error = detector
9155 .try_generate_operations()
9156 .expect_err("ambiguous rename candidates must fail");
9157 let message = error.to_string();
9158
9159 assert!(
9160 message.contains("Ambiguous field rename candidates"),
9161 "unexpected error: {message}"
9162 );
9163 assert!(
9164 message.contains("legacy_code")
9165 && message.contains("old_code")
9166 && message.contains("project_code"),
9167 "error should name candidate fields: {message}"
9168 );
9169 }
9170
9171 #[rstest]
9172 fn try_generate_operations_preserves_unrelated_add_and_drop() {
9173 let from_model = build_model_state(
9174 "projects",
9175 "Project",
9176 vec![FieldState::new(
9177 "legacy_payload",
9178 super::super::FieldType::Text,
9179 true,
9180 )],
9181 Vec::new(),
9182 Vec::new(),
9183 );
9184 let to_model = build_model_state(
9185 "projects",
9186 "Project",
9187 vec![FieldState::new(
9188 "retry_count",
9189 super::super::FieldType::Integer,
9190 false,
9191 )],
9192 Vec::new(),
9193 Vec::new(),
9194 );
9195 let detector = MigrationAutodetector::new(
9196 build_project_state(vec![(
9197 ("projects".to_string(), "Project".to_string()),
9198 from_model,
9199 )]),
9200 build_project_state(vec![(
9201 ("projects".to_string(), "Project".to_string()),
9202 to_model,
9203 )]),
9204 );
9205
9206 let operations = detector
9207 .try_generate_operations()
9208 .expect("unrelated add/drop should remain valid");
9209
9210 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
9211 assert!(
9212 operations.iter().any(|op| matches!(
9213 op,
9214 super::super::Operation::AddColumn { column, .. } if column.name == "retry_count"
9215 )),
9216 "expected AddColumn, got: {operations:?}"
9217 );
9218 assert!(
9219 operations.iter().any(|op| matches!(
9220 op,
9221 super::super::Operation::DropColumn { column, .. } if column == "legacy_payload"
9222 )),
9223 "expected DropColumn, got: {operations:?}"
9224 );
9225 assert!(
9226 operations
9227 .iter()
9228 .all(|op| !matches!(op, super::super::Operation::RenameColumn { .. })),
9229 "unrelated add/drop must not be collapsed into RenameColumn: {operations:?}"
9230 );
9231 }
9232
9233 #[rstest]
9234 fn to_database_schema_uses_app_prefixed_table_key() {
9235 let model = build_model_state(
9237 "blog",
9238 "Post",
9239 vec![FieldState::new(
9240 "id",
9241 super::super::FieldType::Integer,
9242 false,
9243 )],
9244 Vec::new(),
9245 Vec::new(),
9246 );
9247 let state = build_project_state(vec![(("blog".to_string(), "Post".to_string()), model)]);
9248
9249 let schema = state.to_database_schema();
9251
9252 assert_eq!(schema.tables.len(), 1);
9254 assert!(
9255 schema.tables.contains_key("blog_post"),
9256 "table key should be app_label + '_' + lowercase model name"
9257 );
9258 let table = &schema.tables["blog_post"];
9259 assert_eq!(table.name, "blog_post");
9260 }
9261
9262 #[rstest]
9263 fn to_database_schema_prevents_cross_app_collision() {
9264 let blog_user = build_model_state(
9267 "blog",
9268 "User",
9269 vec![FieldState::new(
9270 "id",
9271 super::super::FieldType::Integer,
9272 false,
9273 )],
9274 Vec::new(),
9275 Vec::new(),
9276 );
9277 let auth_user = build_model_state(
9278 "auth",
9279 "User",
9280 vec![FieldState::new(
9281 "id",
9282 super::super::FieldType::Integer,
9283 false,
9284 )],
9285 Vec::new(),
9286 Vec::new(),
9287 );
9288 let state = build_project_state(vec![
9289 (("blog".to_string(), "User".to_string()), blog_user),
9290 (("auth".to_string(), "User".to_string()), auth_user),
9291 ]);
9292
9293 let schema = state.to_database_schema();
9295
9296 assert_eq!(schema.tables.len(), 2);
9298 assert!(schema.tables.contains_key("blog_user"));
9299 assert!(schema.tables.contains_key("auth_user"));
9300 }
9301
9302 #[rstest]
9303 fn to_database_schema_propagates_indexes() {
9304 let indexes = vec![
9306 IndexDefinition {
9307 name: "idx_title".to_string(),
9308 fields: vec!["title".to_string()],
9309 unique: false,
9310 where_clause: None,
9311 index_type: None,
9312 expressions: None,
9313 concurrently: false,
9314 mysql_options: None,
9315 operator_class: None,
9316 },
9317 IndexDefinition {
9318 name: "idx_slug_unique".to_string(),
9319 fields: vec!["slug".to_string()],
9320 unique: true,
9321 where_clause: None,
9322 index_type: None,
9323 expressions: None,
9324 concurrently: false,
9325 mysql_options: None,
9326 operator_class: None,
9327 },
9328 ];
9329 let model = build_model_state(
9330 "blog",
9331 "Post",
9332 vec![
9333 FieldState::new("title", super::super::FieldType::VarChar(255), false),
9334 FieldState::new("slug", super::super::FieldType::VarChar(100), false),
9335 ],
9336 indexes,
9337 Vec::new(),
9338 );
9339 let state = build_project_state(vec![(("blog".to_string(), "Post".to_string()), model)]);
9340
9341 let schema = state.to_database_schema();
9343
9344 let table = &schema.tables["blog_post"];
9346 assert_eq!(table.indexes.len(), 2);
9347 assert_eq!(table.indexes[0].name, "idx_title");
9348 assert_eq!(table.indexes[0].columns, vec!["title".to_string()]);
9349 assert!(!table.indexes[0].unique);
9350 assert_eq!(table.indexes[1].name, "idx_slug_unique");
9351 assert!(table.indexes[1].unique);
9352 }
9353
9354 #[rstest]
9355 fn to_database_schema_propagates_constraints() {
9356 let constraints = vec![ConstraintDefinition {
9358 name: "uq_email".to_string(),
9359 constraint_type: "unique".to_string(),
9360 fields: vec!["email".to_string()],
9361 expression: None,
9362 foreign_key_info: None,
9363 }];
9364 let model = build_model_state(
9365 "auth",
9366 "Account",
9367 vec![FieldState::new(
9368 "email",
9369 super::super::FieldType::VarChar(255),
9370 false,
9371 )],
9372 Vec::new(),
9373 constraints,
9374 );
9375 let state = build_project_state(vec![(("auth".to_string(), "Account".to_string()), model)]);
9376
9377 let schema = state.to_database_schema();
9379
9380 let table = &schema.tables["auth_account"];
9382 assert_eq!(table.constraints.len(), 1);
9383 assert_eq!(table.constraints[0].name, "uq_email");
9384 assert_eq!(table.constraints[0].constraint_type, "unique");
9385 assert_eq!(table.constraints[0].definition, "email");
9386 }
9387
9388 #[rstest]
9389 fn to_database_schema_maps_field_params() {
9390 let mut field = FieldState::new("id", super::super::FieldType::Integer, false);
9392 field
9393 .params
9394 .insert("primary_key".to_string(), "true".to_string());
9395 field
9396 .params
9397 .insert("auto_increment".to_string(), "true".to_string());
9398 field.params.insert("default".to_string(), "0".to_string());
9399
9400 let mut nullable_field = FieldState::new("bio", super::super::FieldType::Text, true);
9401 nullable_field
9402 .params
9403 .insert("default".to_string(), "''".to_string());
9404
9405 let model = build_model_state(
9406 "users",
9407 "Profile",
9408 vec![field, nullable_field],
9409 Vec::new(),
9410 Vec::new(),
9411 );
9412 let state =
9413 build_project_state(vec![(("users".to_string(), "Profile".to_string()), model)]);
9414
9415 let schema = state.to_database_schema();
9417
9418 let table = &schema.tables["users_profile"];
9420 let id_col = &table.columns["id"];
9421 assert!(id_col.primary_key);
9422 assert!(id_col.auto_increment);
9423 assert_eq!(id_col.default, Some("0".to_string()));
9424 assert!(!id_col.nullable);
9425
9426 let bio_col = &table.columns["bio"];
9427 assert!(!bio_col.primary_key);
9428 assert!(!bio_col.auto_increment);
9429 assert!(bio_col.nullable);
9430 assert_eq!(bio_col.default, Some("''".to_string()));
9431 }
9432
9433 #[rstest]
9434 fn to_database_schema_for_app_filters_by_app_label() {
9435 let blog_post = build_model_state(
9437 "blog",
9438 "Post",
9439 vec![FieldState::new(
9440 "id",
9441 super::super::FieldType::Integer,
9442 false,
9443 )],
9444 Vec::new(),
9445 Vec::new(),
9446 );
9447 let auth_user = build_model_state(
9448 "auth",
9449 "User",
9450 vec![FieldState::new(
9451 "id",
9452 super::super::FieldType::Integer,
9453 false,
9454 )],
9455 Vec::new(),
9456 Vec::new(),
9457 );
9458 let state = build_project_state(vec![
9459 (("blog".to_string(), "Post".to_string()), blog_post),
9460 (("auth".to_string(), "User".to_string()), auth_user),
9461 ]);
9462
9463 let blog_schema = state.to_database_schema_for_app("blog");
9465 let auth_schema = state.to_database_schema_for_app("auth");
9466 let empty_schema = state.to_database_schema_for_app("nonexistent");
9467
9468 assert_eq!(blog_schema.tables.len(), 1);
9470 assert!(blog_schema.tables.contains_key("blog_post"));
9471
9472 assert_eq!(auth_schema.tables.len(), 1);
9473 assert!(auth_schema.tables.contains_key("auth_user"));
9474
9475 assert_eq!(empty_schema.tables.len(), 0);
9476 }
9477
9478 #[rstest]
9479 fn to_database_schema_for_app_propagates_indexes_and_constraints() {
9480 let indexes = vec![IndexDefinition {
9482 name: "idx_created".to_string(),
9483 fields: vec!["created_at".to_string()],
9484 unique: false,
9485 where_clause: None,
9486 index_type: None,
9487 expressions: None,
9488 concurrently: false,
9489 mysql_options: None,
9490 operator_class: None,
9491 }];
9492 let constraints = vec![ConstraintDefinition {
9493 name: "ck_status".to_string(),
9494 constraint_type: "check".to_string(),
9495 fields: vec!["status".to_string()],
9496 expression: Some("status IN ('draft', 'published')".to_string()),
9497 foreign_key_info: None,
9498 }];
9499 let model = build_model_state(
9500 "blog",
9501 "Post",
9502 vec![
9503 FieldState::new("created_at", super::super::FieldType::DateTime, false),
9504 FieldState::new("status", super::super::FieldType::VarChar(20), false),
9505 ],
9506 indexes,
9507 constraints,
9508 );
9509 let state = build_project_state(vec![(("blog".to_string(), "Post".to_string()), model)]);
9510
9511 let schema = state.to_database_schema_for_app("blog");
9513
9514 let table = &schema.tables["blog_post"];
9516 assert_eq!(table.indexes.len(), 1);
9517 assert_eq!(table.indexes[0].name, "idx_created");
9518 assert_eq!(table.indexes[0].columns, vec!["created_at".to_string()]);
9519
9520 assert_eq!(table.constraints.len(), 1);
9521 assert_eq!(table.constraints[0].name, "ck_status");
9522 assert_eq!(table.constraints[0].constraint_type, "check");
9523 assert_eq!(table.constraints[0].definition, "status");
9524 }
9525
9526 fn build_model_state_with_table_name(
9528 app_label: &str,
9529 name: &str,
9530 table_name: &str,
9531 fields: Vec<FieldState>,
9532 ) -> ModelState {
9533 let mut field_map = std::collections::BTreeMap::new();
9534 for f in fields {
9535 field_map.insert(f.name.clone(), f);
9536 }
9537 ModelState {
9538 app_label: app_label.to_string(),
9539 name: name.to_string(),
9540 table_name: table_name.to_string(),
9541 fields: field_map,
9542 options: std::collections::HashMap::new(),
9543 base_model: None,
9544 inheritance_type: None,
9545 discriminator_column: None,
9546 indexes: Vec::new(),
9547 constraints: Vec::new(),
9548 many_to_many_fields: Vec::new(),
9549 }
9550 }
9551
9552 #[rstest]
9553 fn to_database_schema_respects_custom_table_name() {
9554 let model = build_model_state_with_table_name(
9556 "blog",
9557 "Post",
9558 "custom_posts_table",
9559 vec![FieldState::new(
9560 "id",
9561 super::super::FieldType::Integer,
9562 false,
9563 )],
9564 );
9565 let state = build_project_state(vec![(("blog".to_string(), "Post".to_string()), model)]);
9566
9567 let schema = state.to_database_schema();
9569
9570 assert!(schema.tables.contains_key("blog_post"));
9573 let table = &schema.tables["blog_post"];
9575 assert_eq!(table.name, "custom_posts_table");
9576 }
9577
9578 #[rstest]
9579 fn to_database_schema_for_app_respects_custom_table_name() {
9580 let model = build_model_state_with_table_name(
9582 "blog",
9583 "Post",
9584 "custom_posts_table",
9585 vec![FieldState::new(
9586 "id",
9587 super::super::FieldType::Integer,
9588 false,
9589 )],
9590 );
9591 let state = build_project_state(vec![(("blog".to_string(), "Post".to_string()), model)]);
9592
9593 let schema = state.to_database_schema_for_app("blog");
9595
9596 assert!(schema.tables.contains_key("blog_post"));
9598 let table = &schema.tables["blog_post"];
9599 assert_eq!(table.name, "custom_posts_table");
9600 }
9601
9602 fn sample_fields() -> Vec<FieldState> {
9606 vec![
9607 FieldState::new("id", super::super::FieldType::Integer, false),
9608 FieldState::new("name", super::super::FieldType::VarChar(255), false),
9609 ]
9610 }
9611
9612 #[rstest]
9628 fn detect_created_many_to_many_recognises_existing_through_table_by_table_name() {
9629 use super::super::model_registry::ManyToManyMetadata;
9630
9631 let from_room = build_model_state_with_table_name("dm", "Room", "dm_room", sample_fields());
9636 let from_through = build_model_state_with_table_name(
9637 "dm",
9638 "RoomMembers",
9639 "dm_room_members",
9640 sample_fields(),
9641 );
9642 let from_state = build_project_state(vec![
9643 (("dm".to_string(), "Room".to_string()), from_room),
9644 (("dm".to_string(), "RoomMembers".to_string()), from_through),
9645 ]);
9646
9647 let mut to_room =
9652 build_model_state_with_table_name("dm", "DMRoom", "dm_room", sample_fields());
9653 to_room
9654 .many_to_many_fields
9655 .push(ManyToManyMetadata::new("members", "User"));
9656 let to_through = build_model_state_with_table_name(
9657 "dm",
9658 "DMRoomMembers",
9659 "dm_room_members",
9660 sample_fields(),
9661 );
9662 let to_state = build_project_state(vec![
9663 (("dm".to_string(), "DMRoom".to_string()), to_room),
9664 (("dm".to_string(), "DMRoomMembers".to_string()), to_through),
9665 ]);
9666
9667 let detector = MigrationAutodetector::new(from_state, to_state);
9668
9669 let changes = detector.detect_changes();
9671
9672 assert!(
9675 changes.created_many_to_many.is_empty(),
9676 "M2M through table already exists in from_state; expected no \
9677 created_many_to_many, got {:?}",
9678 changes.created_many_to_many
9679 );
9680 }
9681
9682 #[rstest]
9683 fn generate_migrations_resolves_unqualified_many_to_many_target_across_apps() {
9684 use super::super::Operation;
9685 use super::super::model_registry::ManyToManyMetadata;
9686 use super::super::operations::Constraint;
9687
9688 let auth_user =
9689 build_model_state_with_table_name("auth", "User", "auth_user", sample_fields());
9690 let mut dm_room =
9691 build_model_state_with_table_name("dm", "DMRoom", "dm_room", sample_fields());
9692 dm_room
9693 .many_to_many_fields
9694 .push(ManyToManyMetadata::new("members", "User"));
9695 let to_state = build_project_state(vec![
9696 (("auth".to_string(), "User".to_string()), auth_user),
9697 (("dm".to_string(), "DMRoom".to_string()), dm_room),
9698 ]);
9699 let detector = MigrationAutodetector::new(ProjectState::new(), to_state);
9700
9701 let migrations = detector.generate_migrations();
9702 let dm_migration = migrations
9703 .iter()
9704 .find(|migration| migration.app_label == "dm")
9705 .expect("dm migration should be generated");
9706 let Operation::CreateTable {
9707 name, constraints, ..
9708 } = dm_migration
9709 .operations
9710 .iter()
9711 .find(|operation| {
9712 matches!(
9713 operation,
9714 Operation::CreateTable { name, .. } if name == "dm_room_members"
9715 )
9716 })
9717 .expect("dm_room_members through table should be generated")
9718 else {
9719 panic!("expected CreateTable operation");
9720 };
9721 assert_eq!(name, "dm_room_members");
9722
9723 let target_fk = constraints
9724 .iter()
9725 .find_map(|constraint| match constraint {
9726 Constraint::ForeignKey {
9727 columns,
9728 referenced_table,
9729 ..
9730 } if columns == &vec!["auth_user_id".to_string()] => Some(referenced_table),
9731 _ => None,
9732 })
9733 .expect("auth_user_id foreign key should be generated");
9734 assert_eq!(target_fk, "auth_user");
9735 }
9736
9737 #[rstest]
9738 fn detect_created_many_to_many_skips_existing_to_state_through_table() {
9739 use super::super::model_registry::ManyToManyMetadata;
9740
9741 let auth_user =
9742 build_model_state_with_table_name("auth", "User", "auth_user", sample_fields());
9743 let mut dm_room =
9744 build_model_state_with_table_name("dm", "DMRoom", "dm_room", sample_fields());
9745 dm_room
9746 .many_to_many_fields
9747 .push(ManyToManyMetadata::new("members", "User"));
9748 let dm_room_members = build_model_state_with_table_name(
9749 "dm",
9750 "DMRoomMembers",
9751 "dm_room_members",
9752 sample_fields(),
9753 );
9754 let to_state = build_project_state(vec![
9755 (("auth".to_string(), "User".to_string()), auth_user),
9756 (("dm".to_string(), "DMRoom".to_string()), dm_room),
9757 (
9758 ("dm".to_string(), "DMRoomMembers".to_string()),
9759 dm_room_members,
9760 ),
9761 ]);
9762 let detector = MigrationAutodetector::new(ProjectState::new(), to_state);
9763
9764 let changes = detector.detect_changes();
9765
9766 assert!(
9767 changes.created_many_to_many.is_empty(),
9768 "to_state already contains the through table model; expected no \
9769 synthetic created_many_to_many, got {:?}",
9770 changes.created_many_to_many
9771 );
9772 }
9773
9774 #[rstest]
9775 fn detect_renamed_models_skips_struct_only_rename_with_same_table_name() {
9776 let from_model =
9778 build_model_state_with_table_name("myapp", "Clusters", "clusters", sample_fields());
9779 let to_model =
9780 build_model_state_with_table_name("myapp", "Cluster", "clusters", sample_fields());
9781
9782 let from_state = build_project_state(vec![(
9783 ("myapp".to_string(), "Clusters".to_string()),
9784 from_model,
9785 )]);
9786 let to_state = build_project_state(vec![(
9787 ("myapp".to_string(), "Cluster".to_string()),
9788 to_model,
9789 )]);
9790
9791 let detector = MigrationAutodetector::new(from_state, to_state);
9792
9793 let changes = detector.detect_changes();
9795
9796 assert!(
9798 changes.renamed_models.is_empty(),
9799 "struct-only rename with same table name should not produce renamed_models"
9800 );
9801 }
9802
9803 #[rstest]
9804 fn detect_renamed_models_detects_actual_table_rename() {
9805 let from_model =
9807 build_model_state_with_table_name("myapp", "OldModel", "old_table", sample_fields());
9808 let to_model =
9809 build_model_state_with_table_name("myapp", "NewModel", "new_table", sample_fields());
9810
9811 let from_state = build_project_state(vec![(
9812 ("myapp".to_string(), "OldModel".to_string()),
9813 from_model,
9814 )]);
9815 let to_state = build_project_state(vec![(
9816 ("myapp".to_string(), "NewModel".to_string()),
9817 to_model,
9818 )]);
9819
9820 let detector = MigrationAutodetector::new(from_state, to_state);
9821
9822 let changes = detector.detect_changes();
9824 let migrations = detector.generate_migrations();
9825
9826 assert_eq!(
9829 changes.renamed_models.len(),
9830 1,
9831 "actual table rename should be detected"
9832 );
9833 assert_eq!(changes.renamed_models[0].1, "OldModel");
9834 assert_eq!(changes.renamed_models[0].2, "NewModel");
9835 assert!(
9836 changes.created_models.is_empty(),
9837 "confirmed model rename must not leave created_models noise: {:?}",
9838 changes.created_models
9839 );
9840 assert!(
9841 changes.deleted_models.is_empty(),
9842 "confirmed model rename must not leave deleted_models noise: {:?}",
9843 changes.deleted_models
9844 );
9845 assert_eq!(migrations.len(), 1, "unexpected migrations: {migrations:?}");
9846 assert_eq!(migrations[0].app_label, "myapp");
9847 let operations = &migrations[0].operations;
9848 assert_eq!(operations.len(), 1, "unexpected operations: {operations:?}");
9849 assert!(matches!(
9850 &operations[0],
9851 super::super::Operation::RenameTable { old_name, new_name }
9852 if old_name == "old_table" && new_name == "new_table"
9853 ));
9854 }
9855
9856 #[rstest]
9857 fn table_rename_recreates_single_field_unique_constraint_with_new_name() {
9858 let email = FieldState::new("email", super::super::FieldType::VarChar(255), false);
9860 let old_constraint = ConstraintDefinition {
9861 name: "old_table_email_uniq".to_string(),
9862 constraint_type: "unique".to_string(),
9863 fields: vec!["email".to_string()],
9864 expression: None,
9865 foreign_key_info: None,
9866 };
9867 let new_constraint = ConstraintDefinition {
9868 name: "new_table_email_uniq".to_string(),
9869 ..old_constraint.clone()
9870 };
9871 let mut from_model = build_model_state_with_table_name(
9872 "myapp",
9873 "OldModel",
9874 "old_table",
9875 vec![email.clone()],
9876 );
9877 from_model.constraints.push(old_constraint);
9878 let mut to_model =
9879 build_model_state_with_table_name("myapp", "NewModel", "new_table", vec![email]);
9880 to_model.constraints.push(new_constraint.clone());
9881 let detector = MigrationAutodetector::new(
9882 build_project_state(vec![(
9883 ("myapp".to_string(), "OldModel".to_string()),
9884 from_model,
9885 )]),
9886 build_project_state(vec![(
9887 ("myapp".to_string(), "NewModel".to_string()),
9888 to_model,
9889 )]),
9890 );
9891
9892 let migrations = detector.generate_migrations();
9894
9895 assert_eq!(migrations.len(), 1);
9897 assert_eq!(
9898 migrations[0].operations,
9899 vec![
9900 super::super::Operation::DropConstraint {
9901 table: "old_table".to_string(),
9902 constraint_name: "old_table_email_uniq".to_string(),
9903 },
9904 super::super::Operation::RenameTable {
9905 old_name: "old_table".to_string(),
9906 new_name: "new_table".to_string(),
9907 },
9908 super::super::Operation::AddConstraint {
9909 table: "new_table".to_string(),
9910 constraint_sql: new_constraint.to_constraint().to_string(),
9911 },
9912 ]
9913 );
9914 }
9915
9916 #[rstest]
9917 fn has_field_changed_ignores_non_schema_params() {
9918 let from_field = FieldState {
9920 name: "email".to_string(),
9921 field_type: super::super::FieldType::VarChar(255),
9922 nullable: false,
9923 params: std::collections::HashMap::new(),
9924 foreign_key: None,
9925 };
9926 let mut to_params = std::collections::HashMap::new();
9927 to_params.insert("max_length".to_string(), "255".to_string());
9928 to_params.insert("null".to_string(), "false".to_string());
9929 to_params.insert("blank".to_string(), "false".to_string());
9930 let to_field = FieldState {
9931 name: "email".to_string(),
9932 field_type: super::super::FieldType::VarChar(255),
9933 nullable: false,
9934 params: to_params,
9935 foreign_key: None,
9936 };
9937
9938 let detector = MigrationAutodetector::new(ProjectState::new(), ProjectState::new());
9939
9940 let changed =
9942 detector.has_field_changed_with_unique("email", &from_field, &to_field, None, None);
9943
9944 assert!(
9946 !changed,
9947 "fields with identical schema but different non-schema params should not be detected as changed"
9948 );
9949 }
9950
9951 #[rstest]
9952 fn has_field_changed_detects_database_default_changes() {
9953 let from_field = FieldState::new("is_active", super::super::FieldType::Boolean, false);
9955 let mut to_field = FieldState::new("is_active", super::super::FieldType::Boolean, false);
9956 to_field
9957 .params
9958 .insert("default".to_string(), "true".to_string());
9959 let detector = MigrationAutodetector::new(ProjectState::new(), ProjectState::new());
9960
9961 let changed =
9963 detector.has_field_changed_with_unique("is_active", &from_field, &to_field, None, None);
9964
9965 assert!(
9967 changed,
9968 "database default changes must be detected as schema-affecting field changes"
9969 );
9970 }
9971
9972 #[rstest]
9973 fn generate_operations_carries_old_definition_for_database_default_changes() {
9974 let mut from_field = FieldState::new("is_active", super::super::FieldType::Boolean, false);
9976 from_field
9977 .params
9978 .insert("default".to_string(), "true".to_string());
9979 let to_field = FieldState::new("is_active", super::super::FieldType::Boolean, false);
9980 let from_model =
9981 build_model_state("accounts", "User", vec![from_field], Vec::new(), Vec::new());
9982 let to_model =
9983 build_model_state("accounts", "User", vec![to_field], Vec::new(), Vec::new());
9984 let detector = MigrationAutodetector::new(
9985 build_project_state(vec![(
9986 ("accounts".to_string(), "User".to_string()),
9987 from_model,
9988 )]),
9989 build_project_state(vec![(
9990 ("accounts".to_string(), "User".to_string()),
9991 to_model,
9992 )]),
9993 );
9994
9995 let operations = detector.generate_operations();
9997
9998 let operation = operations
10000 .iter()
10001 .find(|operation| {
10002 matches!(
10003 operation,
10004 super::super::Operation::AlterColumn { column, .. } if column == "is_active"
10005 )
10006 })
10007 .expect("default removal should emit AlterColumn");
10008 let super::super::Operation::AlterColumn {
10009 old_definition,
10010 new_definition,
10011 ..
10012 } = operation
10013 else {
10014 unreachable!("matched AlterColumn above");
10015 };
10016 assert_eq!(
10017 old_definition
10018 .as_ref()
10019 .and_then(|definition| definition.default.as_deref()),
10020 Some("true")
10021 );
10022 assert_eq!(new_definition.default, None);
10023 }
10024
10025 #[rstest]
10026 fn generate_operations_empty_for_struct_only_rename() {
10027 let from_model =
10029 build_model_state_with_table_name("myapp", "Clusters", "clusters", sample_fields());
10030 let to_model =
10031 build_model_state_with_table_name("myapp", "Cluster", "clusters", sample_fields());
10032
10033 let from_state = build_project_state(vec![(
10034 ("myapp".to_string(), "Clusters".to_string()),
10035 from_model,
10036 )]);
10037 let to_state = build_project_state(vec![(
10038 ("myapp".to_string(), "Cluster".to_string()),
10039 to_model,
10040 )]);
10041
10042 let detector = MigrationAutodetector::new(from_state, to_state);
10043
10044 let operations = detector.generate_operations();
10046
10047 assert!(
10049 operations.is_empty(),
10050 "struct-only rename with same table name and identical fields should produce no operations, got: {:?}",
10051 operations
10052 );
10053 }
10054
10055 #[rstest]
10056 fn detect_composite_pk_added_emits_create_composite_primary_key() {
10057 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
10059 let tenant_id_field = FieldState::new("tenant_id", super::super::FieldType::Integer, false);
10060
10061 let from_model = build_model_state(
10062 "billing",
10063 "Invoice",
10064 vec![id_field.clone(), tenant_id_field.clone()],
10065 Vec::new(),
10066 Vec::new(),
10067 );
10068 let composite_pk = ConstraintDefinition {
10069 name: "billing_invoice_pkey".to_string(),
10070 constraint_type: "primary_key".to_string(),
10071 fields: vec!["id".to_string(), "tenant_id".to_string()],
10072 expression: None,
10073 foreign_key_info: None,
10074 };
10075 let to_model = build_model_state(
10076 "billing",
10077 "Invoice",
10078 vec![id_field, tenant_id_field],
10079 Vec::new(),
10080 vec![composite_pk],
10081 );
10082
10083 let from_state = build_project_state(vec![(
10084 ("billing".to_string(), "Invoice".to_string()),
10085 from_model,
10086 )]);
10087 let to_state = build_project_state(vec![(
10088 ("billing".to_string(), "Invoice".to_string()),
10089 to_model,
10090 )]);
10091 let detector = MigrationAutodetector::new(from_state, to_state);
10092
10093 let operations = detector.generate_operations();
10095
10096 assert_eq!(operations.len(), 1);
10098 assert!(
10099 matches!(
10100 &operations[0],
10101 super::super::Operation::CreateCompositePrimaryKey {
10102 table,
10103 columns,
10104 ..
10105 } if table == "billing_invoice"
10106 && columns == &["id".to_string(), "tenant_id".to_string()]
10107 ),
10108 "expected CreateCompositePrimaryKey, got: {:?}",
10109 operations
10110 );
10111 }
10112
10113 #[rstest]
10114 fn detect_composite_pk_unchanged_emits_no_operations() {
10115 let composite_pk = ConstraintDefinition {
10117 name: "billing_invoice_pkey".to_string(),
10118 constraint_type: "primary_key".to_string(),
10119 fields: vec!["id".to_string(), "tenant_id".to_string()],
10120 expression: None,
10121 foreign_key_info: None,
10122 };
10123 let from_model = build_model_state(
10124 "billing",
10125 "Invoice",
10126 vec![
10127 FieldState::new("id", super::super::FieldType::Integer, false),
10128 FieldState::new("tenant_id", super::super::FieldType::Integer, false),
10129 ],
10130 Vec::new(),
10131 vec![composite_pk.clone()],
10132 );
10133 let to_model = build_model_state(
10134 "billing",
10135 "Invoice",
10136 vec![
10137 FieldState::new("id", super::super::FieldType::Integer, false),
10138 FieldState::new("tenant_id", super::super::FieldType::Integer, false),
10139 ],
10140 Vec::new(),
10141 vec![composite_pk],
10142 );
10143
10144 let from_state = build_project_state(vec![(
10145 ("billing".to_string(), "Invoice".to_string()),
10146 from_model,
10147 )]);
10148 let to_state = build_project_state(vec![(
10149 ("billing".to_string(), "Invoice".to_string()),
10150 to_model,
10151 )]);
10152 let detector = MigrationAutodetector::new(from_state, to_state);
10153
10154 let operations = detector.generate_operations();
10156
10157 assert!(
10159 operations.is_empty(),
10160 "unchanged composite PK should produce no operations, got: {:?}",
10161 operations
10162 );
10163 }
10164
10165 #[rstest]
10166 fn detect_composite_pk_changed_fields_emits_drop_and_create() {
10167 let composite_pk_from = ConstraintDefinition {
10169 name: "billing_invoice_pkey".to_string(),
10170 constraint_type: "primary_key".to_string(),
10171 fields: vec!["id".to_string(), "tenant_id".to_string()],
10172 expression: None,
10173 foreign_key_info: None,
10174 };
10175 let composite_pk_to = ConstraintDefinition {
10176 name: "billing_invoice_pkey".to_string(),
10177 constraint_type: "primary_key".to_string(),
10178 fields: vec!["id".to_string(), "org_id".to_string()],
10179 expression: None,
10180 foreign_key_info: None,
10181 };
10182 let from_model = build_model_state(
10183 "billing",
10184 "Invoice",
10185 vec![
10186 FieldState::new("id", super::super::FieldType::Integer, false),
10187 FieldState::new("tenant_id", super::super::FieldType::Integer, false),
10188 ],
10189 Vec::new(),
10190 vec![composite_pk_from],
10191 );
10192 let to_model = build_model_state(
10193 "billing",
10194 "Invoice",
10195 vec![
10196 FieldState::new("id", super::super::FieldType::Integer, false),
10197 FieldState::new("org_id", super::super::FieldType::Integer, false),
10198 ],
10199 Vec::new(),
10200 vec![composite_pk_to],
10201 );
10202 let from_state = build_project_state(vec![(
10203 ("billing".to_string(), "Invoice".to_string()),
10204 from_model,
10205 )]);
10206 let to_state = build_project_state(vec![(
10207 ("billing".to_string(), "Invoice".to_string()),
10208 to_model,
10209 )]);
10210 let detector = MigrationAutodetector::new(from_state, to_state);
10211
10212 let operations = detector.generate_operations();
10214
10215 let drop_op = operations.iter().find(|op| {
10217 matches!(op, super::super::Operation::DropConstraint { constraint_name, .. }
10218 if constraint_name == "billing_invoice_pkey")
10219 });
10220 let create_op = operations.iter().find(|op| {
10221 matches!(op, super::super::Operation::CreateCompositePrimaryKey { columns, .. }
10222 if columns == &["id".to_string(), "org_id".to_string()])
10223 });
10224 assert!(
10225 drop_op.is_some(),
10226 "expected DropConstraint for modified composite PK, got: {:?}",
10227 operations
10228 );
10229 assert!(
10230 create_op.is_some(),
10231 "expected CreateCompositePrimaryKey with new fields, got: {:?}",
10232 operations
10233 );
10234 }
10235
10236 #[rstest]
10237 fn detect_sequence_reset_emits_set_auto_increment_value() {
10238 let mut id_field = FieldState::new("id", super::super::FieldType::BigInteger, false);
10240 id_field
10241 .params
10242 .insert("auto_increment".to_string(), "true".to_string());
10243
10244 let from_model = build_model_state(
10245 "shop",
10246 "Order",
10247 vec![id_field.clone()],
10248 Vec::new(),
10249 Vec::new(),
10250 );
10251 let mut to_model =
10252 build_model_state("shop", "Order", vec![id_field], Vec::new(), Vec::new());
10253 to_model
10254 .options
10255 .insert("sequence_reset".to_string(), "1000".to_string());
10256
10257 let from_state = build_project_state(vec![(
10258 ("shop".to_string(), "Order".to_string()),
10259 from_model,
10260 )]);
10261 let to_state =
10262 build_project_state(vec![(("shop".to_string(), "Order".to_string()), to_model)]);
10263 let detector = MigrationAutodetector::new(from_state, to_state);
10264
10265 let operations = detector.generate_operations();
10267
10268 assert_eq!(operations.len(), 1);
10270 assert!(
10271 matches!(
10272 &operations[0],
10273 super::super::Operation::SetAutoIncrementValue {
10274 table,
10275 column,
10276 value,
10277 } if table == "shop_order" && column == "id" && *value == 1000
10278 ),
10279 "expected SetAutoIncrementValue, got: {:?}",
10280 operations
10281 );
10282 }
10283
10284 #[rstest]
10285 fn detect_added_unique_together_emits_add_constraint() {
10286 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
10290 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
10291 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
10292
10293 let from_model = build_model_state(
10294 "clusters",
10295 "Cluster",
10296 vec![id_field.clone(), org_field.clone(), name_field.clone()],
10297 Vec::new(),
10298 Vec::new(),
10299 );
10300 let unique_constraint = ConstraintDefinition {
10301 name: "clusters_cluster_organization_id_name_uniq".to_string(),
10302 constraint_type: "unique".to_string(),
10303 fields: vec!["organization_id".to_string(), "name".to_string()],
10304 expression: None,
10305 foreign_key_info: None,
10306 };
10307 let to_model = build_model_state(
10308 "clusters",
10309 "Cluster",
10310 vec![id_field, org_field, name_field],
10311 Vec::new(),
10312 vec![unique_constraint],
10313 );
10314
10315 let from_state = build_project_state(vec![(
10316 ("clusters".to_string(), "Cluster".to_string()),
10317 from_model,
10318 )]);
10319 let to_state = build_project_state(vec![(
10320 ("clusters".to_string(), "Cluster".to_string()),
10321 to_model,
10322 )]);
10323 let detector = MigrationAutodetector::new(from_state, to_state);
10324
10325 let operations = detector.generate_operations();
10327
10328 assert_eq!(
10331 operations.len(),
10332 1,
10333 "expected exactly one AddConstraint operation, got: {:?}",
10334 operations
10335 );
10336 let super::super::Operation::AddConstraint {
10337 table,
10338 constraint_sql,
10339 } = &operations[0]
10340 else {
10341 panic!(
10342 "expected Operation::AddConstraint, got: {:?}",
10343 operations[0]
10344 );
10345 };
10346 assert_eq!(table, "clusters_cluster");
10347 assert!(
10348 constraint_sql.contains("UNIQUE"),
10349 "constraint SQL should declare UNIQUE, got: {}",
10350 constraint_sql
10351 );
10352 assert!(
10353 constraint_sql.contains("organization_id"),
10354 "constraint SQL should reference organization_id, got: {}",
10355 constraint_sql
10356 );
10357 assert!(
10358 constraint_sql.contains("name"),
10359 "constraint SQL should reference name, got: {}",
10360 constraint_sql
10361 );
10362 assert!(
10363 constraint_sql.contains("clusters_cluster_organization_id_name_uniq"),
10364 "constraint SQL should carry the constraint name, got: {}",
10365 constraint_sql
10366 );
10367 }
10368
10369 #[rstest]
10370 fn detect_removed_unique_together_emits_drop_constraint() {
10371 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
10375 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
10376 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
10377
10378 let unique_constraint = ConstraintDefinition {
10379 name: "clusters_cluster_organization_id_name_uniq".to_string(),
10380 constraint_type: "unique".to_string(),
10381 fields: vec!["organization_id".to_string(), "name".to_string()],
10382 expression: None,
10383 foreign_key_info: None,
10384 };
10385 let from_model = build_model_state(
10386 "clusters",
10387 "Cluster",
10388 vec![id_field.clone(), org_field.clone(), name_field.clone()],
10389 Vec::new(),
10390 vec![unique_constraint],
10391 );
10392 let to_model = build_model_state(
10393 "clusters",
10394 "Cluster",
10395 vec![id_field, org_field, name_field],
10396 Vec::new(),
10397 Vec::new(),
10398 );
10399
10400 let from_state = build_project_state(vec![(
10401 ("clusters".to_string(), "Cluster".to_string()),
10402 from_model,
10403 )]);
10404 let to_state = build_project_state(vec![(
10405 ("clusters".to_string(), "Cluster".to_string()),
10406 to_model,
10407 )]);
10408 let detector = MigrationAutodetector::new(from_state, to_state);
10409
10410 let operations = detector.generate_operations();
10412
10413 assert_eq!(
10415 operations.len(),
10416 1,
10417 "expected exactly one DropConstraint operation, got: {:?}",
10418 operations
10419 );
10420 let super::super::Operation::DropConstraint {
10421 table,
10422 constraint_name,
10423 } = &operations[0]
10424 else {
10425 panic!(
10426 "expected Operation::DropConstraint, got: {:?}",
10427 operations[0]
10428 );
10429 };
10430 assert_eq!(table, "clusters_cluster");
10431 assert_eq!(
10432 constraint_name,
10433 "clusters_cluster_organization_id_name_uniq"
10434 );
10435 }
10436
10437 #[rstest]
10438 fn detect_added_unique_together_via_offline_reconstructed_from_state() {
10439 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
10452 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
10453 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
10454
10455 let mut from_model = build_model_state(
10457 "clusters",
10458 "Clusters",
10459 vec![id_field.clone(), org_field.clone(), name_field.clone()],
10460 Vec::new(),
10461 Vec::new(),
10462 );
10463 from_model.table_name = "clusters_cluster".to_string();
10464
10465 let unique_constraint = ConstraintDefinition {
10467 name: "clusters_cluster_organization_id_name_uniq".to_string(),
10468 constraint_type: "unique".to_string(),
10469 fields: vec!["organization_id".to_string(), "name".to_string()],
10470 expression: None,
10471 foreign_key_info: None,
10472 };
10473 let to_model = build_model_state(
10474 "clusters",
10475 "Cluster",
10476 vec![id_field, org_field, name_field],
10477 Vec::new(),
10478 vec![unique_constraint],
10479 );
10480
10481 let from_state = build_project_state(vec![(
10482 ("clusters".to_string(), "Clusters".to_string()),
10483 from_model,
10484 )]);
10485 let to_state = build_project_state(vec![(
10486 ("clusters".to_string(), "Cluster".to_string()),
10487 to_model,
10488 )]);
10489 let detector = MigrationAutodetector::new(from_state, to_state);
10490
10491 let operations = detector.generate_operations();
10493
10494 assert_eq!(
10498 operations.len(),
10499 1,
10500 "expected exactly one AddConstraint operation, got: {:?}",
10501 operations
10502 );
10503 let super::super::Operation::AddConstraint {
10504 table,
10505 constraint_sql,
10506 } = &operations[0]
10507 else {
10508 panic!(
10509 "expected Operation::AddConstraint, got: {:?}",
10510 operations[0]
10511 );
10512 };
10513 assert_eq!(table, "clusters_cluster");
10514 assert!(
10515 constraint_sql.contains("clusters_cluster_organization_id_name_uniq"),
10516 "constraint SQL should carry the constraint name, got: {}",
10517 constraint_sql
10518 );
10519 }
10520
10521 #[rstest]
10522 fn detect_removed_unique_together_via_offline_reconstructed_from_state() {
10523 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
10529 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
10530 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
10531
10532 let unique_constraint = ConstraintDefinition {
10533 name: "clusters_cluster_organization_id_name_uniq".to_string(),
10534 constraint_type: "unique".to_string(),
10535 fields: vec!["organization_id".to_string(), "name".to_string()],
10536 expression: None,
10537 foreign_key_info: None,
10538 };
10539 let mut from_model = build_model_state(
10540 "clusters",
10541 "Clusters",
10542 vec![id_field.clone(), org_field.clone(), name_field.clone()],
10543 Vec::new(),
10544 vec![unique_constraint],
10545 );
10546 from_model.table_name = "clusters_cluster".to_string();
10547
10548 let to_model = build_model_state(
10549 "clusters",
10550 "Cluster",
10551 vec![id_field, org_field, name_field],
10552 Vec::new(),
10553 Vec::new(),
10554 );
10555
10556 let from_state = build_project_state(vec![(
10557 ("clusters".to_string(), "Clusters".to_string()),
10558 from_model,
10559 )]);
10560 let to_state = build_project_state(vec![(
10561 ("clusters".to_string(), "Cluster".to_string()),
10562 to_model,
10563 )]);
10564 let detector = MigrationAutodetector::new(from_state, to_state);
10565
10566 let operations = detector.generate_operations();
10568
10569 assert_eq!(
10571 operations.len(),
10572 1,
10573 "expected exactly one DropConstraint operation, got: {:?}",
10574 operations
10575 );
10576 let super::super::Operation::DropConstraint {
10577 table,
10578 constraint_name,
10579 } = &operations[0]
10580 else {
10581 panic!(
10582 "expected Operation::DropConstraint, got: {:?}",
10583 operations[0]
10584 );
10585 };
10586 assert_eq!(table, "clusters_cluster");
10587 assert_eq!(
10588 constraint_name,
10589 "clusters_cluster_organization_id_name_uniq"
10590 );
10591 }
10592
10593 #[rstest]
10594 fn has_field_changed_ignores_param_population_skew() {
10595 let mut from_params = std::collections::HashMap::new();
10608 from_params.insert("primary_key".to_string(), "true".to_string());
10609 from_params.insert("auto_increment".to_string(), "true".to_string());
10610 let from_field = FieldState {
10611 name: "id".to_string(),
10612 field_type: super::super::FieldType::BigInteger,
10613 nullable: false,
10614 params: from_params,
10615 foreign_key: None,
10616 };
10617
10618 let mut to_params = std::collections::HashMap::new();
10626 to_params.insert("primary_key".to_string(), "true".to_string());
10627 to_params.insert("auto_increment".to_string(), "true".to_string());
10628 to_params.insert("not_null".to_string(), "true".to_string());
10629 to_params.insert("null".to_string(), "false".to_string());
10630 to_params.insert("unique".to_string(), "false".to_string());
10631 let to_field = FieldState {
10632 name: "id".to_string(),
10633 field_type: super::super::FieldType::BigInteger,
10634 nullable: false,
10635 params: to_params,
10636 foreign_key: None,
10637 };
10638
10639 let detector = MigrationAutodetector::new(ProjectState::new(), ProjectState::new());
10640
10641 let changed =
10643 detector.has_field_changed_with_unique("id", &from_field, &to_field, None, None);
10644
10645 assert!(
10648 !changed,
10649 "identical schema with asymmetric param populations between migration replay and macro registry must not be detected as changed"
10650 );
10651 }
10652
10653 #[rstest]
10654 fn generate_operations_no_spurious_altercolumn_for_pk_via_offline_reconstructed_state() {
10655 let mut from_id_params = std::collections::HashMap::new();
10669 from_id_params.insert("primary_key".to_string(), "true".to_string());
10670 from_id_params.insert("auto_increment".to_string(), "true".to_string());
10671 let from_id_field = FieldState {
10672 name: "id".to_string(),
10673 field_type: super::super::FieldType::BigInteger,
10674 nullable: false,
10675 params: from_id_params,
10676 foreign_key: None,
10677 };
10678 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
10679 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
10680
10681 let mut from_model = build_model_state(
10684 "clusters",
10685 "Clusters",
10686 vec![from_id_field, org_field.clone(), name_field.clone()],
10687 Vec::new(),
10688 Vec::new(),
10689 );
10690 from_model.table_name = "clusters_cluster".to_string();
10691
10692 let mut to_id_params = std::collections::HashMap::new();
10699 to_id_params.insert("primary_key".to_string(), "true".to_string());
10700 to_id_params.insert("auto_increment".to_string(), "true".to_string());
10701 to_id_params.insert("not_null".to_string(), "true".to_string());
10702 to_id_params.insert("null".to_string(), "false".to_string());
10703 to_id_params.insert("unique".to_string(), "false".to_string());
10704 let to_id_field = FieldState {
10705 name: "id".to_string(),
10706 field_type: super::super::FieldType::BigInteger,
10707 nullable: false,
10708 params: to_id_params,
10709 foreign_key: None,
10710 };
10711 let unique_constraint = ConstraintDefinition {
10712 name: "clusters_cluster_organization_id_name_uniq".to_string(),
10713 constraint_type: "unique".to_string(),
10714 fields: vec!["organization_id".to_string(), "name".to_string()],
10715 expression: None,
10716 foreign_key_info: None,
10717 };
10718 let to_model = build_model_state(
10719 "clusters",
10720 "Cluster",
10721 vec![to_id_field, org_field, name_field],
10722 Vec::new(),
10723 vec![unique_constraint],
10724 );
10725
10726 let from_state = build_project_state(vec![(
10727 ("clusters".to_string(), "Clusters".to_string()),
10728 from_model,
10729 )]);
10730 let to_state = build_project_state(vec![(
10731 ("clusters".to_string(), "Cluster".to_string()),
10732 to_model,
10733 )]);
10734 let detector = MigrationAutodetector::new(from_state, to_state);
10735
10736 let operations = detector.generate_operations();
10738
10739 assert!(
10743 !operations
10744 .iter()
10745 .any(|op| matches!(op, super::super::Operation::AlterColumn { .. })),
10746 "no AlterColumn must be emitted for unchanged PK under offline state reconstruction, got: {:?}",
10747 operations
10748 );
10749 assert_eq!(
10750 operations.len(),
10751 1,
10752 "expected exactly one AddConstraint operation, got: {:?}",
10753 operations
10754 );
10755 assert!(
10756 matches!(
10757 &operations[0],
10758 super::super::Operation::AddConstraint { .. }
10759 ),
10760 "expected the single operation to be AddConstraint, got: {:?}",
10761 operations[0]
10762 );
10763 }
10764
10765 #[rstest]
10766 fn generate_operations_no_spurious_altercolumn_for_option_pk_via_apply_migration_operations() {
10767 let mut id_meta =
10798 super::super::model_registry::FieldMetadata::new(super::super::FieldType::BigInteger);
10799 id_meta = id_meta
10800 .with_param("primary_key", "true")
10801 .with_param("auto_increment", "true")
10802 .with_param("not_null", "true")
10803 .with_nullable(false);
10804 let mut name_meta =
10805 super::super::model_registry::FieldMetadata::new(super::super::FieldType::VarChar(255));
10806 name_meta = name_meta
10807 .with_param("max_length", "255")
10808 .with_param("not_null", "true")
10809 .with_nullable(false);
10810
10811 let mut metadata =
10812 super::super::model_registry::ModelMetadata::new("clusters", "Cluster", "clusters");
10813 metadata.add_field("id".to_string(), id_meta);
10814 metadata.add_field("name".to_string(), name_meta);
10815
10816 let to_model = metadata.to_model_state();
10817 let to_id = to_model.fields.get("id").expect("id field present");
10821 assert!(
10822 !to_id.nullable,
10823 "to_state PK FieldState.nullable must be false; got nullable=true \
10824 with params={:?}. Did the #[model] macro regress to emitting \
10825 null=\"true\" for Option<T> PKs?",
10826 to_id.params
10827 );
10828
10829 let to_state = build_project_state(vec![(
10830 ("clusters".to_string(), "Cluster".to_string()),
10831 to_model,
10832 )]);
10833
10834 let create_clusters = super::super::Operation::CreateTable {
10839 name: "clusters".to_string(),
10840 columns: vec![
10841 super::super::ColumnDefinition {
10842 name: "id".to_string(),
10843 type_definition: super::super::FieldType::BigInteger,
10844 not_null: true,
10845 unique: false,
10846 primary_key: true,
10847 auto_increment: true,
10848 default: None,
10849 },
10850 super::super::ColumnDefinition {
10851 name: "name".to_string(),
10852 type_definition: super::super::FieldType::VarChar(255),
10853 not_null: true,
10854 unique: false,
10855 primary_key: false,
10856 auto_increment: false,
10857 default: None,
10858 },
10859 ],
10860 constraints: vec![],
10861 without_rowid: None,
10862 interleave_in_parent: None,
10863 partition: None,
10864 };
10865 let mut from_state = ProjectState::new();
10866 from_state.apply_migration_operations(&[create_clusters], "clusters");
10867
10868 let from_clusters = from_state
10871 .find_model_by_table("clusters")
10872 .expect("clusters model present in from_state");
10873 assert!(
10874 !from_clusters
10875 .fields
10876 .get("id")
10877 .expect("id field in from_state")
10878 .nullable,
10879 "from_state PK FieldState.nullable must be false (column_def_to_field_state derives \
10880 from not_null); got nullable=true"
10881 );
10882
10883 let detector = MigrationAutodetector::new(from_state, to_state);
10884
10885 let direct_ops = detector.generate_operations();
10889 let migrations = detector.generate_migrations();
10890 let migration_ops: Vec<&super::super::Operation> = migrations
10891 .iter()
10892 .flat_map(|m| m.operations.iter())
10893 .collect();
10894
10895 assert!(
10898 !direct_ops.iter().any(|op| matches!(
10899 op,
10900 super::super::Operation::AlterColumn { column, .. } if column == "id"
10901 )),
10902 "generate_operations() emitted spurious AlterColumn for unchanged `id` PK \
10903 under apply_migration_operations from_state. ops={:?}",
10904 direct_ops
10905 );
10906 assert!(
10907 !migration_ops.iter().any(|op| matches!(
10908 op,
10909 super::super::Operation::AlterColumn { column, .. } if column == "id"
10910 )),
10911 "generate_migrations() emitted spurious AlterColumn for unchanged `id` PK \
10912 under apply_migration_operations from_state. ops={:?}",
10913 migration_ops
10914 );
10915 }
10916
10917 #[rstest]
10918 fn generate_operations_no_spurious_altercolumn_for_replayed_foreign_key_column() {
10919 let mut target_metadata = super::super::model_registry::ModelMetadata::new(
10929 "fk_drift_target_app",
10930 "FkDriftTarget",
10931 "fk_drift_targets",
10932 );
10933 target_metadata.add_field(
10934 "id".to_string(),
10935 super::super::model_registry::FieldMetadata::new(super::super::FieldType::BigInteger)
10936 .with_param("primary_key", "true")
10937 .with_param("auto_increment", "true")
10938 .with_param("not_null", "true")
10939 .with_nullable(false),
10940 );
10941 super::super::model_registry::global_registry().register_model(target_metadata);
10942
10943 let create_sources = super::super::Operation::CreateTable {
10944 name: "fk_drift_sources".to_string(),
10945 columns: vec![
10946 super::super::ColumnDefinition {
10947 name: "id".to_string(),
10948 type_definition: super::super::FieldType::BigInteger,
10949 not_null: true,
10950 unique: false,
10951 primary_key: true,
10952 auto_increment: true,
10953 default: None,
10954 },
10955 super::super::ColumnDefinition {
10956 name: "target_id".to_string(),
10957 type_definition: super::super::FieldType::BigInteger,
10958 not_null: true,
10959 unique: false,
10960 primary_key: false,
10961 auto_increment: false,
10962 default: None,
10963 },
10964 ],
10965 constraints: vec![],
10966 without_rowid: None,
10967 interleave_in_parent: None,
10968 partition: None,
10969 };
10970 let mut from_state = ProjectState::new();
10971 from_state.apply_migration_operations(&[create_sources], "fk_drift_source_app");
10972
10973 let mut source_metadata = super::super::model_registry::ModelMetadata::new(
10974 "fk_drift_source_app",
10975 "FkDriftSource",
10976 "fk_drift_sources",
10977 );
10978 source_metadata.add_field(
10979 "id".to_string(),
10980 super::super::model_registry::FieldMetadata::new(super::super::FieldType::BigInteger)
10981 .with_param("primary_key", "true")
10982 .with_param("auto_increment", "true")
10983 .with_param("not_null", "true")
10984 .with_nullable(false),
10985 );
10986 source_metadata.add_field(
10987 "target_id".to_string(),
10988 super::super::model_registry::FieldMetadata::new(super::super::FieldType::Uuid)
10989 .with_param("fk_target", "FkDriftTarget")
10990 .with_param("fk_target_app", "fk_drift_target_app")
10991 .with_param("not_null", "true")
10992 .with_nullable(false),
10993 );
10994 let to_state = build_project_state(vec![(
10995 (
10996 "fk_drift_source_app".to_string(),
10997 "FkDriftSource".to_string(),
10998 ),
10999 source_metadata.to_model_state(),
11000 )]);
11001 let detector = MigrationAutodetector::new(from_state, to_state);
11002
11003 let operations = detector.generate_operations();
11005
11006 assert!(
11008 !operations.iter().any(|op| matches!(
11009 op,
11010 super::super::Operation::AlterColumn { column, .. } if column == "target_id"
11011 )),
11012 "unchanged FK _id column must not emit no-op AlterColumn, got: {:?}",
11013 operations
11014 );
11015 assert!(
11016 operations.is_empty(),
11017 "replayed FK column should be in sync with registry state, got: {:?}",
11018 operations
11019 );
11020 }
11021
11022 #[rstest]
11023 fn generate_operations_no_spurious_drift_for_replayed_auth_schema() {
11024 let create_auth_users = super::super::Operation::CreateTable {
11034 name: "auth_users".to_string(),
11035 columns: vec![
11036 super::super::ColumnDefinition {
11037 name: "id".to_string(),
11038 type_definition: super::super::FieldType::Uuid,
11039 not_null: true,
11040 unique: false,
11041 primary_key: true,
11042 auto_increment: false,
11043 default: None,
11044 },
11045 super::super::ColumnDefinition {
11046 name: "username".to_string(),
11047 type_definition: super::super::FieldType::VarChar(150),
11048 not_null: true,
11049 unique: true,
11050 primary_key: false,
11051 auto_increment: false,
11052 default: None,
11053 },
11054 super::super::ColumnDefinition {
11055 name: "email".to_string(),
11056 type_definition: super::super::FieldType::VarChar(254),
11057 not_null: true,
11058 unique: false,
11059 primary_key: false,
11060 auto_increment: false,
11061 default: None,
11062 },
11063 super::super::ColumnDefinition {
11064 name: "first_name".to_string(),
11065 type_definition: super::super::FieldType::VarChar(150),
11066 not_null: true,
11067 unique: false,
11068 primary_key: false,
11069 auto_increment: false,
11070 default: Some("''".to_string()),
11071 },
11072 super::super::ColumnDefinition {
11073 name: "last_name".to_string(),
11074 type_definition: super::super::FieldType::VarChar(150),
11075 not_null: true,
11076 unique: false,
11077 primary_key: false,
11078 auto_increment: false,
11079 default: Some("''".to_string()),
11080 },
11081 super::super::ColumnDefinition {
11082 name: "is_active".to_string(),
11083 type_definition: super::super::FieldType::Boolean,
11084 not_null: true,
11085 unique: false,
11086 primary_key: false,
11087 auto_increment: false,
11088 default: Some("true".to_string()),
11089 },
11090 super::super::ColumnDefinition {
11091 name: "is_staff".to_string(),
11092 type_definition: super::super::FieldType::Boolean,
11093 not_null: true,
11094 unique: false,
11095 primary_key: false,
11096 auto_increment: false,
11097 default: Some("false".to_string()),
11098 },
11099 super::super::ColumnDefinition {
11100 name: "is_superuser".to_string(),
11101 type_definition: super::super::FieldType::Boolean,
11102 not_null: true,
11103 unique: false,
11104 primary_key: false,
11105 auto_increment: false,
11106 default: Some("false".to_string()),
11107 },
11108 ],
11109 constraints: vec![super::super::operations::Constraint::Unique {
11110 name: "auth_user_username_uniq".to_string(),
11111 columns: vec!["username".to_string()],
11112 }],
11113 without_rowid: None,
11114 interleave_in_parent: None,
11115 partition: None,
11116 };
11117 let add_email_unique = super::super::Operation::AddConstraint {
11118 table: "auth_users".to_string(),
11119 constraint_sql: "CONSTRAINT auth_user_email_uniq UNIQUE (email)".to_string(),
11120 };
11121 let create_auth_permission = super::super::Operation::CreateTable {
11122 name: "auth_permission".to_string(),
11123 columns: vec![
11124 super::super::ColumnDefinition {
11125 name: "id".to_string(),
11126 type_definition: super::super::FieldType::Uuid,
11127 not_null: true,
11128 unique: false,
11129 primary_key: true,
11130 auto_increment: true,
11131 default: None,
11132 },
11133 super::super::ColumnDefinition {
11134 name: "name".to_string(),
11135 type_definition: super::super::FieldType::VarChar(255),
11136 not_null: true,
11137 unique: false,
11138 primary_key: false,
11139 auto_increment: false,
11140 default: None,
11141 },
11142 ],
11143 constraints: vec![],
11144 without_rowid: None,
11145 interleave_in_parent: None,
11146 partition: None,
11147 };
11148
11149 let mut from_state = ProjectState::new();
11150 from_state.apply_migration_operations(
11151 &[create_auth_users, add_email_unique, create_auth_permission],
11152 "auth",
11153 );
11154
11155 let mut user_metadata =
11156 super::super::model_registry::ModelMetadata::new("auth", "User", "auth_users");
11157 user_metadata.add_field(
11158 "id".to_string(),
11159 super::super::model_registry::FieldMetadata::new(super::super::FieldType::Uuid)
11160 .with_param("primary_key", "true")
11161 .with_param("not_null", "true")
11162 .with_nullable(false),
11163 );
11164 user_metadata.add_field(
11165 "username".to_string(),
11166 super::super::model_registry::FieldMetadata::new(super::super::FieldType::VarChar(150))
11167 .with_param("max_length", "150")
11168 .with_param("unique", "true")
11169 .with_param("not_null", "true")
11170 .with_nullable(false),
11171 );
11172 user_metadata.add_field(
11173 "email".to_string(),
11174 super::super::model_registry::FieldMetadata::new(super::super::FieldType::VarChar(254))
11175 .with_param("max_length", "254")
11176 .with_param("unique", "true")
11177 .with_param("not_null", "true")
11178 .with_nullable(false),
11179 );
11180 user_metadata.add_field(
11181 "first_name".to_string(),
11182 super::super::model_registry::FieldMetadata::new(super::super::FieldType::VarChar(150))
11183 .with_param("max_length", "150")
11184 .with_param("default", "''")
11185 .with_param("not_null", "true")
11186 .with_nullable(false),
11187 );
11188 user_metadata.add_field(
11189 "last_name".to_string(),
11190 super::super::model_registry::FieldMetadata::new(super::super::FieldType::VarChar(150))
11191 .with_param("max_length", "150")
11192 .with_param("default", "''")
11193 .with_param("not_null", "true")
11194 .with_nullable(false),
11195 );
11196 user_metadata.add_field(
11197 "is_active".to_string(),
11198 super::super::model_registry::FieldMetadata::new(super::super::FieldType::Boolean)
11199 .with_param("default", "true")
11200 .with_param("not_null", "true")
11201 .with_nullable(false),
11202 );
11203 user_metadata.add_field(
11204 "is_staff".to_string(),
11205 super::super::model_registry::FieldMetadata::new(super::super::FieldType::Boolean)
11206 .with_param("default", "false")
11207 .with_param("not_null", "true")
11208 .with_nullable(false),
11209 );
11210 user_metadata.add_field(
11211 "is_superuser".to_string(),
11212 super::super::model_registry::FieldMetadata::new(super::super::FieldType::Boolean)
11213 .with_param("default", "false")
11214 .with_param("not_null", "true")
11215 .with_nullable(false),
11216 );
11217
11218 let mut permission_metadata = super::super::model_registry::ModelMetadata::new(
11219 "auth",
11220 "AuthPermission",
11221 "auth_permission",
11222 );
11223 permission_metadata.add_field(
11224 "id".to_string(),
11225 super::super::model_registry::FieldMetadata::new(super::super::FieldType::Uuid)
11226 .with_param("primary_key", "true")
11227 .with_param("not_null", "true")
11228 .with_nullable(false),
11229 );
11230 permission_metadata.add_field(
11231 "name".to_string(),
11232 super::super::model_registry::FieldMetadata::new(super::super::FieldType::VarChar(255))
11233 .with_param("max_length", "255")
11234 .with_param("not_null", "true")
11235 .with_nullable(false),
11236 );
11237
11238 let to_state = build_project_state(vec![
11239 (
11240 ("auth".to_string(), "User".to_string()),
11241 user_metadata.to_model_state(),
11242 ),
11243 (
11244 ("auth".to_string(), "AuthPermission".to_string()),
11245 permission_metadata.to_model_state(),
11246 ),
11247 ]);
11248 let detector = MigrationAutodetector::new(from_state, to_state);
11249
11250 let operations = detector.generate_operations();
11252
11253 assert!(
11255 operations.is_empty(),
11256 "replayed auth schema should be in sync with registry state, got: {:?}",
11257 operations
11258 );
11259 }
11260
11261 #[rstest]
11262 fn generate_migrations_emits_add_constraint_for_added_unique_together() {
11263 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11276 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
11277 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
11278
11279 let mut from_model = build_model_state(
11283 "clusters",
11284 "Cluster",
11285 vec![id_field.clone(), org_field.clone(), name_field.clone()],
11286 Vec::new(),
11287 Vec::new(),
11288 );
11289 from_model.table_name = "clusters_cluster".to_string();
11290
11291 let unique_constraint = ConstraintDefinition {
11294 name: "clusters_cluster_organization_id_name_uniq".to_string(),
11295 constraint_type: "unique".to_string(),
11296 fields: vec!["organization_id".to_string(), "name".to_string()],
11297 expression: None,
11298 foreign_key_info: None,
11299 };
11300 let mut to_model = build_model_state(
11301 "clusters",
11302 "Cluster",
11303 vec![id_field, org_field, name_field],
11304 Vec::new(),
11305 vec![unique_constraint],
11306 );
11307 to_model.table_name = "clusters_cluster".to_string();
11308
11309 let from_state = build_project_state(vec![(
11310 ("clusters".to_string(), "Cluster".to_string()),
11311 from_model,
11312 )]);
11313 let to_state = build_project_state(vec![(
11314 ("clusters".to_string(), "Cluster".to_string()),
11315 to_model,
11316 )]);
11317 let detector = MigrationAutodetector::new(from_state, to_state);
11318
11319 let migrations = detector.generate_migrations();
11321
11322 assert_eq!(
11325 migrations.len(),
11326 1,
11327 "expected exactly one Migration, got: {:?}",
11328 migrations
11329 );
11330 assert_eq!(migrations[0].app_label, "clusters");
11331 assert_eq!(
11332 migrations[0].operations.len(),
11333 1,
11334 "expected exactly one operation in the migration, got: {:?}",
11335 migrations[0].operations
11336 );
11337 let super::super::Operation::AddConstraint {
11338 table,
11339 constraint_sql,
11340 } = &migrations[0].operations[0]
11341 else {
11342 panic!(
11343 "expected Operation::AddConstraint, got: {:?}",
11344 migrations[0].operations[0]
11345 );
11346 };
11347 assert_eq!(table, "clusters_cluster");
11348 assert!(
11349 constraint_sql.contains("clusters_cluster_organization_id_name_uniq"),
11350 "constraint SQL should carry the constraint name, got: {}",
11351 constraint_sql
11352 );
11353 }
11354
11355 #[rstest]
11356 fn generate_migrations_emits_drop_constraint_for_removed_unique_together() {
11357 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11362 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
11363 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
11364
11365 let unique_constraint = ConstraintDefinition {
11366 name: "clusters_cluster_organization_id_name_uniq".to_string(),
11367 constraint_type: "unique".to_string(),
11368 fields: vec!["organization_id".to_string(), "name".to_string()],
11369 expression: None,
11370 foreign_key_info: None,
11371 };
11372 let mut from_model = build_model_state(
11373 "clusters",
11374 "Cluster",
11375 vec![id_field.clone(), org_field.clone(), name_field.clone()],
11376 Vec::new(),
11377 vec![unique_constraint],
11378 );
11379 from_model.table_name = "clusters_cluster".to_string();
11380
11381 let mut to_model = build_model_state(
11382 "clusters",
11383 "Cluster",
11384 vec![id_field, org_field, name_field],
11385 Vec::new(),
11386 Vec::new(),
11387 );
11388 to_model.table_name = "clusters_cluster".to_string();
11389
11390 let from_state = build_project_state(vec![(
11391 ("clusters".to_string(), "Cluster".to_string()),
11392 from_model,
11393 )]);
11394 let to_state = build_project_state(vec![(
11395 ("clusters".to_string(), "Cluster".to_string()),
11396 to_model,
11397 )]);
11398 let detector = MigrationAutodetector::new(from_state, to_state);
11399
11400 let migrations = detector.generate_migrations();
11402
11403 assert_eq!(
11405 migrations.len(),
11406 1,
11407 "expected exactly one Migration, got: {:?}",
11408 migrations
11409 );
11410 assert_eq!(migrations[0].app_label, "clusters");
11411 assert_eq!(
11412 migrations[0].operations.len(),
11413 1,
11414 "expected exactly one operation in the migration, got: {:?}",
11415 migrations[0].operations
11416 );
11417 let super::super::Operation::DropConstraint {
11418 table,
11419 constraint_name,
11420 } = &migrations[0].operations[0]
11421 else {
11422 panic!(
11423 "expected Operation::DropConstraint, got: {:?}",
11424 migrations[0].operations[0]
11425 );
11426 };
11427 assert_eq!(table, "clusters_cluster");
11428 assert_eq!(
11429 constraint_name,
11430 "clusters_cluster_organization_id_name_uniq"
11431 );
11432 }
11433
11434 #[rstest]
11435 fn shared_per_app_emissions_are_consistent_between_generate_paths() {
11436 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11452 let org_field = FieldState::new("organization_id", super::super::FieldType::Integer, false);
11453 let name_field = FieldState::new("name", super::super::FieldType::VarChar(255), false);
11454 let new_col = FieldState::new("region", super::super::FieldType::VarChar(64), false);
11455
11456 let mut from_model = build_model_state(
11457 "clusters",
11458 "Cluster",
11459 vec![id_field.clone(), org_field.clone(), name_field.clone()],
11460 Vec::new(),
11461 Vec::new(),
11462 );
11463 from_model.table_name = "clusters_cluster".to_string();
11464
11465 let unique_constraint = ConstraintDefinition {
11466 name: "clusters_cluster_organization_id_name_uniq".to_string(),
11467 constraint_type: "unique".to_string(),
11468 fields: vec!["organization_id".to_string(), "name".to_string()],
11469 expression: None,
11470 foreign_key_info: None,
11471 };
11472 let mut to_model = build_model_state(
11473 "clusters",
11474 "Cluster",
11475 vec![id_field, org_field, name_field, new_col],
11476 Vec::new(),
11477 vec![unique_constraint],
11478 );
11479 to_model.table_name = "clusters_cluster".to_string();
11480
11481 let from_state = build_project_state(vec![(
11482 ("clusters".to_string(), "Cluster".to_string()),
11483 from_model,
11484 )]);
11485 let to_state = build_project_state(vec![(
11486 ("clusters".to_string(), "Cluster".to_string()),
11487 to_model,
11488 )]);
11489 let detector = MigrationAutodetector::new(from_state, to_state);
11490
11491 let ops = detector.generate_operations();
11493 let migrations = detector.generate_migrations();
11494
11495 let mig_ops: Vec<&super::super::Operation> = migrations
11500 .iter()
11501 .flat_map(|m| m.operations.iter())
11502 .collect();
11503
11504 assert_eq!(
11505 ops.len(),
11506 mig_ops.len(),
11507 "shared per-app emissions diverged between generate_operations() ({:?}) and generate_migrations() ({:?})",
11508 ops,
11509 mig_ops
11510 );
11511 for op in &ops {
11514 assert!(
11515 mig_ops.iter().any(|m| *m == op),
11516 "generate_operations() produced {:?} but generate_migrations() did not",
11517 op
11518 );
11519 }
11520 for op in &mig_ops {
11522 assert!(
11523 ops.iter().any(|o| o == *op),
11524 "generate_migrations() produced {:?} but generate_operations() did not",
11525 op
11526 );
11527 }
11528 }
11529
11530 #[rstest]
11531 fn detect_added_composite_pk_does_not_double_emit_add_constraint() {
11532 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11537 let tenant_field = FieldState::new("tenant_id", super::super::FieldType::Integer, false);
11538
11539 let from_model = build_model_state(
11540 "billing",
11541 "Invoice",
11542 vec![id_field.clone(), tenant_field.clone()],
11543 Vec::new(),
11544 Vec::new(),
11545 );
11546 let composite_pk = ConstraintDefinition {
11547 name: "billing_invoice_pkey".to_string(),
11548 constraint_type: "primary_key".to_string(),
11549 fields: vec!["id".to_string(), "tenant_id".to_string()],
11550 expression: None,
11551 foreign_key_info: None,
11552 };
11553 let to_model = build_model_state(
11554 "billing",
11555 "Invoice",
11556 vec![id_field, tenant_field],
11557 Vec::new(),
11558 vec![composite_pk],
11559 );
11560 let from_state = build_project_state(vec![(
11561 ("billing".to_string(), "Invoice".to_string()),
11562 from_model,
11563 )]);
11564 let to_state = build_project_state(vec![(
11565 ("billing".to_string(), "Invoice".to_string()),
11566 to_model,
11567 )]);
11568 let detector = MigrationAutodetector::new(from_state, to_state);
11569
11570 let operations = detector.generate_operations();
11572
11573 assert_eq!(operations.len(), 1, "got: {:?}", operations);
11576 assert!(
11577 matches!(
11578 &operations[0],
11579 super::super::Operation::CreateCompositePrimaryKey { columns, .. }
11580 if columns == &["id".to_string(), "tenant_id".to_string()]
11581 ),
11582 "expected only CreateCompositePrimaryKey, got: {:?}",
11583 operations
11584 );
11585 }
11586
11587 #[rstest]
11595 fn inline_unique_param_on_from_side_does_not_emit_redundant_add_constraint() {
11596 let mut username_field =
11600 FieldState::new("username", super::super::FieldType::VarChar(150), false);
11601 username_field
11602 .params
11603 .insert("unique".to_string(), "true".to_string());
11604 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11605 let from_model = build_model_state(
11606 "users",
11607 "User",
11608 vec![id_field.clone(), username_field.clone()],
11609 Vec::new(),
11610 Vec::new(),
11611 );
11612
11613 let synthesised = ConstraintDefinition {
11618 name: "users_username_uniq".to_string(),
11619 constraint_type: "unique".to_string(),
11620 fields: vec!["username".to_string()],
11621 expression: None,
11622 foreign_key_info: None,
11623 };
11624 let to_model = build_model_state(
11625 "users",
11626 "User",
11627 vec![
11628 id_field,
11629 FieldState::new("username", super::super::FieldType::VarChar(150), false),
11630 ],
11631 Vec::new(),
11632 vec![synthesised],
11633 );
11634
11635 let from_state = build_project_state(vec![(
11636 ("users".to_string(), "User".to_string()),
11637 from_model,
11638 )]);
11639 let to_state =
11640 build_project_state(vec![(("users".to_string(), "User".to_string()), to_model)]);
11641 let detector = MigrationAutodetector::new(from_state, to_state);
11642
11643 let operations = detector.generate_operations();
11645
11646 assert!(
11649 operations
11650 .iter()
11651 .all(|op| !matches!(op, super::super::Operation::AddConstraint { .. })),
11652 "expected NO Operation::AddConstraint, got: {:?}",
11653 operations
11654 );
11655 }
11656
11657 #[test]
11658 fn legacy_inline_and_named_unique_removal_changes_the_field_definition() {
11659 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11661 let mut legacy_username =
11662 FieldState::new("username", super::super::FieldType::VarChar(150), false);
11663 legacy_username
11664 .params
11665 .insert("unique".to_string(), "true".to_string());
11666 let named_constraint = ConstraintDefinition {
11667 name: "users_username_uniq".to_string(),
11668 constraint_type: "unique".to_string(),
11669 fields: vec!["username".to_string()],
11670 expression: None,
11671 foreign_key_info: None,
11672 };
11673 let from_model = build_model_state(
11674 "users",
11675 "User",
11676 vec![id_field.clone(), legacy_username],
11677 Vec::new(),
11678 vec![named_constraint],
11679 );
11680 let to_model = build_model_state(
11681 "users",
11682 "User",
11683 vec![
11684 id_field,
11685 FieldState::new("username", super::super::FieldType::VarChar(150), false),
11686 ],
11687 Vec::new(),
11688 Vec::new(),
11689 );
11690 let detector = MigrationAutodetector::new(
11691 build_project_state(vec![(
11692 ("users".to_string(), "User".to_string()),
11693 from_model,
11694 )]),
11695 build_project_state(vec![(("users".to_string(), "User".to_string()), to_model)]),
11696 );
11697
11698 let operations = detector.generate_operations();
11700
11701 assert!(operations.iter().any(|operation| {
11703 matches!(
11704 operation,
11705 super::super::Operation::AlterColumn {
11706 new_definition,
11707 column,
11708 ..
11709 } if column == "username" && !new_definition.unique
11710 )
11711 }));
11712 assert!(operations.iter().any(|operation| {
11713 matches!(
11714 operation,
11715 super::super::Operation::DropConstraint { constraint_name, .. }
11716 if constraint_name == "users_username_uniq"
11717 )
11718 }));
11719 }
11720
11721 #[test]
11722 fn constraint_definition_parser_handles_quoted_unique_identifiers() {
11723 let sql = "CONSTRAINT uq_profile UNIQUE (\"profile,id\", \"name)\", \"display\"\"name\")";
11725
11726 let constraint = ProjectState::constraint_definition_from_sql(sql)
11728 .expect("quoted UNIQUE columns should parse");
11729
11730 assert_eq!(constraint.name, "uq_profile");
11732 assert_eq!(
11733 constraint.fields,
11734 vec!["profile,id", "name)", "display\"name"]
11735 );
11736 }
11737
11738 #[test]
11739 fn removed_unique_constraint_precedes_removed_column_without_alter_column() {
11740 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11741 let username_field =
11742 FieldState::new("username", super::super::FieldType::VarChar(150), false);
11743 let unique_constraint = ConstraintDefinition {
11744 name: "users_username_uniq".to_string(),
11745 constraint_type: "unique".to_string(),
11746 fields: vec!["username".to_string()],
11747 expression: None,
11748 foreign_key_info: None,
11749 };
11750 let from_model = build_model_state(
11751 "users",
11752 "User",
11753 vec![id_field.clone(), username_field],
11754 Vec::new(),
11755 vec![unique_constraint],
11756 );
11757 let to_model = build_model_state("users", "User", vec![id_field], Vec::new(), Vec::new());
11758 let detector = MigrationAutodetector::new(
11759 build_project_state(vec![(
11760 ("users".to_string(), "User".to_string()),
11761 from_model,
11762 )]),
11763 build_project_state(vec![(("users".to_string(), "User".to_string()), to_model)]),
11764 );
11765
11766 let operations = detector.generate_operations();
11767 assert_eq!(operations.len(), 2, "unexpected operations: {operations:?}");
11768 assert!(matches!(
11769 &operations[0],
11770 super::super::Operation::DropConstraint { constraint_name, .. }
11771 if constraint_name == "users_username_uniq"
11772 ));
11773 assert!(matches!(
11774 &operations[1],
11775 super::super::Operation::DropColumn { column, .. } if column == "username"
11776 ));
11777 assert!(
11778 operations
11779 .iter()
11780 .all(|operation| !matches!(operation, super::super::Operation::AlterColumn { .. }))
11781 );
11782 }
11783
11784 #[rstest]
11792 fn single_field_unique_constraint_renames_do_not_emit_redundant_add_constraint() {
11793 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11795 let username_field =
11796 FieldState::new("username", super::super::FieldType::VarChar(150), false);
11797 let auto_named = ConstraintDefinition {
11798 name: "sqlite_autoindex_users_1".to_string(),
11799 constraint_type: "unique".to_string(),
11800 fields: vec!["username".to_string()],
11801 expression: None,
11802 foreign_key_info: None,
11803 };
11804 let model_named = ConstraintDefinition {
11805 name: "users_username_uniq".to_string(),
11806 constraint_type: "unique".to_string(),
11807 fields: vec!["username".to_string()],
11808 expression: None,
11809 foreign_key_info: None,
11810 };
11811 let from_model = build_model_state(
11812 "users",
11813 "User",
11814 vec![id_field.clone(), username_field.clone()],
11815 Vec::new(),
11816 vec![auto_named],
11817 );
11818 let to_model = build_model_state(
11819 "users",
11820 "User",
11821 vec![id_field, username_field],
11822 Vec::new(),
11823 vec![model_named],
11824 );
11825 let from_state = build_project_state(vec![(
11826 ("users".to_string(), "User".to_string()),
11827 from_model,
11828 )]);
11829 let to_state =
11830 build_project_state(vec![(("users".to_string(), "User".to_string()), to_model)]);
11831 let detector = MigrationAutodetector::new(from_state, to_state);
11832
11833 let operations = detector.generate_operations();
11835
11836 let constraint_ops: Vec<_> = operations
11842 .iter()
11843 .filter(|op| {
11844 matches!(
11845 op,
11846 super::super::Operation::AddConstraint { .. }
11847 | super::super::Operation::DropConstraint { .. }
11848 )
11849 })
11850 .collect();
11851 assert!(
11852 constraint_ops.is_empty(),
11853 "expected no Add/DropConstraint ops, got: {:?}",
11854 constraint_ops
11855 );
11856 }
11857
11858 #[rstest]
11865 fn from_side_unique_constraint_matched_by_inline_unique_on_to_side_emits_no_drop() {
11866 let id_field = FieldState::new("id", super::super::FieldType::Integer, false);
11868 let mut username_field =
11869 FieldState::new("username", super::super::FieldType::VarChar(150), false);
11870 username_field
11871 .params
11872 .insert("unique".to_string(), "true".to_string());
11873 let unique_constraint = ConstraintDefinition {
11874 name: "users_username_uniq".to_string(),
11875 constraint_type: "unique".to_string(),
11876 fields: vec!["username".to_string()],
11877 expression: None,
11878 foreign_key_info: None,
11879 };
11880 let bare_username =
11882 FieldState::new("username", super::super::FieldType::VarChar(150), false);
11883 let from_model = build_model_state(
11884 "users",
11885 "User",
11886 vec![id_field.clone(), bare_username],
11887 Vec::new(),
11888 vec![unique_constraint],
11889 );
11890 let to_model = build_model_state(
11892 "users",
11893 "User",
11894 vec![id_field, username_field],
11895 Vec::new(),
11896 Vec::new(),
11897 );
11898 let from_state = build_project_state(vec![(
11899 ("users".to_string(), "User".to_string()),
11900 from_model,
11901 )]);
11902 let to_state =
11903 build_project_state(vec![(("users".to_string(), "User".to_string()), to_model)]);
11904 let detector = MigrationAutodetector::new(from_state, to_state);
11905
11906 let operations = detector.generate_operations();
11908
11909 assert!(
11912 operations
11913 .iter()
11914 .all(|op| !matches!(op, super::super::Operation::DropConstraint { .. })),
11915 "expected NO Operation::DropConstraint, got: {:?}",
11916 operations
11917 );
11918 }
11919
11920 #[rstest]
11929 fn dedup_pass_drops_add_constraint_redundant_with_unique_add_column() {
11930 let ops = vec![
11932 super::super::Operation::AddColumn {
11933 table: "users".to_string(),
11934 column: super::super::ColumnDefinition {
11935 name: "username".to_string(),
11936 type_definition: super::super::FieldType::VarChar(150),
11937 not_null: true,
11938 unique: true,
11939 primary_key: false,
11940 auto_increment: false,
11941 default: None,
11942 },
11943 mysql_options: None,
11944 },
11945 super::super::Operation::AddConstraint {
11946 table: "users".to_string(),
11947 constraint_sql: "CONSTRAINT users_username_uniq UNIQUE (username)".to_string(),
11948 },
11949 ];
11950 let mut by_app: std::collections::BTreeMap<String, Vec<super::super::Operation>> =
11951 std::collections::BTreeMap::new();
11952 by_app.insert("users".to_string(), ops);
11953
11954 MigrationAutodetector::dedup_redundant_unique_add_constraints(&mut by_app);
11956
11957 let remaining = &by_app["users"];
11959 assert_eq!(
11960 remaining.len(),
11961 1,
11962 "expected one operation after dedup, got: {:?}",
11963 remaining
11964 );
11965 assert!(
11966 matches!(remaining[0], super::super::Operation::AddColumn { .. }),
11967 "expected the surviving op to be AddColumn, got: {:?}",
11968 remaining[0]
11969 );
11970 }
11971
11972 fn integer_id_field() -> FieldState {
11973 FieldState::new("id", super::super::FieldType::Integer, false)
11974 }
11975
11976 fn integer_fk_field(name: &str, referenced_table: &str) -> FieldState {
11977 FieldState::with_foreign_key(
11978 name,
11979 super::super::FieldType::Integer,
11980 false,
11981 ForeignKeyInfo {
11982 referenced_table: referenced_table.to_string(),
11983 referenced_column: "id".to_string(),
11984 on_delete: ForeignKeyAction::Cascade,
11985 on_update: ForeignKeyAction::Cascade,
11986 },
11987 )
11988 }
11989
11990 fn model_with_table(
11991 app_label: &str,
11992 name: &str,
11993 table_name: &str,
11994 fields: Vec<FieldState>,
11995 ) -> ModelState {
11996 let mut model = ModelState::new(app_label, name);
11997 model.table_name = table_name.to_string();
11998 for field in fields {
11999 let field_name = field.name.clone();
12000 model.add_field(field);
12001 if model
12002 .fields
12003 .get(&field_name)
12004 .and_then(|field| field.foreign_key.as_ref())
12005 .is_some()
12006 {
12007 model.add_foreign_key_constraint_from_field(&field_name);
12008 }
12009 }
12010 model
12011 }
12012
12013 fn create_table_names(migration: &super::super::Migration) -> Vec<String> {
12014 migration
12015 .operations
12016 .iter()
12017 .filter_map(|operation| match operation {
12018 super::super::Operation::CreateTable { name, .. } => Some(name.clone()),
12019 _ => None,
12020 })
12021 .collect()
12022 }
12023
12024 #[rstest]
12025 fn detect_model_dependencies_reads_scalar_foreign_key_metadata() {
12026 let mut to_state = ProjectState::new();
12029 to_state.add_model(model_with_table(
12030 "auth",
12031 "User",
12032 "auth_users",
12033 vec![integer_id_field()],
12034 ));
12035 to_state.add_model(model_with_table(
12036 "auth",
12037 "ApiKey",
12038 "auth_api_keys",
12039 vec![
12040 integer_id_field(),
12041 integer_fk_field("user_id", "auth_users"),
12042 ],
12043 ));
12044 let detector = MigrationAutodetector::new(ProjectState::new(), to_state);
12045
12046 let changes = detector.detect_changes();
12048
12049 let deps = changes
12051 .model_dependencies
12052 .get(&("auth".to_string(), "ApiKey".to_string()))
12053 .expect("ApiKey must record a dependency on User");
12054 assert_eq!(
12055 deps,
12056 &vec![("auth".to_string(), "User".to_string())],
12057 "scalar Integer FK columns must contribute model_dependencies"
12058 );
12059 }
12060
12061 #[rstest]
12062 fn generate_migrations_orders_same_app_create_tables_by_foreign_key() {
12063 let mut to_state = ProjectState::new();
12065 to_state.add_model(model_with_table(
12066 "auth",
12067 "ApiKey",
12068 "auth_api_keys",
12069 vec![
12070 integer_id_field(),
12071 integer_fk_field("user_id", "auth_users"),
12072 ],
12073 ));
12074 to_state.add_model(model_with_table(
12075 "auth",
12076 "User",
12077 "auth_users",
12078 vec![integer_id_field()],
12079 ));
12080 let detector = MigrationAutodetector::new(ProjectState::new(), to_state);
12081
12082 let migrations = detector.generate_migrations();
12084 let operations = detector.generate_operations();
12085
12086 let auth = migrations
12088 .iter()
12089 .find(|migration| migration.app_label == "auth")
12090 .expect("auth migration");
12091 let table_names = create_table_names(auth);
12092 let users = table_names
12093 .iter()
12094 .position(|name| name == "auth_users")
12095 .expect("auth_users CreateTable");
12096 let api_keys = table_names
12097 .iter()
12098 .position(|name| name == "auth_api_keys")
12099 .expect("auth_api_keys CreateTable");
12100 assert!(
12101 users < api_keys,
12102 "auth_users must be created before auth_api_keys, got {table_names:?}"
12103 );
12104
12105 let operation_tables: Vec<String> = operations
12106 .iter()
12107 .filter_map(|operation| match operation {
12108 super::super::Operation::CreateTable { name, .. } => Some(name.clone()),
12109 _ => None,
12110 })
12111 .collect();
12112 let users = operation_tables
12113 .iter()
12114 .position(|name| name == "auth_users")
12115 .expect("auth_users in generate_operations");
12116 let api_keys = operation_tables
12117 .iter()
12118 .position(|name| name == "auth_api_keys")
12119 .expect("auth_api_keys in generate_operations");
12120 assert!(
12121 users < api_keys,
12122 "generate_operations must also emit auth_users before auth_api_keys, got {operation_tables:?}"
12123 );
12124 }
12125
12126 #[rstest]
12127 fn generate_migrations_records_cross_app_foreign_key_graph() {
12128 let mut to_state = ProjectState::new();
12130 to_state.add_model(model_with_table(
12131 "auth",
12132 "User",
12133 "auth_users",
12134 vec![integer_id_field()],
12135 ));
12136 to_state.add_model(model_with_table(
12137 "organizations",
12138 "Organization",
12139 "organizations",
12140 vec![
12141 integer_id_field(),
12142 integer_fk_field("owner_id", "auth_users"),
12143 ],
12144 ));
12145 to_state.add_model(model_with_table(
12146 "clusters",
12147 "Cluster",
12148 "clusters",
12149 vec![
12150 integer_id_field(),
12151 integer_fk_field("organization_id", "organizations"),
12152 ],
12153 ));
12154 to_state.add_model(model_with_table(
12155 "deployments",
12156 "Deployment",
12157 "deployments",
12158 vec![
12159 integer_id_field(),
12160 integer_fk_field("organization_id", "organizations"),
12161 integer_fk_field("cluster_id", "clusters"),
12162 ],
12163 ));
12164 to_state.add_model(model_with_table(
12165 "github",
12166 "Project",
12167 "github_projects",
12168 vec![
12169 integer_id_field(),
12170 integer_fk_field("organization_id", "organizations"),
12171 integer_fk_field("deployment_id", "deployments"),
12172 ],
12173 ));
12174 let detector = MigrationAutodetector::new(ProjectState::new(), to_state.clone());
12175
12176 let changes = detector.detect_changes();
12178 let migrations = detector.generate_migrations();
12179 let github_ops = migrations
12180 .iter()
12181 .find(|migration| migration.app_label == "github")
12182 .expect("github migration")
12183 .operations
12184 .as_slice();
12185 let providers =
12186 MigrationAutodetector::foreign_key_provider_apps(&to_state, github_ops, "github");
12187 let second_pass =
12188 MigrationAutodetector::new(to_state.clone(), to_state).generate_migrations();
12189
12190 assert_eq!(
12192 changes
12193 .model_dependencies
12194 .get(&("organizations".to_string(), "Organization".to_string()))
12195 .expect("organizations depends on auth"),
12196 &vec![("auth".to_string(), "User".to_string())]
12197 );
12198 let cluster_deps = changes
12199 .model_dependencies
12200 .get(&("clusters".to_string(), "Cluster".to_string()))
12201 .expect("clusters depends on organizations");
12202 assert_eq!(
12203 cluster_deps,
12204 &vec![("organizations".to_string(), "Organization".to_string())]
12205 );
12206 let deployment_deps = changes
12207 .model_dependencies
12208 .get(&("deployments".to_string(), "Deployment".to_string()))
12209 .expect("deployments depends on organizations and clusters");
12210 assert!(
12211 deployment_deps.contains(&("organizations".to_string(), "Organization".to_string()))
12212 );
12213 assert!(deployment_deps.contains(&("clusters".to_string(), "Cluster".to_string())));
12214 assert_eq!(
12215 providers,
12216 vec!["deployments".to_string(), "organizations".to_string()]
12217 );
12218 assert!(
12219 second_pass.is_empty(),
12220 "second autodetect against the same state must not emit extra operations, got {second_pass:?}"
12221 );
12222 }
12223
12224 #[rstest]
12225 fn generate_migrations_survives_circular_foreign_keys() {
12226 let mut to_state = ProjectState::new();
12229 to_state.add_model(model_with_table(
12230 "cycles",
12231 "Alpha",
12232 "cycles_alpha",
12233 vec![
12234 integer_id_field(),
12235 integer_fk_field("beta_id", "cycles_beta"),
12236 ],
12237 ));
12238 to_state.add_model(model_with_table(
12239 "cycles",
12240 "Beta",
12241 "cycles_beta",
12242 vec![
12243 integer_id_field(),
12244 integer_fk_field("alpha_id", "cycles_alpha"),
12245 ],
12246 ));
12247 let detector = MigrationAutodetector::new(ProjectState::new(), to_state);
12248
12249 let migrations = detector.generate_migrations();
12251
12252 let cycle_migration = migrations
12254 .iter()
12255 .find(|migration| migration.app_label == "cycles")
12256 .expect("cycles migration");
12257 let tables = create_table_names(cycle_migration);
12258 assert!(tables.contains(&"cycles_alpha".to_string()));
12259 assert!(tables.contains(&"cycles_beta".to_string()));
12260 }
12261}