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(), &self.change_tracker));
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(), &mut self.change_tracker);
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(
130                    txn.conn(),
131                    &*self.provider,
132                    set.as_mut(),
133                    &self.change_tracker,
134                    meta,
135                )
136                .await
137            {
138                Ok(n) => n,
139                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
140            };
141            total_added += inserted;
142
143            let link_indices: Vec<usize> = fixup_links
144                .iter()
145                .enumerate()
146                .filter(|(_, l)| l.parent_type_id == *type_id && l.through_table.is_none())
147                .map(|(i, _)| i)
148                .collect();
149
150            let mut self_ref_updates: Vec<(String, i64, i64)> = Vec::new();
151
152            for link_idx in &link_indices {
153                let link = &fixup_links[*link_idx];
154                let parent_pk = {
155                    let Some(parent_saver) = self.savers.get(&link.parent_type_id) else {
156                        continue;
157                    };
158                    let Some(parent_set) = self.sets.get(&link.parent_type_id) else {
159                        continue;
160                    };
161                    parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
162                };
163                let Some(pk) = parent_pk else {
164                    continue;
165                };
166
167                {
168                    let Some(child_saver) = self.savers.get(&link.child_type_id) else {
169                        continue;
170                    };
171                    let Some(child_set) = self.sets.get_mut(&link.child_type_id) else {
172                        continue;
173                    };
174                    for &child_idx in &link.child_entry_indices {
175                        child_saver.set_fk_at(
176                            child_set.as_mut(),
177                            child_idx,
178                            link.fk_target_type_id,
179                            pk,
180                        );
181                    }
182                }
183
184                if link.child_type_id == link.parent_type_id {
185                    let Some(child_meta) = configured_metas
186                        .get(&link.child_type_id)
187                        .or_else(|| self.entity_metas.get(&link.child_type_id))
188                    else {
189                        continue;
190                    };
191                    let fk_col = child_meta
192                        .properties
193                        .iter()
194                        .find(|p| p.is_foreign_key)
195                        .map(|p| p.column_name.as_ref());
196                    let pk_col = child_meta
197                        .properties
198                        .iter()
199                        .find(|p| p.is_primary_key)
200                        .map(|p| p.column_name.as_ref())
201                        .unwrap_or("id");
202                    if let Some(fk_col) = fk_col {
203                        for &child_idx in &link.child_entry_indices {
204                            let child_pk = {
205                                let Some(child_saver) = self.savers.get(&link.child_type_id) else {
206                                    continue;
207                                };
208                                let Some(child_set) = self.sets.get(&link.child_type_id) else {
209                                    continue;
210                                };
211                                child_saver.get_pk_at(child_set.as_ref(), child_idx)
212                            };
213                            if let Some(child_pk) = child_pk {
214                                let sql = format!(
215                                    "UPDATE {} SET {} = ? WHERE {} = ?",
216                                    child_meta.table_name, fk_col, pk_col
217                                );
218                                self_ref_updates.push((sql, pk, child_pk));
219                            }
220                        }
221                    }
222                }
223            }
224
225            for (sql, fk_val, pk_val) in self_ref_updates {
226                if let Err(e) = txn
227                    .conn()
228                    .execute(&sql, &[DbValue::from(fk_val), DbValue::from(pk_val)])
229                    .await
230                {
231                    return self.fail_save(txn, &save_ctx, e).await;
232                }
233            }
234        }
235
236        // --- M2M join row insertion (after all entity INSERTs) ---
237        for link in &fixup_links {
238            if link.through_table.is_none() {
239                continue;
240            }
241            let (Some(table), Some(parent_col), Some(child_col)) = (
242                link.through_table.as_ref(),
243                link.through_parent_fk_col.as_ref(),
244                link.through_child_fk_col.as_ref(),
245            ) else {
246                continue;
247            };
248
249            let parent_pk = {
250                let Some(parent_saver) = self.savers.get(&link.parent_type_id) else {
251                    continue;
252                };
253                let Some(parent_set) = self.sets.get(&link.parent_type_id) else {
254                    continue;
255                };
256                parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
257            };
258            let Some(parent_pk) = parent_pk else {
259                continue;
260            };
261
262            let mut child_pks: Vec<i64> = Vec::new();
263            {
264                let Some(child_saver) = self.savers.get(&link.child_type_id) else {
265                    continue;
266                };
267                let Some(child_set) = self.sets.get(&link.child_type_id) else {
268                    continue;
269                };
270                for &child_idx in &link.child_entry_indices {
271                    if let Some(child_pk) = child_saver.get_pk_at(child_set.as_ref(), child_idx) {
272                        child_pks.push(child_pk);
273                    }
274                }
275            }
276
277            if !child_pks.is_empty() {
278                let sql = cascade::m2m_insert_sql(table, parent_col, child_col, child_pks.len());
279                let mut params: Vec<DbValue> = Vec::with_capacity(child_pks.len() * 2);
280                for child_pk in &child_pks {
281                    params.push(DbValue::from(parent_pk));
282                    params.push(DbValue::from(*child_pk));
283                }
284                if let Err(e) = txn.conn().execute(&sql, &params).await {
285                    return self.fail_save(txn, &save_ctx, e).await;
286                }
287                total_added += child_pks.len();
288            }
289        }
290
291        // --- UPSERT phase ---
292        for type_id in &insert_order {
293            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
294                continue;
295            }
296            let Some(saver) = self.savers.get(type_id) else {
297                continue;
298            };
299            let Some(set) = self.sets.get_mut(type_id) else {
300                continue;
301            };
302            let Some(meta) = configured_metas
303                .get(type_id)
304                .or_else(|| self.entity_metas.get(type_id))
305            else {
306                return self
307                    .fail_save(
308                        txn,
309                        &save_ctx,
310                        EFError::configuration(format!(
311                            "entity metadata not found for {:?}",
312                            type_id
313                        )),
314                    )
315                    .await;
316            };
317            let n = match saver
318                .upsert_added(
319                    txn.conn(),
320                    &*self.provider,
321                    set.as_mut(),
322                    &self.change_tracker,
323                    meta,
324                )
325                .await
326            {
327                Ok(n) => n,
328                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
329            };
330            total_added += n;
331        }
332
333        // --- UPDATE phase ---
334        for type_id in &insert_order {
335            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
336                continue;
337            }
338            let Some(saver) = self.savers.get(type_id) else {
339                continue;
340            };
341            let Some(set) = self.sets.get_mut(type_id) else {
342                continue;
343            };
344            let Some(meta) = configured_metas
345                .get(type_id)
346                .or_else(|| self.entity_metas.get(type_id))
347            else {
348                return self
349                    .fail_save(
350                        txn,
351                        &save_ctx,
352                        EFError::configuration(format!(
353                            "entity metadata not found for {:?}",
354                            type_id
355                        )),
356                    )
357                    .await;
358            };
359            let n = match saver
360                .update_modified(
361                    txn.conn(),
362                    &*self.provider,
363                    set.as_mut(),
364                    &self.change_tracker,
365                    meta,
366                )
367                .await
368            {
369                Ok(n) => n,
370                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
371            };
372            total_updated += n;
373        }
374
375        // --- Direct cascade SET NULL SQL (before PK-based deletes) ---
376        for directive in &delete_directives {
377            if directive.action != CascadeDeleteAction::SetNull {
378                continue;
379            }
380            let sql = format!(
381                "UPDATE {} SET {} = NULL WHERE {} = ?",
382                directive.table, directive.fk_column, directive.fk_column
383            );
384            let params = vec![DbValue::from(directive.principal_pk)];
385            if let Err(e) = txn.conn().execute(&sql, &params).await {
386                return self.fail_save(txn, &save_ctx, e).await;
387            }
388        }
389
390        // --- DELETE phase (reverse topological order: dependents first) ---
391        for type_id in &delete_order {
392            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
393                continue;
394            }
395            let Some(saver) = self.savers.get(type_id) else {
396                continue;
397            };
398            let Some(set) = self.sets.get_mut(type_id) else {
399                continue;
400            };
401            let Some(meta) = configured_metas
402                .get(type_id)
403                .or_else(|| self.entity_metas.get(type_id))
404            else {
405                return self
406                    .fail_save(
407                        txn,
408                        &save_ctx,
409                        EFError::configuration(format!(
410                            "entity metadata not found for {:?}",
411                            type_id
412                        )),
413                    )
414                    .await;
415            };
416            let n = match saver
417                .delete_deleted(
418                    txn.conn(),
419                    &*self.provider,
420                    set.as_mut(),
421                    &self.change_tracker,
422                    meta,
423                )
424                .await
425            {
426                Ok(n) => n,
427                Err(e) => return self.fail_save(txn, &save_ctx, e).await,
428            };
429            total_deleted += n;
430        }
431
432        // --- Direct cascade DELETE SQL (after PK-based deletes) ---
433        for directive in &delete_directives {
434            if directive.action != CascadeDeleteAction::Delete {
435                continue;
436            }
437            let sql = format!(
438                "DELETE FROM {} WHERE {} = ?",
439                directive.table, directive.fk_column
440            );
441            let params = vec![DbValue::from(directive.principal_pk)];
442            if let Err(e) = txn.conn().execute(&sql, &params).await {
443                return self.fail_save(txn, &save_ctx, e).await;
444            }
445        }
446
447        // --- Commit ---
448        match txn {
449            TxnSource::Ambient(t) => {
450                self.ambient_transaction = Some(t);
451            }
452            TxnSource::Managed(mut conn) => {
453                if let Err(e) = conn.commit_transaction().await {
454                    self.interceptor_pipeline
455                        .on_save_failed(&save_ctx, &e)
456                        .await;
457                    return Err(e);
458                }
459            }
460        }
461        // --- Accept changes: refresh tracker originals, drop Deleted entries ---
462        for type_id in &type_ids {
463            let Some(saver) = self.savers.get(type_id) else {
464                continue;
465            };
466            let Some(set) = self.sets.get_mut(type_id) else {
467                continue;
468            };
469            saver.accept_all_changes(set.as_mut(), &mut self.change_tracker);
470        }
471
472        let result_ctx = SaveChangesResultContext {
473            added: total_added,
474            updated: total_updated,
475            deleted: total_deleted,
476        };
477        self.interceptor_pipeline
478            .on_saved(&save_ctx, &result_ctx)
479            .await?;
480
481        Ok(SaveChangesResult {
482            added: total_added,
483            updated: total_updated,
484            deleted: total_deleted,
485        })
486    }
487
488    /// Error recovery: rollback managed transaction (or restore ambient),
489    /// fire `on_save_failed` interceptor, return the error.
490    async fn fail_save(
491        &mut self,
492        txn: TxnSource,
493        save_ctx: &SaveChangesContext,
494        e: EFError,
495    ) -> EFResult<SaveChangesResult> {
496        match txn {
497            TxnSource::Managed(mut conn) => {
498                let _ = conn.rollback_transaction().await;
499            }
500            TxnSource::Ambient(t) => {
501                self.ambient_transaction = Some(t);
502            }
503        }
504        self.interceptor_pipeline.on_save_failed(save_ctx, &e).await;
505        Err(e)
506    }
507}