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    /// Returns `true` if the navigation has been loaded (either eagerly
72    /// via `Include` or lazily via [`load`]).
73    pub fn is_loaded(&self) -> bool {
74        self.loaded
75    }
76
77    /// Attaches a lazy-loading context to this container.
78    ///
79    /// Called by the `#[derive(EntityType)]` macro's `ILazyInit`
80    /// implementation when lazy loading is enabled on the `DbContext`.
81    pub fn set_lazy_context(&mut self, ctx: Arc<dyn LazyContext>) {
82        self.lazy_ctx = Some(ctx);
83    }
84
85    /// Triggers lazy loading of this reference navigation.
86    ///
87    /// If the navigation is already loaded, this is a no-op. If no
88    /// [`LazyContext`] is attached (lazy loading disabled), returns
89    /// `Ok(())` without loading.
90    ///
91    /// After this call, [`get`] returns the loaded entity (or `None` if
92    /// no matching row was found).
93    pub async fn load(&mut self) -> EFResult<()>
94    where
95        T: IFromRow + IEntityType + ILazyInit,
96    {
97        if self.loaded {
98            return Ok(());
99        }
100        let Some(ctx) = self.lazy_ctx.clone() else {
101            // No lazy context attached — lazy loading disabled.
102            return Ok(());
103        };
104        if ctx.depth() >= MAX_LAZY_DEPTH {
105            return Err(EFError::other("lazy loading recursion limit exceeded"));
106        }
107        let maybe_entity = load_scalar_lazy::<T>(ctx.as_ref()).await?;
108        if let Some(mut entity) = maybe_entity {
109            // Attach lazy contexts to the child for nested lazy loading.
110            let provider = ctx.provider().clone();
111            let filter_map = ctx.filter_map().map(|m| Arc::new(m.clone()));
112            entity.attach_lazy_contexts(provider, filter_map, ctx.depth() + 1);
113            self.inner = Some(Box::new(entity));
114        }
115        self.loaded = true;
116        Ok(())
117    }
118}
119
120impl<T> Default for BelongsTo<T> {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl<T> Clone for BelongsTo<T> {
127    fn clone(&self) -> Self {
128        Self {
129            inner: None, // Navigation properties are not deep-cloned
130            lazy_ctx: None,
131            loaded: false,
132            _phantom: PhantomData,
133        }
134    }
135}
136
137impl<T> std::fmt::Debug for BelongsTo<T> {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        f.debug_struct("BelongsTo")
140            .field("loaded", &self.loaded)
141            .finish()
142    }
143}
144
145// ---------------------------------------------------------------------------
146// HasMany<T>
147// ---------------------------------------------------------------------------
148
149/// Represents a "has-many" navigation  - ?a collection of related entities.
150///
151/// Corresponds to EFCore's collection navigation property
152/// (e.g., `ICollection<Post>`).
153pub struct HasMany<T, Join = ()> {
154    items: Vec<T>,
155    lazy_ctx: Option<Arc<dyn LazyContext>>,
156    loaded: bool,
157    _phantom: PhantomData<(T, Join)>,
158}
159
160impl<T, Join> HasMany<T, Join> {
161    pub fn new() -> Self {
162        Self {
163            items: Vec::new(),
164            lazy_ctx: None,
165            loaded: false,
166            _phantom: PhantomData,
167        }
168    }
169
170    pub fn with(items: Vec<T>) -> Self {
171        Self {
172            items,
173            lazy_ctx: None,
174            loaded: true,
175            _phantom: PhantomData,
176        }
177    }
178
179    pub fn items(&self) -> &[T] {
180        &self.items
181    }
182
183    pub fn items_mut(&mut self) -> &mut Vec<T> {
184        &mut self.items
185    }
186
187    pub fn add(&mut self, item: T) {
188        self.items.push(item);
189    }
190
191    pub fn remove(&mut self, index: usize) -> Option<T> {
192        if index < self.items.len() {
193            Some(self.items.remove(index))
194        } else {
195            None
196        }
197    }
198
199    pub fn len(&self) -> usize {
200        self.items.len()
201    }
202
203    pub fn is_empty(&self) -> bool {
204        self.items.is_empty()
205    }
206
207    /// Returns `true` if the navigation has been loaded (either eagerly
208    /// via `Include` or lazily via [`load`]).
209    pub fn is_loaded(&self) -> bool {
210        self.loaded
211    }
212
213    /// Attaches a lazy-loading context to this container.
214    ///
215    /// Called by the `#[derive(EntityType)]` macro's `ILazyInit`
216    /// implementation when lazy loading is enabled on the `DbContext`.
217    pub fn set_lazy_context(&mut self, ctx: Arc<dyn LazyContext>) {
218        self.lazy_ctx = Some(ctx);
219    }
220
221    /// Triggers lazy loading of this collection navigation.
222    ///
223    /// If the navigation is already loaded, this is a no-op. If no
224    /// [`LazyContext`] is attached (lazy loading disabled), returns
225    /// `Ok(())` without loading.
226    ///
227    /// After this call, [`items`] returns the loaded collection.
228    pub async fn load(&mut self) -> EFResult<()>
229    where
230        T: IFromRow + IEntityType + ILazyInit,
231        Join: 'static,
232    {
233        if self.loaded {
234            return Ok(());
235        }
236        let Some(ctx) = self.lazy_ctx.clone() else {
237            // No lazy context attached — lazy loading disabled.
238            return Ok(());
239        };
240        if ctx.depth() >= MAX_LAZY_DEPTH {
241            return Err(EFError::other("lazy loading recursion limit exceeded"));
242        }
243        let mut entities = load_collection_lazy::<T>(ctx.as_ref()).await?;
244        // Attach lazy contexts to each child for nested lazy loading.
245        let provider = ctx.provider().clone();
246        let filter_map = ctx.filter_map().map(|m| Arc::new(m.clone()));
247        let depth = ctx.depth() + 1;
248        for entity in &mut entities {
249            // Re-bind to split let style for readability.
250            let p = provider.clone();
251            let fm = filter_map.clone();
252            entity.attach_lazy_contexts(p, fm, depth);
253        }
254        self.items = entities;
255        self.loaded = true;
256        Ok(())
257    }
258}
259
260impl<T, Join> Default for HasMany<T, Join> {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266impl<T, Join> Clone for HasMany<T, Join> {
267    fn clone(&self) -> Self {
268        Self {
269            items: Vec::new(), // Navigation collections are not deep-cloned
270            lazy_ctx: None,
271            loaded: false,
272            _phantom: PhantomData,
273        }
274    }
275}
276
277impl<T, Join> std::fmt::Debug for HasMany<T, Join> {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        f.debug_struct("HasMany")
280            .field("loaded", &self.loaded)
281            .field("len", &self.items.len())
282            .finish()
283    }
284}
285
286/// Type alias for HasMany with an explicit join entity (many-to-many).
287pub type Through<Join> = Join;
288
289// ---------------------------------------------------------------------------
290// HasOne<T>
291// ---------------------------------------------------------------------------
292
293/// Represents a "has-one" navigation  - ?a single related entity
294/// where the foreign key lives on the other side.
295pub struct HasOne<T> {
296    inner: Option<Box<T>>,
297    lazy_ctx: Option<Arc<dyn LazyContext>>,
298    loaded: bool,
299    _phantom: PhantomData<T>,
300}
301
302impl<T> HasOne<T> {
303    pub fn new() -> Self {
304        Self {
305            inner: None,
306            lazy_ctx: None,
307            loaded: false,
308            _phantom: PhantomData,
309        }
310    }
311
312    pub fn with(entity: T) -> Self {
313        Self {
314            inner: Some(Box::new(entity)),
315            lazy_ctx: None,
316            loaded: true,
317            _phantom: PhantomData,
318        }
319    }
320
321    pub fn get(&self) -> Option<&T> {
322        self.inner.as_deref()
323    }
324
325    pub fn get_mut(&mut self) -> Option<&mut T> {
326        self.inner.as_deref_mut()
327    }
328
329    /// Returns `true` if the navigation has been loaded.
330    pub fn is_loaded(&self) -> bool {
331        self.loaded
332    }
333
334    /// Attaches a lazy-loading context to this container.
335    pub fn set_lazy_context(&mut self, ctx: Arc<dyn LazyContext>) {
336        self.lazy_ctx = Some(ctx);
337    }
338
339    /// Triggers lazy loading of this reference navigation.
340    pub async fn load(&mut self) -> EFResult<()>
341    where
342        T: IFromRow + IEntityType + ILazyInit,
343    {
344        if self.loaded {
345            return Ok(());
346        }
347        let Some(ctx) = self.lazy_ctx.clone() else {
348            return Ok(());
349        };
350        if ctx.depth() >= MAX_LAZY_DEPTH {
351            return Err(EFError::other("lazy loading recursion limit exceeded"));
352        }
353        let maybe_entity = load_scalar_lazy::<T>(ctx.as_ref()).await?;
354        if let Some(mut entity) = maybe_entity {
355            let provider = ctx.provider().clone();
356            let filter_map = ctx.filter_map().map(|m| Arc::new(m.clone()));
357            entity.attach_lazy_contexts(provider, filter_map, ctx.depth() + 1);
358            self.inner = Some(Box::new(entity));
359        }
360        self.loaded = true;
361        Ok(())
362    }
363}
364
365impl<T> Default for HasOne<T> {
366    fn default() -> Self {
367        Self::new()
368    }
369}
370
371impl<T> Clone for HasOne<T> {
372    fn clone(&self) -> Self {
373        Self {
374            inner: None,
375            lazy_ctx: None,
376            loaded: false,
377            _phantom: PhantomData,
378        }
379    }
380}
381
382impl<T> std::fmt::Debug for HasOne<T> {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        f.debug_struct("HasOne")
385            .field("loaded", &self.loaded)
386            .finish()
387    }
388}
389
390// ---------------------------------------------------------------------------
391// DeleteBehavior (for cascade configuration)
392// ---------------------------------------------------------------------------
393
394/// Specifies the delete behavior for a relationship.
395/// Corresponds to EFCore's `DeleteBehavior`.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum DeleteBehavior {
398    Cascade,
399    Restrict,
400    SetNull,
401    NoAction,
402}