Skip to main content

sqlmodel_core/
relationship.rs

1//! Relationship metadata for SQLModel Rust.
2//!
3//! Relationships are defined at compile-time (via derive macros) and represented
4//! as static metadata on each `Model`. This allows higher-level layers (query
5//! builder, session/UoW, eager/lazy loaders) to generate correct SQL and load
6//! related objects without runtime reflection.
7
8use crate::field::FieldInfo;
9use crate::{Error, Model, Value};
10use asupersync::{Cx, Outcome};
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use std::fmt;
13use std::future::Future;
14use std::sync::OnceLock;
15
16/// The type of relationship between two models.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum RelationshipKind {
19    /// One-to-one: `Hero` has one `Profile`.
20    OneToOne,
21    /// Many-to-one: many `Hero`s belong to one `Team`.
22    #[default]
23    ManyToOne,
24    /// One-to-many: one `Team` has many `Hero`s.
25    OneToMany,
26    /// Many-to-many: `Hero`s have many `Power`s via a link table.
27    ManyToMany,
28}
29
30/// Passive delete behavior for relationships.
31///
32/// Controls whether the ORM emits DELETE statements for related objects
33/// or relies on the database's foreign key ON DELETE cascade behavior.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub enum PassiveDeletes {
36    /// ORM emits DELETE for related objects (default behavior).
37    #[default]
38    Active,
39    /// ORM relies on database ON DELETE CASCADE; no DELETE emitted.
40    /// The database foreign key must have ON DELETE CASCADE configured.
41    Passive,
42    /// Like Passive, but also disables orphan tracking entirely.
43    /// Use when you want complete database-side cascade with no ORM overhead.
44    All,
45}
46
47/// Lazy loading strategy for relationships.
48///
49/// Controls how and when related objects are loaded from the database.
50/// Maps to SQLAlchemy's relationship lazy parameter.
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub enum LazyLoadStrategy {
53    /// Load items on first access via separate SELECT (default).
54    #[default]
55    Select,
56    /// Eager load via JOIN in parent query.
57    Joined,
58    /// Eager load via separate SELECT using IN clause.
59    Subquery,
60    /// Eager load via subquery correlated to parent.
61    Selectin,
62    /// Return a query object instead of loading items (for large collections).
63    Dynamic,
64    /// Never load - access raises error (useful for write-only relationships).
65    NoLoad,
66    /// Always raise error on access (strict write-only).
67    RaiseOnSql,
68    /// Write-only collection (append/remove only, no iteration).
69    WriteOnly,
70}
71
72/// Information about a link/join table for many-to-many relationships.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct LinkTableInfo {
75    /// The link table name (e.g., `"hero_powers"`).
76    pub table_name: &'static str,
77
78    /// Column in link table pointing to the local model (e.g., `"hero_id"`).
79    pub local_column: &'static str,
80
81    /// Column in link table pointing to the remote model (e.g., `"power_id"`).
82    pub remote_column: &'static str,
83
84    /// Composite local key columns (for composite PK parents).
85    ///
86    /// If set, this takes precedence over `local_column`.
87    pub local_columns: Option<&'static [&'static str]>,
88
89    /// Composite remote key columns (for composite PK children).
90    ///
91    /// If set, this takes precedence over `remote_column`.
92    pub remote_columns: Option<&'static [&'static str]>,
93}
94
95impl LinkTableInfo {
96    /// Create a new link-table definition.
97    #[must_use]
98    pub const fn new(
99        table_name: &'static str,
100        local_column: &'static str,
101        remote_column: &'static str,
102    ) -> Self {
103        Self {
104            table_name,
105            local_column,
106            remote_column,
107            local_columns: None,
108            remote_columns: None,
109        }
110    }
111
112    /// Create a new composite link-table definition.
113    ///
114    /// Column order matters:
115    /// - `local_columns` must match the parent PK value ordering
116    /// - `remote_columns` must match the child PK value ordering
117    #[must_use]
118    pub const fn composite(
119        table_name: &'static str,
120        local_columns: &'static [&'static str],
121        remote_columns: &'static [&'static str],
122    ) -> Self {
123        Self {
124            table_name,
125            local_column: "",
126            remote_column: "",
127            local_columns: Some(local_columns),
128            remote_columns: Some(remote_columns),
129        }
130    }
131
132    /// Return the local key columns (single or composite).
133    #[must_use]
134    pub fn local_cols(&self) -> &[&'static str] {
135        if let Some(cols) = self.local_columns {
136            return cols;
137        }
138        if self.local_column.is_empty() {
139            return &[];
140        }
141        std::slice::from_ref(&self.local_column)
142    }
143
144    /// Return the remote key columns (single or composite).
145    #[must_use]
146    pub fn remote_cols(&self) -> &[&'static str] {
147        if let Some(cols) = self.remote_columns {
148            return cols;
149        }
150        if self.remote_column.is_empty() {
151            return &[];
152        }
153        std::slice::from_ref(&self.remote_column)
154    }
155}
156
157/// Metadata about a relationship between models.
158#[derive(Debug, Clone, Copy)]
159pub struct RelationshipInfo {
160    /// Name of the relationship field.
161    pub name: &'static str,
162
163    /// The related model's table name.
164    pub related_table: &'static str,
165
166    /// Kind of relationship.
167    pub kind: RelationshipKind,
168
169    /// Local foreign key column (for ManyToOne).
170    /// e.g., `"team_id"` on `Hero`.
171    pub local_key: Option<&'static str>,
172
173    /// Composite local foreign key columns (for ManyToOne).
174    ///
175    /// If set, this takes precedence over `local_key`.
176    pub local_keys: Option<&'static [&'static str]>,
177
178    /// Remote foreign key column (for OneToMany).
179    /// e.g., `"team_id"` on `Hero` when accessed from `Team`.
180    pub remote_key: Option<&'static str>,
181
182    /// Composite remote foreign key columns (for OneToMany / OneToOne).
183    ///
184    /// If set, this takes precedence over `remote_key`.
185    pub remote_keys: Option<&'static [&'static str]>,
186
187    /// Link table for ManyToMany relationships.
188    pub link_table: Option<LinkTableInfo>,
189
190    /// The field on the related model that points back.
191    pub back_populates: Option<&'static str>,
192
193    /// Whether to use lazy loading (simple flag).
194    pub lazy: bool,
195
196    /// Cascade delete behavior.
197    pub cascade_delete: bool,
198
199    /// Passive delete behavior - whether ORM emits DELETE or relies on DB cascade.
200    pub passive_deletes: PassiveDeletes,
201
202    /// Default ordering for related items (e.g., "name", "created_at DESC").
203    pub order_by: Option<&'static str>,
204
205    /// Loading strategy for this relationship.
206    pub lazy_strategy: Option<LazyLoadStrategy>,
207
208    /// Full cascade options string (e.g., "all, delete-orphan").
209    pub cascade: Option<&'static str>,
210
211    /// Force list or single (override field type inference).
212    /// - `Some(true)`: Always return a list
213    /// - `Some(false)`: Always return a single item
214    /// - `None`: Infer from field type
215    pub uselist: Option<bool>,
216
217    /// Function pointer returning the related model's fields metadata.
218    ///
219    /// This keeps relationship metadata "zero-cost" (static + no allocation) while still
220    /// letting higher layers (query builder, eager loaders) build stable projections for
221    /// related models without runtime reflection.
222    pub related_fields_fn: fn() -> &'static [FieldInfo],
223}
224
225impl PartialEq for RelationshipInfo {
226    fn eq(&self, other: &Self) -> bool {
227        // Intentionally ignore `related_fields_fn`: function-pointer equality is not stable
228        // across codegen units and is not part of the semantic identity of a relationship.
229        self.name == other.name
230            && self.related_table == other.related_table
231            && self.kind == other.kind
232            && self.local_key_cols() == other.local_key_cols()
233            && self.remote_key_cols() == other.remote_key_cols()
234            && self.link_table == other.link_table
235            && self.back_populates == other.back_populates
236            && self.lazy == other.lazy
237            && self.cascade_delete == other.cascade_delete
238            && self.passive_deletes == other.passive_deletes
239            && self.order_by == other.order_by
240            && self.lazy_strategy == other.lazy_strategy
241            && self.cascade == other.cascade
242            && self.uselist == other.uselist
243    }
244}
245
246impl Eq for RelationshipInfo {}
247
248impl RelationshipInfo {
249    fn empty_related_fields() -> &'static [FieldInfo] {
250        &[]
251    }
252
253    /// Create a new relationship with required fields.
254    #[must_use]
255    pub const fn new(
256        name: &'static str,
257        related_table: &'static str,
258        kind: RelationshipKind,
259    ) -> Self {
260        Self {
261            name,
262            related_table,
263            kind,
264            local_key: None,
265            local_keys: None,
266            remote_key: None,
267            remote_keys: None,
268            link_table: None,
269            back_populates: None,
270            lazy: false,
271            cascade_delete: false,
272            passive_deletes: PassiveDeletes::Active,
273            order_by: None,
274            lazy_strategy: None,
275            cascade: None,
276            uselist: None,
277            related_fields_fn: Self::empty_related_fields,
278        }
279    }
280
281    /// Return the local key columns for this relationship (empty slice if unset).
282    ///
283    /// For single-column relationships, this returns a 1-element slice backed by `self.local_key`.
284    #[must_use]
285    pub fn local_key_cols(&self) -> &[&'static str] {
286        if let Some(keys) = self.local_keys {
287            return keys;
288        }
289        match &self.local_key {
290            Some(key) => std::slice::from_ref(key),
291            None => &[],
292        }
293    }
294
295    /// Return the remote key columns for this relationship (empty slice if unset).
296    ///
297    /// For single-column relationships, this returns a 1-element slice backed by `self.remote_key`.
298    #[must_use]
299    pub fn remote_key_cols(&self) -> &[&'static str] {
300        if let Some(keys) = self.remote_keys {
301            return keys;
302        }
303        match &self.remote_key {
304            Some(key) => std::slice::from_ref(key),
305            None => &[],
306        }
307    }
308
309    /// Provide the related model's `Model::fields()` function pointer.
310    ///
311    /// Derive macros should set this for relationship fields so query builders can
312    /// project and alias the related columns deterministically.
313    #[must_use]
314    pub const fn related_fields(mut self, f: fn() -> &'static [FieldInfo]) -> Self {
315        self.related_fields_fn = f;
316        self
317    }
318
319    /// Set the local foreign key column (ManyToOne).
320    #[must_use]
321    pub const fn local_key(mut self, key: &'static str) -> Self {
322        self.local_key = Some(key);
323        self.local_keys = None;
324        self
325    }
326
327    /// Set composite local foreign key columns (ManyToOne).
328    ///
329    /// The column order must match the parent primary key value ordering.
330    #[must_use]
331    pub const fn local_keys(mut self, keys: &'static [&'static str]) -> Self {
332        self.local_keys = Some(keys);
333        self.local_key = None;
334        self
335    }
336
337    /// Set the remote foreign key column (OneToMany).
338    #[must_use]
339    pub const fn remote_key(mut self, key: &'static str) -> Self {
340        self.remote_key = Some(key);
341        self.remote_keys = None;
342        self
343    }
344
345    /// Set composite remote foreign key columns (OneToMany / OneToOne).
346    ///
347    /// The column order must match the parent primary key value ordering.
348    #[must_use]
349    pub const fn remote_keys(mut self, keys: &'static [&'static str]) -> Self {
350        self.remote_keys = Some(keys);
351        self.remote_key = None;
352        self
353    }
354
355    /// Set the link table metadata (ManyToMany).
356    #[must_use]
357    pub const fn link_table(mut self, info: LinkTableInfo) -> Self {
358        self.link_table = Some(info);
359        self
360    }
361
362    /// Set the back-populates field name (bidirectional relationships).
363    #[must_use]
364    pub const fn back_populates(mut self, field: &'static str) -> Self {
365        self.back_populates = Some(field);
366        self
367    }
368
369    /// Enable/disable lazy loading.
370    #[must_use]
371    pub const fn lazy(mut self, value: bool) -> Self {
372        self.lazy = value;
373        self
374    }
375
376    /// Enable/disable cascade delete behavior.
377    #[must_use]
378    pub const fn cascade_delete(mut self, value: bool) -> Self {
379        self.cascade_delete = value;
380        self
381    }
382
383    /// Set passive delete behavior.
384    ///
385    /// - `PassiveDeletes::Active` (default): ORM emits DELETE for related objects
386    /// - `PassiveDeletes::Passive`: Relies on DB ON DELETE CASCADE
387    /// - `PassiveDeletes::All`: Passive + disables orphan tracking
388    #[must_use]
389    pub const fn passive_deletes(mut self, value: PassiveDeletes) -> Self {
390        self.passive_deletes = value;
391        self
392    }
393
394    /// Set default ordering for related items.
395    #[must_use]
396    pub const fn order_by(mut self, ordering: &'static str) -> Self {
397        self.order_by = Some(ordering);
398        self
399    }
400
401    /// Set default ordering from optional.
402    #[must_use]
403    pub const fn order_by_opt(mut self, ordering: Option<&'static str>) -> Self {
404        self.order_by = ordering;
405        self
406    }
407
408    /// Set the lazy loading strategy.
409    #[must_use]
410    pub const fn lazy_strategy(mut self, strategy: LazyLoadStrategy) -> Self {
411        self.lazy_strategy = Some(strategy);
412        self
413    }
414
415    /// Set the lazy loading strategy from optional.
416    #[must_use]
417    pub const fn lazy_strategy_opt(mut self, strategy: Option<LazyLoadStrategy>) -> Self {
418        self.lazy_strategy = strategy;
419        self
420    }
421
422    /// Set full cascade options string.
423    #[must_use]
424    pub const fn cascade(mut self, opts: &'static str) -> Self {
425        self.cascade = Some(opts);
426        self
427    }
428
429    /// Set cascade options from optional.
430    #[must_use]
431    pub const fn cascade_opt(mut self, opts: Option<&'static str>) -> Self {
432        self.cascade = opts;
433        self
434    }
435
436    /// Force list or single.
437    #[must_use]
438    pub const fn uselist(mut self, value: bool) -> Self {
439        self.uselist = Some(value);
440        self
441    }
442
443    /// Set uselist from optional.
444    #[must_use]
445    pub const fn uselist_opt(mut self, value: Option<bool>) -> Self {
446        self.uselist = value;
447        self
448    }
449
450    /// Check if passive deletes are enabled (Passive or All).
451    #[must_use]
452    pub const fn is_passive_deletes(&self) -> bool {
453        matches!(
454            self.passive_deletes,
455            PassiveDeletes::Passive | PassiveDeletes::All
456        )
457    }
458
459    /// Check if orphan tracking is disabled (passive_deletes='all').
460    #[must_use]
461    pub const fn is_passive_deletes_all(&self) -> bool {
462        matches!(self.passive_deletes, PassiveDeletes::All)
463    }
464}
465
466impl Default for RelationshipInfo {
467    fn default() -> Self {
468        Self::new("", "", RelationshipKind::default())
469    }
470}
471
472// ============================================================================
473// Relationship Lookup Helpers
474// ============================================================================
475
476/// Find a relationship by field name in a model's RELATIONSHIPS.
477///
478/// # Example
479///
480/// ```ignore
481/// let rel = find_relationship::<Hero>("team");
482/// assert_eq!(rel.unwrap().related_table, "teams");
483/// ```
484pub fn find_relationship<M: crate::Model>(field_name: &str) -> Option<&'static RelationshipInfo> {
485    M::RELATIONSHIPS.iter().find(|r| r.name == field_name)
486}
487
488/// Find the back-relationship from a target model back to the source.
489///
490/// Given `Hero::team` with `back_populates = "heroes"`, this finds
491/// `Team::heroes` which should have `back_populates = "team"`.
492///
493/// # Arguments
494///
495/// * `source_rel` - The relationship on the source model
496/// * `target_relationships` - The RELATIONSHIPS slice from the target model
497pub fn find_back_relationship(
498    source_rel: &RelationshipInfo,
499    target_relationships: &'static [RelationshipInfo],
500) -> Option<&'static RelationshipInfo> {
501    let back_field = source_rel.back_populates?;
502    target_relationships.iter().find(|r| r.name == back_field)
503}
504
505/// Validate that back_populates is symmetric between two models.
506///
507/// If `Hero::team` has `back_populates = "heroes"`, then `Team::heroes`
508/// must exist and have `back_populates = "team"`.
509///
510/// Returns Ok(()) if valid, Err with message if invalid.
511pub fn validate_back_populates<Source: crate::Model, Target: crate::Model>(
512    source_field: &str,
513) -> Result<(), String> {
514    let source_rel = find_relationship::<Source>(source_field).ok_or_else(|| {
515        format!(
516            "No relationship '{}' on {}",
517            source_field,
518            Source::TABLE_NAME
519        )
520    })?;
521
522    let Some(back_field) = source_rel.back_populates else {
523        // No back_populates, nothing to validate
524        return Ok(());
525    };
526
527    let target_rel = find_relationship::<Target>(back_field).ok_or_else(|| {
528        format!(
529            "{}.{} has back_populates='{}' but {}.{} does not exist",
530            Source::TABLE_NAME,
531            source_field,
532            back_field,
533            Target::TABLE_NAME,
534            back_field
535        )
536    })?;
537
538    // Validate that target points back to source
539    if let Some(target_back) = target_rel.back_populates {
540        if target_back != source_field {
541            return Err(format!(
542                "{}.{} has back_populates='{}' but {}.{} has back_populates='{}' (expected '{}')",
543                Source::TABLE_NAME,
544                source_field,
545                back_field,
546                Target::TABLE_NAME,
547                back_field,
548                target_back,
549                source_field
550            ));
551        }
552    }
553
554    Ok(())
555}
556
557/// Minimal session interface needed to load lazy relationships.
558///
559/// This trait lives in `sqlmodel-core` to avoid circular dependencies: the
560/// concrete `Session` type is defined in `sqlmodel-session` (which depends on
561/// `sqlmodel-core`). `sqlmodel-session` provides the blanket impl.
562pub trait LazyLoader<M: Model> {
563    /// Load an object by primary key.
564    fn get(&mut self, cx: &Cx, pk: Value)
565    -> impl Future<Output = Outcome<Option<M>, Error>> + Send;
566}
567
568/// A related single object (many-to-one or one-to-one).
569///
570/// This wrapper can be in one of three states:
571/// - **Empty**: no relationship (`fk_value` is None)
572/// - **Unloaded**: has FK value but not fetched yet (`fk_value` is Some, `loaded` unset)
573/// - **Loaded**: the object has been fetched and cached (`loaded` set)
574pub struct Related<T: Model> {
575    fk_value: Option<Value>,
576    loaded: OnceLock<Option<T>>,
577}
578
579impl<T: Model> Related<T> {
580    /// Create an empty relationship (null FK, not loaded).
581    #[must_use]
582    pub const fn empty() -> Self {
583        Self {
584            fk_value: None,
585            loaded: OnceLock::new(),
586        }
587    }
588
589    /// Create from a foreign key value (not yet loaded).
590    #[must_use]
591    pub fn from_fk(fk: impl Into<Value>) -> Self {
592        Self {
593            fk_value: Some(fk.into()),
594            loaded: OnceLock::new(),
595        }
596    }
597
598    /// Create with an already-loaded object.
599    #[must_use]
600    pub fn loaded(obj: T) -> Self {
601        let cell = OnceLock::new();
602        let _ = cell.set(Some(obj));
603        Self {
604            fk_value: None,
605            loaded: cell,
606        }
607    }
608
609    /// Get the loaded object (None if not loaded or loaded as null).
610    #[must_use]
611    pub fn get(&self) -> Option<&T> {
612        self.loaded.get().and_then(|o| o.as_ref())
613    }
614
615    /// Check if the relationship has been loaded (including loaded-null).
616    #[must_use]
617    pub fn is_loaded(&self) -> bool {
618        self.loaded.get().is_some()
619    }
620
621    /// Check if the relationship is empty (null FK).
622    #[must_use]
623    pub fn is_empty(&self) -> bool {
624        self.fk_value.is_none()
625    }
626
627    /// Get the foreign key value (if present).
628    #[must_use]
629    pub fn fk(&self) -> Option<&Value> {
630        self.fk_value.as_ref()
631    }
632
633    /// Set the loaded object (internal use by query system).
634    pub fn set_loaded(&self, obj: Option<T>) -> Result<(), Option<T>> {
635        self.loaded.set(obj)
636    }
637}
638
639impl<T: Model> Default for Related<T> {
640    fn default() -> Self {
641        Self::empty()
642    }
643}
644
645impl<T: Model + Clone> Clone for Related<T> {
646    fn clone(&self) -> Self {
647        let cloned = Self {
648            fk_value: self.fk_value.clone(),
649            loaded: OnceLock::new(),
650        };
651
652        if let Some(value) = self.loaded.get() {
653            let _ = cloned.loaded.set(value.clone());
654        }
655
656        cloned
657    }
658}
659
660impl<T: Model + fmt::Debug> fmt::Debug for Related<T> {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        let state = if self.is_loaded() {
663            "loaded"
664        } else if self.is_empty() {
665            "empty"
666        } else {
667            "unloaded"
668        };
669
670        f.debug_struct("Related")
671            .field("state", &state)
672            .field("fk_value", &self.fk_value)
673            .field("loaded", &self.get())
674            .finish()
675    }
676}
677
678impl<T> Serialize for Related<T>
679where
680    T: Model + Serialize,
681{
682    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
683        match self.loaded.get() {
684            Some(Some(obj)) => obj.serialize(serializer),
685            Some(None) | None => serializer.serialize_none(),
686        }
687    }
688}
689
690impl<'de, T> Deserialize<'de> for Related<T>
691where
692    T: Model + Deserialize<'de>,
693{
694    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
695        let opt = Option::<T>::deserialize(deserializer)?;
696        Ok(match opt {
697            Some(obj) => Self::loaded(obj),
698            None => Self::empty(),
699        })
700    }
701}
702
703/// A collection of related objects (one-to-many or many-to-many).
704///
705/// This wrapper can be in one of two states:
706/// - **Unloaded**: the collection has not been fetched yet
707/// - **Loaded**: the objects have been fetched and cached
708///
709/// For many-to-many relationships, use `link()` and `unlink()` to track
710/// changes that will be flushed to the link table.
711pub struct RelatedMany<T: Model> {
712    /// The loaded objects (if fetched).
713    loaded: OnceLock<Vec<T>>,
714    /// Foreign key column on the related model (for one-to-many).
715    fk_column: &'static str,
716    /// Parent's primary key value.
717    parent_pk: Option<Value>,
718    /// Link table info for many-to-many relationships.
719    link_table: Option<LinkTableInfo>,
720    /// Pending link operations (PK values to INSERT into link table).
721    pending_links: std::sync::Mutex<Vec<Vec<Value>>>,
722    /// Pending unlink operations (PK values to DELETE from link table).
723    pending_unlinks: std::sync::Mutex<Vec<Vec<Value>>>,
724}
725
726impl<T: Model> RelatedMany<T> {
727    /// Create a new unloaded RelatedMany with the FK column name.
728    ///
729    /// Use this for one-to-many relationships where the related model
730    /// has a foreign key column pointing back to this model.
731    #[must_use]
732    pub fn new(fk_column: &'static str) -> Self {
733        Self {
734            loaded: OnceLock::new(),
735            fk_column,
736            parent_pk: None,
737            link_table: None,
738            pending_links: std::sync::Mutex::new(Vec::new()),
739            pending_unlinks: std::sync::Mutex::new(Vec::new()),
740        }
741    }
742
743    /// Create for a many-to-many relationship with a link table.
744    ///
745    /// # Example
746    ///
747    /// ```ignore
748    /// let link = LinkTableInfo::new("hero_powers", "hero_id", "power_id");
749    /// let powers: RelatedMany<Power> = RelatedMany::with_link_table(link);
750    /// ```
751    #[must_use]
752    pub fn with_link_table(link_table: LinkTableInfo) -> Self {
753        Self {
754            loaded: OnceLock::new(),
755            fk_column: "",
756            parent_pk: None,
757            link_table: Some(link_table),
758            pending_links: std::sync::Mutex::new(Vec::new()),
759            pending_unlinks: std::sync::Mutex::new(Vec::new()),
760        }
761    }
762
763    /// Create with a parent primary key for loading.
764    #[must_use]
765    pub fn with_parent_pk(fk_column: &'static str, pk: impl Into<Value>) -> Self {
766        Self {
767            loaded: OnceLock::new(),
768            fk_column,
769            parent_pk: Some(pk.into()),
770            link_table: None,
771            pending_links: std::sync::Mutex::new(Vec::new()),
772            pending_unlinks: std::sync::Mutex::new(Vec::new()),
773        }
774    }
775
776    /// Check if the collection has been loaded.
777    #[must_use]
778    pub fn is_loaded(&self) -> bool {
779        self.loaded.get().is_some()
780    }
781
782    /// Get the loaded objects as a slice (None if not loaded).
783    #[must_use]
784    pub fn get(&self) -> Option<&[T]> {
785        self.loaded.get().map(Vec::as_slice)
786    }
787
788    /// Get the number of loaded items (0 if not loaded).
789    #[must_use]
790    pub fn len(&self) -> usize {
791        self.loaded.get().map_or(0, Vec::len)
792    }
793
794    /// Check if the collection is empty (true if not loaded or loaded empty).
795    #[must_use]
796    pub fn is_empty(&self) -> bool {
797        self.loaded.get().is_none_or(Vec::is_empty)
798    }
799
800    /// Set the loaded objects (internal use by query system).
801    pub fn set_loaded(&self, objects: Vec<T>) -> Result<(), Vec<T>> {
802        self.loaded.set(objects)
803    }
804
805    /// Iterate over the loaded items.
806    pub fn iter(&self) -> impl Iterator<Item = &T> {
807        self.loaded.get().map_or([].iter(), |v| v.iter())
808    }
809
810    /// Get the FK column name.
811    #[must_use]
812    pub fn fk_column(&self) -> &'static str {
813        self.fk_column
814    }
815
816    /// Get the parent PK value (if set).
817    #[must_use]
818    pub fn parent_pk(&self) -> Option<&Value> {
819        self.parent_pk.as_ref()
820    }
821
822    /// Set the parent PK value.
823    pub fn set_parent_pk(&mut self, pk: impl Into<Value>) {
824        self.parent_pk = Some(pk.into());
825    }
826
827    /// Get the link table info (if this is a many-to-many relationship).
828    #[must_use]
829    pub fn link_table(&self) -> Option<&LinkTableInfo> {
830        self.link_table.as_ref()
831    }
832
833    /// Check if this is a many-to-many relationship (has link table).
834    #[must_use]
835    pub fn is_many_to_many(&self) -> bool {
836        self.link_table.is_some()
837    }
838
839    /// Track a link operation (will INSERT into link table on flush).
840    ///
841    /// The object should already exist in the database. This method
842    /// records the relationship to be persisted when flush() is called.
843    ///
844    /// Duplicate links to the same object are ignored (only one INSERT will occur).
845    ///
846    /// # Example
847    ///
848    /// ```ignore
849    /// hero.powers.link(&fireball);
850    /// session.flush().await?; // Inserts into hero_powers table
851    /// ```
852    pub fn link(&self, obj: &T) {
853        let pk = obj.primary_key_value();
854        match self.pending_links.lock() {
855            Ok(mut pending) => {
856                // Prevent duplicates - only add if not already pending
857                if !pending.contains(&pk) {
858                    pending.push(pk);
859                }
860            }
861            Err(poisoned) => {
862                // Mutex was poisoned - recover by taking the lock anyway
863                // This is safe because we're just adding to a Vec
864                let mut pending = poisoned.into_inner();
865                if !pending.contains(&pk) {
866                    pending.push(pk);
867                }
868            }
869        }
870    }
871
872    /// Track an unlink operation (will DELETE from link table on flush).
873    ///
874    /// This method records the relationship removal to be persisted
875    /// when flush() is called.
876    ///
877    /// Duplicate unlinks to the same object are ignored (only one DELETE will occur).
878    ///
879    /// # Example
880    ///
881    /// ```ignore
882    /// hero.powers.unlink(&fireball);
883    /// session.flush().await?; // Deletes from hero_powers table
884    /// ```
885    pub fn unlink(&self, obj: &T) {
886        let pk = obj.primary_key_value();
887        match self.pending_unlinks.lock() {
888            Ok(mut pending) => {
889                // Prevent duplicates - only add if not already pending
890                if !pending.contains(&pk) {
891                    pending.push(pk);
892                }
893            }
894            Err(poisoned) => {
895                // Mutex was poisoned - recover by taking the lock anyway
896                // This is safe because we're just adding to a Vec
897                let mut pending = poisoned.into_inner();
898                if !pending.contains(&pk) {
899                    pending.push(pk);
900                }
901            }
902        }
903    }
904
905    /// Get and clear pending link operations.
906    ///
907    /// Returns the PK values that should be INSERTed into the link table.
908    /// This is used by the flush system.
909    pub fn take_pending_links(&self) -> Vec<Vec<Value>> {
910        match self.pending_links.lock() {
911            Ok(mut v) => std::mem::take(&mut *v),
912            Err(poisoned) => {
913                // Recover data from poisoned mutex - consistent with link()/unlink()
914                std::mem::take(&mut *poisoned.into_inner())
915            }
916        }
917    }
918
919    /// Get and clear pending unlink operations.
920    ///
921    /// Returns the PK values that should be DELETEd from the link table.
922    /// This is used by the flush system.
923    pub fn take_pending_unlinks(&self) -> Vec<Vec<Value>> {
924        match self.pending_unlinks.lock() {
925            Ok(mut v) => std::mem::take(&mut *v),
926            Err(poisoned) => {
927                // Recover data from poisoned mutex - consistent with link()/unlink()
928                std::mem::take(&mut *poisoned.into_inner())
929            }
930        }
931    }
932
933    /// Check if there are pending link/unlink operations.
934    #[must_use]
935    pub fn has_pending_ops(&self) -> bool {
936        let has_links = match self.pending_links.lock() {
937            Ok(v) => !v.is_empty(),
938            Err(poisoned) => !poisoned.into_inner().is_empty(),
939        };
940        let has_unlinks = match self.pending_unlinks.lock() {
941            Ok(v) => !v.is_empty(),
942            Err(poisoned) => !poisoned.into_inner().is_empty(),
943        };
944        has_links || has_unlinks
945    }
946}
947
948impl<T: Model> Default for RelatedMany<T> {
949    fn default() -> Self {
950        Self::new("")
951    }
952}
953
954impl<T: Model + Clone> Clone for RelatedMany<T> {
955    fn clone(&self) -> Self {
956        // Clone pending_links, recovering from poisoned mutex
957        let cloned_links = match self.pending_links.lock() {
958            Ok(v) => v.clone(),
959            Err(poisoned) => poisoned.into_inner().clone(),
960        };
961
962        // Clone pending_unlinks, recovering from poisoned mutex
963        let cloned_unlinks = match self.pending_unlinks.lock() {
964            Ok(v) => v.clone(),
965            Err(poisoned) => poisoned.into_inner().clone(),
966        };
967
968        let cloned = Self {
969            loaded: OnceLock::new(),
970            fk_column: self.fk_column,
971            parent_pk: self.parent_pk.clone(),
972            link_table: self.link_table,
973            pending_links: std::sync::Mutex::new(cloned_links),
974            pending_unlinks: std::sync::Mutex::new(cloned_unlinks),
975        };
976
977        if let Some(vec) = self.loaded.get() {
978            let _ = cloned.loaded.set(vec.clone());
979        }
980
981        cloned
982    }
983}
984
985impl<T: Model + fmt::Debug> fmt::Debug for RelatedMany<T> {
986    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
987        let pending_links_count = self.pending_links.lock().map_or(0, |v| v.len());
988        let pending_unlinks_count = self.pending_unlinks.lock().map_or(0, |v| v.len());
989
990        f.debug_struct("RelatedMany")
991            .field("loaded", &self.loaded.get())
992            .field("fk_column", &self.fk_column)
993            .field("parent_pk", &self.parent_pk)
994            .field("link_table", &self.link_table)
995            .field("pending_links_count", &pending_links_count)
996            .field("pending_unlinks_count", &pending_unlinks_count)
997            .finish()
998    }
999}
1000
1001impl<T> Serialize for RelatedMany<T>
1002where
1003    T: Model + Serialize,
1004{
1005    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1006        match self.loaded.get() {
1007            Some(vec) => vec.serialize(serializer),
1008            None => Vec::<T>::new().serialize(serializer),
1009        }
1010    }
1011}
1012
1013impl<'de, T> Deserialize<'de> for RelatedMany<T>
1014where
1015    T: Model + Deserialize<'de>,
1016{
1017    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1018        let vec = Vec::<T>::deserialize(deserializer)?;
1019        let rel = Self::new("");
1020        let _ = rel.loaded.set(vec);
1021        Ok(rel)
1022    }
1023}
1024
1025impl<'a, T: Model> IntoIterator for &'a RelatedMany<T> {
1026    type Item = &'a T;
1027    type IntoIter = std::slice::Iter<'a, T>;
1028
1029    fn into_iter(self) -> Self::IntoIter {
1030        self.loaded.get().map_or([].iter(), |v| v.iter())
1031    }
1032}
1033
1034// ============================================================================
1035// Lazy<T> - Deferred Loading
1036// ============================================================================
1037
1038/// A lazily-loaded related object that requires explicit load() call.
1039///
1040/// Unlike `Related<T>` which is loaded during the query via JOIN, `Lazy<T>`
1041/// defers loading until explicitly requested with a Session reference.
1042///
1043/// # States
1044///
1045/// - **Empty**: No FK value (null relationship)
1046/// - **Unloaded**: Has FK but not fetched yet
1047/// - **Loaded**: Object fetched and cached
1048///
1049/// # Example
1050///
1051/// ```ignore
1052/// // Field definition
1053/// struct Hero {
1054///     team: Lazy<Team>,
1055/// }
1056///
1057/// // Loading (requires Session)
1058/// let team = hero.team.load(&mut session, &cx).await?;
1059///
1060/// // After loading, access is fast
1061/// if let Some(team) = hero.team.get() {
1062///     println!("Team: {}", team.name);
1063/// }
1064/// ```
1065///
1066/// # N+1 Prevention
1067///
1068/// Use `Session::load_many()` to batch-load lazy relationships:
1069///
1070/// ```ignore
1071/// // Load all teams in one query
1072/// session.load_many(&mut heroes, |h| &mut h.team).await?;
1073/// ```
1074pub struct Lazy<T: Model> {
1075    /// Foreign key value (if any).
1076    fk_value: Option<Value>,
1077    /// Loaded object (cached after first load).
1078    loaded: OnceLock<Option<T>>,
1079    /// Whether load() has been called.
1080    load_attempted: std::sync::atomic::AtomicBool,
1081}
1082
1083impl<T: Model> Lazy<T> {
1084    /// Create an empty lazy relationship (null FK).
1085    #[must_use]
1086    pub fn empty() -> Self {
1087        Self {
1088            fk_value: None,
1089            loaded: OnceLock::new(),
1090            load_attempted: std::sync::atomic::AtomicBool::new(false),
1091        }
1092    }
1093
1094    /// Create from a foreign key value (not yet loaded).
1095    #[must_use]
1096    pub fn from_fk(fk: impl Into<Value>) -> Self {
1097        Self {
1098            fk_value: Some(fk.into()),
1099            loaded: OnceLock::new(),
1100            load_attempted: std::sync::atomic::AtomicBool::new(false),
1101        }
1102    }
1103
1104    /// Create with an already-loaded object.
1105    #[must_use]
1106    pub fn loaded(obj: T) -> Self {
1107        let cell = OnceLock::new();
1108        let _ = cell.set(Some(obj));
1109        Self {
1110            fk_value: None,
1111            loaded: cell,
1112            load_attempted: std::sync::atomic::AtomicBool::new(true),
1113        }
1114    }
1115
1116    /// Load the related object via the provided loader (cached after first success).
1117    ///
1118    /// - If the FK is NULL, this caches `None` and returns `Ok(None)`.
1119    /// - If the loader errors/cancels/panics, this does **not** mark the
1120    ///   relationship as loaded, allowing retries.
1121    pub async fn load<L>(&mut self, cx: &Cx, loader: &mut L) -> Outcome<Option<&T>, Error>
1122    where
1123        L: LazyLoader<T> + ?Sized,
1124    {
1125        if self.is_loaded() {
1126            return Outcome::Ok(self.get());
1127        }
1128
1129        let Some(fk) = self.fk_value.clone() else {
1130            let _ = self.set_loaded(None);
1131            return Outcome::Ok(None);
1132        };
1133
1134        match loader.get(cx, fk).await {
1135            Outcome::Ok(obj) => {
1136                let _ = self.set_loaded(obj);
1137                Outcome::Ok(self.get())
1138            }
1139            Outcome::Err(e) => Outcome::Err(e),
1140            Outcome::Cancelled(r) => Outcome::Cancelled(r),
1141            Outcome::Panicked(p) => Outcome::Panicked(p),
1142        }
1143    }
1144
1145    /// Get the loaded object (None if not loaded or FK is null).
1146    #[must_use]
1147    pub fn get(&self) -> Option<&T> {
1148        self.loaded.get().and_then(|o| o.as_ref())
1149    }
1150
1151    /// Check if load() has been called.
1152    #[must_use]
1153    pub fn is_loaded(&self) -> bool {
1154        self.load_attempted
1155            .load(std::sync::atomic::Ordering::Acquire)
1156    }
1157
1158    /// Check if the relationship is empty (null FK).
1159    #[must_use]
1160    pub fn is_empty(&self) -> bool {
1161        self.fk_value.is_none()
1162    }
1163
1164    /// Get the foreign key value.
1165    #[must_use]
1166    pub fn fk(&self) -> Option<&Value> {
1167        self.fk_value.as_ref()
1168    }
1169
1170    /// Set the loaded object (internal use by Session::load_many).
1171    ///
1172    /// Returns `Ok(())` if successfully set, `Err` if already loaded.
1173    pub fn set_loaded(&self, obj: Option<T>) -> Result<(), Option<T>> {
1174        match self.loaded.set(obj) {
1175            Ok(()) => {
1176                self.load_attempted
1177                    .store(true, std::sync::atomic::Ordering::Release);
1178                Ok(())
1179            }
1180            Err(v) => Err(v),
1181        }
1182    }
1183
1184    /// Reset the lazy relationship to unloaded state.
1185    ///
1186    /// This is useful when refreshing an object after commit.
1187    pub fn reset(&mut self) {
1188        self.loaded = OnceLock::new();
1189        self.load_attempted = std::sync::atomic::AtomicBool::new(false);
1190    }
1191}
1192
1193impl<T: Model> Default for Lazy<T> {
1194    fn default() -> Self {
1195        Self::empty()
1196    }
1197}
1198
1199impl<T: Model + Clone> Clone for Lazy<T> {
1200    fn clone(&self) -> Self {
1201        let cloned = Self {
1202            fk_value: self.fk_value.clone(),
1203            loaded: OnceLock::new(),
1204            load_attempted: std::sync::atomic::AtomicBool::new(
1205                self.load_attempted
1206                    .load(std::sync::atomic::Ordering::Acquire),
1207            ),
1208        };
1209
1210        if let Some(value) = self.loaded.get() {
1211            let _ = cloned.loaded.set(value.clone());
1212        }
1213
1214        cloned
1215    }
1216}
1217
1218impl<T: Model + fmt::Debug> fmt::Debug for Lazy<T> {
1219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1220        let state = if self.is_loaded() {
1221            "loaded"
1222        } else if self.is_empty() {
1223            "empty"
1224        } else {
1225            "unloaded"
1226        };
1227
1228        f.debug_struct("Lazy")
1229            .field("state", &state)
1230            .field("fk_value", &self.fk_value)
1231            .field("loaded", &self.get())
1232            .field(
1233                "load_attempted",
1234                &self
1235                    .load_attempted
1236                    .load(std::sync::atomic::Ordering::Acquire),
1237            )
1238            .finish()
1239    }
1240}
1241
1242impl<T> Serialize for Lazy<T>
1243where
1244    T: Model + Serialize,
1245{
1246    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1247        match self.loaded.get() {
1248            Some(Some(obj)) => obj.serialize(serializer),
1249            Some(None) | None => serializer.serialize_none(),
1250        }
1251    }
1252}
1253
1254impl<'de, T> Deserialize<'de> for Lazy<T>
1255where
1256    T: Model + Deserialize<'de>,
1257{
1258    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1259        let opt = Option::<T>::deserialize(deserializer)?;
1260        Ok(match opt {
1261            Some(obj) => Self::loaded(obj),
1262            None => Self::empty(),
1263        })
1264    }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::*;
1270    use crate::{FieldInfo, Result, Row};
1271    use asupersync::runtime::RuntimeBuilder;
1272    use serde::{Deserialize, Serialize};
1273
1274    #[test]
1275    fn test_relationship_kind_default() {
1276        assert_eq!(RelationshipKind::default(), RelationshipKind::ManyToOne);
1277    }
1278
1279    #[test]
1280    fn test_relationship_info_builder_chain() {
1281        let info = RelationshipInfo::new("team", "teams", RelationshipKind::ManyToOne)
1282            .local_key("team_id")
1283            .back_populates("heroes")
1284            .lazy(true)
1285            .cascade_delete(true)
1286            .passive_deletes(PassiveDeletes::Passive);
1287
1288        assert_eq!(info.name, "team");
1289        assert_eq!(info.related_table, "teams");
1290        assert_eq!(info.kind, RelationshipKind::ManyToOne);
1291        assert_eq!(info.local_key, Some("team_id"));
1292        assert_eq!(info.remote_key, None);
1293        assert_eq!(info.link_table, None);
1294        assert_eq!(info.back_populates, Some("heroes"));
1295        assert!(info.lazy);
1296        assert!(info.cascade_delete);
1297        assert_eq!(info.passive_deletes, PassiveDeletes::Passive);
1298    }
1299
1300    #[test]
1301    fn test_passive_deletes_default() {
1302        assert_eq!(PassiveDeletes::default(), PassiveDeletes::Active);
1303    }
1304
1305    #[test]
1306    fn test_passive_deletes_helper_methods() {
1307        // Active: ORM handles deletes
1308        let active_info = RelationshipInfo::new("test", "test", RelationshipKind::OneToMany)
1309            .passive_deletes(PassiveDeletes::Active);
1310        assert!(!active_info.is_passive_deletes());
1311        assert!(!active_info.is_passive_deletes_all());
1312
1313        // Passive: DB handles deletes
1314        let passive_info = RelationshipInfo::new("test", "test", RelationshipKind::OneToMany)
1315            .passive_deletes(PassiveDeletes::Passive);
1316        assert!(passive_info.is_passive_deletes());
1317        assert!(!passive_info.is_passive_deletes_all());
1318
1319        // All: DB handles + no orphan tracking
1320        let all_info = RelationshipInfo::new("test", "test", RelationshipKind::OneToMany)
1321            .passive_deletes(PassiveDeletes::All);
1322        assert!(all_info.is_passive_deletes());
1323        assert!(all_info.is_passive_deletes_all());
1324    }
1325
1326    #[test]
1327    fn test_relationship_info_new_has_active_passive_deletes() {
1328        // New relationship should default to Active
1329        let info = RelationshipInfo::new("test", "test", RelationshipKind::ManyToOne);
1330        assert_eq!(info.passive_deletes, PassiveDeletes::Active);
1331        assert!(!info.is_passive_deletes());
1332    }
1333
1334    #[test]
1335    fn test_link_table_info_new() {
1336        let link = LinkTableInfo::new("hero_powers", "hero_id", "power_id");
1337        assert_eq!(link.table_name, "hero_powers");
1338        assert_eq!(link.local_column, "hero_id");
1339        assert_eq!(link.remote_column, "power_id");
1340    }
1341
1342    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1343    struct Team {
1344        id: Option<i64>,
1345        name: String,
1346    }
1347
1348    impl Model for Team {
1349        const TABLE_NAME: &'static str = "teams";
1350        const PRIMARY_KEY: &'static [&'static str] = &["id"];
1351
1352        fn fields() -> &'static [FieldInfo] {
1353            &[]
1354        }
1355
1356        fn to_row(&self) -> Vec<(&'static str, Value)> {
1357            vec![]
1358        }
1359
1360        fn from_row(_row: &Row) -> Result<Self> {
1361            Ok(Self {
1362                id: None,
1363                name: String::new(),
1364            })
1365        }
1366
1367        fn primary_key_value(&self) -> Vec<Value> {
1368            match self.id {
1369                Some(id) => vec![Value::from(id)],
1370                None => vec![],
1371            }
1372        }
1373
1374        fn is_new(&self) -> bool {
1375            self.id.is_none()
1376        }
1377    }
1378
1379    #[test]
1380    fn test_related_empty_creates_unloaded_state() {
1381        let rel = Related::<Team>::empty();
1382        assert!(rel.is_empty());
1383        assert!(!rel.is_loaded());
1384        assert!(rel.get().is_none());
1385        assert!(rel.fk().is_none());
1386    }
1387
1388    #[test]
1389    fn test_related_from_fk_stores_value() {
1390        let rel = Related::<Team>::from_fk(42_i64);
1391        assert!(!rel.is_empty());
1392        assert_eq!(rel.fk(), Some(&Value::from(42_i64)));
1393        assert!(!rel.is_loaded());
1394        assert!(rel.get().is_none());
1395    }
1396
1397    #[test]
1398    fn test_related_loaded_sets_object() {
1399        let team = Team {
1400            id: Some(1),
1401            name: "Avengers".to_string(),
1402        };
1403        let rel = Related::loaded(team.clone());
1404        assert!(rel.is_loaded());
1405        assert!(rel.fk().is_none());
1406        assert_eq!(rel.get(), Some(&team));
1407    }
1408
1409    #[test]
1410    fn test_related_set_loaded_succeeds_first_time() {
1411        let rel = Related::<Team>::from_fk(1_i64);
1412        let team = Team {
1413            id: Some(1),
1414            name: "Avengers".to_string(),
1415        };
1416        assert!(rel.set_loaded(Some(team.clone())).is_ok());
1417        assert!(rel.is_loaded());
1418        assert_eq!(rel.get(), Some(&team));
1419    }
1420
1421    #[test]
1422    fn test_related_set_loaded_fails_second_time() {
1423        let rel = Related::<Team>::empty();
1424        assert!(rel.set_loaded(None).is_ok());
1425        assert!(rel.is_loaded());
1426        assert!(rel.set_loaded(None).is_err());
1427    }
1428
1429    #[test]
1430    fn test_related_default_is_empty() {
1431        let rel: Related<Team> = Related::default();
1432        assert!(rel.is_empty());
1433    }
1434
1435    #[test]
1436    fn test_related_clone_unloaded_is_unloaded() {
1437        let rel = Related::<Team>::from_fk(7_i64);
1438        let cloned = rel.clone();
1439        assert!(!cloned.is_loaded());
1440        assert_eq!(cloned.fk(), rel.fk());
1441    }
1442
1443    #[test]
1444    fn test_related_clone_loaded_preserves_object() {
1445        let team = Team {
1446            id: Some(1),
1447            name: "Avengers".to_string(),
1448        };
1449        let rel = Related::loaded(team.clone());
1450        let cloned = rel.clone();
1451        assert!(cloned.is_loaded());
1452        assert_eq!(cloned.get(), Some(&team));
1453    }
1454
1455    #[test]
1456    fn test_related_debug_output_shows_state() {
1457        let rel = Related::<Team>::from_fk(1_i64);
1458        let s = format!("{rel:?}");
1459        assert!(s.contains("state"));
1460        assert!(s.contains("unloaded"));
1461    }
1462
1463    #[test]
1464    fn test_related_serde_serialize_loaded_outputs_object() {
1465        let rel = Related::loaded(Team {
1466            id: Some(1),
1467            name: "Avengers".to_string(),
1468        });
1469        let json = serde_json::to_value(&rel).unwrap();
1470        assert_eq!(
1471            json,
1472            serde_json::json!({
1473                "id": 1,
1474                "name": "Avengers"
1475            })
1476        );
1477    }
1478
1479    #[test]
1480    fn test_related_serde_serialize_unloaded_outputs_null() {
1481        let rel = Related::<Team>::from_fk(1_i64);
1482        let json = serde_json::to_value(&rel).unwrap();
1483        assert_eq!(json, serde_json::Value::Null);
1484    }
1485
1486    #[test]
1487    fn test_related_serde_deserialize_object_creates_loaded() {
1488        let rel: Related<Team> = serde_json::from_value(serde_json::json!({
1489            "id": 1,
1490            "name": "Avengers"
1491        }))
1492        .unwrap();
1493
1494        let expected = Team {
1495            id: Some(1),
1496            name: "Avengers".to_string(),
1497        };
1498        assert!(rel.is_loaded());
1499        assert_eq!(rel.get(), Some(&expected));
1500    }
1501
1502    #[test]
1503    fn test_related_serde_deserialize_null_creates_empty() {
1504        let rel: Related<Team> = serde_json::from_value(serde_json::Value::Null).unwrap();
1505        assert!(rel.is_empty());
1506        assert!(!rel.is_loaded());
1507        assert!(rel.get().is_none());
1508    }
1509
1510    #[test]
1511    fn test_related_serde_roundtrip_preserves_data() {
1512        let rel = Related::loaded(Team {
1513            id: Some(1),
1514            name: "Avengers".to_string(),
1515        });
1516        let json = serde_json::to_string(&rel).unwrap();
1517        let decoded: Related<Team> = serde_json::from_str(&json).unwrap();
1518        assert!(decoded.is_loaded());
1519        assert_eq!(decoded.get(), rel.get());
1520    }
1521
1522    // ========================================================================
1523    // RelatedMany<T> Tests
1524    // ========================================================================
1525
1526    #[test]
1527    fn test_related_many_new_is_unloaded() {
1528        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1529        assert!(!rel.is_loaded());
1530        assert!(rel.get().is_none());
1531        assert_eq!(rel.len(), 0);
1532        assert!(rel.is_empty());
1533    }
1534
1535    #[test]
1536    fn test_related_many_set_loaded() {
1537        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1538        let teams = vec![
1539            Team {
1540                id: Some(1),
1541                name: "Avengers".to_string(),
1542            },
1543            Team {
1544                id: Some(2),
1545                name: "X-Men".to_string(),
1546            },
1547        ];
1548        assert!(rel.set_loaded(teams.clone()).is_ok());
1549        assert!(rel.is_loaded());
1550        assert_eq!(rel.len(), 2);
1551        assert!(!rel.is_empty());
1552    }
1553
1554    #[test]
1555    fn test_related_many_get_returns_slice() {
1556        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1557        let teams = vec![Team {
1558            id: Some(1),
1559            name: "Avengers".to_string(),
1560        }];
1561        rel.set_loaded(teams.clone()).unwrap();
1562        let slice = rel.get().unwrap();
1563        assert_eq!(slice.len(), 1);
1564        assert_eq!(slice[0].name, "Avengers");
1565    }
1566
1567    #[test]
1568    fn test_related_many_iter() {
1569        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1570        let teams = vec![
1571            Team {
1572                id: Some(1),
1573                name: "A".to_string(),
1574            },
1575            Team {
1576                id: Some(2),
1577                name: "B".to_string(),
1578            },
1579        ];
1580        rel.set_loaded(teams).unwrap();
1581        let names: Vec<_> = rel.iter().map(|t| t.name.as_str()).collect();
1582        assert_eq!(names, vec!["A", "B"]);
1583    }
1584
1585    #[test]
1586    fn test_related_many_default() {
1587        let rel: RelatedMany<Team> = RelatedMany::default();
1588        assert!(!rel.is_loaded());
1589        assert!(rel.is_empty());
1590    }
1591
1592    #[test]
1593    fn test_related_many_clone() {
1594        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1595        rel.set_loaded(vec![Team {
1596            id: Some(1),
1597            name: "Test".to_string(),
1598        }])
1599        .unwrap();
1600        let cloned = rel.clone();
1601        assert!(cloned.is_loaded());
1602        assert_eq!(cloned.len(), 1);
1603    }
1604
1605    #[test]
1606    fn test_related_many_debug() {
1607        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1608        let debug_str = format!("{:?}", rel);
1609        assert!(debug_str.contains("RelatedMany"));
1610        assert!(debug_str.contains("fk_column"));
1611    }
1612
1613    #[test]
1614    fn test_related_many_serde_serialize_loaded() {
1615        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1616        rel.set_loaded(vec![Team {
1617            id: Some(1),
1618            name: "A".to_string(),
1619        }])
1620        .unwrap();
1621        let json = serde_json::to_value(&rel).unwrap();
1622        assert!(json.is_array());
1623        assert_eq!(json.as_array().unwrap().len(), 1);
1624    }
1625
1626    #[test]
1627    fn test_related_many_serde_serialize_unloaded() {
1628        let rel: RelatedMany<Team> = RelatedMany::new("team_id");
1629        let json = serde_json::to_value(&rel).unwrap();
1630        assert!(json.is_array());
1631        assert!(json.as_array().unwrap().is_empty());
1632    }
1633
1634    #[test]
1635    fn test_related_many_serde_deserialize() {
1636        let rel: RelatedMany<Team> = serde_json::from_value(serde_json::json!([
1637            {"id": 1, "name": "A"},
1638            {"id": 2, "name": "B"}
1639        ]))
1640        .unwrap();
1641        assert!(rel.is_loaded());
1642        assert_eq!(rel.len(), 2);
1643    }
1644
1645    // ========================================================================
1646    // RelatedMany Many-to-Many Tests
1647    // ========================================================================
1648
1649    #[test]
1650    fn test_related_many_with_link_table() {
1651        let link = LinkTableInfo::new("hero_powers", "hero_id", "power_id");
1652        let rel: RelatedMany<Team> = RelatedMany::with_link_table(link);
1653
1654        assert!(rel.is_many_to_many());
1655        assert_eq!(rel.link_table().unwrap().table_name, "hero_powers");
1656        assert_eq!(rel.link_table().unwrap().local_column, "hero_id");
1657        assert_eq!(rel.link_table().unwrap().remote_column, "power_id");
1658    }
1659
1660    #[test]
1661    fn test_related_many_link_tracks_pending() {
1662        let rel: RelatedMany<Team> = RelatedMany::new("");
1663        let team = Team {
1664            id: Some(1),
1665            name: "A".to_string(),
1666        };
1667
1668        assert!(!rel.has_pending_ops());
1669        rel.link(&team);
1670        assert!(rel.has_pending_ops());
1671
1672        let pending = rel.take_pending_links();
1673        assert_eq!(pending.len(), 1);
1674        assert_eq!(pending[0], vec![Value::from(1_i64)]);
1675
1676        // Should be cleared
1677        assert!(!rel.has_pending_ops());
1678    }
1679
1680    #[test]
1681    fn test_related_many_unlink_tracks_pending() {
1682        let rel: RelatedMany<Team> = RelatedMany::new("");
1683        let team = Team {
1684            id: Some(2),
1685            name: "B".to_string(),
1686        };
1687
1688        rel.unlink(&team);
1689        assert!(rel.has_pending_ops());
1690
1691        let pending = rel.take_pending_unlinks();
1692        assert_eq!(pending.len(), 1);
1693        assert_eq!(pending[0], vec![Value::from(2_i64)]);
1694    }
1695
1696    #[test]
1697    fn test_related_many_multiple_links() {
1698        let rel: RelatedMany<Team> = RelatedMany::new("");
1699        let team1 = Team {
1700            id: Some(1),
1701            name: "A".to_string(),
1702        };
1703        let team2 = Team {
1704            id: Some(2),
1705            name: "B".to_string(),
1706        };
1707
1708        rel.link(&team1);
1709        rel.link(&team2);
1710
1711        let pending = rel.take_pending_links();
1712        assert_eq!(pending.len(), 2);
1713    }
1714
1715    #[test]
1716    fn test_related_many_link_and_unlink_together() {
1717        let rel: RelatedMany<Team> = RelatedMany::new("");
1718        let team1 = Team {
1719            id: Some(1),
1720            name: "A".to_string(),
1721        };
1722        let team2 = Team {
1723            id: Some(2),
1724            name: "B".to_string(),
1725        };
1726
1727        rel.link(&team1);
1728        rel.unlink(&team2);
1729        assert!(rel.has_pending_ops());
1730
1731        let links = rel.take_pending_links();
1732        let unlinks = rel.take_pending_unlinks();
1733
1734        assert_eq!(links.len(), 1);
1735        assert_eq!(unlinks.len(), 1);
1736        assert!(!rel.has_pending_ops());
1737    }
1738
1739    #[test]
1740    fn test_related_many_clone_preserves_pending() {
1741        let rel: RelatedMany<Team> = RelatedMany::new("");
1742        let team = Team {
1743            id: Some(1),
1744            name: "A".to_string(),
1745        };
1746
1747        rel.link(&team);
1748        let cloned = rel.clone();
1749
1750        assert!(cloned.has_pending_ops());
1751        let pending = cloned.take_pending_links();
1752        assert_eq!(pending.len(), 1);
1753    }
1754
1755    #[test]
1756    fn test_related_many_set_parent_pk() {
1757        let mut rel: RelatedMany<Team> = RelatedMany::new("team_id");
1758        assert!(rel.parent_pk().is_none());
1759
1760        rel.set_parent_pk(42_i64);
1761        assert_eq!(rel.parent_pk(), Some(&Value::from(42_i64)));
1762    }
1763
1764    // ========================================================================
1765    // Lazy<T> Tests
1766    // ========================================================================
1767
1768    #[test]
1769    fn test_lazy_empty_has_no_fk() {
1770        let lazy = Lazy::<Team>::empty();
1771        assert!(lazy.fk().is_none());
1772        assert!(lazy.is_empty());
1773        assert!(!lazy.is_loaded());
1774        assert!(lazy.get().is_none());
1775    }
1776
1777    #[test]
1778    fn test_lazy_from_fk_stores_value() {
1779        let lazy = Lazy::<Team>::from_fk(42_i64);
1780        assert!(!lazy.is_empty());
1781        assert_eq!(lazy.fk(), Some(&Value::from(42_i64)));
1782        assert!(!lazy.is_loaded());
1783        assert!(lazy.get().is_none());
1784    }
1785
1786    #[test]
1787    fn test_lazy_not_loaded_initially() {
1788        let lazy = Lazy::<Team>::from_fk(1_i64);
1789        assert!(!lazy.is_loaded());
1790    }
1791
1792    #[test]
1793    fn test_lazy_loaded_creates_loaded_state() {
1794        let team = Team {
1795            id: Some(1),
1796            name: "Avengers".to_string(),
1797        };
1798        let lazy = Lazy::loaded(team.clone());
1799        assert!(lazy.is_loaded());
1800        assert!(lazy.fk().is_none()); // No FK needed when pre-loaded
1801        assert_eq!(lazy.get(), Some(&team));
1802    }
1803
1804    #[test]
1805    fn test_lazy_set_loaded_succeeds_first_time() {
1806        let lazy = Lazy::<Team>::from_fk(1_i64);
1807        let team = Team {
1808            id: Some(1),
1809            name: "Avengers".to_string(),
1810        };
1811        assert!(lazy.set_loaded(Some(team.clone())).is_ok());
1812        assert!(lazy.is_loaded());
1813        assert_eq!(lazy.get(), Some(&team));
1814    }
1815
1816    #[test]
1817    fn test_lazy_set_loaded_fails_second_time() {
1818        let lazy = Lazy::<Team>::empty();
1819        assert!(lazy.set_loaded(None).is_ok());
1820        assert!(lazy.is_loaded());
1821        assert!(lazy.set_loaded(None).is_err());
1822    }
1823
1824    #[test]
1825    fn test_lazy_load_fetches_from_loader_and_caches() {
1826        #[derive(Default)]
1827        struct Loader {
1828            calls: usize,
1829        }
1830
1831        impl LazyLoader<Team> for Loader {
1832            fn get(
1833                &mut self,
1834                _cx: &Cx,
1835                pk: Value,
1836            ) -> impl Future<Output = Outcome<Option<Team>, Error>> + Send {
1837                self.calls += 1;
1838                let team = match pk {
1839                    Value::BigInt(1) => Some(Team {
1840                        id: Some(1),
1841                        name: "Avengers".to_string(),
1842                    }),
1843                    _ => None,
1844                };
1845                async move { Outcome::Ok(team) }
1846            }
1847        }
1848
1849        let rt = RuntimeBuilder::current_thread()
1850            .build()
1851            .expect("create asupersync runtime");
1852        let cx = Cx::for_testing();
1853
1854        rt.block_on(async {
1855            let mut lazy = Lazy::<Team>::from_fk(1_i64);
1856            let mut loader = Loader::default();
1857
1858            let outcome = lazy.load(&cx, &mut loader).await;
1859            assert!(matches!(outcome, Outcome::Ok(Some(_))));
1860            assert!(lazy.is_loaded());
1861            assert_eq!(loader.calls, 1);
1862
1863            // Cached: no second call to the loader
1864            let outcome2 = lazy.load(&cx, &mut loader).await;
1865            assert!(matches!(outcome2, Outcome::Ok(Some(_))));
1866            assert_eq!(loader.calls, 1);
1867        });
1868    }
1869
1870    #[test]
1871    fn test_lazy_load_empty_returns_none_without_calling_loader() {
1872        #[derive(Default)]
1873        struct Loader {
1874            calls: usize,
1875        }
1876
1877        impl LazyLoader<Team> for Loader {
1878            fn get(
1879                &mut self,
1880                _cx: &Cx,
1881                _pk: Value,
1882            ) -> impl Future<Output = Outcome<Option<Team>, Error>> + Send {
1883                self.calls += 1;
1884                async { Outcome::Ok(None) }
1885            }
1886        }
1887
1888        let rt = RuntimeBuilder::current_thread()
1889            .build()
1890            .expect("create asupersync runtime");
1891        let cx = Cx::for_testing();
1892
1893        rt.block_on(async {
1894            let mut lazy = Lazy::<Team>::empty();
1895            let mut loader = Loader::default();
1896
1897            let outcome = lazy.load(&cx, &mut loader).await;
1898            assert!(matches!(outcome, Outcome::Ok(None)));
1899            assert!(lazy.is_loaded());
1900            assert_eq!(loader.calls, 0);
1901        });
1902    }
1903
1904    #[test]
1905    fn test_lazy_load_error_does_not_mark_loaded() {
1906        #[derive(Default)]
1907        struct Loader {
1908            calls: usize,
1909        }
1910
1911        impl LazyLoader<Team> for Loader {
1912            fn get(
1913                &mut self,
1914                _cx: &Cx,
1915                _pk: Value,
1916            ) -> impl Future<Output = Outcome<Option<Team>, Error>> + Send {
1917                self.calls += 1;
1918                async { Outcome::Err(Error::Custom("boom".to_string())) }
1919            }
1920        }
1921
1922        let rt = RuntimeBuilder::current_thread()
1923            .build()
1924            .expect("create asupersync runtime");
1925        let cx = Cx::for_testing();
1926
1927        rt.block_on(async {
1928            let mut lazy = Lazy::<Team>::from_fk(1_i64);
1929            let mut loader = Loader::default();
1930
1931            let outcome = lazy.load(&cx, &mut loader).await;
1932            assert!(matches!(outcome, Outcome::Err(_)));
1933            assert!(!lazy.is_loaded());
1934            assert_eq!(loader.calls, 1);
1935        });
1936    }
1937
1938    #[test]
1939    fn test_lazy_get_before_load_returns_none() {
1940        let lazy = Lazy::<Team>::from_fk(1_i64);
1941        assert!(lazy.get().is_none());
1942    }
1943
1944    #[test]
1945    fn test_lazy_default_is_empty() {
1946        let lazy: Lazy<Team> = Lazy::default();
1947        assert!(lazy.is_empty());
1948        assert!(!lazy.is_loaded());
1949    }
1950
1951    #[test]
1952    fn test_lazy_clone_unloaded_is_unloaded() {
1953        let lazy = Lazy::<Team>::from_fk(7_i64);
1954        let cloned = lazy.clone();
1955        assert!(!cloned.is_loaded());
1956        assert_eq!(cloned.fk(), lazy.fk());
1957    }
1958
1959    #[test]
1960    fn test_lazy_clone_loaded_preserves_object() {
1961        let team = Team {
1962            id: Some(1),
1963            name: "Avengers".to_string(),
1964        };
1965        let lazy = Lazy::loaded(team.clone());
1966        let cloned = lazy.clone();
1967        assert!(cloned.is_loaded());
1968        assert_eq!(cloned.get(), Some(&team));
1969    }
1970
1971    #[test]
1972    fn test_lazy_debug_output_shows_state() {
1973        let lazy = Lazy::<Team>::from_fk(1_i64);
1974        let s = format!("{lazy:?}");
1975        assert!(s.contains("state"));
1976        assert!(s.contains("unloaded"));
1977    }
1978
1979    #[test]
1980    fn test_lazy_serde_serialize_loaded_outputs_object() {
1981        let lazy = Lazy::loaded(Team {
1982            id: Some(1),
1983            name: "Avengers".to_string(),
1984        });
1985        let json = serde_json::to_value(&lazy).unwrap();
1986        assert_eq!(
1987            json,
1988            serde_json::json!({
1989                "id": 1,
1990                "name": "Avengers"
1991            })
1992        );
1993    }
1994
1995    #[test]
1996    fn test_lazy_serde_serialize_unloaded_outputs_null() {
1997        let lazy = Lazy::<Team>::from_fk(1_i64);
1998        let json = serde_json::to_value(&lazy).unwrap();
1999        assert_eq!(json, serde_json::Value::Null);
2000    }
2001
2002    #[test]
2003    fn test_lazy_serde_deserialize_object_creates_loaded() {
2004        let lazy: Lazy<Team> = serde_json::from_value(serde_json::json!({
2005            "id": 1,
2006            "name": "Avengers"
2007        }))
2008        .unwrap();
2009
2010        let expected = Team {
2011            id: Some(1),
2012            name: "Avengers".to_string(),
2013        };
2014        assert!(lazy.is_loaded());
2015        assert_eq!(lazy.get(), Some(&expected));
2016    }
2017
2018    #[test]
2019    fn test_lazy_serde_deserialize_null_creates_empty() {
2020        let lazy: Lazy<Team> = serde_json::from_value(serde_json::Value::Null).unwrap();
2021        assert!(lazy.is_empty());
2022        assert!(!lazy.is_loaded());
2023        assert!(lazy.get().is_none());
2024    }
2025
2026    #[test]
2027    fn test_lazy_serde_roundtrip_preserves_data() {
2028        let lazy = Lazy::loaded(Team {
2029            id: Some(1),
2030            name: "Avengers".to_string(),
2031        });
2032        let json = serde_json::to_string(&lazy).unwrap();
2033        let decoded: Lazy<Team> = serde_json::from_str(&json).unwrap();
2034        assert!(decoded.is_loaded());
2035        assert_eq!(decoded.get(), lazy.get());
2036    }
2037
2038    #[test]
2039    fn test_lazy_reset_clears_loaded_state() {
2040        let mut lazy = Lazy::loaded(Team {
2041            id: Some(1),
2042            name: "Test".to_string(),
2043        });
2044        assert!(lazy.is_loaded());
2045
2046        lazy.reset();
2047        assert!(!lazy.is_loaded());
2048        assert!(lazy.get().is_none());
2049    }
2050
2051    #[test]
2052    fn test_lazy_is_empty_accurate() {
2053        let empty = Lazy::<Team>::empty();
2054        assert!(empty.is_empty());
2055
2056        let with_fk = Lazy::<Team>::from_fk(1_i64);
2057        assert!(!with_fk.is_empty());
2058
2059        let loaded = Lazy::loaded(Team {
2060            id: Some(1),
2061            name: "Test".to_string(),
2062        });
2063        assert!(loaded.is_empty()); // loaded() doesn't set FK value
2064    }
2065
2066    #[test]
2067    fn test_lazy_load_missing_object_caches_none() {
2068        let lazy = Lazy::<Team>::from_fk(999_i64);
2069        // Simulate what Session::load_many does when object not found
2070        assert!(lazy.set_loaded(None).is_ok());
2071        assert!(lazy.is_loaded());
2072        assert!(lazy.get().is_none());
2073
2074        // Second attempt should fail (already set)
2075        assert!(lazy.set_loaded(None).is_err());
2076    }
2077
2078    // ========================================================================
2079    // Relationship Lookup Helper Tests
2080    // ========================================================================
2081
2082    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2083    struct Hero {
2084        id: Option<i64>,
2085        name: String,
2086    }
2087
2088    impl Model for Hero {
2089        const TABLE_NAME: &'static str = "heroes";
2090        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2091        const RELATIONSHIPS: &'static [RelationshipInfo] = &[RelationshipInfo {
2092            name: "team",
2093            related_table: "teams",
2094            kind: RelationshipKind::ManyToOne,
2095            local_key: Some("team_id"),
2096            local_keys: None,
2097            remote_key: None,
2098            remote_keys: None,
2099            link_table: None,
2100            back_populates: Some("heroes"),
2101            lazy: false,
2102            cascade_delete: false,
2103            passive_deletes: PassiveDeletes::Active,
2104            order_by: None,
2105            lazy_strategy: None,
2106            cascade: None,
2107            uselist: None,
2108            related_fields_fn: TeamWithRelationships::fields,
2109        }];
2110
2111        fn fields() -> &'static [FieldInfo] {
2112            &[]
2113        }
2114
2115        fn to_row(&self) -> Vec<(&'static str, Value)> {
2116            vec![]
2117        }
2118
2119        fn from_row(_row: &Row) -> Result<Self> {
2120            Ok(Self {
2121                id: None,
2122                name: String::new(),
2123            })
2124        }
2125
2126        fn primary_key_value(&self) -> Vec<Value> {
2127            match self.id {
2128                Some(id) => vec![Value::from(id)],
2129                None => vec![],
2130            }
2131        }
2132
2133        fn is_new(&self) -> bool {
2134            self.id.is_none()
2135        }
2136    }
2137
2138    // TeamWithRelationships has back_populates pointing to Hero
2139    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2140    struct TeamWithRelationships {
2141        id: Option<i64>,
2142        name: String,
2143    }
2144
2145    impl Model for TeamWithRelationships {
2146        const TABLE_NAME: &'static str = "teams";
2147        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2148        const RELATIONSHIPS: &'static [RelationshipInfo] = &[RelationshipInfo {
2149            name: "heroes",
2150            related_table: "heroes",
2151            kind: RelationshipKind::OneToMany,
2152            local_key: None,
2153            local_keys: None,
2154            remote_key: Some("team_id"),
2155            remote_keys: None,
2156            link_table: None,
2157            back_populates: Some("team"),
2158            lazy: false,
2159            cascade_delete: false,
2160            passive_deletes: PassiveDeletes::Active,
2161            order_by: None,
2162            lazy_strategy: None,
2163            cascade: None,
2164            uselist: None,
2165            related_fields_fn: Hero::fields,
2166        }];
2167
2168        fn fields() -> &'static [FieldInfo] {
2169            &[]
2170        }
2171
2172        fn to_row(&self) -> Vec<(&'static str, Value)> {
2173            vec![]
2174        }
2175
2176        fn from_row(_row: &Row) -> Result<Self> {
2177            Ok(Self {
2178                id: None,
2179                name: String::new(),
2180            })
2181        }
2182
2183        fn primary_key_value(&self) -> Vec<Value> {
2184            match self.id {
2185                Some(id) => vec![Value::from(id)],
2186                None => vec![],
2187            }
2188        }
2189
2190        fn is_new(&self) -> bool {
2191            self.id.is_none()
2192        }
2193    }
2194
2195    #[test]
2196    fn test_find_relationship_found() {
2197        let rel = find_relationship::<Hero>("team");
2198        assert!(rel.is_some());
2199        let rel = rel.unwrap();
2200        assert_eq!(rel.name, "team");
2201        assert_eq!(rel.related_table, "teams");
2202        assert_eq!(rel.back_populates, Some("heroes"));
2203    }
2204
2205    #[test]
2206    fn test_find_relationship_not_found() {
2207        let rel = find_relationship::<Hero>("powers");
2208        assert!(rel.is_none());
2209    }
2210
2211    #[test]
2212    fn test_find_relationship_empty_relationships() {
2213        // Team has no relationships defined
2214        let rel = find_relationship::<Team>("heroes");
2215        assert!(rel.is_none());
2216    }
2217
2218    #[test]
2219    fn test_find_back_relationship_found() {
2220        let hero_team_rel = find_relationship::<Hero>("team").unwrap();
2221        let back = find_back_relationship(hero_team_rel, TeamWithRelationships::RELATIONSHIPS);
2222        assert!(back.is_some());
2223        let back = back.unwrap();
2224        assert_eq!(back.name, "heroes");
2225        assert_eq!(back.back_populates, Some("team"));
2226    }
2227
2228    #[test]
2229    fn test_find_back_relationship_no_back_populates() {
2230        let rel = RelationshipInfo::new("team", "teams", RelationshipKind::ManyToOne);
2231        let back = find_back_relationship(&rel, TeamWithRelationships::RELATIONSHIPS);
2232        assert!(back.is_none());
2233    }
2234
2235    #[test]
2236    fn test_validate_back_populates_valid() {
2237        let result = validate_back_populates::<Hero, TeamWithRelationships>("team");
2238        assert!(result.is_ok());
2239    }
2240
2241    #[test]
2242    fn test_validate_back_populates_no_source_relationship() {
2243        let result = validate_back_populates::<Hero, TeamWithRelationships>("nonexistent");
2244        assert!(result.is_err());
2245        assert!(result.unwrap_err().contains("No relationship"));
2246    }
2247
2248    #[test]
2249    fn test_validate_back_populates_no_target_relationship() {
2250        // Team has no RELATIONSHIPS, so validation will fail
2251        let result = validate_back_populates::<Hero, Team>("team");
2252        assert!(result.is_err());
2253        assert!(result.unwrap_err().contains("does not exist"));
2254    }
2255}