prax_schema/ast/
relation.rs

1//! Relation analysis for the Prax schema AST.
2
3use serde::{Deserialize, Serialize};
4use smol_str::SmolStr;
5
6use super::ReferentialAction;
7
8/// The type of relation between two models.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum RelationType {
11    /// One-to-one relation.
12    OneToOne,
13    /// One-to-many relation.
14    OneToMany,
15    /// Many-to-one relation (inverse of one-to-many).
16    ManyToOne,
17    /// Many-to-many relation.
18    ManyToMany,
19}
20
21impl RelationType {
22    /// Check if this is a "to-one" relation.
23    pub fn is_to_one(&self) -> bool {
24        matches!(self, Self::OneToOne | Self::ManyToOne)
25    }
26
27    /// Check if this is a "to-many" relation.
28    pub fn is_to_many(&self) -> bool {
29        matches!(self, Self::OneToMany | Self::ManyToMany)
30    }
31
32    /// Check if this is a "from-many" relation.
33    pub fn is_from_many(&self) -> bool {
34        matches!(self, Self::ManyToOne | Self::ManyToMany)
35    }
36}
37
38impl std::fmt::Display for RelationType {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            Self::OneToOne => write!(f, "1:1"),
42            Self::OneToMany => write!(f, "1:n"),
43            Self::ManyToOne => write!(f, "n:1"),
44            Self::ManyToMany => write!(f, "m:n"),
45        }
46    }
47}
48
49/// A resolved relation between two models.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct Relation {
52    /// Relation name (for disambiguation when multiple relations exist).
53    pub name: Option<SmolStr>,
54    /// The model containing the foreign key.
55    pub from_model: SmolStr,
56    /// The field on the from model.
57    pub from_field: SmolStr,
58    /// The foreign key field(s) on the from model.
59    pub from_fields: Vec<SmolStr>,
60    /// The model being referenced.
61    pub to_model: SmolStr,
62    /// The field on the to model (back-relation).
63    pub to_field: Option<SmolStr>,
64    /// The referenced field(s) on the to model.
65    pub to_fields: Vec<SmolStr>,
66    /// The type of relation.
67    pub relation_type: RelationType,
68    /// On delete action.
69    pub on_delete: Option<ReferentialAction>,
70    /// On update action.
71    pub on_update: Option<ReferentialAction>,
72}
73
74impl Relation {
75    /// Create a new relation.
76    pub fn new(
77        from_model: impl Into<SmolStr>,
78        from_field: impl Into<SmolStr>,
79        to_model: impl Into<SmolStr>,
80        relation_type: RelationType,
81    ) -> Self {
82        Self {
83            name: None,
84            from_model: from_model.into(),
85            from_field: from_field.into(),
86            from_fields: vec![],
87            to_model: to_model.into(),
88            to_field: None,
89            to_fields: vec![],
90            relation_type,
91            on_delete: None,
92            on_update: None,
93        }
94    }
95
96    /// Set the relation name.
97    pub fn with_name(mut self, name: impl Into<SmolStr>) -> Self {
98        self.name = Some(name.into());
99        self
100    }
101
102    /// Set the foreign key fields.
103    pub fn with_from_fields(mut self, fields: Vec<SmolStr>) -> Self {
104        self.from_fields = fields;
105        self
106    }
107
108    /// Set the referenced fields.
109    pub fn with_to_fields(mut self, fields: Vec<SmolStr>) -> Self {
110        self.to_fields = fields;
111        self
112    }
113
114    /// Set the back-relation field.
115    pub fn with_to_field(mut self, field: impl Into<SmolStr>) -> Self {
116        self.to_field = Some(field.into());
117        self
118    }
119
120    /// Set the on delete action.
121    pub fn with_on_delete(mut self, action: ReferentialAction) -> Self {
122        self.on_delete = Some(action);
123        self
124    }
125
126    /// Set the on update action.
127    pub fn with_on_update(mut self, action: ReferentialAction) -> Self {
128        self.on_update = Some(action);
129        self
130    }
131
132    /// Check if this is an implicit many-to-many relation.
133    pub fn is_implicit_many_to_many(&self) -> bool {
134        self.relation_type == RelationType::ManyToMany && self.from_fields.is_empty()
135    }
136
137    /// Get the join table name for many-to-many relations.
138    pub fn join_table_name(&self) -> Option<String> {
139        if self.relation_type != RelationType::ManyToMany {
140            return None;
141        }
142
143        // Sort model names for consistent naming
144        let mut names = [self.from_model.as_str(), self.to_model.as_str()];
145        names.sort();
146
147        Some(format!("_{}_to_{}", names[0], names[1]))
148    }
149}
150
151/// Index definition for a model.
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct Index {
154    /// Index name (auto-generated if not specified).
155    pub name: Option<SmolStr>,
156    /// Fields included in the index.
157    pub fields: Vec<IndexField>,
158    /// Whether this is a unique index.
159    pub is_unique: bool,
160    /// Index type (btree, hash, etc.).
161    pub index_type: Option<IndexType>,
162}
163
164impl Index {
165    /// Create a new index.
166    pub fn new(fields: Vec<IndexField>) -> Self {
167        Self {
168            name: None,
169            fields,
170            is_unique: false,
171            index_type: None,
172        }
173    }
174
175    /// Create a unique index.
176    pub fn unique(fields: Vec<IndexField>) -> Self {
177        Self {
178            name: None,
179            fields,
180            is_unique: true,
181            index_type: None,
182        }
183    }
184
185    /// Set the index name.
186    pub fn with_name(mut self, name: impl Into<SmolStr>) -> Self {
187        self.name = Some(name.into());
188        self
189    }
190
191    /// Set the index type.
192    pub fn with_type(mut self, index_type: IndexType) -> Self {
193        self.index_type = Some(index_type);
194        self
195    }
196}
197
198/// A field in an index.
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct IndexField {
201    /// Field name.
202    pub name: SmolStr,
203    /// Sort order.
204    pub sort: SortOrder,
205}
206
207impl IndexField {
208    /// Create a new index field with ascending order.
209    pub fn asc(name: impl Into<SmolStr>) -> Self {
210        Self {
211            name: name.into(),
212            sort: SortOrder::Asc,
213        }
214    }
215
216    /// Create a new index field with descending order.
217    pub fn desc(name: impl Into<SmolStr>) -> Self {
218        Self {
219            name: name.into(),
220            sort: SortOrder::Desc,
221        }
222    }
223}
224
225/// Sort order for index fields.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
227pub enum SortOrder {
228    /// Ascending order.
229    #[default]
230    Asc,
231    /// Descending order.
232    Desc,
233}
234
235/// Index type.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237pub enum IndexType {
238    /// B-tree index (default).
239    BTree,
240    /// Hash index.
241    Hash,
242    /// GiST index (PostgreSQL).
243    Gist,
244    /// GIN index (PostgreSQL).
245    Gin,
246    /// Full-text search index.
247    FullText,
248}
249
250impl IndexType {
251    /// Parse from string.
252    #[allow(clippy::should_implement_trait)]
253    pub fn from_str(s: &str) -> Option<Self> {
254        match s.to_lowercase().as_str() {
255            "btree" => Some(Self::BTree),
256            "hash" => Some(Self::Hash),
257            "gist" => Some(Self::Gist),
258            "gin" => Some(Self::Gin),
259            "fulltext" => Some(Self::FullText),
260            _ => None,
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    // ==================== RelationType Tests ====================
270
271    #[test]
272    fn test_relation_type_one_to_one() {
273        let rt = RelationType::OneToOne;
274        assert!(rt.is_to_one());
275        assert!(!rt.is_to_many());
276        assert!(!rt.is_from_many());
277    }
278
279    #[test]
280    fn test_relation_type_one_to_many() {
281        let rt = RelationType::OneToMany;
282        assert!(!rt.is_to_one());
283        assert!(rt.is_to_many());
284        assert!(!rt.is_from_many());
285    }
286
287    #[test]
288    fn test_relation_type_many_to_one() {
289        let rt = RelationType::ManyToOne;
290        assert!(rt.is_to_one());
291        assert!(!rt.is_to_many());
292        assert!(rt.is_from_many());
293    }
294
295    #[test]
296    fn test_relation_type_many_to_many() {
297        let rt = RelationType::ManyToMany;
298        assert!(!rt.is_to_one());
299        assert!(rt.is_to_many());
300        assert!(rt.is_from_many());
301    }
302
303    #[test]
304    fn test_relation_type_display() {
305        assert_eq!(format!("{}", RelationType::OneToOne), "1:1");
306        assert_eq!(format!("{}", RelationType::OneToMany), "1:n");
307        assert_eq!(format!("{}", RelationType::ManyToOne), "n:1");
308        assert_eq!(format!("{}", RelationType::ManyToMany), "m:n");
309    }
310
311    #[test]
312    fn test_relation_type_equality() {
313        assert_eq!(RelationType::OneToOne, RelationType::OneToOne);
314        assert_ne!(RelationType::OneToOne, RelationType::OneToMany);
315    }
316
317    // ==================== Relation Tests ====================
318
319    #[test]
320    fn test_relation_new() {
321        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne);
322
323        assert!(rel.name.is_none());
324        assert_eq!(rel.from_model.as_str(), "Post");
325        assert_eq!(rel.from_field.as_str(), "author");
326        assert_eq!(rel.to_model.as_str(), "User");
327        assert!(rel.to_field.is_none());
328        assert_eq!(rel.relation_type, RelationType::ManyToOne);
329        assert!(rel.on_delete.is_none());
330        assert!(rel.on_update.is_none());
331    }
332
333    #[test]
334    fn test_relation_with_name() {
335        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
336            .with_name("PostAuthor");
337
338        assert_eq!(rel.name, Some("PostAuthor".into()));
339    }
340
341    #[test]
342    fn test_relation_with_from_fields() {
343        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
344            .with_from_fields(vec!["author_id".into()]);
345
346        assert_eq!(rel.from_fields, vec!["author_id".to_string()]);
347    }
348
349    #[test]
350    fn test_relation_with_to_fields() {
351        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
352            .with_to_fields(vec!["id".into()]);
353
354        assert_eq!(rel.to_fields, vec!["id".to_string()]);
355    }
356
357    #[test]
358    fn test_relation_with_to_field() {
359        let rel =
360            Relation::new("Post", "author", "User", RelationType::ManyToOne).with_to_field("posts");
361
362        assert_eq!(rel.to_field, Some("posts".into()));
363    }
364
365    #[test]
366    fn test_relation_with_on_delete() {
367        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
368            .with_on_delete(ReferentialAction::Cascade);
369
370        assert_eq!(rel.on_delete, Some(ReferentialAction::Cascade));
371    }
372
373    #[test]
374    fn test_relation_with_on_update() {
375        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
376            .with_on_update(ReferentialAction::Restrict);
377
378        assert_eq!(rel.on_update, Some(ReferentialAction::Restrict));
379    }
380
381    #[test]
382    fn test_relation_is_implicit_many_to_many_true() {
383        let rel = Relation::new("Post", "tags", "Tag", RelationType::ManyToMany);
384        assert!(rel.is_implicit_many_to_many());
385    }
386
387    #[test]
388    fn test_relation_is_implicit_many_to_many_false_explicit() {
389        let rel = Relation::new("Post", "tags", "Tag", RelationType::ManyToMany)
390            .with_from_fields(vec!["post_id".into()]);
391        assert!(!rel.is_implicit_many_to_many());
392    }
393
394    #[test]
395    fn test_relation_is_implicit_many_to_many_false_not_mtm() {
396        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne);
397        assert!(!rel.is_implicit_many_to_many());
398    }
399
400    #[test]
401    fn test_relation_join_table_name_mtm() {
402        let rel = Relation::new("Post", "tags", "Tag", RelationType::ManyToMany);
403        assert_eq!(rel.join_table_name(), Some("_Post_to_Tag".to_string()));
404    }
405
406    #[test]
407    fn test_relation_join_table_name_mtm_sorted() {
408        // Should sort alphabetically
409        let rel = Relation::new("Tag", "posts", "Post", RelationType::ManyToMany);
410        assert_eq!(rel.join_table_name(), Some("_Post_to_Tag".to_string()));
411    }
412
413    #[test]
414    fn test_relation_join_table_name_not_mtm() {
415        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne);
416        assert!(rel.join_table_name().is_none());
417    }
418
419    #[test]
420    fn test_relation_builder_chain() {
421        let rel = Relation::new("Post", "author", "User", RelationType::ManyToOne)
422            .with_name("PostAuthor")
423            .with_from_fields(vec!["author_id".into()])
424            .with_to_fields(vec!["id".into()])
425            .with_to_field("posts")
426            .with_on_delete(ReferentialAction::Cascade)
427            .with_on_update(ReferentialAction::Restrict);
428
429        assert_eq!(rel.name, Some("PostAuthor".into()));
430        assert_eq!(rel.from_fields.len(), 1);
431        assert_eq!(rel.to_fields.len(), 1);
432        assert!(rel.to_field.is_some());
433        assert!(rel.on_delete.is_some());
434        assert!(rel.on_update.is_some());
435    }
436
437    #[test]
438    fn test_relation_equality() {
439        let rel1 = Relation::new("Post", "author", "User", RelationType::ManyToOne);
440        let rel2 = Relation::new("Post", "author", "User", RelationType::ManyToOne);
441
442        assert_eq!(rel1, rel2);
443    }
444
445    // ==================== Index Tests ====================
446
447    #[test]
448    fn test_index_new() {
449        let idx = Index::new(vec![IndexField::asc("email")]);
450
451        assert!(idx.name.is_none());
452        assert_eq!(idx.fields.len(), 1);
453        assert!(!idx.is_unique);
454        assert!(idx.index_type.is_none());
455    }
456
457    #[test]
458    fn test_index_unique() {
459        let idx = Index::unique(vec![IndexField::asc("email")]);
460
461        assert!(idx.is_unique);
462    }
463
464    #[test]
465    fn test_index_with_name() {
466        let idx = Index::new(vec![IndexField::asc("email")]).with_name("idx_user_email");
467
468        assert_eq!(idx.name, Some("idx_user_email".into()));
469    }
470
471    #[test]
472    fn test_index_with_type() {
473        let idx = Index::new(vec![IndexField::asc("data")]).with_type(IndexType::Gin);
474
475        assert_eq!(idx.index_type, Some(IndexType::Gin));
476    }
477
478    #[test]
479    fn test_index_multiple_fields() {
480        let idx = Index::unique(vec![
481            IndexField::asc("first_name"),
482            IndexField::asc("last_name"),
483        ]);
484
485        assert_eq!(idx.fields.len(), 2);
486    }
487
488    // ==================== IndexField Tests ====================
489
490    #[test]
491    fn test_index_field_asc() {
492        let field = IndexField::asc("email");
493
494        assert_eq!(field.name.as_str(), "email");
495        assert_eq!(field.sort, SortOrder::Asc);
496    }
497
498    #[test]
499    fn test_index_field_desc() {
500        let field = IndexField::desc("created_at");
501
502        assert_eq!(field.name.as_str(), "created_at");
503        assert_eq!(field.sort, SortOrder::Desc);
504    }
505
506    #[test]
507    fn test_index_field_equality() {
508        let f1 = IndexField::asc("email");
509        let f2 = IndexField::asc("email");
510        let f3 = IndexField::desc("email");
511
512        assert_eq!(f1, f2);
513        assert_ne!(f1, f3);
514    }
515
516    // ==================== SortOrder Tests ====================
517
518    #[test]
519    fn test_sort_order_default() {
520        let order = SortOrder::default();
521        assert_eq!(order, SortOrder::Asc);
522    }
523
524    #[test]
525    fn test_sort_order_equality() {
526        assert_eq!(SortOrder::Asc, SortOrder::Asc);
527        assert_eq!(SortOrder::Desc, SortOrder::Desc);
528        assert_ne!(SortOrder::Asc, SortOrder::Desc);
529    }
530
531    // ==================== IndexType Tests ====================
532
533    #[test]
534    fn test_index_type_from_str_btree() {
535        assert_eq!(IndexType::from_str("btree"), Some(IndexType::BTree));
536        assert_eq!(IndexType::from_str("BTree"), Some(IndexType::BTree));
537        assert_eq!(IndexType::from_str("BTREE"), Some(IndexType::BTree));
538    }
539
540    #[test]
541    fn test_index_type_from_str_hash() {
542        assert_eq!(IndexType::from_str("hash"), Some(IndexType::Hash));
543        assert_eq!(IndexType::from_str("Hash"), Some(IndexType::Hash));
544    }
545
546    #[test]
547    fn test_index_type_from_str_gist() {
548        assert_eq!(IndexType::from_str("gist"), Some(IndexType::Gist));
549        assert_eq!(IndexType::from_str("GiST"), Some(IndexType::Gist));
550    }
551
552    #[test]
553    fn test_index_type_from_str_gin() {
554        assert_eq!(IndexType::from_str("gin"), Some(IndexType::Gin));
555        assert_eq!(IndexType::from_str("GIN"), Some(IndexType::Gin));
556    }
557
558    #[test]
559    fn test_index_type_from_str_fulltext() {
560        assert_eq!(IndexType::from_str("fulltext"), Some(IndexType::FullText));
561        assert_eq!(IndexType::from_str("FullText"), Some(IndexType::FullText));
562    }
563
564    #[test]
565    fn test_index_type_from_str_unknown() {
566        assert_eq!(IndexType::from_str("unknown"), None);
567        assert_eq!(IndexType::from_str(""), None);
568    }
569
570    #[test]
571    fn test_index_type_equality() {
572        assert_eq!(IndexType::BTree, IndexType::BTree);
573        assert_ne!(IndexType::BTree, IndexType::Hash);
574    }
575}