Skip to main content

rust_ef/db_context/
save_pipeline.rs

1//! `DbContext::save_changes` — the unit-of-work commit pipeline.
2//!
3//! Orchestrates change detection, interceptor hooks, cascade drain,
4//! topological DML execution, and transaction management.
5
6use crate::cascade::{self, CascadeDeleteAction};
7use crate::dependency_graph::DependencyGraph;
8use crate::error::{EFError, EFResult};
9use crate::interceptor::{SaveChangesContext, SaveChangesResultContext};
10use crate::metadata::EntityTypeMeta;
11use crate::provider::{DbValue, IAsyncConnection};
12use crate::transaction::ITransaction;
13use std::any::TypeId;
14use std::collections::HashMap;
15
16use super::save_phases::SaveChangesResult;
17
18/// Transaction source for `save_changes`: either an ambient transaction
19/// (registered by `use_transaction`) or a self-managed connection.
20pub(super) enum TxnSource {
21    Ambient(Box<dyn ITransaction>),
22    Managed(Box<dyn IAsyncConnection>),
23}
24
25impl TxnSource {
26    fn conn(&mut self) -> &mut dyn IAsyncConnection {
27        match self {
28            TxnSource::Ambient(t) => t.connection(),
29            TxnSource::Managed(c) => c.as_mut(),
30        }
31    }
32}
33
34impl super::DbContext {
35    /// Builds the interceptor `SaveChangesContext` from the actual pending
36    /// entries across all `DbSet`s (the real save data source), instead of
37    /// the legacy `change_tracker` which is never populated by `DbSet::add`.
38    /// This keeps interceptor snapshots consistent with what will be committed.
39    fn build_save_context(&self) -> SaveChangesContext {
40        let mut views: Vec<crate::tracking::EntityEntryView> = Vec::new();
41        for (type_id, set) in &self.sets {
42            if let Some(saver) = self.savers.get(type_id) {
43                views.extend(saver.collect_entries(set.as_ref()));
44            }
45        }
46        SaveChangesContext::from_views(views)
47    }
48
49    /// Saves all pending changes across all DbSets.
50    ///
51    /// Detects changes, runs interceptors, executes INSERT/UPDATE/DELETE in a
52    /// transaction, and clears tracked entries on success.
53    pub async fn save_changes(&mut self) -> EFResult<SaveChangesResult> {
54        let _save_guard = crate::observability::SaveChangesGuard::new();
55        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
56        for type_id in &type_ids {
57            let Some(set) = self.sets.get_mut(type_id) else {
58                continue;
59            };
60            let Some(saver) = self.savers.get(type_id) else {
61                continue;
62            };
63            saver.detect_changes(set.as_mut());
64        }
65
66        let configured_metas: HashMap<TypeId, EntityTypeMeta> = self
67            .model_builder
68            .build()
69            .into_iter()
70            .map(|m| (m.type_id, m))
71            .collect();
72
73        let save_ctx = self.build_save_context();
74        self.interceptor_pipeline.on_saving(&save_ctx).await?;
75
76        let mut txn = match self.ambient_transaction.take() {
77            Some(t) => TxnSource::Ambient(t),
78            None => {
79                let mut c = self.provider.get_connection().await?;
80                c.begin_transaction().await?;
81                TxnSource::Managed(c)
82            }
83        };
84
85        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
86
87        // --- Cascade drain: Added + Deleted ---
88        let fixup_links = self.drain_cascade_adds(&type_ids, &configured_metas)?;
89        let mut processed: std::collections::HashSet<(TypeId, usize)> =
90            std::collections::HashSet::new();
91        let delete_directives =
92            self.drain_cascade_deletes(&type_ids, &configured_metas, &mut processed)?;
93
94        let graph = DependencyGraph::build(&configured_metas);
95        let insert_order = graph.topological_sort();
96        let delete_order = graph.deletion_order();
97
98        let mut total_added = 0usize;
99        let mut total_updated = 0usize;
100        let mut total_deleted = 0usize;
101
102        // --- INSERT phase (topological order) + FK fixup ---
103        for type_id in &insert_order {
104            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
105                continue;
106            }
107            let Some(saver) = self.savers.get(type_id) else {
108                continue;
109            };
110            let Some(set) = self.sets.get_mut(type_id) else {
111                continue;
112            };
113            let Some(meta) = configured_metas
114                .get(type_id)
115                .or_else(|| self.entity_metas.get(type_id))
116            else {
117                return self
118                    .fail_save(
119                        txn,
120                        &save_ctx,
121                        EFError::configuration(format!(
122                            "entity metadata not found for {:?}",
123                            type_id
124                        )),
125                    )
126                    .await;
127            };
128            let inserted = match saver
129                .insert_added(txn.conn(), &*self.provider, set.as_mut(), meta)
130                .await
131            {
132                Ok(n) => n,
133                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
134            };
135            total_added += inserted;
136
137            let link_indices: Vec<usize> = fixup_links
138                .iter()
139                .enumerate()
140                .filter(|(_, l)| l.parent_type_id == *type_id && l.through_table.is_none())
141                .map(|(i, _)| i)
142                .collect();
143
144            let mut self_ref_updates: Vec<(String, i64, i64)> = Vec::new();
145
146            for link_idx in &link_indices {
147                let link = &fixup_links[*link_idx];
148                let parent_pk = {
149                    let Some(parent_saver) = self.savers.get(&link.parent_type_id) else {
150                        continue;
151                    };
152                    let Some(parent_set) = self.sets.get(&link.parent_type_id) else {
153                        continue;
154                    };
155                    parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
156                };
157                let Some(pk) = parent_pk else {
158                    continue;
159                };
160
161                {
162                    let Some(child_saver) = self.savers.get(&link.child_type_id) else {
163                        continue;
164                    };
165                    let Some(child_set) = self.sets.get_mut(&link.child_type_id) else {
166                        continue;
167                    };
168                    for &child_idx in &link.child_entry_indices {
169                        child_saver.set_fk_at(
170                            child_set.as_mut(),
171                            child_idx,
172                            link.fk_target_type_id,
173                            pk,
174                        );
175                    }
176                }
177
178                if link.child_type_id == link.parent_type_id {
179                    let Some(child_meta) = configured_metas
180                        .get(&link.child_type_id)
181                        .or_else(|| self.entity_metas.get(&link.child_type_id))
182                    else {
183                        continue;
184                    };
185                    let fk_col = child_meta
186                        .properties
187                        .iter()
188                        .find(|p| p.is_foreign_key)
189                        .map(|p| p.column_name.as_ref());
190                    let pk_col = child_meta
191                        .properties
192                        .iter()
193                        .find(|p| p.is_primary_key)
194                        .map(|p| p.column_name.as_ref())
195                        .unwrap_or("id");
196                    if let Some(fk_col) = fk_col {
197                        for &child_idx in &link.child_entry_indices {
198                            let child_pk = {
199                                let Some(child_saver) = self.savers.get(&link.child_type_id) else {
200                                    continue;
201                                };
202                                let Some(child_set) = self.sets.get(&link.child_type_id) else {
203                                    continue;
204                                };
205                                child_saver.get_pk_at(child_set.as_ref(), child_idx)
206                            };
207                            if let Some(child_pk) = child_pk {
208                                let sql = format!(
209                                    "UPDATE {} SET {} = ? WHERE {} = ?",
210                                    child_meta.table_name, fk_col, pk_col
211                                );
212                                self_ref_updates.push((sql, pk, child_pk));
213                            }
214                        }
215                    }
216                }
217            }
218
219            for (sql, fk_val, pk_val) in self_ref_updates {
220                if let Err(e) = txn
221                    .conn()
222                    .execute(&sql, &[DbValue::from(fk_val), DbValue::from(pk_val)])
223                    .await
224                {
225                    return self.fail_save(txn, &save_ctx, e).await;
226                }
227            }
228        }
229
230        // --- M2M join row insertion (after all entity INSERTs) ---
231        for link in &fixup_links {
232            if link.through_table.is_none() {
233                continue;
234            }
235            let (Some(table), Some(parent_col), Some(child_col)) = (
236                link.through_table.as_ref(),
237                link.through_parent_fk_col.as_ref(),
238                link.through_child_fk_col.as_ref(),
239            ) else {
240                continue;
241            };
242
243            let parent_pk = {
244                let Some(parent_saver) = self.savers.get(&link.parent_type_id) else {
245                    continue;
246                };
247                let Some(parent_set) = self.sets.get(&link.parent_type_id) else {
248                    continue;
249                };
250                parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
251            };
252            let Some(parent_pk) = parent_pk else {
253                continue;
254            };
255
256            let mut child_pks: Vec<i64> = Vec::new();
257            {
258                let Some(child_saver) = self.savers.get(&link.child_type_id) else {
259                    continue;
260                };
261                let Some(child_set) = self.sets.get(&link.child_type_id) else {
262                    continue;
263                };
264                for &child_idx in &link.child_entry_indices {
265                    if let Some(child_pk) = child_saver.get_pk_at(child_set.as_ref(), child_idx) {
266                        child_pks.push(child_pk);
267                    }
268                }
269            }
270
271            if !child_pks.is_empty() {
272                let sql = cascade::m2m_insert_sql(table, parent_col, child_col, child_pks.len());
273                let mut params: Vec<DbValue> = Vec::with_capacity(child_pks.len() * 2);
274                for child_pk in &child_pks {
275                    params.push(DbValue::from(parent_pk));
276                    params.push(DbValue::from(*child_pk));
277                }
278                if let Err(e) = txn.conn().execute(&sql, &params).await {
279                    return self.fail_save(txn, &save_ctx, e).await;
280                }
281                total_added += child_pks.len();
282            }
283        }
284
285        // --- UPSERT phase ---
286        for type_id in &insert_order {
287            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
288                continue;
289            }
290            let Some(saver) = self.savers.get(type_id) else {
291                continue;
292            };
293            let Some(set) = self.sets.get_mut(type_id) else {
294                continue;
295            };
296            let Some(meta) = configured_metas
297                .get(type_id)
298                .or_else(|| self.entity_metas.get(type_id))
299            else {
300                return self
301                    .fail_save(
302                        txn,
303                        &save_ctx,
304                        EFError::configuration(format!(
305                            "entity metadata not found for {:?}",
306                            type_id
307                        )),
308                    )
309                    .await;
310            };
311            let n = match saver
312                .upsert_added(txn.conn(), &*self.provider, set.as_mut(), meta)
313                .await
314            {
315                Ok(n) => n,
316                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
317            };
318            total_added += n;
319        }
320
321        // --- UPDATE phase ---
322        for type_id in &insert_order {
323            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
324                continue;
325            }
326            let Some(saver) = self.savers.get(type_id) else {
327                continue;
328            };
329            let Some(set) = self.sets.get_mut(type_id) else {
330                continue;
331            };
332            let Some(meta) = configured_metas
333                .get(type_id)
334                .or_else(|| self.entity_metas.get(type_id))
335            else {
336                return self
337                    .fail_save(
338                        txn,
339                        &save_ctx,
340                        EFError::configuration(format!(
341                            "entity metadata not found for {:?}",
342                            type_id
343                        )),
344                    )
345                    .await;
346            };
347            let n = match saver
348                .update_modified(txn.conn(), &*self.provider, set.as_mut(), meta)
349                .await
350            {
351                Ok(n) => n,
352                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
353            };
354            total_updated += n;
355        }
356
357        // --- Direct cascade SET NULL SQL (before PK-based deletes) ---
358        for directive in &delete_directives {
359            if directive.action != CascadeDeleteAction::SetNull {
360                continue;
361            }
362            let sql = format!(
363                "UPDATE {} SET {} = NULL WHERE {} = ?",
364                directive.table, directive.fk_column, directive.fk_column
365            );
366            let params = vec![DbValue::from(directive.principal_pk)];
367            if let Err(e) = txn.conn().execute(&sql, &params).await {
368                return self.fail_save(txn, &save_ctx, e).await;
369            }
370        }
371
372        // --- DELETE phase (reverse topological order: dependents first) ---
373        for type_id in &delete_order {
374            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
375                continue;
376            }
377            let Some(saver) = self.savers.get(type_id) else {
378                continue;
379            };
380            let Some(set) = self.sets.get_mut(type_id) else {
381                continue;
382            };
383            let Some(meta) = configured_metas
384                .get(type_id)
385                .or_else(|| self.entity_metas.get(type_id))
386            else {
387                return self
388                    .fail_save(
389                        txn,
390                        &save_ctx,
391                        EFError::configuration(format!(
392                            "entity metadata not found for {:?}",
393                            type_id
394                        )),
395                    )
396                    .await;
397            };
398            let n = match saver
399                .delete_deleted(txn.conn(), &*self.provider, set.as_mut(), meta)
400                .await
401            {
402                Ok(n) => n,
403                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
404            };
405            total_deleted += n;
406        }
407
408        // --- Direct cascade DELETE SQL (after PK-based deletes) ---
409        for directive in &delete_directives {
410            if directive.action != CascadeDeleteAction::Delete {
411                continue;
412            }
413            let sql = format!(
414                "DELETE FROM {} WHERE {} = ?",
415                directive.table, directive.fk_column
416            );
417            let params = vec![DbValue::from(directive.principal_pk)];
418            if let Err(e) = txn.conn().execute(&sql, &params).await {
419                return self.fail_save(txn, &save_ctx, e).await;
420            }
421        }
422
423        // --- Commit ---
424        match txn {
425            TxnSource::Ambient(t) => {
426                self.ambient_transaction = Some(t);
427            }
428            TxnSource::Managed(mut conn) => {
429                if let Err(e) = conn.commit_transaction().await {
430                    self.interceptor_pipeline
431                        .on_save_failed(&save_ctx, &e)
432                        .await;
433                    return Err(e);
434                }
435            }
436        }
437        self.change_tracker.accept_all_changes();
438        for type_id in &type_ids {
439            let Some(saver) = self.savers.get(type_id) else {
440                continue;
441            };
442            let Some(set) = self.sets.get_mut(type_id) else {
443                continue;
444            };
445            saver.accept_all_changes(set.as_mut());
446        }
447
448        let result_ctx = SaveChangesResultContext {
449            added: total_added,
450            updated: total_updated,
451            deleted: total_deleted,
452        };
453        self.interceptor_pipeline
454            .on_saved(&save_ctx, &result_ctx)
455            .await?;
456
457        Ok(SaveChangesResult {
458            added: total_added,
459            updated: total_updated,
460            deleted: total_deleted,
461        })
462    }
463
464    /// Error recovery: rollback managed transaction (or restore ambient),
465    /// fire `on_save_failed` interceptor, return the error.
466    async fn fail_save(
467        &mut self,
468        txn: TxnSource,
469        save_ctx: &SaveChangesContext,
470        e: EFError,
471    ) -> EFResult<SaveChangesResult> {
472        match txn {
473            TxnSource::Managed(mut conn) => {
474                let _ = conn.rollback_transaction().await;
475            }
476            TxnSource::Ambient(t) => {
477                self.ambient_transaction = Some(t);
478            }
479        }
480        self.interceptor_pipeline.on_save_failed(save_ctx, &e).await;
481        Err(e)
482    }
483}