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(
151 name.clone(),
152 field_meta.field_type.clone(),
153 field_meta.is_nullable(),
154 );
155 for (key, value) in &field_meta.params {
156 field_state.params.insert(key.clone(), value.clone());
157 }
158 if let Some(ref fk_info) = field_meta.foreign_key {
160 field_state.foreign_key = Some(fk_info.clone());
161 }
162 model_state.add_field(field_state);
163 }
164
165 model_state.options = self.options.clone();
167
168 for (field_name, field_meta) in &self.fields {
170 if field_meta.foreign_key.is_some() {
171 model_state.add_foreign_key_constraint_from_field(field_name);
172 }
173 }
174
175 model_state.many_to_many_fields = self.many_to_many_fields.clone();
177
178 for (field_name, field_meta) in &self.fields {
180 if field_meta.params.get("unique").map(String::as_str) == Some("true") {
181 let constraint = ConstraintDefinition {
182 name: format!(
183 "{}_{}_{}_uniq",
184 self.app_label,
185 self.model_name.to_lowercase(),
186 field_name
187 ),
188 constraint_type: "unique".to_string(),
189 fields: vec![field_name.clone()],
190 expression: None,
191 foreign_key_info: None,
192 };
193 model_state.constraints.push(constraint);
194 }
195 }
196
197 model_state
201 .constraints
202 .extend(self.constraints.iter().cloned());
203
204 model_state
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct FieldMetadata {
211 pub field_type: super::FieldType,
213 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 params: HashMap::new(),
230 foreign_key: None,
231 }
232 }
233
234 pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
236 self.params.insert(key.into(), value.into());
237 self
238 }
239
240 pub fn with_nullable(mut self, nullable: bool) -> Self {
245 self.params.insert("null".to_string(), nullable.to_string());
246 self
247 }
248
249 pub fn is_nullable(&self) -> bool {
254 self.params
255 .get("null")
256 .and_then(|v| v.parse::<bool>().ok())
257 .unwrap_or(false)
258 }
259
260 pub fn with_foreign_key(mut self, foreign_key: super::autodetector::ForeignKeyInfo) -> Self {
262 self.foreign_key = Some(foreign_key);
263 self
264 }
265}
266
267#[derive(Debug, Clone)]
272pub struct RelationshipMetadata {
273 pub field_name: String,
275 pub rel_type: String,
277 pub to_model: Option<String>,
279 pub related_name: Option<String>,
281 pub through_table: Option<String>,
283 pub composite: Option<String>,
285 pub source_app_label: Option<String>,
287 pub source_model_name: Option<String>,
289}
290
291impl RelationshipMetadata {
292 pub fn new(field_name: impl Into<String>, rel_type: impl Into<String>) -> Self {
294 Self {
295 field_name: field_name.into(),
296 rel_type: rel_type.into(),
297 to_model: None,
298 related_name: None,
299 through_table: None,
300 composite: None,
301 source_app_label: None,
302 source_model_name: None,
303 }
304 }
305
306 pub fn with_to_model(mut self, to_model: impl Into<String>) -> Self {
308 self.to_model = Some(to_model.into());
309 self
310 }
311
312 pub fn with_related_name(mut self, related_name: impl Into<String>) -> Self {
314 self.related_name = Some(related_name.into());
315 self
316 }
317
318 pub fn with_through_table(mut self, through_table: impl Into<String>) -> Self {
320 self.through_table = Some(through_table.into());
321 self
322 }
323
324 pub fn with_composite(mut self, composite: impl Into<String>) -> Self {
326 self.composite = Some(composite.into());
327 self
328 }
329
330 pub fn with_source_info(
332 mut self,
333 app_label: impl Into<String>,
334 model_name: impl Into<String>,
335 ) -> Self {
336 self.source_app_label = Some(app_label.into());
337 self.source_model_name = Some(model_name.into());
338 self
339 }
340
341 pub fn is_many_to_many(&self) -> bool {
343 self.rel_type == "many_to_many" || self.rel_type == "polymorphic_many_to_many"
344 }
345}
346
347#[derive(Debug, Clone, PartialEq)]
352pub struct ManyToManyMetadata {
353 pub field_name: String,
355 pub to_model: String,
357 pub related_name: Option<String>,
359 pub through: Option<String>,
361 pub source_field: Option<String>,
363 pub target_field: Option<String>,
365 pub db_constraint_prefix: Option<String>,
367}
368
369impl ManyToManyMetadata {
370 pub fn new(field_name: impl Into<String>, to_model: impl Into<String>) -> Self {
372 Self {
373 field_name: field_name.into(),
374 to_model: to_model.into(),
375 related_name: None,
376 through: None,
377 source_field: None,
378 target_field: None,
379 db_constraint_prefix: None,
380 }
381 }
382
383 pub fn with_related_name(mut self, related_name: impl Into<String>) -> Self {
385 self.related_name = Some(related_name.into());
386 self
387 }
388
389 pub fn with_through(mut self, through: impl Into<String>) -> Self {
391 self.through = Some(through.into());
392 self
393 }
394
395 pub fn with_source_field(mut self, source_field: impl Into<String>) -> Self {
397 self.source_field = Some(source_field.into());
398 self
399 }
400
401 pub fn with_target_field(mut self, target_field: impl Into<String>) -> Self {
403 self.target_field = Some(target_field.into());
404 self
405 }
406
407 pub fn with_db_constraint_prefix(mut self, prefix: impl Into<String>) -> Self {
409 self.db_constraint_prefix = Some(prefix.into());
410 self
411 }
412}
413
414#[derive(Debug, Clone)]
437pub struct ModelRegistry {
438 models: Arc<RwLock<HashMap<(String, String), ModelMetadata>>>,
440}
441
442impl ModelRegistry {
443 pub fn new() -> Self {
445 Self {
446 models: Arc::new(RwLock::new(HashMap::new())),
447 }
448 }
449
450 pub fn register_model(&self, metadata: ModelMetadata) {
463 let key = (metadata.app_label.clone(), metadata.model_name.clone());
464 if let Ok(mut models) = self.models.write() {
465 models.insert(key, metadata);
466 }
467 }
468
469 pub fn get_models(&self) -> Vec<ModelMetadata> {
487 if let Ok(models) = self.models.read() {
488 models.values().cloned().collect()
489 } else {
490 Vec::new()
491 }
492 }
493
494 pub fn get_model(&self, app_label: &str, model_name: &str) -> Option<ModelMetadata> {
506 if let Ok(models) = self.models.read() {
507 models
508 .get(&(app_label.to_string(), model_name.to_string()))
509 .cloned()
510 } else {
511 None
512 }
513 }
514
515 pub fn find_model_qualified(&self, app_label: &str, model_name: &str) -> Option<ModelMetadata> {
527 self.get_model(app_label, model_name)
528 }
529
530 pub fn find_model_by_name(&self, model_name: &str) -> Option<ModelMetadata> {
550 let models = self.models.read().ok()?;
551 let mut matches = models.values().filter(|m| m.model_name == model_name);
552 let first = matches.next()?.clone();
553 if matches.next().is_some() {
554 tracing::warn!(
555 model_name,
556 "ModelRegistry::find_model_by_name: ambiguous model name registered \
557 under multiple app labels; returning None. Use \
558 ModelRegistry::find_model_qualified(app, name) to disambiguate.",
559 );
560 return None;
561 }
562 Some(first)
563 }
564
565 pub fn count_models_by_name(&self, model_name: &str) -> usize {
578 if let Ok(models) = self.models.read() {
579 models
580 .values()
581 .filter(|m| m.model_name == model_name)
582 .count()
583 } else {
584 0
585 }
586 }
587
588 pub fn get_app_models(&self, app_label: &str) -> Vec<ModelMetadata> {
590 if let Ok(models) = self.models.read() {
591 models
592 .iter()
593 .filter(|((app, _), _)| app == app_label)
594 .map(|(_, meta)| meta.clone())
595 .collect()
596 } else {
597 Vec::new()
598 }
599 }
600
601 pub fn remove_model(&self, app_label: &str, model_name: &str) -> bool {
603 if let Ok(mut models) = self.models.write() {
604 models
605 .remove(&(app_label.to_string(), model_name.to_string()))
606 .is_some()
607 } else {
608 false
609 }
610 }
611
612 pub fn clear(&self) {
614 if let Ok(mut models) = self.models.write() {
615 models.clear();
616 }
617 }
618
619 pub fn count(&self) -> usize {
621 if let Ok(models) = self.models.read() {
622 models.len()
623 } else {
624 0
625 }
626 }
627}
628
629impl Default for ModelRegistry {
630 fn default() -> Self {
631 Self::new()
632 }
633}
634
635pub fn global_registry() -> &'static ModelRegistry {
639 use once_cell::sync::Lazy;
640 static REGISTRY: Lazy<ModelRegistry> = Lazy::new(ModelRegistry::new);
641 ®ISTRY
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use crate::migrations::FieldType;
648 use rstest::rstest;
649
650 #[test]
651 fn test_model_registry_new() {
652 let registry = ModelRegistry::new();
653 assert_eq!(registry.count(), 0);
654 }
655
656 #[test]
657 fn test_register_model() {
658 let registry = ModelRegistry::new();
659 let metadata = ModelMetadata::new("blog", "Post", "blog_post");
660 registry.register_model(metadata);
661 assert_eq!(registry.count(), 1);
662 }
663
664 #[test]
665 fn test_get_model() {
666 let registry = ModelRegistry::new();
667 let metadata = ModelMetadata::new("auth", "User", "auth_user");
668 registry.register_model(metadata);
669
670 let retrieved = registry.get_model("auth", "User");
671 assert!(retrieved.is_some());
672 assert_eq!(retrieved.unwrap().table_name, "auth_user");
673 }
674
675 #[test]
676 fn test_get_models() {
677 let registry = ModelRegistry::new();
678 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
679 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
680
681 let models = registry.get_models();
682 assert_eq!(models.len(), 2);
683 }
684
685 #[test]
686 fn test_find_model_qualified_hit() {
687 let registry = ModelRegistry::new();
689 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
690 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
691
692 let hit = registry.find_model_qualified("auth", "User");
694
695 assert!(hit.is_some());
697 let model = hit.unwrap();
698 assert_eq!(model.app_label, "auth");
699 assert_eq!(model.model_name, "User");
700 assert_eq!(model.table_name, "auth_user");
701 }
702
703 #[test]
704 fn test_find_model_qualified_miss_wrong_app() {
705 let registry = ModelRegistry::new();
707 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
708
709 assert!(registry.find_model_qualified("billing", "User").is_none());
712 }
713
714 #[test]
715 fn test_find_model_by_name_unique() {
716 let registry = ModelRegistry::new();
718 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
719 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
720
721 let hit = registry.find_model_by_name("Post");
723
724 assert!(hit.is_some());
726 assert_eq!(hit.unwrap().app_label, "blog");
727 }
728
729 #[test]
730 fn test_find_model_by_name_missing() {
731 let registry = ModelRegistry::new();
733 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
734
735 assert!(registry.find_model_by_name("NoSuchModel").is_none());
737 }
738
739 #[test]
740 fn test_find_model_by_name_ambiguous_returns_none() {
741 let registry = ModelRegistry::new();
745 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
746 registry.register_model(ModelMetadata::new("billing", "User", "billing_user"));
747
748 let hit = registry.find_model_by_name("User");
750
751 assert!(hit.is_none());
753 }
754
755 #[test]
756 fn test_get_app_models() {
757 let registry = ModelRegistry::new();
758 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
759 registry.register_model(ModelMetadata::new("auth", "Group", "auth_group"));
760 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
761
762 let auth_models = registry.get_app_models("auth");
763 assert_eq!(auth_models.len(), 2);
764
765 let blog_models = registry.get_app_models("blog");
766 assert_eq!(blog_models.len(), 1);
767 }
768
769 #[test]
770 fn test_remove_model() {
771 let registry = ModelRegistry::new();
772 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
773
774 assert!(registry.remove_model("auth", "User"));
775 assert_eq!(registry.count(), 0);
776 }
777
778 #[test]
779 fn test_migrations_registry_clear() {
780 let registry = ModelRegistry::new();
781 registry.register_model(ModelMetadata::new("auth", "User", "auth_user"));
782 registry.register_model(ModelMetadata::new("blog", "Post", "blog_post"));
783
784 registry.clear();
785 assert_eq!(registry.count(), 0);
786 }
787
788 #[test]
789 fn test_model_metadata_to_model_state() {
790 let mut metadata = ModelMetadata::new("blog", "Post", "blog_post");
791
792 let mut title_field = FieldMetadata::new(FieldType::Custom("CharField".to_string()));
793 title_field
794 .params
795 .insert("max_length".to_string(), "200".to_string());
796 metadata.add_field("title".to_string(), title_field);
797
798 let model_state = metadata.to_model_state();
799 assert_eq!(model_state.name, "Post");
800 assert_eq!(model_state.fields.len(), 1);
801 assert!(model_state.fields.contains_key("title"));
802 }
803
804 #[test]
805 fn test_field_metadata_builder() {
806 let field = FieldMetadata::new(FieldType::Custom("CharField".to_string()))
807 .with_param("max_length", "100")
808 .with_param("null", "False");
809
810 assert_eq!(field.field_type, FieldType::Custom("CharField".to_string()));
811 assert_eq!(field.params.get("max_length").unwrap(), "100");
812 assert_eq!(field.params.get("null").unwrap(), "False");
813 }
814
815 #[rstest]
816 #[case("true", true)]
817 #[case("false", false)]
818 fn test_to_model_state_overrides_nullable_from_params(
819 #[case] null_param: &str,
820 #[case] expected_nullable: bool,
821 ) {
822 let mut metadata = ModelMetadata::new("blog", "Post", "blog_post");
824 let field = FieldMetadata::new(FieldType::Custom("CharField".to_string()))
825 .with_param("max_length", "200")
826 .with_param("null", null_param);
827 metadata.add_field("description".to_string(), field);
828
829 let model_state = metadata.to_model_state();
831
832 let field_state = model_state.fields.get("description").unwrap();
834 assert_eq!(field_state.nullable, expected_nullable);
835 }
836
837 #[rstest]
838 fn to_model_state_nullable_false_for_primary_key_matches_macro_contract() {
839 let mut metadata = ModelMetadata::new("clusters", "Cluster", "clusters");
859 let id_field = FieldMetadata::new(FieldType::BigInteger)
864 .with_param("primary_key", "true")
865 .with_param("auto_increment", "true")
866 .with_param("not_null", "true")
867 .with_param("null", "false");
868 metadata.add_field("id".to_string(), id_field);
869
870 let model_state = metadata.to_model_state();
872
873 let id_state = model_state
876 .fields
877 .get("id")
878 .expect("id field present in to_model_state output");
879 assert!(
880 !id_state.nullable,
881 "PK FieldState.nullable must be false even when the Rust type is \
882 Option<i64>. Did the #[model] macro regress to emitting \
883 null=\"true\" for Option<T> PKs? params={:?}",
884 id_state.params
885 );
886 assert_eq!(
887 id_state.params.get("null").map(String::as_str),
888 Some("false"),
889 "PK params[\"null\"] must be \"false\" (fixed macro contract). \
890 Got params={:?}",
891 id_state.params
892 );
893 }
894}