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