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}