Skip to main content

myko/server/
relationship_manager.rs

1//! Cell-based RelationshipManager for handling entity relationship cascades.
2//!
3//! This module handles cascade operations based on relationships registered via
4//! `#[belongs_to]`, `#[owns_many]`, and `#[ensure_for]` attribute macros.
5//!
6//! Uses CellServerCtx for queries and event publishing, keeping this module
7//! decoupled from direct store and event processor access.
8//!
9//! # Relationship Types
10//!
11//! ## BelongsTo (Foreign Key)
12//!
13//! A child entity has a foreign key pointing to a parent. When the parent is deleted,
14//! all children with matching foreign keys are cascade-deleted.
15//!
16//! ```text
17//! use myko::prelude::*;
18//! use std::sync::Arc;
19//!
20//! #[myko_item]
21//! pub struct Scene {
22//!     pub name: String,
23//! }
24//!
25//! #[myko_item]
26//! pub struct Binding {
27//!     #[belongs_to(Scene)]
28//!     pub scope_id: Arc<str>,
29//! }
30//! ```
31//!
32//! ## OwnsMany (Parent has array of child IDs)
33//!
34//! A parent entity owns an array of child IDs. Deleting the parent deletes all children.
35//! Deleting a child removes its ID from the parent's array.
36//!
37//! ```text
38//! use myko::prelude::*;
39//! use std::sync::Arc;
40//!
41//! #[myko_item]
42//! pub struct BindingNode {
43//!     pub name: String,
44//! }
45//!
46//! #[myko_item]
47//! pub struct Scene {
48//!     #[owns_many(BindingNode)]
49//!     pub node_ids: Vec<Arc<str>>,
50//! }
51//! ```
52//!
53//! ## EnsureFor (Auto-create for combinations)
54//!
55//! Automatically create one entity for each combination of dependency entities.
56//!
57//! ```text
58//! use myko::prelude::*;
59//! use std::sync::Arc;
60//!
61//! #[myko_item]
62//! pub struct Session {
63//!     pub name: String,
64//! }
65//!
66//! #[myko_item]
67//! pub struct Bundle {
68//!     pub name: String,
69//! }
70//!
71//! #[myko_item]
72//! pub struct BundleStatus {
73//!     #[ensure_for(Session)]
74//!     pub session_id: Arc<str>,
75//!     #[ensure_for(Bundle)]
76//!     pub bundle_id: Arc<str>,
77//! }
78//! ```
79
80use std::{
81    collections::{BTreeSet, HashMap, HashSet},
82    sync::Arc,
83};
84
85use dashmap::DashMap;
86use hyphae::Gettable;
87use tracing::{debug, info, trace};
88
89use super::{CellServerCtx, persister::PersistError};
90use crate::{
91    core::item::AnyItem,
92    relationship::{
93        ArrayExtractor, ArrayRemover, EnsureForDependency, EntityFactory, FkExtractor, Relation,
94        iter_relations,
95    },
96};
97
98/// Lookup info for BelongsTo cascades
99#[derive(Clone)]
100struct BelongsToLookup {
101    id: u64,
102    local_type: &'static str,
103    foreign_type: &'static str,
104    extract_fk: FkExtractor,
105}
106
107/// Lookup info for OwnsMany cascades
108#[derive(Clone)]
109struct OwnsManyLookup {
110    local_type: &'static str,
111    foreign_type: &'static str,
112    extract_ids: ArrayExtractor,
113    remove_id: ArrayRemover,
114}
115
116/// Lookup info for EnsureFor cascades
117#[derive(Clone)]
118struct EnsureForLookup {
119    local_type: &'static str,
120    dependencies: Vec<EnsureForDependency>,
121    make_entity: EntityFactory,
122}
123
124/// Cell-based RelationshipManager for handling entity relationship cascades.
125///
126/// This manager discovers relationships via [`inventory`] at initialization,
127/// builds lookup indexes for efficient cascade processing, and provides
128/// methods for processing events and establishing relations on startup.
129///
130/// Unlike the actor-based version, this implementation uses CellServerCtx
131/// for queries and event publishing, keeping it decoupled from direct
132/// store and event processor access.
133pub struct RelationshipManager {
134    /// BelongsTo relations indexed by foreign_type (the parent type)
135    /// When a parent is deleted, look up children to cascade delete
136    belongs_to_by_foreign: HashMap<&'static str, Vec<BelongsToLookup>>,
137
138    /// BelongsTo relations indexed by local_type (the child type)
139    /// Used for orphan cleanup on startup
140    belongs_to_by_local: HashMap<&'static str, Vec<BelongsToLookup>>,
141
142    /// OwnsMany relations indexed by local_type (the parent type)
143    /// When a parent is deleted, delete all owned children
144    owns_many_by_local: HashMap<&'static str, Vec<OwnsManyLookup>>,
145
146    /// OwnsMany relations indexed by foreign_type (the child type)
147    /// When a child is deleted, update parent arrays
148    owns_many_by_foreign: HashMap<&'static str, Vec<OwnsManyLookup>>,
149
150    /// EnsureFor relations indexed by their dependency types
151    /// When a dependency entity is created, ensure derived entities exist
152    ensure_for_by_dependency: HashMap<&'static str, Vec<EnsureForLookup>>,
153
154    /// Reverse belongs_to index: lookup_id -> parent_id -> child_ids
155    belongs_to_children_by_parent: DashMap<u64, DashMap<Arc<str>, BTreeSet<Arc<str>>>>,
156
157    /// Reverse belongs_to index: lookup_id -> child_id -> parent_id
158    belongs_to_parent_by_child: DashMap<u64, DashMap<Arc<str>, Arc<str>>>,
159}
160
161impl RelationshipManager {
162    /// Create a new RelationshipManager with lookup tables built from inventory.
163    pub fn new() -> Self {
164        trace!("RelationshipManager: Initializing from inventory");
165
166        let mut belongs_to_by_foreign: HashMap<&'static str, Vec<BelongsToLookup>> = HashMap::new();
167        let mut belongs_to_by_local: HashMap<&'static str, Vec<BelongsToLookup>> = HashMap::new();
168        let mut owns_many_by_local: HashMap<&'static str, Vec<OwnsManyLookup>> = HashMap::new();
169        let mut owns_many_by_foreign: HashMap<&'static str, Vec<OwnsManyLookup>> = HashMap::new();
170        let mut ensure_for_by_dependency: HashMap<&'static str, Vec<EnsureForLookup>> =
171            HashMap::new();
172
173        let mut next_belongs_to_id = 1u64;
174        for registration in iter_relations() {
175            match &registration.relation {
176                Relation::BelongsTo {
177                    local_type,
178                    foreign_type,
179                    extract_fk,
180                    ..
181                } => {
182                    trace!(
183                        "RelationshipManager: Registered BelongsTo {} -> {}",
184                        local_type, foreign_type
185                    );
186                    let lookup = BelongsToLookup {
187                        id: next_belongs_to_id,
188                        local_type,
189                        foreign_type,
190                        extract_fk: *extract_fk,
191                    };
192                    next_belongs_to_id += 1;
193                    belongs_to_by_foreign
194                        .entry(foreign_type)
195                        .or_default()
196                        .push(lookup.clone());
197                    belongs_to_by_local
198                        .entry(local_type)
199                        .or_default()
200                        .push(lookup);
201                }
202                Relation::OwnsMany {
203                    local_type,
204                    foreign_type,
205                    extract_ids,
206                    remove_id,
207                    ..
208                } => {
209                    trace!(
210                        "RelationshipManager: Registered OwnsMany {} ->> {}",
211                        local_type, foreign_type
212                    );
213                    let lookup = OwnsManyLookup {
214                        local_type,
215                        foreign_type,
216                        extract_ids: *extract_ids,
217                        remove_id: *remove_id,
218                    };
219                    owns_many_by_local
220                        .entry(local_type)
221                        .or_default()
222                        .push(lookup.clone());
223                    owns_many_by_foreign
224                        .entry(foreign_type)
225                        .or_default()
226                        .push(lookup);
227                }
228                Relation::EnsureFor {
229                    local_type,
230                    dependencies,
231                    make_entity,
232                    ..
233                } => {
234                    trace!(
235                        "RelationshipManager: Registered EnsureFor {} for {:?}",
236                        local_type,
237                        dependencies
238                            .iter()
239                            .map(|d| d.foreign_type)
240                            .collect::<Vec<_>>()
241                    );
242                    let deps: Vec<_> = dependencies.to_vec();
243
244                    // Index by each dependency type
245                    for dep in dependencies.iter() {
246                        ensure_for_by_dependency
247                            .entry(dep.foreign_type)
248                            .or_default()
249                            .push(EnsureForLookup {
250                                local_type,
251                                dependencies: deps.clone(),
252                                make_entity: *make_entity,
253                            });
254                    }
255                }
256            }
257        }
258
259        let relation_count =
260            belongs_to_by_foreign.len() + owns_many_by_local.len() + ensure_for_by_dependency.len();
261        trace!(
262            "RelationshipManager: {} relation types indexed",
263            relation_count
264        );
265
266        Self {
267            belongs_to_by_foreign,
268            belongs_to_by_local,
269            owns_many_by_local,
270            owns_many_by_foreign,
271            ensure_for_by_dependency,
272            belongs_to_children_by_parent: DashMap::new(),
273            belongs_to_parent_by_child: DashMap::new(),
274        }
275    }
276
277    /// Forward a SET event for relationship processing.
278    ///
279    /// Handles EnsureFor: when a dependency entity is created, ensures
280    /// all derived entities exist for all combinations.
281    pub fn forward_set(
282        &self,
283        item: Arc<dyn AnyItem>,
284        ctx: &CellServerCtx,
285    ) -> Result<(), PersistError> {
286        let item_type = item.entity_type();
287
288        if let Some(lookups) = self.belongs_to_by_local.get(item_type) {
289            for lookup in lookups {
290                self.index_belongs_to_child(lookup, &item);
291            }
292        }
293
294        // Handle EnsureFor (dependency created → ensure derived entities exist)
295        if self.ensure_for_by_dependency.contains_key(item_type) {
296            self.handle_ensure_for(&item, ctx)?;
297        }
298
299        Ok(())
300    }
301
302    /// Forward a DEL event for relationship processing.
303    ///
304    /// Handles:
305    /// - BelongsTo cascade deletes (parent deleted → delete children)
306    /// - OwnsMany parent deletes (parent deleted → delete owned children)
307    /// - OwnsMany child deletes (child deleted → update parent arrays)
308    pub fn forward_del(
309        &self,
310        item: Arc<dyn AnyItem>,
311        ctx: &CellServerCtx,
312    ) -> Result<(), PersistError> {
313        // Handle BelongsTo cascades (parent deleted → delete children)
314        self.handle_belongs_to_cascade(&item, ctx)?;
315
316        // Handle OwnsMany parent deleted → delete owned children
317        self.handle_owns_many_parent_delete(&item, ctx)?;
318
319        // Handle OwnsMany child deleted → update parent arrays
320        self.handle_owns_many_child_delete(&item, ctx)?;
321
322        if let Some(lookups) = self.belongs_to_by_local.get(item.entity_type()) {
323            for lookup in lookups {
324                self.remove_belongs_to_child(lookup, &item.id());
325            }
326        }
327
328        Ok(())
329    }
330
331    /// Forward a batch of DEL events for relationship processing.
332    ///
333    /// Items should all be the same entity type. This keeps cascade deletes grouped
334    /// so downstream stores and views can process one wider delete wave instead of
335    /// thousands of tiny per-parent cascades.
336    pub fn forward_del_batch(
337        &self,
338        items: &[Arc<dyn AnyItem>],
339        ctx: &CellServerCtx,
340    ) -> Result<(), PersistError> {
341        if items.is_empty() {
342            return Ok(());
343        }
344
345        self.handle_belongs_to_cascade_batch(items, ctx)?;
346        self.handle_owns_many_parent_delete_batch(items, ctx)?;
347
348        for item in items {
349            self.handle_owns_many_child_delete(item, ctx)?;
350
351            if let Some(lookups) = self.belongs_to_by_local.get(item.entity_type()) {
352                for lookup in lookups {
353                    self.remove_belongs_to_child(lookup, &item.id());
354                }
355            }
356        }
357
358        Ok(())
359    }
360
361    /// Establish relations on startup (called after durable backend catchup).
362    ///
363    /// This performs:
364    /// 1. BelongsTo orphan cleanup: Delete children pointing to non-existent parents
365    /// 2. OwnsMany orphan cleanup: Delete children not referenced by any parent
366    /// 3. EnsureFor initialization: Create missing entities for all dependency combinations
367    pub fn establish_relations(&self, ctx: &CellServerCtx) -> Result<(), PersistError> {
368        info!("RelationshipManager: Establishing relations on startup");
369        trace!(
370            "RelationshipManager: BelongsTo relations by local: {:?}",
371            self.belongs_to_by_local.keys().collect::<Vec<_>>()
372        );
373        debug!(
374            "RelationshipManager: OwnsMany relations by local: {:?}",
375            self.owns_many_by_local.keys().collect::<Vec<_>>()
376        );
377
378        // 1. Orphan cleanup for BelongsTo relationships
379        self.cleanup_belongs_to_orphans(ctx)?;
380
381        // 2. Orphan cleanup for OwnsMany relationships
382        self.cleanup_owns_many_orphans(ctx)?;
383
384        // 3. EnsureFor initialization
385        self.initialize_ensure_for(ctx)?;
386
387        info!("RelationshipManager: Relations established");
388        Ok(())
389    }
390
391    // ─────────────────────────────────────────────────────────────────────────────
392    // Cascade handlers
393    // ─────────────────────────────────────────────────────────────────────────────
394
395    /// Handle BelongsTo cascades: when a parent is deleted, delete all children
396    fn handle_belongs_to_cascade(
397        &self,
398        item: &Arc<dyn AnyItem>,
399        ctx: &CellServerCtx,
400    ) -> Result<(), PersistError> {
401        let item_type = item.entity_type();
402        let Some(lookups) = self.belongs_to_by_foreign.get(item_type) else {
403            return Ok(());
404        };
405
406        let parent_id = item.id();
407
408        for lookup in lookups {
409            // Find children whose FK matches the deleted parent ID using extractor
410            let children = self.find_children_by_fk(ctx, lookup, &parent_id);
411            if children.is_empty() {
412                continue;
413            }
414
415            trace!(
416                "RelationshipManager: Cascade delete batch {} count={} (parent {} deleted)",
417                lookup.local_type,
418                children.len(),
419                parent_id
420            );
421            self.publish_del_cascade_batch(ctx, &children)?;
422        }
423
424        Ok(())
425    }
426
427    fn handle_belongs_to_cascade_batch(
428        &self,
429        items: &[Arc<dyn AnyItem>],
430        ctx: &CellServerCtx,
431    ) -> Result<(), PersistError> {
432        let Some(first) = items.first() else {
433            return Ok(());
434        };
435        let item_type = first.entity_type();
436        let Some(lookups) = self.belongs_to_by_foreign.get(item_type) else {
437            return Ok(());
438        };
439
440        let parent_ids: Vec<Arc<str>> = items.iter().map(|item| item.id()).collect();
441
442        for lookup in lookups {
443            let mut children_by_id: HashMap<Arc<str>, Arc<dyn AnyItem>> = HashMap::new();
444            for parent_id in &parent_ids {
445                for child in self.find_children_by_fk(ctx, lookup, parent_id) {
446                    children_by_id.entry(child.id()).or_insert(child);
447                }
448            }
449
450            if children_by_id.is_empty() {
451                continue;
452            }
453
454            let children: Vec<_> = children_by_id.into_values().collect();
455            trace!(
456                "RelationshipManager: Cascade delete batch {} count={} ({} parents deleted)",
457                lookup.local_type,
458                children.len(),
459                parent_ids.len()
460            );
461            self.publish_del_cascade_batch(ctx, &children)?;
462        }
463
464        Ok(())
465    }
466
467    /// Find children whose FK matches a given parent ID
468    fn find_children_by_fk(
469        &self,
470        ctx: &CellServerCtx,
471        lookup: &BelongsToLookup,
472        parent_id: &str,
473    ) -> Vec<Arc<dyn AnyItem>> {
474        self.ensure_belongs_to_index_loaded(ctx, lookup);
475        if let Some(parent_map) = self.belongs_to_children_by_parent.get(&lookup.id) {
476            let store = ctx.registry.get_or_create(lookup.local_type);
477            let Some(child_ids) = parent_map.get(parent_id) else {
478                return Vec::new();
479            };
480            return child_ids
481                .iter()
482                .filter_map(|child_id| store.get_value(child_id))
483                .collect();
484        }
485
486        let store = ctx.registry.get_or_create(lookup.local_type);
487        store
488            .entries()
489            .get()
490            .into_iter()
491            .filter(|(_, item)| {
492                (lookup.extract_fk)(item.as_any())
493                    .map(|fk| fk.as_ref() == parent_id)
494                    .unwrap_or(false)
495            })
496            .map(|(_, item)| item)
497            .collect()
498    }
499
500    fn index_belongs_to_child(&self, lookup: &BelongsToLookup, item: &Arc<dyn AnyItem>) {
501        let child_id = item.id();
502        self.remove_belongs_to_child(lookup, &child_id);
503
504        let Some(parent_id) = (lookup.extract_fk)(item.as_any()) else {
505            return;
506        };
507
508        self.belongs_to_parent_by_child
509            .entry(lookup.id)
510            .or_default()
511            .insert(child_id.clone(), parent_id.clone());
512        self.belongs_to_children_by_parent
513            .entry(lookup.id)
514            .or_default()
515            .entry(parent_id)
516            .or_default()
517            .insert(child_id);
518    }
519
520    fn remove_belongs_to_child(&self, lookup: &BelongsToLookup, child_id: &Arc<str>) {
521        let Some(parent_map) = self.belongs_to_parent_by_child.get(&lookup.id) else {
522            return;
523        };
524        let Some((_, parent_id)) = parent_map.remove(child_id) else {
525            return;
526        };
527
528        let Some(children_by_parent) = self.belongs_to_children_by_parent.get(&lookup.id) else {
529            return;
530        };
531        let should_remove_parent = children_by_parent
532            .get_mut(parent_id.as_ref())
533            .map(|mut child_ids| {
534                child_ids.remove(child_id);
535                child_ids.is_empty()
536            })
537            .unwrap_or(false);
538
539        if should_remove_parent {
540            children_by_parent.remove(parent_id.as_ref());
541        }
542    }
543
544    fn ensure_belongs_to_index_loaded(&self, ctx: &CellServerCtx, lookup: &BelongsToLookup) {
545        if self.belongs_to_parent_by_child.contains_key(&lookup.id) {
546            return;
547        }
548
549        let child_index = DashMap::<Arc<str>, Arc<str>>::new();
550        let parent_index = DashMap::<Arc<str>, BTreeSet<Arc<str>>>::new();
551        let store = ctx.registry.get_or_create(lookup.local_type);
552
553        for (_, item) in store.snapshot() {
554            let Some(parent_id) = (lookup.extract_fk)(item.as_any()) else {
555                continue;
556            };
557            let child_id = item.id();
558            child_index.insert(child_id.clone(), parent_id.clone());
559            parent_index.entry(parent_id).or_default().insert(child_id);
560        }
561
562        let _ = self
563            .belongs_to_parent_by_child
564            .insert(lookup.id, child_index);
565        let _ = self
566            .belongs_to_children_by_parent
567            .insert(lookup.id, parent_index);
568    }
569
570    /// Handle OwnsMany parent delete: delete all owned children
571    fn handle_owns_many_parent_delete(
572        &self,
573        item: &Arc<dyn AnyItem>,
574        ctx: &CellServerCtx,
575    ) -> Result<(), PersistError> {
576        let item_type = item.entity_type();
577        let Some(lookups) = self.owns_many_by_local.get(item_type) else {
578            return Ok(());
579        };
580
581        for lookup in lookups {
582            // Extract child IDs using the typed extractor
583            let child_ids = match (lookup.extract_ids)(item.as_any()) {
584                Some(ids) => ids,
585                None => continue,
586            };
587
588            if child_ids.is_empty() {
589                continue;
590            }
591
592            let mut children = Vec::new();
593            for child_id in &child_ids {
594                if self.get_by_id(ctx, lookup.foreign_type, child_id).is_some()
595                    && let Some(child) = self.get_by_id(ctx, lookup.foreign_type, child_id)
596                {
597                    children.push(child);
598                }
599            }
600
601            if children.is_empty() {
602                continue;
603            }
604
605            trace!(
606                "RelationshipManager: Cascade delete owned batch {} count={}",
607                lookup.foreign_type,
608                children.len()
609            );
610            self.publish_del_cascade_batch(ctx, &children)?;
611        }
612
613        Ok(())
614    }
615
616    fn handle_owns_many_parent_delete_batch(
617        &self,
618        items: &[Arc<dyn AnyItem>],
619        ctx: &CellServerCtx,
620    ) -> Result<(), PersistError> {
621        let Some(first) = items.first() else {
622            return Ok(());
623        };
624        let item_type = first.entity_type();
625        let Some(lookups) = self.owns_many_by_local.get(item_type) else {
626            return Ok(());
627        };
628
629        for lookup in lookups {
630            let mut child_ids = BTreeSet::new();
631            for item in items {
632                if let Some(ids) = (lookup.extract_ids)(item.as_any()) {
633                    child_ids.extend(ids);
634                }
635            }
636
637            if child_ids.is_empty() {
638                continue;
639            }
640
641            let mut children = Vec::new();
642            for child_id in &child_ids {
643                if let Some(child) = self.get_by_id(ctx, lookup.foreign_type, child_id) {
644                    children.push(child);
645                }
646            }
647
648            if children.is_empty() {
649                continue;
650            }
651
652            trace!(
653                "RelationshipManager: Cascade delete owned batch {} count={} ({} parents deleted)",
654                lookup.foreign_type,
655                children.len(),
656                items.len()
657            );
658            self.publish_del_cascade_batch(ctx, &children)?;
659        }
660
661        Ok(())
662    }
663
664    /// Handle OwnsMany child delete: remove child ID from parent arrays
665    fn handle_owns_many_child_delete(
666        &self,
667        item: &Arc<dyn AnyItem>,
668        ctx: &CellServerCtx,
669    ) -> Result<(), PersistError> {
670        let item_type = item.entity_type();
671        let Some(lookups) = self.owns_many_by_foreign.get(item_type) else {
672            return Ok(());
673        };
674
675        let child_id = item.id();
676
677        for lookup in lookups {
678            // Find parents that contain this child ID using extract_ids
679            let parents = self.find_parents_containing(ctx, lookup, &child_id);
680            let mut updates = Vec::new();
681
682            for parent_item in parents {
683                // Use the remove_id extractor to get updated parent as Value
684                if let Some(updated_parent) = (lookup.remove_id)(parent_item.as_any(), &child_id) {
685                    trace!(
686                        "RelationshipManager: Updating {} {} to remove child {}",
687                        lookup.local_type,
688                        parent_item.id(),
689                        child_id
690                    );
691                    updates.push(updated_parent);
692                }
693            }
694
695            if !updates.is_empty() {
696                self.publish_set_cascade_batch(ctx, &updates)?;
697            }
698        }
699
700        Ok(())
701    }
702
703    /// Find parents whose owned array contains a given child ID
704    fn find_parents_containing(
705        &self,
706        ctx: &CellServerCtx,
707        lookup: &OwnsManyLookup,
708        child_id: &str,
709    ) -> Vec<Arc<dyn AnyItem>> {
710        let store = ctx.registry.get_or_create(lookup.local_type);
711        store
712            .entries()
713            .get()
714            .into_iter()
715            .filter(|(_, item)| {
716                (lookup.extract_ids)(item.as_any())
717                    .map(|ids| ids.iter().any(|id| id.as_ref() == child_id))
718                    .unwrap_or(false)
719            })
720            .map(|(_, item)| item)
721            .collect()
722    }
723
724    /// Handle EnsureFor: when dependency created, ensure derived entities exist
725    fn handle_ensure_for(
726        &self,
727        item: &Arc<dyn AnyItem>,
728        ctx: &CellServerCtx,
729    ) -> Result<(), PersistError> {
730        let item_type = item.entity_type();
731        let Some(lookups) = self.ensure_for_by_dependency.get(item_type) else {
732            return Ok(());
733        };
734
735        for lookup in lookups {
736            // Get all combinations of dependency entities
737            let combinations = self.get_dependency_combinations(ctx, &lookup.dependencies);
738
739            // Snapshot the store once outside the combo loop to avoid
740            // re-materializing entries() for every combination
741            let store = ctx.registry.get_or_create(lookup.local_type);
742            let existing_items = store.snapshot();
743
744            for combo in combinations {
745                // Check if derived entity already exists
746                let existing =
747                    Self::find_ensure_for_entity_in(&existing_items, &lookup.dependencies, &combo);
748
749                if existing.is_none() {
750                    // Create the derived entity using the factory
751                    let entity = (lookup.make_entity)(&combo);
752
753                    trace!(
754                        "RelationshipManager: Creating ensured {} for {:?}",
755                        lookup.local_type, combo
756                    );
757
758                    self.publish_set_cascade(ctx, lookup.local_type, entity)?;
759                }
760            }
761        }
762
763        Ok(())
764    }
765
766    // ─────────────────────────────────────────────────────────────────────────────
767    // Orphan cleanup
768    // ─────────────────────────────────────────────────────────────────────────────
769
770    /// Cleanup orphaned children for BelongsTo relationships
771    /// Boot-time **backstop** sweep for `belongs_to` orphans (children whose FK
772    /// points at a parent that no longer exists).
773    ///
774    /// Runtime orphaning is handled by the transitive DEL cascade
775    /// (`Origin::Cascade` + DEL descends — see `CellServerCtx::apply_effects`),
776    /// so deleting a parent removes its whole subtree without a restart. This
777    /// sweep remains only for the "child written with an FK to a never-existent
778    /// parent" case. We deliberately do **not** delete such orphans eagerly on
779    /// the child's SET: under out-of-order / eventually-consistent ingestion a
780    /// child can legitimately arrive before its parent, so eager deletion would
781    /// be data loss. The sweep runs at boot, once ordering has settled.
782    fn cleanup_belongs_to_orphans(&self, ctx: &CellServerCtx) -> Result<(), PersistError> {
783        trace!(
784            "RelationshipManager: cleanup_belongs_to_orphans - checking {} child types",
785            self.belongs_to_by_local.len()
786        );
787
788        for (child_type, lookups) in &self.belongs_to_by_local {
789            trace!(
790                "RelationshipManager: Checking BelongsTo orphans for child type '{}' ({} lookups)",
791                child_type,
792                lookups.len()
793            );
794
795            for lookup in lookups {
796                // Get all parent IDs that exist
797                let parents = self.get_all_items(ctx, lookup.foreign_type);
798                let parent_ids: HashSet<Arc<str>> = parents.iter().map(|p| p.id()).collect();
799
800                trace!(
801                    "RelationshipManager: {} -> {}: Found {} parents in store",
802                    child_type,
803                    lookup.foreign_type,
804                    parents.len()
805                );
806
807                // Get all children and find orphans using typed extractor
808                let children = self.get_all_items(ctx, child_type);
809                trace!(
810                    "RelationshipManager: {} -> {}: Found {} children in store",
811                    child_type,
812                    lookup.foreign_type,
813                    children.len()
814                );
815
816                let mut orphan_count = 0;
817                let mut valid_count = 0;
818                let mut no_fk_count = 0;
819
820                for child in &children {
821                    // Use the typed extractor to get the FK value
822                    if let Some(fk_value) = (lookup.extract_fk)(child.as_any()) {
823                        if !parent_ids.contains(&fk_value) {
824                            debug!(
825                                "RelationshipManager: ORPHAN {} {} has FK '{}' but parent {} not found (have {} parent IDs)",
826                                child_type,
827                                child.id(),
828                                fk_value,
829                                lookup.foreign_type,
830                                parent_ids.len()
831                            );
832                            self.publish_del_cascade(ctx, child_type, &child.id())?;
833                            orphan_count += 1;
834                        } else {
835                            valid_count += 1;
836                        }
837                    } else {
838                        trace!(
839                            "RelationshipManager: {} {} - extract_fk returned None",
840                            child_type,
841                            child.id()
842                        );
843                        no_fk_count += 1;
844                    }
845                }
846
847                trace!(
848                    "RelationshipManager: {} -> {}: {} orphans deleted, {} valid, {} no FK",
849                    child_type, lookup.foreign_type, orphan_count, valid_count, no_fk_count
850                );
851            }
852        }
853
854        Ok(())
855    }
856
857    /// Cleanup orphaned children for OwnsMany relationships
858    fn cleanup_owns_many_orphans(&self, ctx: &CellServerCtx) -> Result<(), PersistError> {
859        trace!(
860            "RelationshipManager: cleanup_owns_many_orphans - checking {} parent types",
861            self.owns_many_by_local.len()
862        );
863
864        for (parent_type, lookups) in &self.owns_many_by_local {
865            trace!(
866                "RelationshipManager: Checking OwnsMany orphans for parent type '{}' ({} lookups)",
867                parent_type,
868                lookups.len()
869            );
870
871            for lookup in lookups {
872                // Get all child IDs referenced by parents using typed extractors
873                let parents = self.get_all_items(ctx, parent_type);
874                let mut referenced_ids: HashSet<Arc<str>> = HashSet::new();
875
876                trace!(
877                    "RelationshipManager: {} ->> {}: Found {} parents in store",
878                    parent_type,
879                    lookup.foreign_type,
880                    parents.len()
881                );
882
883                let mut parents_with_ids = 0;
884                let mut parents_no_ids = 0;
885                for parent in &parents {
886                    if let Some(ids) = (lookup.extract_ids)(parent.as_any()) {
887                        if !ids.is_empty() {
888                            parents_with_ids += 1;
889                        }
890                        referenced_ids.extend(ids);
891                    } else {
892                        parents_no_ids += 1;
893                    }
894                }
895
896                trace!(
897                    "RelationshipManager: {} ->> {}: {} parents have child IDs, {} have no IDs, {} total referenced child IDs",
898                    parent_type,
899                    lookup.foreign_type,
900                    parents_with_ids,
901                    parents_no_ids,
902                    referenced_ids.len()
903                );
904
905                // Get all children and find orphans
906                let children = self.get_all_items(ctx, lookup.foreign_type);
907                trace!(
908                    "RelationshipManager: {} ->> {}: Found {} children in store",
909                    parent_type,
910                    lookup.foreign_type,
911                    children.len()
912                );
913
914                let mut orphan_count = 0;
915                let mut valid_count = 0;
916
917                for child in children {
918                    let child_id = child.id();
919                    if !referenced_ids.contains(&child_id) {
920                        debug!(
921                            "RelationshipManager: ORPHAN {} {} not referenced by any {} (have {} referenced IDs)",
922                            lookup.foreign_type,
923                            child_id,
924                            parent_type,
925                            referenced_ids.len()
926                        );
927                        self.publish_del_cascade(ctx, lookup.foreign_type, &child_id)?;
928                        orphan_count += 1;
929                    } else {
930                        valid_count += 1;
931                    }
932                }
933
934                if orphan_count > 0 {
935                    info!(
936                        "RelationshipManager: {} ->> {}: {} orphans deleted, {} valid",
937                        parent_type, lookup.foreign_type, orphan_count, valid_count
938                    );
939                } else {
940                    trace!(
941                        "RelationshipManager: {} ->> {}: {} orphans deleted, {} valid",
942                        parent_type, lookup.foreign_type, orphan_count, valid_count
943                    );
944                }
945            }
946        }
947
948        Ok(())
949    }
950
951    /// Initialize EnsureFor relationships (create missing derived entities)
952    fn initialize_ensure_for(&self, ctx: &CellServerCtx) -> Result<(), PersistError> {
953        // Track which local_types we've processed to avoid duplicates
954        let mut processed: HashSet<&'static str> = HashSet::new();
955
956        for lookups in self.ensure_for_by_dependency.values() {
957            for lookup in lookups {
958                if processed.contains(lookup.local_type) {
959                    continue;
960                }
961                processed.insert(lookup.local_type);
962
963                // Get all combinations of dependency entities
964                let combinations = self.get_dependency_combinations(ctx, &lookup.dependencies);
965
966                // Snapshot once outside the combo loop
967                let store = ctx.registry.get_or_create(lookup.local_type);
968                let existing_items = store.snapshot();
969
970                let mut created_count = 0;
971
972                for combo in combinations {
973                    // Check if derived entity already exists
974                    let existing = Self::find_ensure_for_entity_in(
975                        &existing_items,
976                        &lookup.dependencies,
977                        &combo,
978                    );
979
980                    if existing.is_none() {
981                        // Create the derived entity using the factory
982                        let entity = (lookup.make_entity)(&combo);
983                        self.publish_set_cascade(ctx, lookup.local_type, entity)?;
984                        created_count += 1;
985                    }
986                }
987
988                if created_count > 0 {
989                    info!(
990                        "RelationshipManager: Created {} {} entities via EnsureFor",
991                        created_count, lookup.local_type
992                    );
993                }
994            }
995        }
996
997        Ok(())
998    }
999
1000    // ─────────────────────────────────────────────────────────────────────────────
1001    // Query helpers (using CellServerCtx)
1002    // ─────────────────────────────────────────────────────────────────────────────
1003
1004    /// Get an entity by ID
1005    fn get_by_id(
1006        &self,
1007        ctx: &CellServerCtx,
1008        entity_type: &str,
1009        id: &str,
1010    ) -> Option<Arc<dyn AnyItem>> {
1011        let store = ctx.registry.get_or_create(entity_type);
1012        store.get_value(&id.into())
1013    }
1014
1015    /// Get all entities of a type
1016    fn get_all_items(&self, ctx: &CellServerCtx, entity_type: &str) -> Vec<Arc<dyn AnyItem>> {
1017        let store = ctx.registry.get_or_create(entity_type);
1018        store.snapshot().into_iter().map(|(_, item)| item).collect()
1019    }
1020
1021    /// Get all combinations of dependency entity IDs for EnsureFor
1022    fn get_dependency_combinations(
1023        &self,
1024        ctx: &CellServerCtx,
1025        dependencies: &[EnsureForDependency],
1026    ) -> Vec<Vec<Arc<str>>> {
1027        if dependencies.is_empty() {
1028            return vec![];
1029        }
1030
1031        // Get IDs for each dependency type
1032        let mut dep_ids: Vec<Vec<Arc<str>>> = Vec::new();
1033
1034        for dep in dependencies {
1035            let items = self.get_all_items(ctx, dep.foreign_type);
1036            let ids: Vec<Arc<str>> = items.iter().map(|item| item.id()).collect();
1037            dep_ids.push(ids);
1038        }
1039
1040        // Compute Cartesian product
1041        self.cartesian_product(&dep_ids)
1042    }
1043
1044    /// Compute Cartesian product of multiple ID sets
1045    fn cartesian_product(&self, sets: &[Vec<Arc<str>>]) -> Vec<Vec<Arc<str>>> {
1046        if sets.is_empty() {
1047            return vec![];
1048        }
1049
1050        let mut result = vec![vec![]];
1051
1052        for set in sets {
1053            let mut new_result = Vec::new();
1054            for existing in &result {
1055                for item in set {
1056                    let mut new_combo = existing.clone();
1057                    new_combo.push(item.clone());
1058                    new_result.push(new_combo);
1059                }
1060            }
1061            result = new_result;
1062        }
1063
1064        result
1065    }
1066
1067    /// Find an EnsureFor entity matching the given dependency IDs
1068    /// from a pre-computed snapshot of existing items.
1069    fn find_ensure_for_entity_in(
1070        items: &[(Arc<str>, Arc<dyn AnyItem>)],
1071        dependencies: &[EnsureForDependency],
1072        combo: &[Arc<str>],
1073    ) -> Option<Arc<dyn AnyItem>> {
1074        if dependencies.is_empty() || combo.is_empty() {
1075            return None;
1076        }
1077
1078        items.iter().find_map(|(_, item)| {
1079            // Check if all dependency FKs match the combo values
1080            let all_match = dependencies
1081                .iter()
1082                .zip(combo.iter())
1083                .all(|(dep, expected_id)| {
1084                    (dep.extract_fk)(item.as_any())
1085                        .map(|fk| fk == *expected_id)
1086                        .unwrap_or(false)
1087                });
1088
1089            if all_match { Some(item.clone()) } else { None }
1090        })
1091    }
1092
1093    // ─────────────────────────────────────────────────────────────────────────────
1094    // Publishing helpers (using CellServerCtx)
1095    // ─────────────────────────────────────────────────────────────────────────────
1096
1097    /// Publish a SET for cascade operations.
1098    ///
1099    /// Sets prevent_relationship_updates to avoid infinite loops.
1100    fn publish_set_cascade(
1101        &self,
1102        ctx: &CellServerCtx,
1103        _entity_type: &str,
1104        item: Arc<dyn AnyItem>,
1105    ) -> Result<(), PersistError> {
1106        // If the item has an empty #[server_owned] field, bake in the current server's ID
1107        let item = if item.server_owner().is_none() {
1108            item.bake_server_owner(&ctx.host_id.to_string())
1109                .unwrap_or(item)
1110        } else {
1111            item
1112        };
1113
1114        ctx.set_dyn_with_origin(item, super::Origin::Cascade)
1115    }
1116
1117    fn publish_set_cascade_batch(
1118        &self,
1119        ctx: &CellServerCtx,
1120        items: &[Arc<dyn AnyItem>],
1121    ) -> Result<(), PersistError> {
1122        ctx.batch_set_dyn_with_origin(items, super::Origin::Cascade)
1123    }
1124
1125    /// Publish a DEL for cascade operations.
1126    ///
1127    /// Sets prevent_relationship_updates to avoid infinite loops.
1128    fn publish_del_cascade(
1129        &self,
1130        ctx: &CellServerCtx,
1131        entity_type: &str,
1132        id: &str,
1133    ) -> Result<(), PersistError> {
1134        // Get the entity from the store
1135        let id_arc: Arc<str> = id.into();
1136        if let Some(item) = ctx.registry.get_or_create(entity_type).get_value(&id_arc) {
1137            debug!(
1138                "RelationshipManager: publish_del_cascade {} {} - entity found, deleting",
1139                entity_type, id
1140            );
1141            ctx.del_dyn_with_origin(item, super::Origin::Cascade)?;
1142        } else {
1143            trace!(
1144                "RelationshipManager: publish_del_cascade {} {} - entity NOT found in store",
1145                entity_type, id
1146            );
1147        }
1148
1149        Ok(())
1150    }
1151
1152    fn publish_del_cascade_batch(
1153        &self,
1154        ctx: &CellServerCtx,
1155        items: &[Arc<dyn AnyItem>],
1156    ) -> Result<(), PersistError> {
1157        if items.is_empty() {
1158            return Ok(());
1159        }
1160
1161        ctx.batch_del_dyn_with_origin(items, super::Origin::Cascade)
1162    }
1163}
1164
1165impl Default for RelationshipManager {
1166    fn default() -> Self {
1167        Self::new()
1168    }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173    use super::*;
1174
1175    #[test]
1176    fn test_relationship_manager_creation() {
1177        let manager = RelationshipManager::new();
1178
1179        // Should have built lookup tables from inventory
1180        // (actual counts depend on entities linked in test binary)
1181        // Just verify the manager initializes without panic
1182        let _ = manager.belongs_to_by_foreign.len();
1183        let _ = manager.owns_many_by_local.len();
1184    }
1185
1186    #[test]
1187    fn test_cartesian_product() {
1188        let manager = RelationshipManager::new();
1189
1190        let sets = vec![
1191            vec![Arc::from("a"), Arc::from("b")],
1192            vec![Arc::from("1"), Arc::from("2")],
1193        ];
1194
1195        let product = manager.cartesian_product(&sets);
1196
1197        assert_eq!(product.len(), 4);
1198        assert!(product.contains(&vec![Arc::from("a"), Arc::from("1")]));
1199        assert!(product.contains(&vec![Arc::from("a"), Arc::from("2")]));
1200        assert!(product.contains(&vec![Arc::from("b"), Arc::from("1")]));
1201        assert!(product.contains(&vec![Arc::from("b"), Arc::from("2")]));
1202    }
1203
1204    #[test]
1205    fn test_cartesian_product_empty() {
1206        let manager = RelationshipManager::new();
1207
1208        let sets: Vec<Vec<Arc<str>>> = vec![];
1209        let product = manager.cartesian_product(&sets);
1210        assert!(product.is_empty());
1211    }
1212}
1213
1214#[cfg(test)]
1215mod cascade_tests {
1216    //! Transitive relationship cascade (Event Bus Unification, Fix #1).
1217    //!
1218    //! Deleting a parent must remove its children, grandchildren, … at runtime
1219    //! (previously grandchildren survived until the boot-time orphan sweep
1220    //! because the cascade product's `prevent_relationship_updates` flag was
1221    //! read as "do not cascade at all"). A cyclic schema must converge, not loop.
1222
1223    use std::sync::Arc;
1224
1225    use uuid::Uuid;
1226
1227    use self::node::CascadeNode;
1228    use crate::{
1229        hyphae::Gettable,
1230        search::SearchIndex,
1231        server::{CellServerCtx, HandlerRegistry, RelationshipManager, persister::PersisterRouter},
1232        store::StoreRegistry,
1233        test_util::scheduler_test_serial,
1234    };
1235
1236    // `#[myko_item]` re-imports hyphae traits at module scope, so the entity
1237    // lives in its own module (mirrors `bench_entities::tree`).
1238    mod node {
1239        use crate::prelude::*;
1240
1241        /// Self-referential entity: a node `belongs_to` another node of the same
1242        /// type, so one type expresses both a multi-level chain and a cycle.
1243        #[myko_item]
1244        pub struct CascadeNode {
1245            #[belongs_to(CascadeNode)]
1246            pub parent_id: CascadeNodeId,
1247            pub name: String,
1248        }
1249    }
1250
1251    fn make_ctx() -> (CellServerCtx, Arc<StoreRegistry>) {
1252        let registry = Arc::new(StoreRegistry::new());
1253        let ctx = CellServerCtx::new(
1254            Uuid::new_v4(),
1255            registry.clone(),
1256            Arc::new(HandlerRegistry::new()),
1257            Arc::new(RelationshipManager::new()),
1258            Arc::new(PersisterRouter::default()),
1259            Arc::new(SearchIndex::new()),
1260            Arc::new(dashmap::DashMap::new()),
1261            None,
1262            None,
1263        );
1264        (ctx, registry)
1265    }
1266
1267    fn make_node(id: &str, parent_id: &str) -> CascadeNode {
1268        CascadeNode {
1269            id: id.into(),
1270            parent_id: parent_id.into(),
1271            name: format!("node-{id}"),
1272        }
1273    }
1274
1275    fn exists(registry: &StoreRegistry, id: &str) -> bool {
1276        registry
1277            .get("CascadeNode")
1278            .and_then(|store| store.get(&Arc::<str>::from(id)).get())
1279            .is_some()
1280    }
1281
1282    /// A 3-level `belongs_to` chain: deleting the root removes the child *and*
1283    /// the grandchild at runtime. The grandchild regressed before Fix #1.
1284    #[test]
1285    fn del_cascade_descends_to_grandchildren() {
1286        let _serial = scheduler_test_serial();
1287        let (ctx, registry) = make_ctx();
1288
1289        // root <- branch <- leaf
1290        ctx.set(&make_node("root", "")).unwrap();
1291        ctx.set(&make_node("branch", "root")).unwrap();
1292        ctx.set(&make_node("leaf", "branch")).unwrap();
1293
1294        assert!(exists(&registry, "root"));
1295        assert!(exists(&registry, "branch"));
1296        assert!(exists(&registry, "leaf"));
1297
1298        ctx.del(&make_node("root", "")).unwrap();
1299
1300        assert!(!exists(&registry, "root"), "root deleted");
1301        assert!(!exists(&registry, "branch"), "direct child deleted");
1302        assert!(
1303            !exists(&registry, "leaf"),
1304            "grandchild deleted at runtime (Fix #1)"
1305        );
1306    }
1307
1308    /// Regression test: a cascade-triggered recursive `emit_grouped` call
1309    /// (deleting root cascades to branch, which cascades to leaf) must not
1310    /// share its reducing `hyphae::batch` window with the batch that
1311    /// triggered it. `CellMap`'s `diffs_cell` coalesces last-write-wins like
1312    /// any other cell, so if the recursive call's `store.remove_many` landed
1313    /// in the *same* still-open window as the top-level reduce, the later
1314    /// level's diff would silently drop the earlier one on the same
1315    /// `CascadeNode` store (see the batch-scoping comment on `emit_grouped`).
1316    #[test]
1317    fn del_cascade_recursion_does_not_drop_earlier_diffs_in_same_store() {
1318        let _serial = scheduler_test_serial();
1319        let (ctx, registry) = make_ctx();
1320
1321        ctx.set(&make_node("root", "")).unwrap();
1322        ctx.set(&make_node("branch", "root")).unwrap();
1323        ctx.set(&make_node("leaf", "branch")).unwrap();
1324        ctx.set(&make_node("island", "")).unwrap();
1325
1326        let store = registry.get_or_create("CascadeNode");
1327        let seen: Arc<std::sync::Mutex<Vec<String>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
1328        let seen_for_closure = seen.clone();
1329        let _guard = store.subscribe_diffs(move |diff| {
1330            seen_for_closure.lock().unwrap().push(format!("{diff:?}"));
1331        });
1332        // subscribe_diffs replays the current snapshot synchronously on
1333        // subscribe -- drop that so only diffs from the batch below count.
1334        seen.lock().unwrap().clear();
1335
1336        // One wire batch: an unrelated standalone delete (island) alongside
1337        // root's delete, which cascades to branch then leaf -- three
1338        // distinct mutations to the *same* CascadeNode store triggered by
1339        // one top-level call.
1340        let root_item: Arc<dyn crate::core::item::AnyItem> = Arc::new(make_node("root", ""));
1341        let island_item: Arc<dyn crate::core::item::AnyItem> = Arc::new(make_node("island", ""));
1342        ctx.batch_del_dyn(&[root_item, island_item]).unwrap();
1343
1344        assert!(!exists(&registry, "root"));
1345        assert!(!exists(&registry, "branch"), "direct child deleted");
1346        assert!(!exists(&registry, "leaf"), "grandchild deleted");
1347        assert!(!exists(&registry, "island"));
1348
1349        let seen = seen.lock().unwrap();
1350        assert_eq!(
1351            seen.len(),
1352            3,
1353            "expected 3 separate diffs (root+island reduce, branch cascade, leaf cascade), not coalesced: {:?}",
1354            *seen
1355        );
1356    }
1357
1358    /// A 2-cycle (a.parent = b, b.parent = a): the cascade must converge. The
1359    /// store-as-visited-set guarantees it — the second visit finds nothing.
1360    #[test]
1361    fn del_cascade_terminates_on_cycle() {
1362        let _serial = scheduler_test_serial();
1363        let (ctx, registry) = make_ctx();
1364
1365        ctx.set(&make_node("a", "b")).unwrap();
1366        ctx.set(&make_node("b", "a")).unwrap();
1367
1368        ctx.del(&make_node("a", "b")).unwrap();
1369
1370        assert!(!exists(&registry, "a"), "a deleted");
1371        assert!(
1372            !exists(&registry, "b"),
1373            "b deleted via the cycle, then terminated"
1374        );
1375    }
1376}