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;
15use std::sync::atomic::{AtomicU64, Ordering};
16
17use super::save_phases::SaveChangesResult;
18
19/// Transaction source for `save_changes`: either the ambient transaction
20/// registered by `use_transaction` / `transaction`, or a self-managed
21/// connection.
22pub(super) enum TxnSource {
23    /// Ambient transaction (`use_transaction` / `transaction`).
24    Ambient(Box<dyn ITransaction>),
25    /// Self-managed transaction (no explicit transaction around the save).
26    Managed(Box<dyn IAsyncConnection>),
27}
28
29impl TxnSource {
30    fn conn(&mut self) -> &mut dyn IAsyncConnection {
31        match self {
32            TxnSource::Ambient(t) => t.connection(),
33            TxnSource::Managed(c) => c.as_mut(),
34        }
35    }
36
37    /// Only context-scoped transactions get per-save savepoints. A legacy
38    /// standalone handle (`begin_transaction`) does not cover queries or
39    /// `save_changes`, so it must not affect save semantics.
40    fn is_scoped(&self) -> bool {
41        matches!(self, TxnSource::Ambient(t) if t.is_scoped())
42    }
43}
44
45/// Savepoints are per-save unique; a process-wide counter keeps names distinct
46/// across nested / concurrent sessions without allocating UUIDs.
47static SAVEPOINT_SEQ: AtomicU64 = AtomicU64::new(1);
48
49fn next_savepoint_name() -> String {
50    format!(
51        "rust_ef_save_{}",
52        SAVEPOINT_SEQ.fetch_add(1, Ordering::Relaxed)
53    )
54}
55
56impl super::DbContext {
57    /// Builds the interceptor `SaveChangesContext` from the actual pending
58    /// entries across all `DbSet`s (the real save data source), instead of
59    /// the legacy `change_tracker` which is never populated by `DbSet::add`.
60    /// This keeps interceptor snapshots consistent with what will be committed.
61    fn build_save_context(&self) -> SaveChangesContext {
62        let mut views: Vec<crate::tracking::EntityEntryView> = Vec::new();
63        for (type_id, set) in &self.sets {
64            if let Some(saver) = self.savers.get(type_id) {
65                views.extend(saver.collect_entries(set.as_ref(), &self.change_tracker));
66            }
67        }
68        SaveChangesContext::from_views(views)
69    }
70
71    /// Saves all pending changes across all DbSets.
72    ///
73    /// Detects changes, runs interceptors, executes INSERT/UPDATE/DELETE in a
74    /// transaction, and clears tracked entries on success.
75    pub async fn save_changes(&mut self) -> EFResult<SaveChangesResult> {
76        let _save_guard = crate::observability::SaveChangesGuard::new();
77        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
78        for type_id in &type_ids {
79            let Some(set) = self.sets.get_mut(type_id) else {
80                continue;
81            };
82            let Some(saver) = self.savers.get(type_id) else {
83                continue;
84            };
85            saver.detect_changes(set.as_mut(), &mut self.change_tracker);
86        }
87
88        let configured_metas: HashMap<TypeId, EntityTypeMeta> = self
89            .model_builder
90            .build()
91            .into_iter()
92            .map(|m| (m.type_id, m))
93            .collect();
94
95        let save_ctx = self.build_save_context();
96        self.interceptor_pipeline.on_saving(&save_ctx).await?;
97
98        // No-op saves should not open a transaction (or a savepoint): EF Core
99        // returns without touching the database when nothing is pending.
100        if save_ctx.added_count() == 0
101            && save_ctx.modified_count() == 0
102            && save_ctx.deleted_count() == 0
103        {
104            let result_ctx = SaveChangesResultContext {
105                added: 0,
106                updated: 0,
107                deleted: 0,
108            };
109            self.interceptor_pipeline
110                .on_saved(&save_ctx, &result_ctx)
111                .await?;
112            return Ok(SaveChangesResult {
113                added: 0,
114                updated: 0,
115                deleted: 0,
116            });
117        }
118
119        // Note: unlike EF Core's `AutoTransactionBehavior.WhenNeeded`, a
120        // self-managed save always opens a transaction. A single-statement
121        // fast path would need a complete statement plan (cascades, FK fixups,
122        // M2M join rows) before execution; an incorrect count would commit
123        // partially, so it is intentionally not attempted here.
124        let mut txn = match self.ambient_transaction.take() {
125            Some(t) => TxnSource::Ambient(t),
126            None => {
127                let mut c = self.provider.get_connection().await?;
128                c.begin_transaction().await?;
129                TxnSource::Managed(c)
130            }
131        };
132
133        // Context-scoped transactions get a savepoint so a failed save can be
134        // rolled back without discarding work already done in the same
135        // transaction (EF Core does the same around SaveChanges).
136        let scoped = txn.is_scoped();
137        let savepoint = if scoped {
138            let name = next_savepoint_name();
139            if let Err(e) = txn.conn().create_savepoint(&name).await {
140                return self.fail_save(txn, &save_ctx, None, e).await;
141            }
142            Some(name)
143        } else {
144            None
145        };
146
147        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
148
149        // --- Cascade drain: Added + Deleted ---
150        let fixup_links = match self.drain_cascade_adds(&type_ids, &configured_metas) {
151            Ok(links) => links,
152            Err(e) => {
153                return self
154                    .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
155                    .await
156            }
157        };
158        let mut processed: std::collections::HashSet<(TypeId, usize)> =
159            std::collections::HashSet::new();
160        let delete_directives =
161            match self.drain_cascade_deletes(&type_ids, &configured_metas, &mut processed) {
162                Ok(directives) => directives,
163                Err(e) => {
164                    return self
165                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
166                        .await
167                }
168            };
169
170        let graph = DependencyGraph::build(&configured_metas);
171        let insert_order = graph.topological_sort();
172        let delete_order = graph.deletion_order();
173
174        let mut total_added = 0usize;
175        let mut total_updated = 0usize;
176        let mut total_deleted = 0usize;
177
178        // --- INSERT phase (topological order) + FK fixup ---
179        for type_id in &insert_order {
180            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
181                continue;
182            }
183            let Some(saver) = self.savers.get(type_id) else {
184                continue;
185            };
186            let Some(set) = self.sets.get_mut(type_id) else {
187                continue;
188            };
189            let Some(meta) = configured_metas
190                .get(type_id)
191                .or_else(|| self.entity_metas.get(type_id))
192            else {
193                return self
194                    .fail_save(
195                        txn,
196                        &save_ctx,
197                        savepoint.as_deref(),
198                        EFError::configuration(format!(
199                            "entity metadata not found for {:?}",
200                            type_id
201                        )),
202                    )
203                    .await;
204            };
205            let inserted = match saver
206                .insert_added(
207                    txn.conn(),
208                    &*self.provider,
209                    set.as_mut(),
210                    &self.change_tracker,
211                    meta,
212                )
213                .await
214            {
215                Ok(n) => n,
216                Err(e) => {
217                    return self
218                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
219                        .await
220                }
221            };
222            total_added += inserted;
223
224            let link_indices: Vec<usize> = fixup_links
225                .iter()
226                .enumerate()
227                .filter(|(_, l)| l.parent_type_id == *type_id && l.through_table.is_none())
228                .map(|(i, _)| i)
229                .collect();
230
231            let mut self_ref_updates: Vec<(String, i64, i64)> = Vec::new();
232
233            for link_idx in &link_indices {
234                let link = &fixup_links[*link_idx];
235                let parent_pk = {
236                    let Some(parent_saver) = self.savers.get(&link.parent_type_id) else {
237                        continue;
238                    };
239                    let Some(parent_set) = self.sets.get(&link.parent_type_id) else {
240                        continue;
241                    };
242                    parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
243                };
244                let Some(pk) = parent_pk else {
245                    continue;
246                };
247
248                {
249                    let Some(child_saver) = self.savers.get(&link.child_type_id) else {
250                        continue;
251                    };
252                    let Some(child_set) = self.sets.get_mut(&link.child_type_id) else {
253                        continue;
254                    };
255                    for &child_idx in &link.child_entry_indices {
256                        child_saver.set_fk_at(
257                            child_set.as_mut(),
258                            child_idx,
259                            link.fk_target_type_id,
260                            pk,
261                        );
262                    }
263                }
264
265                if link.child_type_id == link.parent_type_id {
266                    let Some(child_meta) = configured_metas
267                        .get(&link.child_type_id)
268                        .or_else(|| self.entity_metas.get(&link.child_type_id))
269                    else {
270                        continue;
271                    };
272                    let fk_col = child_meta
273                        .properties
274                        .iter()
275                        .find(|p| p.is_foreign_key)
276                        .map(|p| p.column_name.as_ref());
277                    let pk_col = child_meta
278                        .properties
279                        .iter()
280                        .find(|p| p.is_primary_key)
281                        .map(|p| p.column_name.as_ref())
282                        .unwrap_or("id");
283                    if let Some(fk_col) = fk_col {
284                        for &child_idx in &link.child_entry_indices {
285                            let child_pk = {
286                                let Some(child_saver) = self.savers.get(&link.child_type_id) else {
287                                    continue;
288                                };
289                                let Some(child_set) = self.sets.get(&link.child_type_id) else {
290                                    continue;
291                                };
292                                child_saver.get_pk_at(child_set.as_ref(), child_idx)
293                            };
294                            if let Some(child_pk) = child_pk {
295                                let sql = format!(
296                                    "UPDATE {} SET {} = ? WHERE {} = ?",
297                                    child_meta.table_name, fk_col, pk_col
298                                );
299                                self_ref_updates.push((sql, pk, child_pk));
300                            }
301                        }
302                    }
303                }
304            }
305
306            for (sql, fk_val, pk_val) in self_ref_updates {
307                if let Err(e) = txn
308                    .conn()
309                    .execute(&sql, &[DbValue::from(fk_val), DbValue::from(pk_val)])
310                    .await
311                {
312                    return self
313                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
314                        .await;
315                }
316            }
317        }
318
319        // --- M2M join row insertion (after all entity INSERTs) ---
320        for link in &fixup_links {
321            if link.through_table.is_none() {
322                continue;
323            }
324            let (Some(table), Some(parent_col), Some(child_col)) = (
325                link.through_table.as_ref(),
326                link.through_parent_fk_col.as_ref(),
327                link.through_child_fk_col.as_ref(),
328            ) else {
329                continue;
330            };
331
332            let parent_pk = {
333                let Some(parent_saver) = self.savers.get(&link.parent_type_id) else {
334                    continue;
335                };
336                let Some(parent_set) = self.sets.get(&link.parent_type_id) else {
337                    continue;
338                };
339                parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
340            };
341            let Some(parent_pk) = parent_pk else {
342                continue;
343            };
344
345            let mut child_pks: Vec<i64> = Vec::new();
346            {
347                let Some(child_saver) = self.savers.get(&link.child_type_id) else {
348                    continue;
349                };
350                let Some(child_set) = self.sets.get(&link.child_type_id) else {
351                    continue;
352                };
353                for &child_idx in &link.child_entry_indices {
354                    if let Some(child_pk) = child_saver.get_pk_at(child_set.as_ref(), child_idx) {
355                        child_pks.push(child_pk);
356                    }
357                }
358            }
359
360            if !child_pks.is_empty() {
361                let sql = cascade::m2m_insert_sql(table, parent_col, child_col, child_pks.len());
362                let mut params: Vec<DbValue> = Vec::with_capacity(child_pks.len() * 2);
363                for child_pk in &child_pks {
364                    params.push(DbValue::from(parent_pk));
365                    params.push(DbValue::from(*child_pk));
366                }
367                if let Err(e) = txn.conn().execute(&sql, &params).await {
368                    return self
369                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
370                        .await;
371                }
372                total_added += child_pks.len();
373            }
374        }
375
376        // --- UPSERT phase ---
377        for type_id in &insert_order {
378            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
379                continue;
380            }
381            let Some(saver) = self.savers.get(type_id) else {
382                continue;
383            };
384            let Some(set) = self.sets.get_mut(type_id) else {
385                continue;
386            };
387            let Some(meta) = configured_metas
388                .get(type_id)
389                .or_else(|| self.entity_metas.get(type_id))
390            else {
391                return self
392                    .fail_save(
393                        txn,
394                        &save_ctx,
395                        savepoint.as_deref(),
396                        EFError::configuration(format!(
397                            "entity metadata not found for {:?}",
398                            type_id
399                        )),
400                    )
401                    .await;
402            };
403            let n = match saver
404                .upsert_added(
405                    txn.conn(),
406                    &*self.provider,
407                    set.as_mut(),
408                    &self.change_tracker,
409                    meta,
410                )
411                .await
412            {
413                Ok(n) => n,
414                Err(e) => {
415                    return self
416                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
417                        .await
418                }
419            };
420            total_added += n;
421        }
422
423        // --- UPDATE phase ---
424        for type_id in &insert_order {
425            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
426                continue;
427            }
428            let Some(saver) = self.savers.get(type_id) else {
429                continue;
430            };
431            let Some(set) = self.sets.get_mut(type_id) else {
432                continue;
433            };
434            let Some(meta) = configured_metas
435                .get(type_id)
436                .or_else(|| self.entity_metas.get(type_id))
437            else {
438                return self
439                    .fail_save(
440                        txn,
441                        &save_ctx,
442                        savepoint.as_deref(),
443                        EFError::configuration(format!(
444                            "entity metadata not found for {:?}",
445                            type_id
446                        )),
447                    )
448                    .await;
449            };
450            let n = match saver
451                .update_modified(
452                    txn.conn(),
453                    &*self.provider,
454                    set.as_mut(),
455                    &self.change_tracker,
456                    meta,
457                )
458                .await
459            {
460                Ok(n) => n,
461                Err(e) => {
462                    return self
463                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
464                        .await
465                }
466            };
467            total_updated += n;
468        }
469
470        // --- Direct cascade SET NULL SQL (before PK-based deletes) ---
471        for directive in &delete_directives {
472            if directive.action != CascadeDeleteAction::SetNull {
473                continue;
474            }
475            let sql = format!(
476                "UPDATE {} SET {} = NULL WHERE {} = ?",
477                directive.table, directive.fk_column, directive.fk_column
478            );
479            let params = vec![DbValue::from(directive.principal_pk)];
480            if let Err(e) = txn.conn().execute(&sql, &params).await {
481                return self
482                    .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
483                    .await;
484            }
485        }
486
487        // --- DELETE phase (reverse topological order: dependents first) ---
488        for type_id in &delete_order {
489            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
490                continue;
491            }
492            let Some(saver) = self.savers.get(type_id) else {
493                continue;
494            };
495            let Some(set) = self.sets.get_mut(type_id) else {
496                continue;
497            };
498            let Some(meta) = configured_metas
499                .get(type_id)
500                .or_else(|| self.entity_metas.get(type_id))
501            else {
502                return self
503                    .fail_save(
504                        txn,
505                        &save_ctx,
506                        savepoint.as_deref(),
507                        EFError::configuration(format!(
508                            "entity metadata not found for {:?}",
509                            type_id
510                        )),
511                    )
512                    .await;
513            };
514            let n = match saver
515                .delete_deleted(
516                    txn.conn(),
517                    &*self.provider,
518                    set.as_mut(),
519                    &self.change_tracker,
520                    meta,
521                )
522                .await
523            {
524                Ok(n) => n,
525                Err(e) => {
526                    return self
527                        .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
528                        .await
529                }
530            };
531            total_deleted += n;
532        }
533
534        // --- Direct cascade DELETE SQL (after PK-based deletes) ---
535        for directive in &delete_directives {
536            if directive.action != CascadeDeleteAction::Delete {
537                continue;
538            }
539            let sql = format!(
540                "DELETE FROM {} WHERE {} = ?",
541                directive.table, directive.fk_column
542            );
543            let params = vec![DbValue::from(directive.principal_pk)];
544            if let Err(e) = txn.conn().execute(&sql, &params).await {
545                return self
546                    .fail_save(txn, &save_ctx, savepoint.as_deref(), e)
547                    .await;
548            }
549        }
550
551        // --- Commit ---
552        if let Some(name) = savepoint.as_deref() {
553            // The DML already succeeded; a failed RELEASE leaves the savepoint
554            // in place but does not invalidate the transaction, so don't fail
555            // the save because of it.
556            let _ = txn.conn().release_savepoint(name).await;
557        }
558        match txn {
559            TxnSource::Ambient(t) => {
560                self.ambient_transaction = Some(t);
561            }
562            TxnSource::Managed(mut conn) => {
563                if let Err(e) = conn.commit_transaction().await {
564                    self.interceptor_pipeline
565                        .on_save_failed(&save_ctx, &e)
566                        .await;
567                    return Err(e);
568                }
569            }
570        }
571        // --- Accept changes: refresh tracker originals, drop Deleted entries ---
572        for type_id in &type_ids {
573            let Some(saver) = self.savers.get(type_id) else {
574                continue;
575            };
576            let Some(set) = self.sets.get_mut(type_id) else {
577                continue;
578            };
579            saver.accept_all_changes(set.as_mut(), &mut self.change_tracker);
580        }
581
582        let result_ctx = SaveChangesResultContext {
583            added: total_added,
584            updated: total_updated,
585            deleted: total_deleted,
586        };
587        self.interceptor_pipeline
588            .on_saved(&save_ctx, &result_ctx)
589            .await?;
590
591        Ok(SaveChangesResult {
592            added: total_added,
593            updated: total_updated,
594            deleted: total_deleted,
595        })
596    }
597
598    /// Error recovery: roll back to the savepoint for caller-managed
599    /// transactions, roll back the whole managed transaction otherwise, fire
600    /// `on_save_failed`, and return the error.
601    async fn fail_save(
602        &mut self,
603        txn: TxnSource,
604        save_ctx: &SaveChangesContext,
605        savepoint: Option<&str>,
606        e: EFError,
607    ) -> EFResult<SaveChangesResult> {
608        match txn {
609            TxnSource::Managed(mut conn) => {
610                let _ = conn.rollback_transaction().await;
611            }
612            TxnSource::Ambient(mut t) => {
613                if let Some(name) = savepoint {
614                    let _ = t.connection().rollback_to_savepoint(name).await;
615                    let _ = t.connection().release_savepoint(name).await;
616                }
617                self.ambient_transaction = Some(t);
618            }
619        }
620        self.interceptor_pipeline.on_save_failed(save_ctx, &e).await;
621        Err(e)
622    }
623}