1use super::ConstraintDefinition;
15use super::autodetector::{
16 FieldState, IndexDefinition, ModelState, default_index_name, index_definitions_equivalent,
17};
18use std::collections::{HashMap, HashSet};
19use std::sync::{Arc, RwLock};
20
21#[cfg_attr(doc, aquamarine::aquamarine)]
22#[derive(Debug, Clone)]
48pub struct ModelMetadata {
49 pub app_label: String,
51 pub model_name: String,
53 pub table_name: String,
55 pub fields: HashMap<String, FieldMetadata>,
57 pub options: HashMap<String, String>,
59 pub many_to_many_fields: Vec<ManyToManyMetadata>,
61 constraints: Vec<ConstraintDefinition>,
70 indexes: Vec<IndexDefinition>,
76}
77
78impl ModelMetadata {
79 const MAX_CONSTRAINT_IDENTIFIER_BYTES: usize = 63;
80
81 pub fn new(
83 app_label: impl Into<String>,
84 model_name: impl Into<String>,
85 table_name: impl Into<String>,
86 ) -> Self {
87 Self {
88 app_label: app_label.into(),
89 model_name: model_name.into(),
90 table_name: table_name.into(),
91 fields: HashMap::new(),
92 options: HashMap::new(),
93 many_to_many_fields: Vec::new(),
94 constraints: Vec::new(),
95 indexes: Vec::new(),
96 }
97 }
98
99 pub fn add_field(&mut self, name: String, field: FieldMetadata) {
101 self.fields.insert(name, field);
102 }
103
104 pub fn set_option(&mut self, key: String, value: String) {
106 self.options.insert(key, value);
107 }
108
109 pub fn add_many_to_many(&mut self, m2m: ManyToManyMetadata) {
111 self.many_to_many_fields.push(m2m);
112 }
113
114 pub fn add_constraint(&mut self, constraint: ConstraintDefinition) {
117 self.constraints.push(constraint);
118 }
119
120 fn synthesized_unique_constraint_name(
121 &self,
122 field_name: &str,
123 generated_names: &HashSet<String>,
124 existing_constraints: &[ConstraintDefinition],
125 ) -> String {
126 let tuple_digest =
130 stable_constraint_name_hash(&format!("{}\0{}", self.table_name, field_name));
131 let base_name = bounded_constraint_identifier(&format!(
132 "{}_{}_uniq_{tuple_digest:08x}",
133 safe_constraint_table_fragment(&self.table_name),
134 safe_constraint_name_fragment(field_name)
135 ));
136 let is_taken = |candidate: &str| {
137 self.constraints
138 .iter()
139 .any(|constraint| constraint.name.eq_ignore_ascii_case(candidate))
140 || existing_constraints
141 .iter()
142 .any(|constraint| constraint.name.eq_ignore_ascii_case(candidate))
143 || generated_names
144 .iter()
145 .any(|name| name.eq_ignore_ascii_case(candidate))
146 };
147 if !is_taken(&base_name) {
148 return base_name;
149 }
150
151 let field_digest = stable_constraint_name_hash(field_name);
152 let mut candidate =
153 bounded_constraint_identifier(&format!("{base_name}_field_{field_digest:08x}"));
154 let mut suffix = 2;
155 while is_taken(&candidate) {
156 candidate = bounded_constraint_identifier(&format!(
157 "{base_name}_field_{field_digest:08x}_{suffix}"
158 ));
159 suffix += 1;
160 }
161 candidate
162 }
163
164 pub fn constraints(&self) -> &[ConstraintDefinition] {
170 &self.constraints
171 }
172
173 pub fn add_index(&mut self, index: IndexDefinition) {
175 self.indexes.push(index);
176 }
177
178 pub fn indexes(&self) -> &[IndexDefinition] {
180 &self.indexes
181 }
182
183 pub fn to_model_state(&self) -> ModelState {
203 let mut model_state = ModelState::new(&self.app_label, &self.model_name);
204
205 model_state.table_name = self.table_name.clone();
208
209 for (name, field_meta) in &self.fields {
211 let is_unique = field_meta.params.get("unique").map(String::as_str) == Some("true");
212 let mut field_state = FieldState::new(
213 name.clone(),
214 field_meta.field_type.clone(),
215 field_meta.nullable,
216 );
217 for (key, value) in &field_meta.params {
218 if key == "null" || (is_unique && key == "unique") {
219 continue;
220 }
221 field_state.params.insert(key.clone(), value.clone());
222 }
223 if let Some(ref fk_info) = field_meta.foreign_key {
225 field_state.foreign_key = Some(fk_info.clone());
226 }
227 model_state.add_field(field_state);
228 }
229
230 model_state.options = self.options.clone();
232
233 for (field_name, field_meta) in &self.fields {
235 if field_meta.foreign_key.is_some() {
236 model_state.add_foreign_key_constraint_from_field(field_name);
237 }
238 }
239
240 model_state.many_to_many_fields = self.many_to_many_fields.clone();
242
243 model_state.indexes.extend(self.indexes.iter().cloned());
246
247 let mut synthesized_indexes = self
250 .fields
251 .iter()
252 .filter_map(|(field_name, field_meta)| {
253 let has_default_index =
254 field_meta.params.get("db_index").map(String::as_str) == Some("true");
255 let is_unique = field_meta.params.get("unique").map(String::as_str) == Some("true")
256 || field_meta.params.get("primary_key").map(String::as_str) == Some("true");
257 if !has_default_index || is_unique {
258 return None;
259 }
260
261 Some(IndexDefinition {
262 name: default_index_name(&self.table_name, std::slice::from_ref(field_name)),
263 fields: vec![field_name.clone()],
264 unique: false,
265 where_clause: None,
266 index_type: None,
267 expressions: None,
268 concurrently: false,
269 mysql_options: None,
270 operator_class: None,
271 })
272 })
273 .collect::<Vec<_>>();
274 synthesized_indexes.sort_by(|left, right| left.name.cmp(&right.name));
275 for index in synthesized_indexes {
276 if !model_state
277 .indexes
278 .iter()
279 .any(|existing| index_definitions_equivalent(existing, &index))
280 {
281 model_state.indexes.push(index);
282 }
283 }
284
285 let mut generated_unique_constraint_names = HashSet::new();
289 let mut unique_fields = self
290 .fields
291 .iter()
292 .filter(|(_, field_meta)| {
293 field_meta.params.get("unique").map(String::as_str) == Some("true")
294 })
295 .collect::<Vec<_>>();
296 unique_fields.sort_unstable_by_key(|(left, _)| *left);
297 for (field_name, field_meta) in unique_fields {
298 if field_meta.params.get("unique").map(String::as_str) == Some("true") {
299 if self.constraints.iter().any(|constraint| {
303 constraint.constraint_type.eq_ignore_ascii_case("unique")
304 && constraint.fields.len() == 1
305 && constraint.fields[0] == *field_name
306 }) {
307 continue;
308 }
309 let constraint = ConstraintDefinition {
310 name: self.synthesized_unique_constraint_name(
311 field_name,
312 &generated_unique_constraint_names,
313 &model_state.constraints,
314 ),
315 constraint_type: "unique".to_string(),
316 fields: vec![field_name.clone()],
317 expression: None,
318 foreign_key_info: None,
319 };
320 generated_unique_constraint_names.insert(constraint.name.clone());
321 model_state.constraints.push(constraint);
322 }
323 }
324
325 model_state
329 .constraints
330 .extend(self.constraints.iter().cloned());
331
332 model_state
333 }
334}
335
336fn safe_constraint_name_fragment(value: &str) -> String {
337 let mut fragment = String::with_capacity(value.len());
338 for character in value.chars() {
339 if character.is_ascii_alphanumeric() || character == '_' {
340 fragment.push(character.to_ascii_lowercase());
341 } else {
342 fragment.push('_');
343 }
344 }
345
346 if fragment.is_empty() {
347 fragment.push_str("table");
348 } else if fragment
349 .as_bytes()
350 .first()
351 .is_some_and(|character| character.is_ascii_digit())
352 {
353 fragment.insert_str(0, "table_");
354 }
355 fragment
356}
357
358fn safe_constraint_table_fragment(value: &str) -> String {
359 let fragment = safe_constraint_name_fragment(value);
360 if fragment == value {
361 return fragment;
362 }
363 format!("{fragment}_{:08x}", stable_constraint_name_hash(value))
364}
365
366fn stable_constraint_name_hash(value: &str) -> u32 {
367 let mut hash = 0x811c9dc5_u32;
368 for byte in value.bytes() {
369 hash ^= u32::from(byte);
370 hash = hash.wrapping_mul(0x01000193);
371 }
372 hash
373}
374
375fn bounded_constraint_identifier(value: &str) -> String {
376 if value.len() <= ModelMetadata::MAX_CONSTRAINT_IDENTIFIER_BYTES {
377 return value.to_owned();
378 }
379
380 let suffix = format!("_{:08x}", stable_constraint_name_hash(value));
381 let prefix_len = ModelMetadata::MAX_CONSTRAINT_IDENTIFIER_BYTES - suffix.len();
382 let mut end = prefix_len;
383 while !value.is_char_boundary(end) {
384 end -= 1;
385 }
386 format!("{}{}", &value[..end], suffix)
387}
388
389#[derive(Debug, Clone)]
391pub struct FieldMetadata {
392 pub field_type: super::FieldType,
394 pub nullable: bool,
400 pub params: HashMap<String, String>,
402 pub foreign_key: Option<super::autodetector::ForeignKeyInfo>,
404}
405
406impl FieldMetadata {
407 pub fn new(field_type: super::FieldType) -> Self {
409 Self {
410 field_type,
411 nullable: false,
412 params: HashMap::new(),
413 foreign_key: None,
414 }
415 }
416
417 pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
424 let key_s: String = key.into();
425 let value_s: String = value.into();
426 if key_s == "null" {
427 let parsed = value_s.parse::<bool>().unwrap_or(false);
428 self.nullable = parsed;
429 self.params.insert(key_s, parsed.to_string());
430 return self;
431 }
432 self.params.insert(key_s, value_s);
433 self
434 }
435
436 pub fn with_nullable(mut self, nullable: bool) -> Self {
441 self.nullable = nullable;
442 self.params.insert("null".to_string(), nullable.to_string());
443 self
444 }
445
446 pub fn is_nullable(&self) -> bool {
448 self.nullable
449 }
450
451 pub fn with_foreign_key(mut self, foreign_key: super::autodetector::ForeignKeyInfo) -> Self {
453 self.foreign_key = Some(foreign_key);
454 self
455 }
456}
457
458#[derive(Debug, Clone)]
463pub struct RelationshipMetadata {
464 pub field_name: String,
466 pub rel_type: String,
468 pub to_model: Option<String>,
470 pub related_name: Option<String>,
472 pub through_table: Option<String>,
474 pub composite: Option<String>,
476 pub source_app_label: Option<String>,
478 pub source_model_name: Option<String>,
480}
481
482impl RelationshipMetadata {
483 pub fn new(field_name: impl Into<String>, rel_type: impl Into<String>) -> Self {
485 Self {
486 field_name: field_name.into(),
487 rel_type: rel_type.into(),
488 to_model: None,
489 related_name: None,
490 through_table: None,
491 composite: None,
492 source_app_label: None,
493 source_model_name: None,
494 }
495 }
496
497 pub fn with_to_model(mut self, to_model: impl Into<String>) -> Self {
499 self.to_model = Some(to_model.into());
500 self
501 }
502
503 pub fn with_related_name(mut self, related_name: impl Into<String>) -> Self {
505 self.related_name = Some(related_name.into());
506 self
507 }
508
509 pub fn with_through_table(mut self, through_table: impl Into<String>) -> Self {
511 self.through_table = Some(through_table.into());
512 self
513 }
514
515 pub fn with_composite(mut self, composite: impl Into<String>) -> Self {
517 self.composite = Some(composite.into());
518 self
519 }
520
521 pub fn with_source_info(
523 mut self,
524 app_label: impl Into<String>,
525 model_name: impl Into<String>,
526 ) -> Self {
527 self.source_app_label = Some(app_label.into());
528 self.source_model_name = Some(model_name.into());
529 self
530 }
531
532 pub fn is_many_to_many(&self) -> bool {
534 self.rel_type == "many_to_many" || self.rel_type == "polymorphic_many_to_many"
535 }
536}
537
538#[derive(Debug, Clone, PartialEq)]
543pub struct ManyToManyMetadata {
544 pub field_name: String,
546 pub to_model: String,
548 pub related_name: Option<String>,
550 pub through: Option<String>,
552 pub source_field: Option<String>,
554 pub target_field: Option<String>,
556 pub db_constraint_prefix: Option<String>,
558}
559
560impl ManyToManyMetadata {
561 pub fn new(field_name: impl Into<String>, to_model: impl Into<String>) -> Self {
563 Self {
564 field_name: field_name.into(),
565 to_model: to_model.into(),
566 related_name: None,
567 through: None,
568 source_field: None,
569 target_field: None,
570 db_constraint_prefix: None,
571 }
572 }
573
574 pub fn with_related_name(mut self, related_name: impl Into<String>) -> Self {
576 self.related_name = Some(related_name.into());
577 self
578 }
579
580 pub fn with_through(mut self, through: impl Into<String>) -> Self {
582 self.through = Some(through.into());
583 self
584 }
585
586 pub fn with_source_field(mut self, source_field: impl Into<String>) -> Self {
588 self.source_field = Some(source_field.into());
589 self
590 }
591
592 pub fn with_target_field(mut self, target_field: impl Into<String>) -> Self {
594 self.target_field = Some(target_field.into());
595 self
596 }
597
598 pub fn with_db_constraint_prefix(mut self, prefix: impl Into<String>) -> Self {
600 self.db_constraint_prefix = Some(prefix.into());
601 self
602 }
603}
604
605#[derive(Debug, Clone)]
628pub struct ModelRegistry {
629 models: Arc<RwLock<HashMap<(String, String), ModelMetadata>>>,
631}
632
633impl ModelRegistry {
634 pub fn new() -> Self {
636 Self {
637 models: Arc::new(RwLock::new(HashMap::new())),
638 }
639 }
640
641 pub fn register_model(&self, metadata: ModelMetadata) {
654 let key = (metadata.app_label.clone(), metadata.model_name.clone());
655 if let Ok(mut models) = self.models.write() {
656 models.insert(key, metadata);
657 }
658 }
659
660 pub fn get_models(&self) -> Vec<ModelMetadata> {
678 if let Ok(models) = self.models.read() {
679 models.values().cloned().collect()
680 } else {
681 Vec::new()
682 }
683 }
684
685 pub fn get_model(&self, app_label: &str, model_name: &str) -> Option<ModelMetadata> {
697 if let Ok(models) = self.models.read() {
698 models
699 .get(&(app_label.to_string(), model_name.to_string()))
700 .cloned()
701 } else {
702 None
703 }
704 }
705
706 pub fn find_model_qualified(&self, app_label: &str, model_name: &str) -> Option<ModelMetadata> {
718 self.get_model(app_label, model_name)
719 }
720
721 pub fn find_model_by_name(&self, model_name: &str) -> Option<ModelMetadata> {
741 let models = self.models.read().ok()?;
742 let mut matches = models.values().filter(|m| m.model_name == model_name);
743 let first = matches.next()?.clone();
744 if matches.next().is_some() {
745 tracing::warn!(
746 model_name,
747 "ModelRegistry::find_model_by_name: ambiguous model name registered \
748 under multiple app labels; returning None. Use \
749 ModelRegistry::find_model_qualified(app, name) to disambiguate.",
750 );
751 return None;
752 }
753 Some(first)
754 }
755
756 pub fn count_models_by_name(&self, model_name: &str) -> usize {
769 if let Ok(models) = self.models.read() {
770 models
771 .values()
772 .filter(|m| m.model_name == model_name)
773 .count()
774 } else {
775 0
776 }
777 }
778
779 pub fn get_app_models(&self, app_label: &str) -> Vec<ModelMetadata> {
781 if let Ok(models) = self.models.read() {
782 models
783 .iter()
784 .filter(|((app, _), _)| app == app_label)
785 .map(|(_, meta)| meta.clone())
786 .collect()
787 } else {
788 Vec::new()
789 }
790 }
791
792 pub fn remove_model(&self, app_label: &str, model_name: &str) -> bool {
794 if let Ok(mut models) = self.models.write() {
795 models
796 .remove(&(app_label.to_string(), model_name.to_string()))
797 .is_some()
798 } else {
799 false
800 }
801 }
802
803 pub fn clear(&self) {
805 if let Ok(mut models) = self.models.write() {
806 models.clear();
807 }
808 }
809
810 pub fn count(&self) -> usize {
812 if let Ok(models) = self.models.read() {
813 models.len()
814 } else {
815 0
816 }
817 }
818}
819
820impl Default for ModelRegistry {
821 fn default() -> Self {
822 Self::new()
823 }
824}
825
826pub fn global_registry() -> &'static ModelRegistry {
830 use once_cell::sync::Lazy;
831 static REGISTRY: Lazy<ModelRegistry> = Lazy::new(ModelRegistry::new);
832 ®ISTRY
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838 use crate::migrations::FieldType;
839 use crate::migrations::autodetector::{ForeignKeyInfo, MigrationAutodetector, ProjectState};
840 use crate::migrations::operations::{Constraint, Operation, SqlDialect};
841 use rstest::rstest;
842
843 #[test]
844 fn test_model_registry_new() {
845 let registry = ModelRegistry::new();
846 assert_eq!(registry.count(), 0);
847 }
848
849 #[test]
850 fn test_register_model() {
851 let registry = ModelRegistry::new();
852 let metadata = ModelMetadata::new("blog", "Post", "blog_post");
853 registry.register_model(metadata);
854 assert_eq!(registry.count(), 1);
855 }
856
857 #[test]
858 fn test_get_model() {
859 let registry = ModelRegistry::new();
860 let metadata = ModelMetadata::new("auth", "User", "auth_user");
861 registry.register_model(metadata);
862
863 let retrieved = registry.get_model("auth", "User");
864 assert!(retrieved.is_some());
865 assert_eq!(retrieved.unwrap().table_name, "auth_user");
866 }
867
868 #[test]
869 fn test_get_models() {
870 let registry = ModelRegistry::new();
871 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
872 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
873
874 let models = registry.get_models();
875 assert_eq!(models.len(), 2);
876 }
877
878 #[test]
879 fn test_find_model_qualified_hit() {
880 let registry = ModelRegistry::new();
882 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
883 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
884
885 let hit = registry.find_model_qualified("auth", "User");
887
888 assert!(hit.is_some());
890 let model = hit.unwrap();
891 assert_eq!(model.app_label, "auth");
892 assert_eq!(model.model_name, "User");
893 assert_eq!(model.table_name, "auth_user");
894 }
895
896 #[test]
897 fn test_find_model_qualified_miss_wrong_app() {
898 let registry = ModelRegistry::new();
900 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
901
902 assert!(registry.find_model_qualified("billing", "User").is_none());
905 }
906
907 #[test]
908 fn test_find_model_by_name_unique() {
909 let registry = ModelRegistry::new();
911 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
912 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
913
914 let hit = registry.find_model_by_name("Post");
916
917 assert!(hit.is_some());
919 assert_eq!(hit.unwrap().app_label, "blog");
920 }
921
922 #[test]
923 fn test_find_model_by_name_missing() {
924 let registry = ModelRegistry::new();
926 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
927
928 assert!(registry.find_model_by_name("NoSuchModel").is_none());
930 }
931
932 #[test]
933 fn test_find_model_by_name_ambiguous_returns_none() {
934 let registry = ModelRegistry::new();
938 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
939 registry.register_model(ModelMetadata::new("billing", "User", "billing_user"));
940
941 let hit = registry.find_model_by_name("User");
943
944 assert!(hit.is_none());
946 }
947
948 #[test]
949 fn test_get_app_models() {
950 let registry = ModelRegistry::new();
951 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
952 registry.register_model(ModelMetadata::new("auth", "Group", "auth_group"));
953 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
954
955 let auth_models = registry.get_app_models("auth");
956 assert_eq!(auth_models.len(), 2);
957
958 let blog_models = registry.get_app_models("blog");
959 assert_eq!(blog_models.len(), 1);
960 }
961
962 #[test]
963 fn test_remove_model() {
964 let registry = ModelRegistry::new();
965 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
966
967 assert!(registry.remove_model("auth", "User"));
968 assert_eq!(registry.count(), 0);
969 }
970
971 #[test]
972 fn test_migrations_registry_clear() {
973 let registry = ModelRegistry::new();
974 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
975 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
976
977 registry.clear();
978 assert_eq!(registry.count(), 0);
979 }
980
981 #[test]
982 fn test_model_metadata_to_model_state() {
983 let mut metadata = ModelMetadata::new("blog", "Post", "blog_post");
984
985 let mut title_field = FieldMetadata::new(FieldType::Custom("CharField".to_string()));
986 title_field
987 .params
988 .insert("max_length".to_string(), "200".to_string());
989 metadata.add_field("title".to_string(), title_field);
990
991 let model_state = metadata.to_model_state();
992 assert_eq!(model_state.name, "Post");
993 assert_eq!(model_state.fields.len(), 1);
994 assert!(model_state.fields.contains_key("title"));
995 }
996
997 #[test]
998 fn test_unique_field_uses_stable_table_constraint_without_inline_duplicate() {
999 let mut metadata = ModelMetadata::new("auth", "RenamedEmailVerificationToken", "auth_evt");
1001 metadata.add_field(
1002 "token_hash".to_string(),
1003 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1004 );
1005
1006 let model_state = metadata.to_model_state();
1008 let mut to_state = ProjectState::new();
1009 to_state.add_model(model_state);
1010 let migrations =
1011 MigrationAutodetector::new(ProjectState::new(), to_state).generate_migrations();
1012
1013 let model_state = &migrations[0].operations;
1015 let Operation::CreateTable {
1016 columns,
1017 constraints,
1018 ..
1019 } = &model_state[0]
1020 else {
1021 panic!("expected an initial CreateTable operation");
1022 };
1023 let expected_constraint_name = format!(
1024 "auth_evt_token_hash_uniq_{:08x}",
1025 stable_constraint_name_hash("auth_evt\0token_hash")
1026 );
1027 assert_eq!(
1028 columns
1029 .iter()
1030 .filter(|column| column.name == "token_hash" && column.unique)
1031 .count(),
1032 0,
1033 "single-column uniqueness must not be emitted inline"
1034 );
1035 assert_eq!(
1036 constraints,
1037 &vec![Constraint::Unique {
1038 name: expected_constraint_name,
1039 columns: vec!["token_hash".to_string()],
1040 }],
1041 "the physical constraint name must derive from the stable table name"
1042 );
1043 assert_eq!(
1044 model_state[0]
1045 .to_sql(&SqlDialect::Postgres)
1046 .matches("UNIQUE")
1047 .count(),
1048 1,
1049 "the generated PostgreSQL DDL must contain one UNIQUE representation"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_explicit_single_field_unique_constraint_name_is_preserved() {
1055 let mut metadata = ModelMetadata::new("auth", "Token", "auth_evt");
1057 metadata.add_field(
1058 "token_hash".to_string(),
1059 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1060 );
1061 metadata.add_constraint(ConstraintDefinition {
1062 name: "auth_evt_token_hash_uniq".to_string(),
1063 constraint_type: "unique".to_string(),
1064 fields: vec!["token_hash".to_string()],
1065 expression: None,
1066 foreign_key_info: None,
1067 });
1068
1069 let model_state = metadata.to_model_state();
1071
1072 assert!(
1074 !model_state.fields["token_hash"]
1075 .params
1076 .contains_key("unique")
1077 );
1078 assert_eq!(model_state.constraints.len(), 1);
1079 assert_eq!(model_state.constraints[0].name, "auth_evt_token_hash_uniq");
1080 }
1081
1082 #[test]
1083 fn test_synthesized_unique_constraint_avoids_model_constraint_name_collision() {
1084 let mut metadata = ModelMetadata::new("accounts", "Account", "accounts");
1086 metadata.add_field(
1087 "a_b".to_string(),
1088 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1089 );
1090 metadata.add_field("a".to_string(), FieldMetadata::new(FieldType::VarChar(255)));
1091 metadata.add_field("b".to_string(), FieldMetadata::new(FieldType::VarChar(255)));
1092 metadata.add_constraint(ConstraintDefinition {
1093 name: "accounts_a_b_uniq".to_string(),
1094 constraint_type: "unique".to_string(),
1095 fields: vec!["a".to_string(), "b".to_string()],
1096 expression: None,
1097 foreign_key_info: None,
1098 });
1099
1100 let model_state = metadata.to_model_state();
1102
1103 let mut names: Vec<_> = model_state
1105 .constraints
1106 .iter()
1107 .map(|constraint| constraint.name.clone())
1108 .collect();
1109 names.sort_unstable();
1110 let generated_name = format!(
1111 "accounts_a_b_uniq_{:08x}",
1112 stable_constraint_name_hash("accounts\0a_b")
1113 );
1114 assert_eq!(names, vec!["accounts_a_b_uniq".to_string(), generated_name]);
1115 }
1116
1117 #[test]
1118 fn test_synthesized_unique_constraint_avoids_foreign_key_name_collision() {
1119 let mut metadata = ModelMetadata::new("billing", "Account", "fk");
1121 metadata.add_field(
1122 "fk_x".to_string(),
1123 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1124 );
1125 metadata.add_field(
1126 "x_uniq".to_string(),
1127 FieldMetadata::new(FieldType::Integer).with_foreign_key(ForeignKeyInfo {
1128 referenced_table: "users".to_string(),
1129 referenced_column: "id".to_string(),
1130 on_delete: crate::migrations::ForeignKeyAction::Cascade,
1131 on_update: crate::migrations::ForeignKeyAction::NoAction,
1132 }),
1133 );
1134
1135 let model_state = metadata.to_model_state();
1137
1138 let mut names: Vec<_> = model_state
1140 .constraints
1141 .iter()
1142 .map(|constraint| constraint.name.clone())
1143 .collect();
1144 names.sort_unstable();
1145 let generated_name = format!(
1146 "fk_fk_x_uniq_{:08x}",
1147 stable_constraint_name_hash("fk\0fk_x")
1148 );
1149 assert_eq!(names, vec!["fk_fk_x_uniq".to_string(), generated_name]);
1150 }
1151
1152 #[test]
1153 fn test_synthesized_unique_constraint_names_avoid_normalized_field_collisions() {
1154 let mut metadata = ModelMetadata::new("accounts", "Account", "accounts");
1156 metadata.add_field(
1157 "é".to_string(),
1158 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1159 );
1160 metadata.add_field(
1161 "ü".to_string(),
1162 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1163 );
1164
1165 let model_state = metadata.to_model_state();
1167
1168 let mut names: Vec<_> = model_state
1170 .constraints
1171 .iter()
1172 .map(|constraint| constraint.name.clone())
1173 .collect();
1174 names.sort_unstable();
1175 let mut expected_names = ["é", "ü"]
1176 .into_iter()
1177 .map(|field| {
1178 format!(
1179 "accounts___uniq_{:08x}",
1180 stable_constraint_name_hash(&format!("accounts\0{field}"))
1181 )
1182 })
1183 .collect::<Vec<_>>();
1184 expected_names.sort_unstable();
1185 assert_eq!(names, expected_names);
1186 }
1187
1188 #[test]
1189 fn test_synthesized_unique_constraint_name_is_stable_when_normalized_field_is_added() {
1190 let mut existing = ModelMetadata::new("accounts", "Account", "accounts");
1192 existing.add_field(
1193 "ü".to_string(),
1194 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1195 );
1196 let existing_name = existing.to_model_state().constraints[0].name.clone();
1197
1198 let mut expanded = ModelMetadata::new("accounts", "Account", "accounts");
1199 expanded.add_field(
1200 "é".to_string(),
1201 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1202 );
1203 expanded.add_field(
1204 "ü".to_string(),
1205 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1206 );
1207
1208 let expanded_state = expanded.to_model_state();
1210 let expanded_name = expanded_state
1211 .constraints
1212 .iter()
1213 .find(|constraint| constraint.fields == vec!["ü".to_string()])
1214 .expect("expanded model must retain the existing unique field")
1215 .name
1216 .clone();
1217
1218 assert_eq!(existing_name, expanded_name);
1220 }
1221
1222 #[test]
1223 fn test_synthesized_unique_constraint_names_encode_table_field_boundaries() {
1224 let mut first = ModelMetadata::new("accounts", "First", "a_b");
1226 first.add_field(
1227 "c".to_string(),
1228 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1229 );
1230 let mut second = ModelMetadata::new("accounts", "Second", "a");
1231 second.add_field(
1232 "b_c".to_string(),
1233 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1234 );
1235
1236 let first_name = first.to_model_state().constraints[0].name.clone();
1238 let second_name = second.to_model_state().constraints[0].name.clone();
1239
1240 assert_ne!(first_name, second_name);
1242 }
1243
1244 #[test]
1245 fn test_synthesized_unique_constraint_name_is_safe_for_custom_table_names() {
1246 let mut metadata = ModelMetadata::new("accounts", "Account", "User-Events");
1248 metadata.add_field(
1249 "token".to_string(),
1250 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1251 );
1252
1253 let model_state = metadata.to_model_state();
1255 let constraint_name = model_state.constraints[0].name.clone();
1256 let expected_constraint_name = format!(
1257 "user_events_{:08x}_token_uniq_{:08x}",
1258 stable_constraint_name_hash("User-Events"),
1259 stable_constraint_name_hash("User-Events\0token")
1260 );
1261 let mut to_state = ProjectState::new();
1262 to_state.add_model(model_state);
1263 let migrations =
1264 MigrationAutodetector::new(ProjectState::new(), to_state).generate_migrations();
1265 let sql = migrations[0].operations[0].to_sql(&SqlDialect::Postgres);
1266
1267 assert_eq!(constraint_name, expected_constraint_name);
1269 assert_eq!(
1270 sql,
1271 format!(
1272 "CREATE TABLE \"User-Events\" (\n token VARCHAR(255) NOT NULL,\n CONSTRAINT {expected_constraint_name} UNIQUE (token)\n);"
1273 )
1274 );
1275 }
1276
1277 #[test]
1278 fn test_synthesized_unique_constraint_names_are_distinct_for_normalized_tables() {
1279 let mut dashed = ModelMetadata::new("accounts", "Dashed", "User-Events");
1281 dashed.add_field(
1282 "token".to_string(),
1283 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1284 );
1285 let mut underscored = ModelMetadata::new("accounts", "Underscored", "user_events");
1286 underscored.add_field(
1287 "token".to_string(),
1288 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1289 );
1290
1291 let dashed_name = dashed.to_model_state().constraints[0].name.clone();
1293 let underscored_name = underscored.to_model_state().constraints[0].name.clone();
1294
1295 assert_ne!(dashed_name, underscored_name);
1297 }
1298
1299 #[test]
1300 fn test_synthesized_unique_constraint_names_are_bounded_and_distinct() {
1301 let long_table = "t".repeat(40);
1303 let long_field = "f".repeat(40);
1304 let other_field = format!("{}g", "f".repeat(39));
1305 let mut metadata = ModelMetadata::new("accounts", "Account", long_table);
1306 metadata.add_field(
1307 long_field,
1308 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1309 );
1310 metadata.add_field(
1311 other_field,
1312 FieldMetadata::new(FieldType::VarChar(255)).with_param("unique", "true"),
1313 );
1314
1315 let constraints = metadata.to_model_state().constraints;
1317
1318 assert_eq!(constraints.len(), 2);
1320 assert!(constraints.iter().all(|constraint| {
1321 constraint.name.len() <= ModelMetadata::MAX_CONSTRAINT_IDENTIFIER_BYTES
1322 }));
1323 assert_ne!(constraints[0].name, constraints[1].name);
1324 }
1325
1326 #[test]
1327 fn test_field_metadata_builder() {
1328 let field = FieldMetadata::new(FieldType::Custom("CharField".to_string()))
1329 .with_param("max_length", "100")
1330 .with_nullable(false);
1331
1332 assert_eq!(field.field_type, FieldType::Custom("CharField".to_string()));
1333 assert_eq!(field.params.get("max_length").unwrap(), "100");
1334 assert!(!field.nullable);
1335 assert_eq!(field.params.get("null").unwrap(), "false");
1336
1337 let field =
1338 FieldMetadata::new(FieldType::Custom("IntegerField".to_string())).with_nullable(true);
1339 assert!(field.nullable);
1340 assert_eq!(field.params.get("null").unwrap(), "true");
1341 }
1342
1343 #[rstest]
1344 #[case(true, true)]
1345 #[case(false, false)]
1346 fn test_to_model_state_overrides_nullable_from_params(
1347 #[case] nullable: bool,
1348 #[case] expected_nullable: bool,
1349 ) {
1350 let mut metadata = ModelMetadata::new("blog", "Post", "blog_post");
1352 let field = FieldMetadata::new(FieldType::Custom("CharField".to_string()))
1353 .with_param("max_length", "200")
1354 .with_nullable(nullable);
1355 metadata.add_field("description".to_string(), field);
1356
1357 let model_state = metadata.to_model_state();
1359
1360 let field_state = model_state.fields.get("description").unwrap();
1362 assert_eq!(field_state.nullable, expected_nullable);
1363 assert!(
1364 !field_state.params.contains_key("null"),
1365 "params must not contain `null` key after to_model_state (it is already carried by FieldState.nullable)"
1366 );
1367 }
1368
1369 #[rstest]
1370 fn to_model_state_nullable_false_for_primary_key_matches_macro_contract() {
1371 let mut metadata = ModelMetadata::new("clusters", "Cluster", "clusters");
1391 let id_field = FieldMetadata::new(FieldType::BigInteger)
1396 .with_param("primary_key", "true")
1397 .with_param("auto_increment", "true")
1398 .with_param("not_null", "true")
1399 .with_nullable(false);
1400 metadata.add_field("id".to_string(), id_field);
1401
1402 let model_state = metadata.to_model_state();
1404
1405 let id_state = model_state
1408 .fields
1409 .get("id")
1410 .expect("id field present in to_model_state output");
1411 assert!(
1412 !id_state.nullable,
1413 "PK FieldState.nullable must be false even when the Rust type is \
1414 Option<i64>. Did the #[model] macro regress to emitting \
1415 null=\"true\" for Option<T> PKs? params={:?}",
1416 id_state.params
1417 );
1418 assert!(
1419 !id_state.params.contains_key("null"),
1420 "PK params must not contain `null` after to_model_state \
1421 (nullable is already carried by FieldState.nullable). \
1422 Got params={:?}",
1423 id_state.params
1424 );
1425 }
1426
1427 #[test]
1428 fn to_model_state_materializes_default_db_index() {
1429 let mut metadata = ModelMetadata::new("blog", "Post", "blog_posts");
1431 metadata.add_field(
1432 "author_id".to_string(),
1433 FieldMetadata::new(FieldType::Uuid).with_param("db_index", "true"),
1434 );
1435
1436 let model_state = metadata.to_model_state();
1438
1439 assert_eq!(model_state.indexes.len(), 1);
1441 assert_eq!(model_state.indexes[0].fields, vec!["author_id"]);
1442 assert!(!model_state.indexes[0].unique);
1443 }
1444
1445 #[test]
1446 fn to_model_state_skips_index_for_unique_field_or_disabled_field() {
1447 let mut metadata = ModelMetadata::new("blog", "Post", "blog_posts");
1449 metadata.add_field(
1450 "author_id".to_string(),
1451 FieldMetadata::new(FieldType::Uuid)
1452 .with_param("db_index", "true")
1453 .with_param("unique", "true"),
1454 );
1455 metadata.add_field(
1456 "category_id".to_string(),
1457 FieldMetadata::new(FieldType::Uuid).with_param("db_index", "false"),
1458 );
1459
1460 let model_state = metadata.to_model_state();
1462
1463 assert!(model_state.indexes.is_empty());
1465 }
1466
1467 #[test]
1468 fn to_model_state_deduplicates_equivalent_explicit_index() {
1469 let mut metadata = ModelMetadata::new("blog", "Post", "blog_posts");
1471 metadata.add_field(
1472 "author_id".to_string(),
1473 FieldMetadata::new(FieldType::Uuid).with_param("db_index", "true"),
1474 );
1475 metadata.add_index(IndexDefinition {
1476 name: "posts_author_explicit".to_string(),
1477 fields: vec!["author_id".to_string()],
1478 unique: false,
1479 where_clause: None,
1480 index_type: None,
1481 expressions: None,
1482 concurrently: false,
1483 mysql_options: None,
1484 operator_class: None,
1485 });
1486
1487 let model_state = metadata.to_model_state();
1489
1490 assert_eq!(model_state.indexes.len(), 1);
1492 assert_eq!(model_state.indexes[0].name, "posts_author_explicit");
1493 }
1494}