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}