prax_query/relations/
spec.rs

1//! Relation specification types.
2
3use std::collections::HashMap;
4
5/// Type of relation between models.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum RelationType {
8    /// One-to-one relation (e.g., User has one Profile).
9    OneToOne,
10    /// One-to-many relation (e.g., User has many Posts).
11    OneToMany,
12    /// Many-to-one relation (e.g., Post belongs to User).
13    ManyToOne,
14    /// Many-to-many relation (e.g., Post has many Tags).
15    ManyToMany,
16}
17
18impl RelationType {
19    /// Check if this relation returns multiple records.
20    pub fn is_many(&self) -> bool {
21        matches!(self, Self::OneToMany | Self::ManyToMany)
22    }
23
24    /// Check if this relation returns a single record.
25    pub fn is_one(&self) -> bool {
26        matches!(self, Self::OneToOne | Self::ManyToOne)
27    }
28}
29
30/// Specification for a relation between models.
31#[derive(Debug, Clone)]
32pub struct RelationSpec {
33    /// Name of the relation (field name).
34    pub name: String,
35    /// Type of relation.
36    pub relation_type: RelationType,
37    /// Name of the related model.
38    pub related_model: String,
39    /// Name of the related table.
40    pub related_table: String,
41    /// Foreign key fields on this model.
42    pub fields: Vec<String>,
43    /// Referenced fields on the related model.
44    pub references: Vec<String>,
45    /// Join table for many-to-many relations.
46    pub join_table: Option<JoinTableSpec>,
47    /// On delete action.
48    pub on_delete: Option<ReferentialAction>,
49    /// On update action.
50    pub on_update: Option<ReferentialAction>,
51}
52
53impl RelationSpec {
54    /// Create a one-to-one relation spec.
55    pub fn one_to_one(
56        name: impl Into<String>,
57        related_model: impl Into<String>,
58        related_table: impl Into<String>,
59    ) -> Self {
60        Self {
61            name: name.into(),
62            relation_type: RelationType::OneToOne,
63            related_model: related_model.into(),
64            related_table: related_table.into(),
65            fields: Vec::new(),
66            references: Vec::new(),
67            join_table: None,
68            on_delete: None,
69            on_update: None,
70        }
71    }
72
73    /// Create a one-to-many relation spec.
74    pub fn one_to_many(
75        name: impl Into<String>,
76        related_model: impl Into<String>,
77        related_table: impl Into<String>,
78    ) -> Self {
79        Self {
80            name: name.into(),
81            relation_type: RelationType::OneToMany,
82            related_model: related_model.into(),
83            related_table: related_table.into(),
84            fields: Vec::new(),
85            references: Vec::new(),
86            join_table: None,
87            on_delete: None,
88            on_update: None,
89        }
90    }
91
92    /// Create a many-to-one relation spec.
93    pub fn many_to_one(
94        name: impl Into<String>,
95        related_model: impl Into<String>,
96        related_table: impl Into<String>,
97    ) -> Self {
98        Self {
99            name: name.into(),
100            relation_type: RelationType::ManyToOne,
101            related_model: related_model.into(),
102            related_table: related_table.into(),
103            fields: Vec::new(),
104            references: Vec::new(),
105            join_table: None,
106            on_delete: None,
107            on_update: None,
108        }
109    }
110
111    /// Create a many-to-many relation spec.
112    pub fn many_to_many(
113        name: impl Into<String>,
114        related_model: impl Into<String>,
115        related_table: impl Into<String>,
116        join_table: JoinTableSpec,
117    ) -> Self {
118        Self {
119            name: name.into(),
120            relation_type: RelationType::ManyToMany,
121            related_model: related_model.into(),
122            related_table: related_table.into(),
123            fields: Vec::new(),
124            references: Vec::new(),
125            join_table: Some(join_table),
126            on_delete: None,
127            on_update: None,
128        }
129    }
130
131    /// Set the foreign key fields.
132    pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
133        self.fields = fields.into_iter().map(Into::into).collect();
134        self
135    }
136
137    /// Set the referenced fields.
138    pub fn references(mut self, refs: impl IntoIterator<Item = impl Into<String>>) -> Self {
139        self.references = refs.into_iter().map(Into::into).collect();
140        self
141    }
142
143    /// Set the on delete action.
144    pub fn on_delete(mut self, action: ReferentialAction) -> Self {
145        self.on_delete = Some(action);
146        self
147    }
148
149    /// Set the on update action.
150    pub fn on_update(mut self, action: ReferentialAction) -> Self {
151        self.on_update = Some(action);
152        self
153    }
154
155    /// Generate the JOIN clause for this relation.
156    pub fn to_join_clause(&self, parent_alias: &str, child_alias: &str) -> String {
157        if let Some(ref jt) = self.join_table {
158            // Many-to-many join through join table
159            format!(
160                "JOIN {} ON {}.{} = {}.{} JOIN {} AS {} ON {}.{} = {}.{}",
161                jt.table_name,
162                parent_alias,
163                self.fields.first().unwrap_or(&"id".to_string()),
164                jt.table_name,
165                jt.source_column,
166                self.related_table,
167                child_alias,
168                jt.table_name,
169                jt.target_column,
170                child_alias,
171                self.references.first().unwrap_or(&"id".to_string()),
172            )
173        } else {
174            // Direct join
175            let join_conditions: Vec<_> = self
176                .fields
177                .iter()
178                .zip(self.references.iter())
179                .map(|(f, r)| format!("{}.{} = {}.{}", parent_alias, f, child_alias, r))
180                .collect();
181
182            format!(
183                "JOIN {} AS {} ON {}",
184                self.related_table,
185                child_alias,
186                join_conditions.join(" AND ")
187            )
188        }
189    }
190}
191
192/// Specification for a join table (many-to-many).
193#[derive(Debug, Clone)]
194pub struct JoinTableSpec {
195    /// Name of the join table.
196    pub table_name: String,
197    /// Column referencing the source model.
198    pub source_column: String,
199    /// Column referencing the target model.
200    pub target_column: String,
201}
202
203impl JoinTableSpec {
204    /// Create a new join table spec.
205    pub fn new(
206        table_name: impl Into<String>,
207        source_column: impl Into<String>,
208        target_column: impl Into<String>,
209    ) -> Self {
210        Self {
211            table_name: table_name.into(),
212            source_column: source_column.into(),
213            target_column: target_column.into(),
214        }
215    }
216}
217
218/// Referential action for cascading operations.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum ReferentialAction {
221    /// Cascade the operation to related records.
222    Cascade,
223    /// Set the foreign key to null.
224    SetNull,
225    /// Set the foreign key to default value.
226    SetDefault,
227    /// Restrict the operation if related records exist.
228    Restrict,
229    /// No action (let database handle).
230    NoAction,
231}
232
233impl ReferentialAction {
234    /// Get the SQL keyword for this action.
235    pub fn as_sql(&self) -> &'static str {
236        match self {
237            Self::Cascade => "CASCADE",
238            Self::SetNull => "SET NULL",
239            Self::SetDefault => "SET DEFAULT",
240            Self::Restrict => "RESTRICT",
241            Self::NoAction => "NO ACTION",
242        }
243    }
244}
245
246/// Registry of relation specifications for a model.
247#[derive(Debug, Clone, Default)]
248pub struct RelationRegistry {
249    relations: HashMap<String, RelationSpec>,
250}
251
252impl RelationRegistry {
253    /// Create a new empty registry.
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    /// Register a relation.
259    pub fn register(&mut self, spec: RelationSpec) {
260        self.relations.insert(spec.name.clone(), spec);
261    }
262
263    /// Get a relation by name.
264    pub fn get(&self, name: &str) -> Option<&RelationSpec> {
265        self.relations.get(name)
266    }
267
268    /// Get all relations.
269    pub fn all(&self) -> impl Iterator<Item = &RelationSpec> {
270        self.relations.values()
271    }
272
273    /// Get all one-to-many relations.
274    pub fn one_to_many(&self) -> impl Iterator<Item = &RelationSpec> {
275        self.relations
276            .values()
277            .filter(|r| r.relation_type == RelationType::OneToMany)
278    }
279
280    /// Get all many-to-one relations.
281    pub fn many_to_one(&self) -> impl Iterator<Item = &RelationSpec> {
282        self.relations
283            .values()
284            .filter(|r| r.relation_type == RelationType::ManyToOne)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_relation_type() {
294        assert!(RelationType::OneToMany.is_many());
295        assert!(RelationType::ManyToMany.is_many());
296        assert!(!RelationType::OneToOne.is_many());
297        assert!(RelationType::OneToOne.is_one());
298    }
299
300    #[test]
301    fn test_relation_spec() {
302        let spec = RelationSpec::one_to_many("posts", "Post", "posts")
303            .fields(["id"])
304            .references(["author_id"]);
305
306        assert_eq!(spec.name, "posts");
307        assert_eq!(spec.relation_type, RelationType::OneToMany);
308        assert_eq!(spec.fields, vec!["id"]);
309        assert_eq!(spec.references, vec!["author_id"]);
310    }
311
312    #[test]
313    fn test_join_table_spec() {
314        let jt = JoinTableSpec::new("_post_tags", "post_id", "tag_id");
315        assert_eq!(jt.table_name, "_post_tags");
316        assert_eq!(jt.source_column, "post_id");
317        assert_eq!(jt.target_column, "tag_id");
318    }
319
320    #[test]
321    fn test_referential_action() {
322        assert_eq!(ReferentialAction::Cascade.as_sql(), "CASCADE");
323        assert_eq!(ReferentialAction::SetNull.as_sql(), "SET NULL");
324    }
325
326    #[test]
327    fn test_relation_registry() {
328        let mut registry = RelationRegistry::new();
329        registry.register(RelationSpec::one_to_many("posts", "Post", "posts"));
330        registry.register(RelationSpec::many_to_one("author", "User", "users"));
331
332        assert!(registry.get("posts").is_some());
333        assert!(registry.get("author").is_some());
334        assert!(registry.get("nonexistent").is_none());
335    }
336}