1use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum RelationType {
8 OneToOne,
10 OneToMany,
12 ManyToOne,
14 ManyToMany,
16}
17
18impl RelationType {
19 pub fn is_many(&self) -> bool {
21 matches!(self, Self::OneToMany | Self::ManyToMany)
22 }
23
24 pub fn is_one(&self) -> bool {
26 matches!(self, Self::OneToOne | Self::ManyToOne)
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct RelationSpec {
33 pub name: String,
35 pub relation_type: RelationType,
37 pub related_model: String,
39 pub related_table: String,
41 pub fields: Vec<String>,
43 pub references: Vec<String>,
45 pub join_table: Option<JoinTableSpec>,
47 pub on_delete: Option<ReferentialAction>,
49 pub on_update: Option<ReferentialAction>,
51}
52
53impl RelationSpec {
54 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 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 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 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 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 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 pub fn on_delete(mut self, action: ReferentialAction) -> Self {
145 self.on_delete = Some(action);
146 self
147 }
148
149 pub fn on_update(mut self, action: ReferentialAction) -> Self {
151 self.on_update = Some(action);
152 self
153 }
154
155 pub fn to_join_clause(&self, parent_alias: &str, child_alias: &str) -> String {
157 if let Some(ref jt) = self.join_table {
158 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 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#[derive(Debug, Clone)]
194pub struct JoinTableSpec {
195 pub table_name: String,
197 pub source_column: String,
199 pub target_column: String,
201}
202
203impl JoinTableSpec {
204 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum ReferentialAction {
221 Cascade,
223 SetNull,
225 SetDefault,
227 Restrict,
229 NoAction,
231}
232
233impl ReferentialAction {
234 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#[derive(Debug, Clone, Default)]
248pub struct RelationRegistry {
249 relations: HashMap<String, RelationSpec>,
250}
251
252impl RelationRegistry {
253 pub fn new() -> Self {
255 Self::default()
256 }
257
258 pub fn register(&mut self, spec: RelationSpec) {
260 self.relations.insert(spec.name.clone(), spec);
261 }
262
263 pub fn get(&self, name: &str) -> Option<&RelationSpec> {
265 self.relations.get(name)
266 }
267
268 pub fn all(&self) -> impl Iterator<Item = &RelationSpec> {
270 self.relations.values()
271 }
272
273 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 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}