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 std::any::{Any, TypeId};
11
12/// A child entity drained from a principal's HasMany navigation, with the
13/// metadata needed to fixup its FK after the principal's PK is backfilled.
14pub struct DrainedChild {
15    /// The principal entity's TypeId (e.g. `Blog`).
16    pub parent_type_id: TypeId,
17    /// Index of the principal entry in its DbSet (for PK lookup after INSERT).
18    pub parent_entry_idx: usize,
19    /// The drained child entity, type-erased.
20    pub child: Box<dyn Any + Send + Sync>,
21    /// The child entity's TypeId (e.g. `Post`).
22    pub child_type_id: TypeId,
23    /// The FK target type for `set_foreign_key` (the principal type).
24    pub fk_target_type_id: TypeId,
25    /// M2M only: the join table name. `None` for one-to-many.
26    pub through_table: Option<String>,
27    /// M2M only: FK column on the join table pointing to the principal.
28    pub through_parent_fk_col: Option<String>,
29    /// M2M only: FK column on the join table pointing to the child.
30    pub through_child_fk_col: Option<String>,
31}
32
33/// Records a parent→child association for post-INSERT FK fixup.
34///
35/// For one-to-many: after the principal is inserted and its PK backfilled,
36/// each child's FK field is set to the principal's PK via `set_foreign_key`.
37///
38/// For M2M: after both principal and child are inserted and their PKs
39/// backfilled, a join-table row is inserted via direct SQL.
40pub struct FixupLink {
41    pub parent_type_id: TypeId,
42    pub parent_entry_idx: usize,
43    pub child_type_id: TypeId,
44    pub child_entry_indices: Vec<usize>,
45    pub fk_target_type_id: TypeId,
46    /// M2M only: the join table name. `None` for one-to-many.
47    pub through_table: Option<String>,
48    pub through_parent_fk_col: Option<String>,
49    pub through_child_fk_col: Option<String>,
50}
51
52/// Generates a batched `INSERT INTO through_table (parent_col, child_col)
53/// VALUES (?, ?), (?, ?), ...` statement for M2M join rows.
54pub fn m2m_insert_sql(table: &str, parent_col: &str, child_col: &str, row_count: usize) -> String {
55    let placeholders: Vec<String> = (0..row_count).map(|_| "(?, ?)".to_string()).collect();
56    format!(
57        "INSERT INTO {} ({}, {}) VALUES {}",
58        table,
59        parent_col,
60        child_col,
61        placeholders.join(", ")
62    )
63}
64
65// ---------------------------------------------------------------------------
66// Cascade delete types
67// ---------------------------------------------------------------------------
68
69/// The action to take for untracked dependents when a principal is deleted.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum CascadeDeleteAction {
72    /// `DELETE FROM child_table WHERE fk = ?`
73    Delete,
74    /// `UPDATE child_table SET fk = NULL WHERE fk = ?`
75    SetNull,
76}
77
78/// A directive to delete or nullify untracked dependents of a Deleted
79/// principal. Generated during the cascade delete drain loop and executed
80/// as direct SQL at the start of the DELETE phase, before PK-based deletes.
81pub struct CascadeDeleteDirective {
82    /// The table to act on (child table for HasMany, join table for M2M).
83    pub table: String,
84    /// The FK column to filter on.
85    pub fk_column: String,
86    /// The principal's PK value.
87    pub principal_pk: i64,
88    /// The action to perform.
89    pub action: CascadeDeleteAction,
90}