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