Skip to main content

rust_ef/
cascade.rs

1//! Cascade save: drain children from principal HasMany navigations, FK fixup
2//! after principal PK backfill, and M2M join-row insertion via direct SQL.
3//!
4//! See `cascade-save-plan.md` for the full design. Key types:
5//! - [`DrainedChild`]: a child entity extracted from a principal's HasMany,
6//!   with parent linkage metadata.
7//! - [`FixupLink`]: records the parent→child association for post-INSERT FK
8//!   fixup (one-to-many) or join-row insertion (M2M).
9
10use crate::metadata::{NavigationKind, NavigationMeta};
11use crate::relations::DeleteBehavior;
12use std::any::{Any, TypeId};
13
14/// A child entity drained from a principal's HasMany navigation, with the
15/// metadata needed to fixup its FK after the principal's PK is backfilled.
16pub struct DrainedChild {
17    /// The principal entity's TypeId (e.g. `Blog`).
18    pub parent_type_id: TypeId,
19    /// Index of the principal entry in its DbSet (for PK lookup after INSERT).
20    pub parent_entry_idx: usize,
21    /// The drained child entity, type-erased.
22    pub child: Box<dyn Any + Send + Sync>,
23    /// The child entity's TypeId (e.g. `Post`).
24    pub child_type_id: TypeId,
25    /// The FK target type for `set_foreign_key` (the principal type).
26    pub fk_target_type_id: TypeId,
27    /// M2M only: the join table name. `None` for one-to-many.
28    pub through_table: Option<String>,
29    /// M2M only: FK column on the join table pointing to the principal.
30    pub through_parent_fk_col: Option<String>,
31    /// M2M only: FK column on the join table pointing to the child.
32    pub through_child_fk_col: Option<String>,
33}
34
35/// Records a parent→child association for post-INSERT FK fixup.
36///
37/// For one-to-many: after the principal is inserted and its PK backfilled,
38/// each child's FK field is set to the principal's PK via `set_foreign_key`.
39///
40/// For M2M: after both principal and child are inserted and their PKs
41/// backfilled, a join-table row is inserted via direct SQL.
42pub struct FixupLink {
43    pub parent_type_id: TypeId,
44    pub parent_entry_idx: usize,
45    pub child_type_id: TypeId,
46    pub child_entry_indices: Vec<usize>,
47    pub fk_target_type_id: TypeId,
48    /// M2M only: the join table name. `None` for one-to-many.
49    pub through_table: Option<String>,
50    pub through_parent_fk_col: Option<String>,
51    pub through_child_fk_col: Option<String>,
52}
53
54/// Generates a batched `INSERT INTO through_table (parent_col, child_col)
55/// VALUES (?, ?), (?, ?), ...` statement for M2M join rows.
56pub fn m2m_insert_sql(table: &str, parent_col: &str, child_col: &str, row_count: usize) -> String {
57    let placeholders: Vec<String> = (0..row_count).map(|_| "(?, ?)".to_string()).collect();
58    format!(
59        "INSERT INTO {} ({}, {}) VALUES {}",
60        table,
61        parent_col,
62        child_col,
63        placeholders.join(", ")
64    )
65}
66
67// ---------------------------------------------------------------------------
68// Cascade delete types
69// ---------------------------------------------------------------------------
70
71/// The action to take for untracked dependents when a principal is deleted.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum CascadeDeleteAction {
74    /// `DELETE FROM child_table WHERE fk = ?`
75    Delete,
76    /// `UPDATE child_table SET fk = NULL WHERE fk = ?`
77    SetNull,
78}
79
80/// A directive to delete or nullify untracked dependents of a Deleted
81/// principal. Generated during the cascade delete drain loop and executed
82/// as direct SQL at the start of the DELETE phase, before PK-based deletes.
83pub struct CascadeDeleteDirective {
84    /// The table to act on (child table for HasMany, join table for M2M).
85    pub table: String,
86    /// The FK column to filter on.
87    pub fk_column: String,
88    /// The principal's PK value.
89    pub principal_pk: i64,
90    /// The action to perform.
91    pub action: CascadeDeleteAction,
92}
93
94// ---------------------------------------------------------------------------
95// Delete behavior resolution
96// ---------------------------------------------------------------------------
97
98/// Resolves the effective `DeleteBehavior` for a navigation, applying
99/// EFCore-style defaults when `delete_behavior` is `None`:
100/// - ManyToMany → Cascade (join rows are always pruned)
101/// - required FK (non-nullable type, e.g. `i32`) → Cascade
102/// - optional FK (nullable type, e.g. `Option<i32>`) → Restrict
103pub fn resolve_delete_behavior(nav: &NavigationMeta) -> DeleteBehavior {
104    if let Some(b) = nav.delete_behavior {
105        return b;
106    }
107    if nav.kind == NavigationKind::ManyToMany {
108        return DeleteBehavior::Cascade;
109    }
110    if let Some(meta_fn) = nav.related_entity_meta {
111        let child_meta = meta_fn();
112        if let Some(fk_prop) = child_meta.properties.iter().find(|p| p.is_foreign_key) {
113            let is_nullable = fk_prop.type_name.contains("Option");
114            return if is_nullable {
115                DeleteBehavior::Restrict
116            } else {
117                DeleteBehavior::Cascade
118            };
119        }
120    }
121    DeleteBehavior::Cascade
122}