Skip to main content

reinhardt_db/orm/
relations.rs

1//! Generic relations for polymorphic model relationships
2//!
3//! This module provides `GenericRelationSet` for reverse lookups in models
4//! that use `GenericForeignKey` to reference the parent model.
5//!
6//! # Overview
7//!
8//! In Django-style contenttypes, a `GenericForeignKey` allows a model to reference
9//! any other model. The `GenericRelationSet` provides the reverse side of this
10//! relationship, enabling queries like "get all comments for this post".
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use reinhardt_db::orm::relations::GenericRelationSet;
16//!
17//! // Post model with generic relations to comments
18//! struct Post {
19//!     id: i64,
20//!     title: String,
21//!     // Reverse relation to Comment model
22//!     comments: GenericRelationSet<Comment>,
23//! }
24//!
25//! // Get all comments for a post
26//! let comments = post.comments.all().await?;
27//! ```
28
29use serde::{Deserialize, Serialize};
30use std::marker::PhantomData;
31
32use crate::orm::Model;
33use crate::orm::custom_manager::CustomManager;
34
35/// A set of objects that have a GenericForeignKey pointing to the owner model
36///
37/// This struct represents a reverse relation for GenericForeignKey relationships.
38/// It provides QuerySet-like operations for querying related objects.
39///
40/// # Type Parameters
41///
42/// - `T`: The model type that has a GenericForeignKey pointing back to the owner
43///
44/// # Example
45///
46/// ```rust
47/// use reinhardt_db::orm::relations::GenericRelationSet;
48///
49/// // Create a relation set configuration
50/// let relation: GenericRelationSet<()> = GenericRelationSet::new(
51///     1,                        // content_type_id of the owner model
52///     42,                       // object_id of the owner instance
53///     "content_type_id",        // field name for content type in related model
54///     "object_id",              // field name for object id in related model
55/// );
56///
57/// assert_eq!(relation.content_type_id(), 1);
58/// assert_eq!(relation.object_id(), 42);
59/// ```
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct GenericRelationSet<T> {
62	/// Content type ID of the owner model
63	content_type_id: i64,
64	/// Object ID of the owner instance
65	object_id: i64,
66	/// Field name for content type in the related model
67	ct_field: String,
68	/// Field name for object id in the related model
69	fk_field: String,
70	/// Phantom data for the related model type
71	#[serde(skip)]
72	_phantom: PhantomData<T>,
73}
74
75impl<T> GenericRelationSet<T> {
76	/// Create a new GenericRelationSet
77	///
78	/// # Arguments
79	///
80	/// - `content_type_id`: Content type ID of the owner model
81	/// - `object_id`: Object ID of the owner instance
82	/// - `ct_field`: Field name for content type in the related model (default: "content_type_id")
83	/// - `fk_field`: Field name for object id in the related model (default: "object_id")
84	///
85	/// # Example
86	///
87	/// ```rust
88	/// use reinhardt_db::orm::relations::GenericRelationSet;
89	///
90	/// let relation: GenericRelationSet<()> = GenericRelationSet::new(
91	///     5, 100, "content_type_id", "object_id"
92	/// );
93	/// ```
94	pub fn new(
95		content_type_id: i64,
96		object_id: i64,
97		ct_field: impl Into<String>,
98		fk_field: impl Into<String>,
99	) -> Self {
100		Self {
101			content_type_id,
102			object_id,
103			ct_field: ct_field.into(),
104			fk_field: fk_field.into(),
105			_phantom: PhantomData,
106		}
107	}
108
109	/// Create a new GenericRelationSet with default field names
110	///
111	/// Uses "content_type_id" and "object_id" as the default field names.
112	///
113	/// # Example
114	///
115	/// ```rust
116	/// use reinhardt_db::orm::relations::GenericRelationSet;
117	///
118	/// let relation: GenericRelationSet<()> = GenericRelationSet::with_defaults(5, 100);
119	/// assert_eq!(relation.ct_field(), "content_type_id");
120	/// assert_eq!(relation.fk_field(), "object_id");
121	/// ```
122	pub fn with_defaults(content_type_id: i64, object_id: i64) -> Self {
123		Self::new(content_type_id, object_id, "content_type_id", "object_id")
124	}
125
126	/// Get the content type ID of the owner model
127	pub fn content_type_id(&self) -> i64 {
128		self.content_type_id
129	}
130
131	/// Get the object ID of the owner instance
132	pub fn object_id(&self) -> i64 {
133		self.object_id
134	}
135
136	/// Get the content type field name
137	pub fn ct_field(&self) -> &str {
138		&self.ct_field
139	}
140
141	/// Get the object id field name
142	pub fn fk_field(&self) -> &str {
143		&self.fk_field
144	}
145
146	/// Generate the WHERE clause for filtering related objects
147	///
148	/// # Example
149	///
150	/// ```rust
151	/// use reinhardt_db::orm::relations::GenericRelationSet;
152	///
153	/// let relation: GenericRelationSet<()> = GenericRelationSet::new(
154	///     1, 42, "content_type_id", "object_id"
155	/// );
156	/// assert_eq!(
157	///     relation.where_clause(),
158	///     "content_type_id = 1 AND object_id = 42"
159	/// );
160	/// ```
161	pub fn where_clause(&self) -> String {
162		format!(
163			"{} = {} AND {} = {}",
164			self.ct_field, self.content_type_id, self.fk_field, self.object_id
165		)
166	}
167
168	/// Generate SQL condition for use in a WHERE clause
169	///
170	/// Returns a tuple of (condition_string, values) for parameterized queries.
171	///
172	/// # Example
173	///
174	/// ```rust
175	/// use reinhardt_db::orm::relations::GenericRelationSet;
176	///
177	/// let relation: GenericRelationSet<()> = GenericRelationSet::new(
178	///     1, 42, "content_type_id", "object_id"
179	/// );
180	/// let (sql, values) = relation.sql_condition();
181	/// assert_eq!(sql, "content_type_id = $1 AND object_id = $2");
182	/// assert_eq!(values, vec![1_i64, 42_i64]);
183	/// ```
184	pub fn sql_condition(&self) -> (String, Vec<i64>) {
185		let sql = format!("{} = $1 AND {} = $2", self.ct_field, self.fk_field);
186		(sql, vec![self.content_type_id, self.object_id])
187	}
188}
189
190impl<T: Model> GenericRelationSet<T> {
191	/// Create a QuerySet for related objects
192	///
193	/// Returns a QuerySet configured to filter by the content type and object ID.
194	/// This allows chaining additional filters before executing the query.
195	///
196	/// # Example
197	///
198	/// ```rust,ignore
199	/// let active_comments = post.comments
200	///     .query()
201	///     .filter(Filter::new("is_active", FilterOperator::Eq, FilterValue::Bool(true)))
202	///     .all()
203	///     .await?;
204	/// ```
205	pub fn query(&self) -> super::query::QuerySet<T> {
206		use crate::orm::query::{Filter, FilterOperator, FilterValue};
207
208		// Create filters for content_type_id and object_id
209		let ct_filter = Filter::new(
210			self.ct_field.clone(),
211			FilterOperator::Eq,
212			FilterValue::Integer(self.content_type_id),
213		);
214		let fk_filter = Filter::new(
215			self.fk_field.clone(),
216			FilterOperator::Eq,
217			FilterValue::Integer(self.object_id),
218		);
219
220		T::objects().all().filter(ct_filter).filter(fk_filter)
221	}
222
223	/// Get all related objects
224	///
225	/// Returns all instances of the related model that have a GenericForeignKey
226	/// pointing to the owner instance.
227	///
228	/// # Returns
229	///
230	/// A Result containing a Vec of related model instances.
231	///
232	/// # Example
233	///
234	/// ```rust,ignore
235	/// let comments = post.comments.all().await?;
236	/// for comment in comments {
237	///     println!("Comment: {}", comment.content);
238	/// }
239	/// ```
240	pub async fn all(&self) -> reinhardt_core::exception::Result<Vec<T>> {
241		self.query().all().await
242	}
243
244	/// Count related objects
245	///
246	/// Returns the count of related model instances.
247	///
248	/// # Example
249	///
250	/// ```rust,ignore
251	/// let comment_count = post.comments.count().await?;
252	/// println!("Post has {} comments", comment_count);
253	/// ```
254	pub async fn count(&self) -> reinhardt_core::exception::Result<usize> {
255		self.query().count().await
256	}
257
258	/// Check if any related objects exist
259	///
260	/// # Example
261	///
262	/// ```rust,ignore
263	/// if post.comments.exists().await? {
264	///     println!("Post has comments");
265	/// }
266	/// ```
267	pub async fn exists(&self) -> reinhardt_core::exception::Result<bool> {
268		Ok(self.count().await? > 0)
269	}
270
271	/// Get first related object
272	///
273	/// # Example
274	///
275	/// ```rust,ignore
276	/// if let Some(first_comment) = post.comments.first().await? {
277	///     println!("First comment: {}", first_comment.content);
278	/// }
279	/// ```
280	pub async fn first(&self) -> reinhardt_core::exception::Result<Option<T>> {
281		self.query().first().await
282	}
283}
284
285/// Configuration for a GenericRelation field in model definition
286///
287/// This struct holds the configuration needed to set up a GenericRelation
288/// on a model, typically used during macro expansion.
289///
290/// # Example
291///
292/// ```rust
293/// use reinhardt_db::orm::relations::GenericRelationConfig;
294///
295/// let config = GenericRelationConfig::new("Comment")
296///     .ct_field("content_type_id")
297///     .fk_field("object_id");
298///
299/// assert_eq!(config.related_model(), "Comment");
300/// ```
301#[non_exhaustive]
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct GenericRelationConfig {
304	/// Name of the related model
305	related_model: String,
306	/// Content type field name in the related model
307	ct_field: String,
308	/// Object id field name in the related model
309	fk_field: String,
310	/// Optional related name for the relation
311	related_name: Option<String>,
312}
313
314impl GenericRelationConfig {
315	/// Create a new GenericRelationConfig
316	///
317	/// # Example
318	///
319	/// ```rust
320	/// use reinhardt_db::orm::relations::GenericRelationConfig;
321	///
322	/// let config = GenericRelationConfig::new("Comment");
323	/// assert_eq!(config.related_model(), "Comment");
324	/// ```
325	pub fn new(related_model: impl Into<String>) -> Self {
326		Self {
327			related_model: related_model.into(),
328			ct_field: "content_type_id".to_string(),
329			fk_field: "object_id".to_string(),
330			related_name: None,
331		}
332	}
333
334	/// Set the content type field name
335	pub fn ct_field(mut self, field: impl Into<String>) -> Self {
336		self.ct_field = field.into();
337		self
338	}
339
340	/// Set the object id field name
341	pub fn fk_field(mut self, field: impl Into<String>) -> Self {
342		self.fk_field = field.into();
343		self
344	}
345
346	/// Set the related name
347	pub fn related_name(mut self, name: impl Into<String>) -> Self {
348		self.related_name = Some(name.into());
349		self
350	}
351
352	/// Get the related model name
353	pub fn related_model(&self) -> &str {
354		&self.related_model
355	}
356
357	/// Get the content type field name
358	pub fn get_ct_field(&self) -> &str {
359		&self.ct_field
360	}
361
362	/// Get the object id field name
363	pub fn get_fk_field(&self) -> &str {
364		&self.fk_field
365	}
366
367	/// Get the related name if set
368	pub fn get_related_name(&self) -> Option<&str> {
369		self.related_name.as_deref()
370	}
371}
372
373#[cfg(test)]
374mod tests {
375	use super::*;
376
377	#[test]
378	fn test_generic_relation_set_new() {
379		let relation: GenericRelationSet<()> =
380			GenericRelationSet::new(1, 42, "content_type_id", "object_id");
381
382		assert_eq!(relation.content_type_id(), 1);
383		assert_eq!(relation.object_id(), 42);
384		assert_eq!(relation.ct_field(), "content_type_id");
385		assert_eq!(relation.fk_field(), "object_id");
386	}
387
388	#[test]
389	fn test_generic_relation_set_with_defaults() {
390		let relation: GenericRelationSet<()> = GenericRelationSet::with_defaults(5, 100);
391
392		assert_eq!(relation.content_type_id(), 5);
393		assert_eq!(relation.object_id(), 100);
394		assert_eq!(relation.ct_field(), "content_type_id");
395		assert_eq!(relation.fk_field(), "object_id");
396	}
397
398	#[test]
399	fn test_generic_relation_set_where_clause() {
400		let relation: GenericRelationSet<()> = GenericRelationSet::new(1, 42, "ct_id", "obj_id");
401
402		assert_eq!(relation.where_clause(), "ct_id = 1 AND obj_id = 42");
403	}
404
405	#[test]
406	fn test_generic_relation_set_sql_condition() {
407		let relation: GenericRelationSet<()> =
408			GenericRelationSet::new(1, 42, "content_type_id", "object_id");
409
410		let (sql, values) = relation.sql_condition();
411		assert_eq!(sql, "content_type_id = $1 AND object_id = $2");
412		assert_eq!(values, vec![1_i64, 42_i64]);
413	}
414
415	#[test]
416	fn test_generic_relation_config_new() {
417		let config = GenericRelationConfig::new("Comment");
418
419		assert_eq!(config.related_model(), "Comment");
420		assert_eq!(config.get_ct_field(), "content_type_id");
421		assert_eq!(config.get_fk_field(), "object_id");
422		assert!(config.get_related_name().is_none());
423	}
424
425	#[test]
426	fn test_generic_relation_config_builder() {
427		let config = GenericRelationConfig::new("Comment")
428			.ct_field("ct_id")
429			.fk_field("obj_id")
430			.related_name("comments");
431
432		assert_eq!(config.related_model(), "Comment");
433		assert_eq!(config.get_ct_field(), "ct_id");
434		assert_eq!(config.get_fk_field(), "obj_id");
435		assert_eq!(config.get_related_name(), Some("comments"));
436	}
437
438	#[test]
439	fn test_generic_relation_set_serialization() {
440		let relation: GenericRelationSet<()> = GenericRelationSet::new(1, 42, "ct", "obj");
441
442		let serialized = serde_json::to_string(&relation).unwrap();
443		assert!(serialized.contains("1"));
444		assert!(serialized.contains("42"));
445
446		let deserialized: GenericRelationSet<()> = serde_json::from_str(&serialized).unwrap();
447		assert_eq!(deserialized.content_type_id(), 1);
448		assert_eq!(deserialized.object_id(), 42);
449	}
450}