1use super::ConstraintDefinition;
15use super::autodetector::{FieldState, ModelState};
16use std::collections::HashMap;
17use std::sync::{Arc, RwLock};
18
19#[cfg_attr(doc, aquamarine::aquamarine)]
20#[derive(Debug, Clone)]
46pub struct ModelMetadata {
47 pub app_label: String,
49 pub model_name: String,
51 pub table_name: String,
53 pub fields: HashMap<String, FieldMetadata>,
55 pub options: HashMap<String, String>,
57 pub many_to_many_fields: Vec<ManyToManyMetadata>,
59 constraints: Vec<ConstraintDefinition>,
68}
69
70impl ModelMetadata {
71 pub fn new(
73 app_label: impl Into<String>,
74 model_name: impl Into<String>,
75 table_name: impl Into<String>,
76 ) -> Self {
77 Self {
78 app_label: app_label.into(),
79 model_name: model_name.into(),
80 table_name: table_name.into(),
81 fields: HashMap::new(),
82 options: HashMap::new(),
83 many_to_many_fields: Vec::new(),
84 constraints: Vec::new(),
85 }
86 }
87
88 pub fn add_field(&mut self, name: String, field: FieldMetadata) {
90 self.fields.insert(name, field);
91 }
92
93 pub fn set_option(&mut self, key: String, value: String) {
95 self.options.insert(key, value);
96 }
97
98 pub fn add_many_to_many(&mut self, m2m: ManyToManyMetadata) {
100 self.many_to_many_fields.push(m2m);
101 }
102
103 pub fn add_constraint(&mut self, constraint: ConstraintDefinition) {
106 self.constraints.push(constraint);
107 }
108
109 pub fn constraints(&self) -> &[ConstraintDefinition] {
115 &self.constraints
116 }
117
118 pub fn to_model_state(&self) -> ModelState {
138 let mut model_state = ModelState::new(&self.app_label, &self.model_name);
139
140 model_state.table_name = self.table_name.clone();
143
144 for (name, field_meta) in &self.fields {
146 let mut field_state = FieldState::new(
147 name.clone(),
148 field_meta.field_type.clone(),
149 field_meta.nullable,
150 );
151 for (key, value) in &field_meta.params {
152 if key == "null" {
153 continue;
154 }
155 field_state.params.insert(key.clone(), value.clone());
156 }
157 if let Some(ref fk_info) = field_meta.foreign_key {
159 field_state.foreign_key = Some(fk_info.clone());
160 }
161 model_state.add_field(field_state);
162 }
163
164 model_state.options = self.options.clone();
166
167 for (field_name, field_meta) in &self.fields {
169 if field_meta.foreign_key.is_some() {
170 model_state.add_foreign_key_constraint_from_field(field_name);
171 }
172 }
173
174 model_state.many_to_many_fields = self.many_to_many_fields.clone();
176
177 for (field_name, field_meta) in &self.fields {
179 if field_meta.params.get("unique").map(String::as_str) == Some("true") {
180 let constraint = ConstraintDefinition {
181 name: format!(
182 "{}_{}_{}_uniq",
183 self.app_label,
184 self.model_name.to_lowercase(),
185 field_name
186 ),
187 constraint_type: "unique".to_string(),
188 fields: vec![field_name.clone()],
189 expression: None,
190 foreign_key_info: None,
191 };
192 model_state.constraints.push(constraint);
193 }
194 }
195
196 model_state
200 .constraints
201 .extend(self.constraints.iter().cloned());
202
203 model_state
204 }
205}
206
207#[derive(Debug, Clone)]
209pub struct FieldMetadata {
210 pub field_type: super::FieldType,
212 pub nullable: bool,
218 pub params: HashMap<String, String>,
220 pub foreign_key: Option<super::autodetector::ForeignKeyInfo>,
222}
223
224impl FieldMetadata {
225 pub fn new(field_type: super::FieldType) -> Self {
227 Self {
228 field_type,
229 nullable: false,
230 params: HashMap::new(),
231 foreign_key: None,
232 }
233 }
234
235 pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
242 let key_s: String = key.into();
243 let value_s: String = value.into();
244 if key_s == "null" {
245 let parsed = value_s.parse::<bool>().unwrap_or(false);
246 self.nullable = parsed;
247 self.params.insert(key_s, parsed.to_string());
248 return self;
249 }
250 self.params.insert(key_s, value_s);
251 self
252 }
253
254 pub fn with_nullable(mut self, nullable: bool) -> Self {
259 self.nullable = nullable;
260 self.params.insert("null".to_string(), nullable.to_string());
261 self
262 }
263
264 pub fn is_nullable(&self) -> bool {
266 self.nullable
267 }
268
269 pub fn with_foreign_key(mut self, foreign_key: super::autodetector::ForeignKeyInfo) -> Self {
271 self.foreign_key = Some(foreign_key);
272 self
273 }
274}
275
276#[derive(Debug, Clone)]
281pub struct RelationshipMetadata {
282 pub field_name: String,
284 pub rel_type: String,
286 pub to_model: Option<String>,
288 pub related_name: Option<String>,
290 pub through_table: Option<String>,
292 pub composite: Option<String>,
294 pub source_app_label: Option<String>,
296 pub source_model_name: Option<String>,
298}
299
300impl RelationshipMetadata {
301 pub fn new(field_name: impl Into<String>, rel_type: impl Into<String>) -> Self {
303 Self {
304 field_name: field_name.into(),
305 rel_type: rel_type.into(),
306 to_model: None,
307 related_name: None,
308 through_table: None,
309 composite: None,
310 source_app_label: None,
311 source_model_name: None,
312 }
313 }
314
315 pub fn with_to_model(mut self, to_model: impl Into<String>) -> Self {
317 self.to_model = Some(to_model.into());
318 self
319 }
320
321 pub fn with_related_name(mut self, related_name: impl Into<String>) -> Self {
323 self.related_name = Some(related_name.into());
324 self
325 }
326
327 pub fn with_through_table(mut self, through_table: impl Into<String>) -> Self {
329 self.through_table = Some(through_table.into());
330 self
331 }
332
333 pub fn with_composite(mut self, composite: impl Into<String>) -> Self {
335 self.composite = Some(composite.into());
336 self
337 }
338
339 pub fn with_source_info(
341 mut self,
342 app_label: impl Into<String>,
343 model_name: impl Into<String>,
344 ) -> Self {
345 self.source_app_label = Some(app_label.into());
346 self.source_model_name = Some(model_name.into());
347 self
348 }
349
350 pub fn is_many_to_many(&self) -> bool {
352 self.rel_type == "many_to_many" || self.rel_type == "polymorphic_many_to_many"
353 }
354}
355
356#[derive(Debug, Clone, PartialEq)]
361pub struct ManyToManyMetadata {
362 pub field_name: String,
364 pub to_model: String,
366 pub related_name: Option<String>,
368 pub through: Option<String>,
370 pub source_field: Option<String>,
372 pub target_field: Option<String>,
374 pub db_constraint_prefix: Option<String>,
376}
377
378impl ManyToManyMetadata {
379 pub fn new(field_name: impl Into<String>, to_model: impl Into<String>) -> Self {
381 Self {
382 field_name: field_name.into(),
383 to_model: to_model.into(),
384 related_name: None,
385 through: None,
386 source_field: None,
387 target_field: None,
388 db_constraint_prefix: None,
389 }
390 }
391
392 pub fn with_related_name(mut self, related_name: impl Into<String>) -> Self {
394 self.related_name = Some(related_name.into());
395 self
396 }
397
398 pub fn with_through(mut self, through: impl Into<String>) -> Self {
400 self.through = Some(through.into());
401 self
402 }
403
404 pub fn with_source_field(mut self, source_field: impl Into<String>) -> Self {
406 self.source_field = Some(source_field.into());
407 self
408 }
409
410 pub fn with_target_field(mut self, target_field: impl Into<String>) -> Self {
412 self.target_field = Some(target_field.into());
413 self
414 }
415
416 pub fn with_db_constraint_prefix(mut self, prefix: impl Into<String>) -> Self {
418 self.db_constraint_prefix = Some(prefix.into());
419 self
420 }
421}
422
423#[derive(Debug, Clone)]
446pub struct ModelRegistry {
447 models: Arc<RwLock<HashMap<(String, String), ModelMetadata>>>,
449}
450
451impl ModelRegistry {
452 pub fn new() -> Self {
454 Self {
455 models: Arc::new(RwLock::new(HashMap::new())),
456 }
457 }
458
459 pub fn register_model(&self, metadata: ModelMetadata) {
472 let key = (metadata.app_label.clone(), metadata.model_name.clone());
473 if let Ok(mut models) = self.models.write() {
474 models.insert(key, metadata);
475 }
476 }
477
478 pub fn get_models(&self) -> Vec<ModelMetadata> {
496 if let Ok(models) = self.models.read() {
497 models.values().cloned().collect()
498 } else {
499 Vec::new()
500 }
501 }
502
503 pub fn get_model(&self, app_label: &str, model_name: &str) -> Option<ModelMetadata> {
515 if let Ok(models) = self.models.read() {
516 models
517 .get(&(app_label.to_string(), model_name.to_string()))
518 .cloned()
519 } else {
520 None
521 }
522 }
523
524 pub fn find_model_qualified(&self, app_label: &str, model_name: &str) -> Option<ModelMetadata> {
536 self.get_model(app_label, model_name)
537 }
538
539 pub fn find_model_by_name(&self, model_name: &str) -> Option<ModelMetadata> {
559 let models = self.models.read().ok()?;
560 let mut matches = models.values().filter(|m| m.model_name == model_name);
561 let first = matches.next()?.clone();
562 if matches.next().is_some() {
563 tracing::warn!(
564 model_name,
565 "ModelRegistry::find_model_by_name: ambiguous model name registered \
566 under multiple app labels; returning None. Use \
567 ModelRegistry::find_model_qualified(app, name) to disambiguate.",
568 );
569 return None;
570 }
571 Some(first)
572 }
573
574 pub fn count_models_by_name(&self, model_name: &str) -> usize {
587 if let Ok(models) = self.models.read() {
588 models
589 .values()
590 .filter(|m| m.model_name == model_name)
591 .count()
592 } else {
593 0
594 }
595 }
596
597 pub fn get_app_models(&self, app_label: &str) -> Vec<ModelMetadata> {
599 if let Ok(models) = self.models.read() {
600 models
601 .iter()
602 .filter(|((app, _), _)| app == app_label)
603 .map(|(_, meta)| meta.clone())
604 .collect()
605 } else {
606 Vec::new()
607 }
608 }
609
610 pub fn remove_model(&self, app_label: &str, model_name: &str) -> bool {
612 if let Ok(mut models) = self.models.write() {
613 models
614 .remove(&(app_label.to_string(), model_name.to_string()))
615 .is_some()
616 } else {
617 false
618 }
619 }
620
621 pub fn clear(&self) {
623 if let Ok(mut models) = self.models.write() {
624 models.clear();
625 }
626 }
627
628 pub fn count(&self) -> usize {
630 if let Ok(models) = self.models.read() {
631 models.len()
632 } else {
633 0
634 }
635 }
636}
637
638impl Default for ModelRegistry {
639 fn default() -> Self {
640 Self::new()
641 }
642}
643
644pub fn global_registry() -> &'static ModelRegistry {
648 use once_cell::sync::Lazy;
649 static REGISTRY: Lazy<ModelRegistry> = Lazy::new(ModelRegistry::new);
650 ®ISTRY
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::migrations::FieldType;
657 use rstest::rstest;
658
659 #[test]
660 fn test_model_registry_new() {
661 let registry = ModelRegistry::new();
662 assert_eq!(registry.count(), 0);
663 }
664
665 #[test]
666 fn test_register_model() {
667 let registry = ModelRegistry::new();
668 let metadata = ModelMetadata::new("blog", "Post", "blog_post");
669 registry.register_model(metadata);
670 assert_eq!(registry.count(), 1);
671 }
672
673 #[test]
674 fn test_get_model() {
675 let registry = ModelRegistry::new();
676 let metadata = ModelMetadata::new("auth", "User", "auth_user");
677 registry.register_model(metadata);
678
679 let retrieved = registry.get_model("auth", "User");
680 assert!(retrieved.is_some());
681 assert_eq!(retrieved.unwrap().table_name, "auth_user");
682 }
683
684 #[test]
685 fn test_get_models() {
686 let registry = ModelRegistry::new();
687 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
688 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
689
690 let models = registry.get_models();
691 assert_eq!(models.len(), 2);
692 }
693
694 #[test]
695 fn test_find_model_qualified_hit() {
696 let registry = ModelRegistry::new();
698 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
699 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
700
701 let hit = registry.find_model_qualified("auth", "User");
703
704 assert!(hit.is_some());
706 let model = hit.unwrap();
707 assert_eq!(model.app_label, "auth");
708 assert_eq!(model.model_name, "User");
709 assert_eq!(model.table_name, "auth_user");
710 }
711
712 #[test]
713 fn test_find_model_qualified_miss_wrong_app() {
714 let registry = ModelRegistry::new();
716 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
717
718 assert!(registry.find_model_qualified("billing", "User").is_none());
721 }
722
723 #[test]
724 fn test_find_model_by_name_unique() {
725 let registry = ModelRegistry::new();
727 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
728 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
729
730 let hit = registry.find_model_by_name("Post");
732
733 assert!(hit.is_some());
735 assert_eq!(hit.unwrap().app_label, "blog");
736 }
737
738 #[test]
739 fn test_find_model_by_name_missing() {
740 let registry = ModelRegistry::new();
742 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
743
744 assert!(registry.find_model_by_name("NoSuchModel").is_none());
746 }
747
748 #[test]
749 fn test_find_model_by_name_ambiguous_returns_none() {
750 let registry = ModelRegistry::new();
754 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
755 registry.register_model(ModelMetadata::new("billing", "User", "billing_user"));
756
757 let hit = registry.find_model_by_name("User");
759
760 assert!(hit.is_none());
762 }
763
764 #[test]
765 fn test_get_app_models() {
766 let registry = ModelRegistry::new();
767 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
768 registry.register_model(ModelMetadata::new("auth", "Group", "auth_group"));
769 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
770
771 let auth_models = registry.get_app_models("auth");
772 assert_eq!(auth_models.len(), 2);
773
774 let blog_models = registry.get_app_models("blog");
775 assert_eq!(blog_models.len(), 1);
776 }
777
778 #[test]
779 fn test_remove_model() {
780 let registry = ModelRegistry::new();
781 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
782
783 assert!(registry.remove_model("auth", "User"));
784 assert_eq!(registry.count(), 0);
785 }
786
787 #[test]
788 fn test_migrations_registry_clear() {
789 let registry = ModelRegistry::new();
790 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
791 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
792
793 registry.clear();
794 assert_eq!(registry.count(), 0);
795 }
796
797 #[test]
798 fn test_model_metadata_to_model_state() {
799 let mut metadata = ModelMetadata::new("blog", "Post", "blog_post");
800
801 let mut title_field = FieldMetadata::new(FieldType::Custom("CharField".to_string()));
802 title_field
803 .params
804 .insert("max_length".to_string(), "200".to_string());
805 metadata.add_field("title".to_string(), title_field);
806
807 let model_state = metadata.to_model_state();
808 assert_eq!(model_state.name, "Post");
809 assert_eq!(model_state.fields.len(), 1);
810 assert!(model_state.fields.contains_key("title"));
811 }
812
813 #[test]
814 fn test_field_metadata_builder() {
815 let field = FieldMetadata::new(FieldType::Custom("CharField".to_string()))
816 .with_param("max_length", "100")
817 .with_nullable(false);
818
819 assert_eq!(field.field_type, FieldType::Custom("CharField".to_string()));
820 assert_eq!(field.params.get("max_length").unwrap(), "100");
821 assert!(!field.nullable);
822 assert_eq!(field.params.get("null").unwrap(), "false");
823
824 let field =
825 FieldMetadata::new(FieldType::Custom("IntegerField".to_string())).with_nullable(true);
826 assert!(field.nullable);
827 assert_eq!(field.params.get("null").unwrap(), "true");
828 }
829
830 #[rstest]
831 #[case(true, true)]
832 #[case(false, false)]
833 fn test_to_model_state_overrides_nullable_from_params(
834 #[case] nullable: bool,
835 #[case] expected_nullable: bool,
836 ) {
837 let mut metadata = ModelMetadata::new("blog", "Post", "blog_post");
839 let field = FieldMetadata::new(FieldType::Custom("CharField".to_string()))
840 .with_param("max_length", "200")
841 .with_nullable(nullable);
842 metadata.add_field("description".to_string(), field);
843
844 let model_state = metadata.to_model_state();
846
847 let field_state = model_state.fields.get("description").unwrap();
849 assert_eq!(field_state.nullable, expected_nullable);
850 assert!(
851 !field_state.params.contains_key("null"),
852 "params must not contain `null` key after to_model_state (it is already carried by FieldState.nullable)"
853 );
854 }
855
856 #[rstest]
857 fn to_model_state_nullable_false_for_primary_key_matches_macro_contract() {
858 let mut metadata = ModelMetadata::new("clusters", "Cluster", "clusters");
878 let id_field = FieldMetadata::new(FieldType::BigInteger)
883 .with_param("primary_key", "true")
884 .with_param("auto_increment", "true")
885 .with_param("not_null", "true")
886 .with_nullable(false);
887 metadata.add_field("id".to_string(), id_field);
888
889 let model_state = metadata.to_model_state();
891
892 let id_state = model_state
895 .fields
896 .get("id")
897 .expect("id field present in to_model_state output");
898 assert!(
899 !id_state.nullable,
900 "PK FieldState.nullable must be false even when the Rust type is \
901 Option<i64>. Did the #[model] macro regress to emitting \
902 null=\"true\" for Option<T> PKs? params={:?}",
903 id_state.params
904 );
905 assert!(
906 !id_state.params.contains_key("null"),
907 "PK params must not contain `null` after to_model_state \
908 (nullable is already carried by FieldState.nullable). \
909 Got params={:?}",
910 id_state.params
911 );
912 }
913}