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 pub map: Option<String>,
53}
54
55impl RelationSpec {
56 pub fn one_to_one(
58 name: impl Into<String>,
59 related_model: impl Into<String>,
60 related_table: impl Into<String>,
61 ) -> Self {
62 Self {
63 name: name.into(),
64 relation_type: RelationType::OneToOne,
65 related_model: related_model.into(),
66 related_table: related_table.into(),
67 fields: Vec::new(),
68 references: Vec::new(),
69 join_table: None,
70 on_delete: None,
71 on_update: None,
72 map: None,
73 }
74 }
75
76 pub fn one_to_many(
78 name: impl Into<String>,
79 related_model: impl Into<String>,
80 related_table: impl Into<String>,
81 ) -> Self {
82 Self {
83 name: name.into(),
84 relation_type: RelationType::OneToMany,
85 related_model: related_model.into(),
86 related_table: related_table.into(),
87 fields: Vec::new(),
88 references: Vec::new(),
89 join_table: None,
90 on_delete: None,
91 on_update: None,
92 map: None,
93 }
94 }
95
96 pub fn many_to_one(
98 name: impl Into<String>,
99 related_model: impl Into<String>,
100 related_table: impl Into<String>,
101 ) -> Self {
102 Self {
103 name: name.into(),
104 relation_type: RelationType::ManyToOne,
105 related_model: related_model.into(),
106 related_table: related_table.into(),
107 fields: Vec::new(),
108 references: Vec::new(),
109 join_table: None,
110 on_delete: None,
111 on_update: None,
112 map: None,
113 }
114 }
115
116 pub fn many_to_many(
118 name: impl Into<String>,
119 related_model: impl Into<String>,
120 related_table: impl Into<String>,
121 join_table: JoinTableSpec,
122 ) -> Self {
123 Self {
124 name: name.into(),
125 relation_type: RelationType::ManyToMany,
126 related_model: related_model.into(),
127 related_table: related_table.into(),
128 fields: Vec::new(),
129 references: Vec::new(),
130 join_table: Some(join_table),
131 on_delete: None,
132 on_update: None,
133 map: None,
134 }
135 }
136
137 pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
139 self.fields = fields.into_iter().map(Into::into).collect();
140 self
141 }
142
143 pub fn references(mut self, refs: impl IntoIterator<Item = impl Into<String>>) -> Self {
145 self.references = refs.into_iter().map(Into::into).collect();
146 self
147 }
148
149 pub fn on_delete(mut self, action: ReferentialAction) -> Self {
151 self.on_delete = Some(action);
152 self
153 }
154
155 pub fn on_update(mut self, action: ReferentialAction) -> Self {
157 self.on_update = Some(action);
158 self
159 }
160
161 pub fn map(mut self, name: impl Into<String>) -> Self {
163 self.map = Some(name.into());
164 self
165 }
166
167 pub fn to_join_clause(&self, parent_alias: &str, child_alias: &str) -> String {
169 if let Some(ref jt) = self.join_table {
170 format!(
172 "JOIN {} ON {}.{} = {}.{} JOIN {} AS {} ON {}.{} = {}.{}",
173 jt.table_name,
174 parent_alias,
175 self.fields.first().unwrap_or(&"id".to_string()),
176 jt.table_name,
177 jt.source_column,
178 self.related_table,
179 child_alias,
180 jt.table_name,
181 jt.target_column,
182 child_alias,
183 self.references.first().unwrap_or(&"id".to_string()),
184 )
185 } else {
186 let join_conditions: Vec<_> = self
188 .fields
189 .iter()
190 .zip(self.references.iter())
191 .map(|(f, r)| format!("{}.{} = {}.{}", parent_alias, f, child_alias, r))
192 .collect();
193
194 format!(
195 "JOIN {} AS {} ON {}",
196 self.related_table,
197 child_alias,
198 join_conditions.join(" AND ")
199 )
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct JoinTableSpec {
207 pub table_name: String,
209 pub source_column: String,
211 pub target_column: String,
213}
214
215impl JoinTableSpec {
216 pub fn new(
218 table_name: impl Into<String>,
219 source_column: impl Into<String>,
220 target_column: impl Into<String>,
221 ) -> Self {
222 Self {
223 table_name: table_name.into(),
224 source_column: source_column.into(),
225 target_column: target_column.into(),
226 }
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub enum ReferentialAction {
233 Cascade,
235 SetNull,
237 SetDefault,
239 Restrict,
241 NoAction,
243}
244
245impl ReferentialAction {
246 pub fn as_sql(&self) -> &'static str {
248 match self {
249 Self::Cascade => "CASCADE",
250 Self::SetNull => "SET NULL",
251 Self::SetDefault => "SET DEFAULT",
252 Self::Restrict => "RESTRICT",
253 Self::NoAction => "NO ACTION",
254 }
255 }
256}
257
258#[derive(Debug, Clone, Default)]
260pub struct RelationRegistry {
261 relations: HashMap<String, RelationSpec>,
262}
263
264impl RelationRegistry {
265 pub fn new() -> Self {
267 Self::default()
268 }
269
270 pub fn register(&mut self, spec: RelationSpec) {
272 self.relations.insert(spec.name.clone(), spec);
273 }
274
275 pub fn get(&self, name: &str) -> Option<&RelationSpec> {
277 self.relations.get(name)
278 }
279
280 pub fn all(&self) -> impl Iterator<Item = &RelationSpec> {
282 self.relations.values()
283 }
284
285 pub fn one_to_many(&self) -> impl Iterator<Item = &RelationSpec> {
287 self.relations
288 .values()
289 .filter(|r| r.relation_type == RelationType::OneToMany)
290 }
291
292 pub fn many_to_one(&self) -> impl Iterator<Item = &RelationSpec> {
294 self.relations
295 .values()
296 .filter(|r| r.relation_type == RelationType::ManyToOne)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_relation_type() {
306 assert!(RelationType::OneToMany.is_many());
307 assert!(RelationType::ManyToMany.is_many());
308 assert!(!RelationType::OneToOne.is_many());
309 assert!(RelationType::OneToOne.is_one());
310 }
311
312 #[test]
313 fn test_relation_spec() {
314 let spec = RelationSpec::one_to_many("posts", "Post", "posts")
315 .fields(["id"])
316 .references(["author_id"]);
317
318 assert_eq!(spec.name, "posts");
319 assert_eq!(spec.relation_type, RelationType::OneToMany);
320 assert_eq!(spec.fields, vec!["id"]);
321 assert_eq!(spec.references, vec!["author_id"]);
322 }
323
324 #[test]
325 fn test_join_table_spec() {
326 let jt = JoinTableSpec::new("_post_tags", "post_id", "tag_id");
327 assert_eq!(jt.table_name, "_post_tags");
328 assert_eq!(jt.source_column, "post_id");
329 assert_eq!(jt.target_column, "tag_id");
330 }
331
332 #[test]
333 fn test_referential_action() {
334 assert_eq!(ReferentialAction::Cascade.as_sql(), "CASCADE");
335 assert_eq!(ReferentialAction::SetNull.as_sql(), "SET NULL");
336 }
337
338 #[test]
339 fn test_relation_registry() {
340 let mut registry = RelationRegistry::new();
341 registry.register(RelationSpec::one_to_many("posts", "Post", "posts"));
342 registry.register(RelationSpec::many_to_one("author", "User", "users"));
343
344 assert!(registry.get("posts").is_some());
345 assert!(registry.get("author").is_some());
346 assert!(registry.get("nonexistent").is_none());
347 }
348}