reinhardt_db/associations/foreign_key.rs
1//! Foreign key relationship definition
2//!
3//! Provides Foreign Key relationship types for defining one-to-many and many-to-one
4//! relationships between models.
5
6use serde::{Deserialize, Serialize};
7use std::marker::PhantomData;
8
9use super::reverse::{ReverseRelationship, generate_reverse_accessor};
10
11/// Cascade action when the referenced object is deleted or updated
12///
13/// # Examples
14///
15/// ```
16/// use reinhardt_db::associations::CascadeAction;
17///
18/// let action = CascadeAction::Cascade;
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
21pub enum CascadeAction {
22 /// Do nothing (default behavior, may cause constraint violations)
23 #[default]
24 NoAction,
25 /// Restrict deletion/update if dependent objects exist
26 Restrict,
27 /// Set foreign key to NULL when referenced object is deleted/updated
28 SetNull,
29 /// Set foreign key to its default value
30 SetDefault,
31 /// Cascade deletion/update to dependent objects
32 Cascade,
33}
34
35/// Foreign key field configuration
36///
37/// # Type Parameters
38///
39/// * `T` - The type of the referenced model
40/// * `K` - The type of the foreign key field
41///
42/// # Examples
43///
44/// ```
45/// use reinhardt_db::associations::{ForeignKey, CascadeAction};
46///
47/// #[derive(Clone)]
48/// struct User {
49/// id: i64,
50/// name: String,
51/// }
52///
53/// #[derive(Clone)]
54/// struct Post {
55/// id: i64,
56/// title: String,
57/// author_id: i64,
58/// }
59///
60/// // Define foreign key relationship
61/// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
62/// .related_name("posts")
63/// .on_delete(CascadeAction::Cascade);
64/// ```
65#[derive(Debug, Clone)]
66pub struct ForeignKey<T, K> {
67 /// The name of the foreign key field
68 pub field_name: String,
69 /// The name of the related field on the target model (usually "id")
70 pub to_field: String,
71 /// The name of the reverse relation accessor on the target model
72 pub related_name: Option<String>,
73 /// Action to take when referenced object is deleted
74 pub on_delete: CascadeAction,
75 /// Action to take when referenced object is updated
76 pub on_update: CascadeAction,
77 /// Whether the foreign key can be null
78 pub null: bool,
79 /// Database index creation
80 pub db_index: bool,
81 /// Database constraint name
82 pub db_constraint: Option<String>,
83 /// Phantom data for type parameters
84 _phantom_t: PhantomData<T>,
85 _phantom_k: PhantomData<K>,
86}
87
88impl<T, K> ForeignKey<T, K> {
89 /// Create a new foreign key field
90 ///
91 /// # Arguments
92 ///
93 /// * `field_name` - The name of the foreign key field
94 ///
95 /// # Examples
96 ///
97 /// ```
98 /// use reinhardt_db::associations::ForeignKey;
99 ///
100 /// #[derive(Clone)]
101 /// struct User {
102 /// id: i64,
103 /// }
104 ///
105 /// let fk: ForeignKey<User, i64> = ForeignKey::new("user_id");
106 /// assert_eq!(fk.field_name(), "user_id");
107 /// ```
108 pub fn new(field_name: impl Into<String>) -> Self {
109 Self {
110 field_name: field_name.into(),
111 to_field: "id".to_string(),
112 related_name: None,
113 on_delete: CascadeAction::default(),
114 on_update: CascadeAction::default(),
115 null: false,
116 db_index: true,
117 db_constraint: None,
118 _phantom_t: PhantomData,
119 _phantom_k: PhantomData,
120 }
121 }
122
123 /// Set the related field name on the target model
124 ///
125 /// # Examples
126 ///
127 /// ```
128 /// use reinhardt_db::associations::ForeignKey;
129 ///
130 /// #[derive(Clone)]
131 /// struct User {
132 /// id: i64,
133 /// }
134 ///
135 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
136 /// .to_field("user_id");
137 /// assert_eq!(fk.get_to_field(), "user_id");
138 /// ```
139 pub fn to_field(mut self, to_field: impl Into<String>) -> Self {
140 self.to_field = to_field.into();
141 self
142 }
143
144 /// Set the reverse relation accessor name
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use reinhardt_db::associations::ForeignKey;
150 ///
151 /// #[derive(Clone)]
152 /// struct User {
153 /// id: i64,
154 /// }
155 ///
156 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
157 /// .related_name("posts");
158 /// assert_eq!(fk.get_related_name(), Some("posts"));
159 /// ```
160 pub fn related_name(mut self, name: impl Into<String>) -> Self {
161 self.related_name = Some(name.into());
162 self
163 }
164
165 /// Set the on_delete cascade action
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// use reinhardt_db::associations::{ForeignKey, CascadeAction};
171 ///
172 /// #[derive(Clone)]
173 /// struct User {
174 /// id: i64,
175 /// }
176 ///
177 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
178 /// .on_delete(CascadeAction::Cascade);
179 /// assert_eq!(fk.get_on_delete(), CascadeAction::Cascade);
180 /// ```
181 pub fn on_delete(mut self, action: CascadeAction) -> Self {
182 self.on_delete = action;
183 self
184 }
185
186 /// Set the on_update cascade action
187 ///
188 /// # Examples
189 ///
190 /// ```
191 /// use reinhardt_db::associations::{ForeignKey, CascadeAction};
192 ///
193 /// #[derive(Clone)]
194 /// struct User {
195 /// id: i64,
196 /// }
197 ///
198 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
199 /// .on_update(CascadeAction::Cascade);
200 /// assert_eq!(fk.get_on_update(), CascadeAction::Cascade);
201 /// ```
202 pub fn on_update(mut self, action: CascadeAction) -> Self {
203 self.on_update = action;
204 self
205 }
206
207 /// Set whether the foreign key can be null
208 ///
209 /// # Examples
210 ///
211 /// ```
212 /// use reinhardt_db::associations::ForeignKey;
213 ///
214 /// #[derive(Clone)]
215 /// struct User {
216 /// id: i64,
217 /// }
218 ///
219 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
220 /// .null(true);
221 /// assert!(fk.is_null());
222 /// ```
223 pub fn null(mut self, null: bool) -> Self {
224 self.null = null;
225 self
226 }
227
228 /// Set whether to create database index
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use reinhardt_db::associations::ForeignKey;
234 ///
235 /// #[derive(Clone)]
236 /// struct User {
237 /// id: i64,
238 /// }
239 ///
240 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
241 /// .db_index(false);
242 /// assert!(!fk.has_db_index());
243 /// ```
244 pub fn db_index(mut self, db_index: bool) -> Self {
245 self.db_index = db_index;
246 self
247 }
248
249 /// Set the database constraint name
250 ///
251 /// # Examples
252 ///
253 /// ```
254 /// use reinhardt_db::associations::ForeignKey;
255 ///
256 /// #[derive(Clone)]
257 /// struct User {
258 /// id: i64,
259 /// }
260 ///
261 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
262 /// .db_constraint("fk_posts_author");
263 /// assert_eq!(fk.get_db_constraint(), Some("fk_posts_author"));
264 /// ```
265 pub fn db_constraint(mut self, name: impl Into<String>) -> Self {
266 self.db_constraint = Some(name.into());
267 self
268 }
269
270 /// Get the field name
271 pub fn field_name(&self) -> &str {
272 &self.field_name
273 }
274
275 /// Get the to_field name
276 pub fn get_to_field(&self) -> &str {
277 &self.to_field
278 }
279
280 /// Get the related_name
281 pub fn get_related_name(&self) -> Option<&str> {
282 self.related_name.as_deref()
283 }
284
285 /// Get the on_delete action
286 pub fn get_on_delete(&self) -> CascadeAction {
287 self.on_delete
288 }
289
290 /// Get the on_update action
291 pub fn get_on_update(&self) -> CascadeAction {
292 self.on_update
293 }
294
295 /// Check if null is allowed
296 pub fn is_null(&self) -> bool {
297 self.null
298 }
299
300 /// Check if database index should be created
301 pub fn has_db_index(&self) -> bool {
302 self.db_index
303 }
304
305 /// Get the database constraint name
306 pub fn get_db_constraint(&self) -> Option<&str> {
307 self.db_constraint.as_deref()
308 }
309}
310
311impl<T, K> Default for ForeignKey<T, K> {
312 fn default() -> Self {
313 Self::new("id")
314 }
315}
316
317impl<T, K> ReverseRelationship for ForeignKey<T, K> {
318 /// Get the reverse accessor name, generating one if not explicitly set
319 ///
320 /// # Examples
321 ///
322 /// ```
323 /// use reinhardt_db::associations::{ForeignKey, ReverseRelationship};
324 ///
325 /// #[derive(Clone)]
326 /// struct User {
327 /// id: i64,
328 /// }
329 ///
330 /// let fk: ForeignKey<User, i64> = ForeignKey::new("author_id");
331 /// assert_eq!(fk.get_or_generate_reverse_name("Post"), "post_set");
332 ///
333 /// let fk_with_name: ForeignKey<User, i64> = ForeignKey::new("author_id")
334 /// .related_name("posts");
335 /// assert_eq!(fk_with_name.get_or_generate_reverse_name("Post"), "posts");
336 /// ```
337 fn get_or_generate_reverse_name(&self, model_name: &str) -> String {
338 self.related_name
339 .clone()
340 .unwrap_or_else(|| generate_reverse_accessor(model_name))
341 }
342
343 fn explicit_reverse_name(&self) -> Option<&str> {
344 self.related_name.as_deref()
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 // Allow dead_code: test model struct used for trait implementation verification
353 #[allow(dead_code)]
354 #[derive(Clone)]
355 struct User {
356 id: i64,
357 name: String,
358 }
359
360 #[test]
361 fn test_foreign_key_creation() {
362 let fk: ForeignKey<User, i64> = ForeignKey::new("author_id");
363 assert_eq!(fk.field_name(), "author_id");
364 assert_eq!(fk.get_to_field(), "id");
365 assert_eq!(fk.get_related_name(), None);
366 assert_eq!(fk.get_on_delete(), CascadeAction::NoAction);
367 assert_eq!(fk.get_on_update(), CascadeAction::NoAction);
368 assert!(!fk.is_null());
369 assert!(fk.has_db_index());
370 }
371
372 #[test]
373 fn test_foreign_key_builder() {
374 let fk: ForeignKey<User, i64> = ForeignKey::new("author_id")
375 .related_name("posts")
376 .on_delete(CascadeAction::Cascade)
377 .on_update(CascadeAction::SetNull)
378 .null(true)
379 .db_index(false)
380 .db_constraint("fk_posts_author");
381
382 assert_eq!(fk.field_name(), "author_id");
383 assert_eq!(fk.get_related_name(), Some("posts"));
384 assert_eq!(fk.get_on_delete(), CascadeAction::Cascade);
385 assert_eq!(fk.get_on_update(), CascadeAction::SetNull);
386 assert!(fk.is_null());
387 assert!(!fk.has_db_index());
388 assert_eq!(fk.get_db_constraint(), Some("fk_posts_author"));
389 }
390
391 #[test]
392 fn test_cascade_action_default() {
393 assert_eq!(CascadeAction::default(), CascadeAction::NoAction);
394 }
395
396 #[test]
397 fn test_cascade_actions() {
398 let actions = vec![
399 CascadeAction::NoAction,
400 CascadeAction::Restrict,
401 CascadeAction::SetNull,
402 CascadeAction::SetDefault,
403 CascadeAction::Cascade,
404 ];
405
406 for action in actions {
407 let fk: ForeignKey<User, i64> = ForeignKey::new("test_id").on_delete(action);
408 assert_eq!(fk.get_on_delete(), action);
409 }
410 }
411
412 #[test]
413 fn test_to_field_customization() {
414 let fk: ForeignKey<User, i64> = ForeignKey::new("author_id").to_field("user_id");
415 assert_eq!(fk.get_to_field(), "user_id");
416 }
417
418 #[test]
419 fn test_null_configuration() {
420 let fk1: ForeignKey<User, i64> = ForeignKey::new("author_id").null(true);
421 assert!(fk1.is_null());
422
423 let fk2: ForeignKey<User, i64> = ForeignKey::new("author_id").null(false);
424 assert!(!fk2.is_null());
425 }
426
427 #[test]
428 fn test_db_index_configuration() {
429 let fk1: ForeignKey<User, i64> = ForeignKey::new("author_id").db_index(true);
430 assert!(fk1.has_db_index());
431
432 let fk2: ForeignKey<User, i64> = ForeignKey::new("author_id").db_index(false);
433 assert!(!fk2.has_db_index());
434 }
435}