elif_orm/relationships/
metadata.rs

1//! Relationship Metadata System - Core metadata definitions for relationships
2
3use crate::error::{ModelError, ModelResult};
4use serde::{Deserialize, Serialize};
5
6/// Defines the type of relationship between models
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8pub enum RelationshipType {
9    /// One-to-one relationship (hasOne)
10    #[default]
11    HasOne,
12    /// One-to-many relationship (hasMany)
13    HasMany,
14    /// Many-to-one relationship (belongsTo)
15    BelongsTo,
16    /// Many-to-many relationship through a pivot table
17    ManyToMany,
18    /// Polymorphic one-to-one relationship
19    MorphOne,
20    /// Polymorphic one-to-many relationship
21    MorphMany,
22    /// Inverse polymorphic relationship
23    MorphTo,
24}
25
26impl RelationshipType {
27    /// Returns true if this relationship type is polymorphic
28    pub fn is_polymorphic(self) -> bool {
29        matches!(self, Self::MorphOne | Self::MorphMany | Self::MorphTo)
30    }
31
32    /// Returns true if this relationship returns a collection
33    pub fn is_collection(self) -> bool {
34        matches!(self, Self::HasMany | Self::ManyToMany | Self::MorphMany)
35    }
36
37    /// Returns true if this relationship requires a pivot table
38    pub fn requires_pivot(self) -> bool {
39        matches!(self, Self::ManyToMany)
40    }
41}
42
43/// Comprehensive relationship metadata containing all necessary information
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
45pub struct RelationshipMetadata {
46    /// The type of relationship
47    pub relationship_type: RelationshipType,
48
49    /// Name of the relationship (field name in the model)
50    pub name: String,
51
52    /// The related model's table name
53    pub related_table: String,
54
55    /// The related model's type name
56    pub related_model: String,
57
58    /// Foreign key configuration
59    pub foreign_key: ForeignKeyConfig,
60
61    /// Local key (primary key on this model, defaults to "id")
62    pub local_key: String,
63
64    /// Optional custom relationship name for queries
65    pub custom_name: Option<String>,
66
67    /// Pivot table configuration for many-to-many relationships
68    pub pivot_config: Option<PivotConfig>,
69
70    /// Polymorphic configuration
71    pub polymorphic_config: Option<PolymorphicConfig>,
72
73    /// Whether this relationship should be eagerly loaded by default
74    pub eager_load: bool,
75
76    /// Additional constraints for the relationship
77    pub constraints: Vec<RelationshipConstraint>,
78
79    /// Inverse relationship name (for automatic detection)
80    pub inverse: Option<String>,
81}
82
83impl RelationshipMetadata {
84    /// Create a new RelationshipMetadata instance
85    pub fn new(
86        relationship_type: RelationshipType,
87        name: String,
88        related_table: String,
89        related_model: String,
90        foreign_key: ForeignKeyConfig,
91    ) -> Self {
92        Self {
93            relationship_type,
94            name,
95            related_table,
96            related_model,
97            foreign_key,
98            local_key: "id".to_string(),
99            custom_name: None,
100            pivot_config: None,
101            polymorphic_config: None,
102            eager_load: false,
103            constraints: Vec::new(),
104            inverse: None,
105        }
106    }
107
108    /// Create a new RelationshipMetadata instance with pivot configuration
109    pub fn new_with_pivot(
110        relationship_type: RelationshipType,
111        name: String,
112        related_table: String,
113        related_model: String,
114        foreign_key: ForeignKeyConfig,
115        pivot_config: PivotConfig,
116    ) -> Self {
117        Self {
118            relationship_type,
119            name,
120            related_table,
121            related_model,
122            foreign_key,
123            local_key: "id".to_string(),
124            custom_name: None,
125            pivot_config: Some(pivot_config),
126            polymorphic_config: None,
127            eager_load: false,
128            constraints: Vec::new(),
129            inverse: None,
130        }
131    }
132
133    /// Set the local key (primary key on this model)
134    pub fn with_local_key(mut self, local_key: String) -> Self {
135        self.local_key = local_key;
136        self
137    }
138
139    /// Set a custom name for the relationship
140    pub fn with_custom_name(mut self, custom_name: String) -> Self {
141        self.custom_name = Some(custom_name);
142        self
143    }
144
145    /// Set pivot table configuration
146    pub fn with_pivot(mut self, pivot_config: PivotConfig) -> Self {
147        self.pivot_config = Some(pivot_config);
148        self
149    }
150
151    /// Set polymorphic configuration
152    pub fn with_polymorphic(mut self, polymorphic_config: PolymorphicConfig) -> Self {
153        self.polymorphic_config = Some(polymorphic_config);
154        self
155    }
156
157    /// Enable eager loading by default
158    pub fn with_eager_load(mut self, eager_load: bool) -> Self {
159        self.eager_load = eager_load;
160        self
161    }
162
163    /// Add constraints to the relationship
164    pub fn with_constraints(mut self, constraints: Vec<RelationshipConstraint>) -> Self {
165        self.constraints = constraints;
166        self
167    }
168
169    /// Set the inverse relationship name
170    pub fn with_inverse(mut self, inverse: String) -> Self {
171        self.inverse = Some(inverse);
172        self
173    }
174
175    /// Validate the relationship metadata for consistency
176    pub fn validate(&self) -> ModelResult<()> {
177        // Check if relationship type matches configuration
178        if self.relationship_type.requires_pivot() && self.pivot_config.is_none() {
179            return Err(ModelError::Configuration(format!(
180                "Relationship '{}' of type {:?} requires pivot configuration",
181                self.name, self.relationship_type
182            )));
183        }
184
185        if self.relationship_type.is_polymorphic() && self.polymorphic_config.is_none() {
186            return Err(ModelError::Configuration(format!(
187                "Relationship '{}' of type {:?} requires polymorphic configuration",
188                self.name, self.relationship_type
189            )));
190        }
191
192        // Validate foreign key configuration
193        self.foreign_key.validate()?;
194
195        // Validate pivot configuration if present
196        if let Some(ref pivot) = self.pivot_config {
197            pivot.validate()?;
198        }
199
200        // Validate polymorphic configuration if present
201        if let Some(ref poly) = self.polymorphic_config {
202            poly.validate()?;
203        }
204
205        Ok(())
206    }
207
208    /// Get the effective relationship name for queries
209    pub fn query_name(&self) -> &str {
210        self.custom_name.as_ref().unwrap_or(&self.name)
211    }
212}
213
214/// Foreign key configuration for relationships
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
216pub struct ForeignKeyConfig {
217    /// The foreign key column name(s)
218    pub columns: Vec<String>,
219
220    /// Whether this is a composite foreign key
221    pub is_composite: bool,
222
223    /// The table where the foreign key is located
224    pub table: String,
225}
226
227impl ForeignKeyConfig {
228    /// Create a simple foreign key configuration
229    pub fn simple(column: String, table: String) -> Self {
230        Self {
231            columns: vec![column],
232            is_composite: false,
233            table,
234        }
235    }
236
237    /// Create a composite foreign key configuration
238    pub fn composite(columns: Vec<String>, table: String) -> Self {
239        Self {
240            columns,
241            is_composite: true,
242            table,
243        }
244    }
245
246    /// Get the primary foreign key column (first in composite keys)
247    pub fn primary_column(&self) -> &str {
248        self.columns.first().map(|s| s.as_str()).unwrap_or("")
249    }
250
251    /// Validate the foreign key configuration
252    pub fn validate(&self) -> ModelResult<()> {
253        if self.columns.is_empty() {
254            return Err(ModelError::Configuration(
255                "Foreign key configuration must have at least one column".to_string(),
256            ));
257        }
258
259        if self.is_composite && self.columns.len() < 2 {
260            return Err(ModelError::Configuration(
261                "Composite foreign key must have at least 2 columns".to_string(),
262            ));
263        }
264
265        if self.table.is_empty() {
266            return Err(ModelError::Configuration(
267                "Foreign key configuration must specify a table".to_string(),
268            ));
269        }
270
271        Ok(())
272    }
273}
274
275/// Pivot table configuration for many-to-many relationships
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277pub struct PivotConfig {
278    /// The pivot table name
279    pub table: String,
280
281    /// The foreign key column for the local model in the pivot table
282    pub local_key: String,
283
284    /// The foreign key column for the related model in the pivot table
285    pub foreign_key: String,
286
287    /// Additional columns to include from the pivot table
288    pub additional_columns: Vec<String>,
289
290    /// Timestamps configuration for the pivot table
291    pub with_timestamps: bool,
292}
293
294impl PivotConfig {
295    /// Create a new pivot configuration
296    pub fn new(table: String, local_key: String, foreign_key: String) -> Self {
297        Self {
298            table,
299            local_key,
300            foreign_key,
301            additional_columns: Vec::new(),
302            with_timestamps: false,
303        }
304    }
305
306    /// Add additional columns to select from the pivot table
307    pub fn with_additional_columns(mut self, columns: Vec<String>) -> Self {
308        self.additional_columns = columns;
309        self
310    }
311
312    /// Enable timestamp columns on the pivot table
313    pub fn with_timestamps(mut self) -> Self {
314        self.with_timestamps = true;
315        self
316    }
317
318    /// Validate the pivot configuration
319    pub fn validate(&self) -> ModelResult<()> {
320        if self.table.is_empty() {
321            return Err(ModelError::Configuration(
322                "Pivot table name cannot be empty".to_string(),
323            ));
324        }
325
326        if self.local_key.is_empty() {
327            return Err(ModelError::Configuration(
328                "Pivot local key cannot be empty".to_string(),
329            ));
330        }
331
332        if self.foreign_key.is_empty() {
333            return Err(ModelError::Configuration(
334                "Pivot foreign key cannot be empty".to_string(),
335            ));
336        }
337
338        if self.local_key == self.foreign_key {
339            return Err(ModelError::Configuration(
340                "Pivot local key and foreign key must be different".to_string(),
341            ));
342        }
343
344        Ok(())
345    }
346}
347
348/// Polymorphic relationship configuration
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct PolymorphicConfig {
351    /// The morph type column name (stores the model type)
352    pub type_column: String,
353
354    /// The morph id column name (stores the foreign key)
355    pub id_column: String,
356
357    /// The name/namespace for this polymorphic relationship
358    pub name: String,
359
360    /// Allowed types for this polymorphic relationship
361    pub allowed_types: Vec<String>,
362}
363
364impl PolymorphicConfig {
365    /// Create a new polymorphic configuration
366    pub fn new(name: String, type_column: String, id_column: String) -> Self {
367        Self {
368            type_column,
369            id_column,
370            name,
371            allowed_types: Vec::new(),
372        }
373    }
374
375    /// Set allowed types for the polymorphic relationship
376    pub fn with_allowed_types(mut self, types: Vec<String>) -> Self {
377        self.allowed_types = types;
378        self
379    }
380
381    /// Validate the polymorphic configuration
382    pub fn validate(&self) -> ModelResult<()> {
383        if self.name.is_empty() {
384            return Err(ModelError::Configuration(
385                "Polymorphic relationship name cannot be empty".to_string(),
386            ));
387        }
388
389        if self.type_column.is_empty() {
390            return Err(ModelError::Configuration(
391                "Polymorphic type column cannot be empty".to_string(),
392            ));
393        }
394
395        if self.id_column.is_empty() {
396            return Err(ModelError::Configuration(
397                "Polymorphic ID column cannot be empty".to_string(),
398            ));
399        }
400
401        if self.type_column == self.id_column {
402            return Err(ModelError::Configuration(
403                "Polymorphic type column and ID column must be different".to_string(),
404            ));
405        }
406
407        Ok(())
408    }
409}
410
411/// Relationship constraint for additional filtering
412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct RelationshipConstraint {
414    /// The column to constrain
415    pub column: String,
416
417    /// The constraint operator
418    pub operator: ConstraintOperator,
419
420    /// The constraint value
421    pub value: String,
422}
423
424/// Constraint operators for relationship constraints
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426pub enum ConstraintOperator {
427    Equal,
428    NotEqual,
429    GreaterThan,
430    LessThan,
431    GreaterThanOrEqual,
432    LessThanOrEqual,
433    In,
434    NotIn,
435    Like,
436    NotLike,
437    IsNull,
438    IsNotNull,
439}
440
441impl ConstraintOperator {
442    /// Convert the operator to its SQL representation
443    pub fn to_sql(&self) -> &'static str {
444        match self {
445            Self::Equal => "=",
446            Self::NotEqual => "!=",
447            Self::GreaterThan => ">",
448            Self::LessThan => "<",
449            Self::GreaterThanOrEqual => ">=",
450            Self::LessThanOrEqual => "<=",
451            Self::In => "IN",
452            Self::NotIn => "NOT IN",
453            Self::Like => "LIKE",
454            Self::NotLike => "NOT LIKE",
455            Self::IsNull => "IS NULL",
456            Self::IsNotNull => "IS NOT NULL",
457        }
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_relationship_type_properties() {
467        assert!(RelationshipType::MorphOne.is_polymorphic());
468        assert!(RelationshipType::MorphMany.is_polymorphic());
469        assert!(RelationshipType::MorphTo.is_polymorphic());
470        assert!(!RelationshipType::HasOne.is_polymorphic());
471
472        assert!(RelationshipType::HasMany.is_collection());
473        assert!(RelationshipType::ManyToMany.is_collection());
474        assert!(RelationshipType::MorphMany.is_collection());
475        assert!(!RelationshipType::HasOne.is_collection());
476
477        assert!(RelationshipType::ManyToMany.requires_pivot());
478        assert!(!RelationshipType::HasMany.requires_pivot());
479    }
480
481    #[test]
482    fn test_relationship_metadata_creation() {
483        let metadata = RelationshipMetadata::new(
484            RelationshipType::HasMany,
485            "posts".to_string(),
486            "posts".to_string(),
487            "Post".to_string(),
488            ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string()),
489        );
490
491        assert_eq!(metadata.relationship_type, RelationshipType::HasMany);
492        assert_eq!(metadata.name, "posts");
493        assert_eq!(metadata.related_table, "posts");
494        assert_eq!(metadata.local_key, "id");
495        assert!(!metadata.eager_load);
496    }
497
498    #[test]
499    fn test_relationship_metadata_validation() {
500        // Valid has-many relationship
501        let metadata = RelationshipMetadata::new(
502            RelationshipType::HasMany,
503            "posts".to_string(),
504            "posts".to_string(),
505            "Post".to_string(),
506            ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string()),
507        );
508        assert!(metadata.validate().is_ok());
509
510        // Invalid many-to-many without pivot config
511        let invalid_metadata = RelationshipMetadata::new(
512            RelationshipType::ManyToMany,
513            "roles".to_string(),
514            "roles".to_string(),
515            "Role".to_string(),
516            ForeignKeyConfig::simple("user_id".to_string(), "user_roles".to_string()),
517        );
518        assert!(invalid_metadata.validate().is_err());
519    }
520
521    #[test]
522    fn test_foreign_key_config() {
523        let simple_fk = ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string());
524        assert!(!simple_fk.is_composite);
525        assert_eq!(simple_fk.primary_column(), "user_id");
526        assert!(simple_fk.validate().is_ok());
527
528        let composite_fk = ForeignKeyConfig::composite(
529            vec!["user_id".to_string(), "company_id".to_string()],
530            "posts".to_string(),
531        );
532        assert!(composite_fk.is_composite);
533        assert_eq!(composite_fk.primary_column(), "user_id");
534        assert!(composite_fk.validate().is_ok());
535    }
536
537    #[test]
538    fn test_pivot_config() {
539        let pivot = PivotConfig::new(
540            "user_roles".to_string(),
541            "user_id".to_string(),
542            "role_id".to_string(),
543        )
544        .with_timestamps();
545
546        assert_eq!(pivot.table, "user_roles");
547        assert_eq!(pivot.local_key, "user_id");
548        assert_eq!(pivot.foreign_key, "role_id");
549        assert!(pivot.with_timestamps);
550        assert!(pivot.validate().is_ok());
551    }
552
553    #[test]
554    fn test_polymorphic_config() {
555        let poly = PolymorphicConfig::new(
556            "commentable".to_string(),
557            "commentable_type".to_string(),
558            "commentable_id".to_string(),
559        )
560        .with_allowed_types(vec!["Post".to_string(), "Video".to_string()]);
561
562        assert_eq!(poly.name, "commentable");
563        assert_eq!(poly.type_column, "commentable_type");
564        assert_eq!(poly.id_column, "commentable_id");
565        assert_eq!(poly.allowed_types.len(), 2);
566        assert!(poly.validate().is_ok());
567    }
568
569    #[test]
570    fn test_constraint_operator_sql() {
571        assert_eq!(ConstraintOperator::Equal.to_sql(), "=");
572        assert_eq!(ConstraintOperator::In.to_sql(), "IN");
573        assert_eq!(ConstraintOperator::IsNull.to_sql(), "IS NULL");
574    }
575
576    #[test]
577    fn test_relationship_metadata_builder_pattern() {
578        let metadata = RelationshipMetadata::new(
579            RelationshipType::HasOne,
580            "profile".to_string(),
581            "profiles".to_string(),
582            "Profile".to_string(),
583            ForeignKeyConfig::simple("user_id".to_string(), "profiles".to_string()),
584        )
585        .with_local_key("uuid".to_string())
586        .with_custom_name("user_profile".to_string())
587        .with_eager_load(true)
588        .with_inverse("user".to_string());
589
590        assert_eq!(metadata.local_key, "uuid");
591        assert_eq!(metadata.custom_name, Some("user_profile".to_string()));
592        assert!(metadata.eager_load);
593        assert_eq!(metadata.inverse, Some("user".to_string()));
594        assert_eq!(metadata.query_name(), "user_profile");
595    }
596}