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(
55 table: &str,
56 parent_col: &str,
57 child_col: &str,
58 row_count: usize,
59) -> String {
60 let placeholders: Vec<String> = (0..row_count).map(|_| "(?, ?)".to_string()).collect();
61 format!(
62 "INSERT INTO {} ({}, {}) VALUES {}",
63 table,
64 parent_col,
65 child_col,
66 placeholders.join(", ")
67 )
68}
69
70// ---------------------------------------------------------------------------
71// Cascade delete types
72// ---------------------------------------------------------------------------
73
74/// The action to take for untracked dependents when a principal is deleted.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum CascadeDeleteAction {
77 /// `DELETE FROM child_table WHERE fk = ?`
78 Delete,
79 /// `UPDATE child_table SET fk = NULL WHERE fk = ?`
80 SetNull,
81}
82
83/// A directive to delete or nullify untracked dependents of a Deleted
84/// principal. Generated during the cascade delete drain loop and executed
85/// as direct SQL at the start of the DELETE phase, before PK-based deletes.
86pub struct CascadeDeleteDirective {
87 /// The table to act on (child table for HasMany, join table for M2M).
88 pub table: String,
89 /// The FK column to filter on.
90 pub fk_column: String,
91 /// The principal's PK value.
92 pub principal_pk: i64,
93 /// The action to perform.
94 pub action: CascadeDeleteAction,
95}