Skip to main content

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}