Skip to main content

rust_ef/
relations.rs

1//! Relationship types (`BelongsTo<T>`, `HasMany<T>`, `HasOne<T>`).
2//!
3//! These types serve as container wrappers for navigation properties,
4//! analogous to how EFCore represents navigation properties in the model.
5//!
6//! They are pure marker/container types and do NOT impose entity trait
7//! bounds  - ?the constraint belongs at the usage site (DbContext, builders),
8//! not on the container itself.
9//!
10//! ## Lazy loading (v1.1+)
11//!
12//! Each container holds an optional [`Arc<dyn LazyContext>`] and a `loaded`
13//! flag. When lazy loading is enabled on the `DbContext`, `to_list()`
14//! attaches a `LazyContext` to every navigation container on every
15//! materialized entity. The user can then call `nav.load().await` to
16//! trigger a single-entity navigation query on first access; subsequent
17//! accesses read from the in-memory cache.
18//!
19//! `Clone` intentionally drops the lazy context and resets `loaded` to
20//! `false`, matching the v1.0 semantics where cloning a navigation
21//! container yields an empty (unloaded) copy.
22
23use crate::entity::{IEntityType, IFromRow, ILazyInit};
24use crate::error::{EFError, EFResult};
25use crate::lazy::{load_collection_lazy, load_scalar_lazy, LazyContext, MAX_LAZY_DEPTH};
26use std::marker::PhantomData;
27use std::sync::Arc;
28
29// ---------------------------------------------------------------------------
30// BelongsTo<T>
31// ---------------------------------------------------------------------------
32
33/// Represents a "belongs-to" navigation  - ?the dependent side of a one-to-many
34/// or one-to-one relationship where the foreign key lives on this entity.
35///
36/// Corresponds to EFCore's reference navigation property.
37pub struct BelongsTo<T> {
38    inner: Option<Box<T>>,
39    lazy_ctx: Option<Arc<dyn LazyContext>>,
40    loaded: bool,
41    _phantom: PhantomData<T>,
42}
43
44impl<T> BelongsTo<T> {
45    pub fn new() -> Self {
46        Self {
47            inner: None,
48            lazy_ctx: None,
49            loaded: false,
50            _phantom: PhantomData,
51        }
52    }
53
54    pub fn with(entity: T) -> Self {
55        Self {
56            inner: Some(Box::new(entity)),
57            lazy_ctx: None,
58            loaded: true,
59            _phantom: PhantomData,
60        }
61    }
62
63    pub fn get(&self) -> Option<&T> {
64        self.inner.as_deref()
65    }
66
67    pub fn get_mut(&mut self) -> Option<&mut T> {
68        self.inner.as_deref_mut()
69    }
70
71    /// Extracts the related entity, leaving the container empty and unloaded.
72    /// Used by batch nested-include loading to collect related entities
73    /// across multiple parents into a single slice for batch SQL.
74    pub fn take(&mut self) -> Option<T> {
75        let taken = self.inner.take();
76        self.loaded = false;
77        taken.map(|b| *b)
78    }
79
80    /// Returns `true` if the navigation has been loaded (either eagerly
81    /// via `Include` or lazily via [`load`]).
82    pub fn is_loaded(&self) -> bool {
83        self.loaded
84    }
85
86    /// Attaches a lazy-loading context to this container.
87    ///
88    /// Called by the `#[derive(EntityType)]` macro's `ILazyInit`
89    /// implementation when lazy loading is enabled on the `DbContext`.
90    pub fn set_lazy_context(&mut self, ctx: Arc<dyn LazyContext>) {
91        self.lazy_ctx = Some(ctx);
92    }
93
94    /// Triggers lazy loading of this reference navigation.
95    ///
96    /// If the navigation is already loaded, this is a no-op. If no
97    /// [`LazyContext`] is attached (lazy loading disabled), returns
98    /// `Ok(())` without loading.
99    ///
100    /// After this call, [`get`] returns the loaded entity (or `None` if
101    /// no matching row was found).
102    pub async fn load(&mut self) -> EFResult<()>
103    where
104        T: IFromRow + IEntityType + ILazyInit,
105    {
106        if self.loaded {
107            return Ok(());
108        }
109        let Some(ctx) = self.lazy_ctx.clone() else {
110            // No lazy context attached — lazy loading disabled.
111            return Ok(());
112        };
113        if ctx.depth() >= MAX_LAZY_DEPTH {
114            return Err(EFError::other("lazy loading recursion limit exceeded"));
115        }
116        let maybe_entity = load_scalar_lazy::<T>(ctx.as_ref()).await?;
117        if let Some(mut entity) = maybe_entity {
118            // Attach lazy contexts to the child for nested lazy loading.
119            let provider = ctx.provider().clone();
120            let filter_map = ctx.filter_map().map(|m| Arc::new(m.clone()));
121            entity.attach_lazy_contexts(provider, filter_map, ctx.depth() + 1);
122            self.inner = Some(Box::new(entity));
123        }
124        self.loaded = true;
125        Ok(())
126    }
127}
128
129impl<T> Default for BelongsTo<T> {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135impl<T> Clone for BelongsTo<T> {
136    fn clone(&self) -> Self {
137        Self {
138            inner: None, // Navigation properties are not deep-cloned
139            lazy_ctx: None,
140            loaded: false,
141            _phantom: PhantomData,
142        }
143    }
144}
145
146impl<T> std::fmt::Debug for BelongsTo<T> {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("BelongsTo")
149            .field("loaded", &self.loaded)
150            .finish()
151    }
152}
153
154// ---------------------------------------------------------------------------
155// HasMany<T>
156// ---------------------------------------------------------------------------
157
158/// Represents a "has-many" navigation  - ?a collection of related entities.
159///
160/// Corresponds to EFCore's collection navigation property
161/// (e.g., `ICollection<Post>`).
162pub struct HasMany<T, Join = ()> {
163    items: Vec<T>,
164    lazy_ctx: Option<Arc<dyn LazyContext>>,
165    loaded: bool,
166    _phantom: PhantomData<(T, Join)>,
167}
168
169impl<T, Join> HasMany<T, Join> {
170    pub fn new() -> Self {
171        Self {
172            items: Vec::new(),
173            lazy_ctx: None,
174            loaded: false,
175            _phantom: PhantomData,
176        }
177    }
178
179    pub fn with(items: Vec<T>) -> Self {
180        Self {
181            items,
182            lazy_ctx: None,
183            loaded: true,
184            _phantom: PhantomData,
185        }
186    }
187
188    pub fn items(&self) -> &[T] {
189        &self.items
190    }
191
192    pub fn items_mut(&mut self) -> &mut Vec<T> {
193        &mut self.items
194    }
195
196    pub fn add(&mut self, item: T) {
197        self.items.push(item);
198    }
199
200    pub fn remove(&mut self, index: usize) -> Option<T> {
201        if index < self.items.len() {
202            Some(self.items.remove(index))
203        } else {
204            None
205        }
206    }
207
208    pub fn len(&self) -> usize {
209        self.items.len()
210    }
211
212    pub fn is_empty(&self) -> bool {
213        self.items.is_empty()
214    }
215
216    /// Returns `true` if the navigation has been loaded (either eagerly
217    /// via `Include` or lazily via [`load`]).
218    pub fn is_loaded(&self) -> bool {
219        self.loaded
220    }
221
222    /// Attaches a lazy-loading context to this container.
223    ///
224    /// Called by the `#[derive(EntityType)]` macro's `ILazyInit`
225    /// implementation when lazy loading is enabled on the `DbContext`.
226    pub fn set_lazy_context(&mut self, ctx: Arc<dyn LazyContext>) {
227        self.lazy_ctx = Some(ctx);
228    }
229
230    /// Triggers lazy loading of this collection navigation.
231    ///
232    /// If the navigation is already loaded, this is a no-op. If no
233    /// [`LazyContext`] is attached (lazy loading disabled), returns
234    /// `Ok(())` without loading.
235    ///
236    /// After this call, [`items`] returns the loaded collection.
237    pub async fn load(&mut self) -> EFResult<()>
238    where
239        T: IFromRow + IEntityType + ILazyInit,
240        Join: 'static,
241    {
242        if self.loaded {
243            return Ok(());
244        }
245        let Some(ctx) = self.lazy_ctx.clone() else {
246            // No lazy context attached — lazy loading disabled.
247            return Ok(());
248        };
249        if ctx.depth() >= MAX_LAZY_DEPTH {
250            return Err(EFError::other("lazy loading recursion limit exceeded"));
251        }
252        let mut entities = load_collection_lazy::<T>(ctx.as_ref()).await?;
253        // Attach lazy contexts to each child for nested lazy loading.
254        let provider = ctx.provider().clone();
255        let filter_map = ctx.filter_map().map(|m| Arc::new(m.clone()));
256        let depth = ctx.depth() + 1;
257        for entity in &mut entities {
258            // Re-bind to split let style for readability.
259            let p = provider.clone();
260            let fm = filter_map.clone();
261            entity.attach_lazy_contexts(p, fm, depth);
262        }
263        self.items = entities;
264        self.loaded = true;
265        Ok(())
266    }
267}
268
269impl<T, Join> Default for HasMany<T, Join> {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275impl<T, Join> Clone for HasMany<T, Join> {
276    fn clone(&self) -> Self {
277        Self {
278            items: Vec::new(), // Navigation collections are not deep-cloned
279            lazy_ctx: None,
280            loaded: false,
281            _phantom: PhantomData,
282        }
283    }
284}
285
286impl<T, Join> std::fmt::Debug for HasMany<T, Join> {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        f.debug_struct("HasMany")
289            .field("loaded", &self.loaded)
290            .field("len", &self.items.len())
291            .finish()
292    }
293}
294
295/// Type alias for HasMany with an explicit join entity (many-to-many).
296pub type Through<Join> = Join;
297
298// ---------------------------------------------------------------------------
299// HasOne<T>
300// ---------------------------------------------------------------------------
301
302/// Represents a "has-one" navigation  - ?a single related entity
303/// where the foreign key lives on the other side.
304pub struct HasOne<T> {
305    inner: Option<Box<T>>,
306    lazy_ctx: Option<Arc<dyn LazyContext>>,
307    loaded: bool,
308    _phantom: PhantomData<T>,
309}
310
311impl<T> HasOne<T> {
312    pub fn new() -> Self {
313        Self {
314            inner: None,
315            lazy_ctx: None,
316            loaded: false,
317            _phantom: PhantomData,
318        }
319    }
320
321    pub fn with(entity: T) -> Self {
322        Self {
323            inner: Some(Box::new(entity)),
324            lazy_ctx: None,
325            loaded: true,
326            _phantom: PhantomData,
327        }
328    }
329
330    pub fn get(&self) -> Option<&T> {
331        self.inner.as_deref()
332    }
333
334    pub fn get_mut(&mut self) -> Option<&mut T> {
335        self.inner.as_deref_mut()
336    }
337
338    /// Extracts the related entity, leaving the container empty and unloaded.
339    /// Used by batch nested-include loading to collect related entities
340    /// across multiple parents into a single slice for batch SQL.
341    pub fn take(&mut self) -> Option<T> {
342        let taken = self.inner.take();
343        self.loaded = false;
344        taken.map(|b| *b)
345    }
346
347    /// Returns `true` if the navigation has been loaded.
348    pub fn is_loaded(&self) -> bool {
349        self.loaded
350    }
351
352    /// Attaches a lazy-loading context to this container.
353    pub fn set_lazy_context(&mut self, ctx: Arc<dyn LazyContext>) {
354        self.lazy_ctx = Some(ctx);
355    }
356
357    /// Triggers lazy loading of this reference navigation.
358    pub async fn load(&mut self) -> EFResult<()>
359    where
360        T: IFromRow + IEntityType + ILazyInit,
361    {
362        if self.loaded {
363            return Ok(());
364        }
365        let Some(ctx) = self.lazy_ctx.clone() else {
366            return Ok(());
367        };
368        if ctx.depth() >= MAX_LAZY_DEPTH {
369            return Err(EFError::other("lazy loading recursion limit exceeded"));
370        }
371        let maybe_entity = load_scalar_lazy::<T>(ctx.as_ref()).await?;
372        if let Some(mut entity) = maybe_entity {
373            let provider = ctx.provider().clone();
374            let filter_map = ctx.filter_map().map(|m| Arc::new(m.clone()));
375            entity.attach_lazy_contexts(provider, filter_map, ctx.depth() + 1);
376            self.inner = Some(Box::new(entity));
377        }
378        self.loaded = true;
379        Ok(())
380    }
381}
382
383impl<T> Default for HasOne<T> {
384    fn default() -> Self {
385        Self::new()
386    }
387}
388
389impl<T> Clone for HasOne<T> {
390    fn clone(&self) -> Self {
391        Self {
392            inner: None,
393            lazy_ctx: None,
394            loaded: false,
395            _phantom: PhantomData,
396        }
397    }
398}
399
400impl<T> std::fmt::Debug for HasOne<T> {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        f.debug_struct("HasOne")
403            .field("loaded", &self.loaded)
404            .finish()
405    }
406}
407
408// ---------------------------------------------------------------------------
409// DeleteBehavior (for cascade configuration)
410// ---------------------------------------------------------------------------
411
412/// Specifies the delete behavior for a relationship.
413/// Corresponds to EFCore's `DeleteBehavior`.
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum DeleteBehavior {
416    Cascade,
417    Restrict,
418    SetNull,
419    NoAction,
420}
421
422impl DeleteBehavior {
423    /// Maps to the SQL `ON DELETE` clause keyword.
424    pub fn to_sql_clause(self) -> &'static str {
425        match self {
426            DeleteBehavior::Cascade => "CASCADE",
427            DeleteBehavior::Restrict => "RESTRICT",
428            DeleteBehavior::SetNull => "SET NULL",
429            DeleteBehavior::NoAction => "NO ACTION",
430        }
431    }
432}