Skip to main content

rust_ef/
db_context.rs

1//! DbContext, DbContextOptions, and ChangeTracker — the session / unit-of-work layer.
2//!
3//! ## Architecture
4//!
5//! `DbContext` is the concrete context type. Entity sets use a type-map:
6//! `ctx.set::<Blog>()` lazy-creates `DbSet<Blog>`. `SetOps<T>` dispatchers
7//! enable `save_changes()` to iterate all entity types.
8//!
9//! ## Provider Factory
10//!
11//! `DbContextOptions` stores a `provider_factory` closure injected by the
12//! provider extension methods (`use_sqlite`, `use_postgres`, `use_mysql`).
13//! `DbContext::from_options()` calls this factory to create the provider.
14//!
15//! ## Ownership and Mutation
16//!
17//! `DbContext` methods (`set::<T>()`, `save_changes()`, `detect_changes()`)
18//! require `&mut self` — this is idiomatic Rust, not a limitation. The DI
19//! integration (`add_dbcontext`) registers the context as **Scoped** and
20//! supports two resolution modes:
21//!
22//! - **Owned** (recommended for handlers): `provider.get_owned::<DbContext>()`
23//!   returns a fresh `DbContext` with direct `&mut self` access. Handlers
24//!   declare a bare `ctx: DbContext` field marked with `#[inject(owned)]`;
25//!   `#[derive(Inject)]` resolves it via `get_owned()`. Unmarked fields fall
26//!   back to `Default::default()`.
27//! - **Shared** (within a scope): `scope.get::<DbContext>()` returns
28//!   `Arc<DbContext>` for consumers that only need `&self` access.
29//!
30//! ```rust,ignore
31//! // Owned — idiomatic &mut self, no locks:
32//! // rust-dix 0.6+: get_owned() returns Result<T, RdiError>
33//! let mut ctx: DbContext = provider.get_owned()?;
34//! ctx.set::<Blog>().add(blog);
35//! ctx.save_changes().await?;
36//! ```
37//!
38//! ## Thread Safety
39//!
40//! `DbContext` is **not** thread-safe — a single instance must not be shared
41//! across threads. This is a design decision (aligned with EFCore), not a
42//! limitation.
43//!
44//! **Correct usage**: each request / operation owns its own `DbContext`
45//! instance (via `get_owned()` or a fresh scope):
46//! ```rust,ignore
47//! let mut ctx: DbContext = provider.get_owned()?;
48//! // This instance is exclusively owned — &mut self works directly.
49//! ```
50//!
51//! > **rust-webapp**: the HTTP pipeline creates a DI scope per request and
52//! > resolves handlers via `get_owned::<Handler>()`. Handlers own a fresh
53//! > `DbContext` — no manual scope management needed.
54//!
55//! **Anti-pattern**: sharing via `Arc<Mutex<DbContext>>` causes tracking
56//! pollution — Thread A's `save_changes()` would commit Thread B's pending
57//! changes. Prefer owned resolution.
58
59use crate::change_executor::ChangeExecutor;
60use crate::db_set::DbSet;
61use crate::entity::{IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter};
62use crate::error::{EFError, EFResult};
63use crate::interceptor::{InterceptorPipeline, SaveChangesContext, SaveChangesResultContext};
64use crate::metadata::EntityTypeMeta;
65use crate::migration::MigrationEngine;
66use crate::model_builder::ModelBuilder;
67use crate::provider::{DbValue, IAsyncConnection, IDatabaseProvider};
68use crate::tracking::{ChangeTracker, EntityEntryView};
69use crate::transaction::{DbTransaction, ITransaction};
70use std::any::{Any, TypeId};
71use std::collections::HashMap;
72use std::future::Future;
73use std::pin::Pin;
74use std::sync::Arc;
75
76// ---------------------------------------------------------------------------
77// DbContextOptions / DbContextOptionsBuilder
78// ---------------------------------------------------------------------------
79
80#[derive(Clone)]
81pub struct DbContextOptions {
82    pub(crate) connection_string: String,
83    pub(crate) provider_tag: Option<String>,
84    #[allow(clippy::type_complexity)]
85    pub(crate) provider_factory:
86        Option<Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>>,
87    /// Process-level cache of the built provider (which owns the connection
88    /// pool). Built once on the first `create_provider()` call and shared
89    /// across every `DbContext` created from the same `Arc<DbContextOptions>`
90    /// (i.e. the same `add_dbcontext` registration). Keeping the provider
91    /// alive for the application lifetime means the connection pool is reused
92    /// across requests instead of being recreated per request.
93    pub(crate) provider_cache: Arc<std::sync::Mutex<Option<Arc<dyn IDatabaseProvider>>>>,
94    pub(crate) interceptors: Vec<Arc<dyn crate::interceptor::ISaveChangesInterceptor>>,
95    /// When `true`, `QueryBuilder::to_list()` attaches `LazyContext` to every
96    /// navigation container on materialized entities, enabling on-demand
97    /// loading via `BelongsTo::load()` / `HasMany::load()` / `HasOne::load()`.
98    ///
99    /// Defaults to `false` (opt-in) to preserve v1.0 eager-only behavior.
100    pub(crate) lazy_loading_enabled: bool,
101    pub(crate) context_key: Option<String>,
102    /// Process-level cache of `discover_entities()` output, keyed by
103    /// `context_key`. Shared across all `DbContext` instances created from
104    /// the same `DbContextOptions` (which is `Arc`-shared per `add_dbcontext`
105    /// registration). The first `from_options()` call builds the metadata;
106    /// subsequent calls `Arc::clone` it.
107    pub(crate) metadata_cache: Arc<crate::metadata_cache::MetadataCache>,
108    /// Slow query threshold for tracing. When set, queries exceeding this
109    /// duration emit a `tracing::warn!` event.
110    #[cfg(feature = "tracing")]
111    pub(crate) slow_query_threshold: Option<std::time::Duration>,
112}
113
114impl std::fmt::Debug for DbContextOptions {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("DbContextOptions")
117            .field(
118                "connection_string",
119                &redact_connection_string(&self.connection_string),
120            )
121            .field("provider_tag", &self.provider_tag)
122            .finish()
123    }
124}
125
126/// Redacts credentials from a connection string so `Debug` output never leaks
127/// passwords. Handles URL form (`scheme://user:pass@host`) and key=value form
128/// (`...;Password=...;...`). SQLite file paths and other credential-free
129/// strings are returned unchanged for debuggability.
130fn redact_connection_string(cs: &str) -> String {
131    // URL form: scheme://[user[:pass]@]host...
132    if let Some(scheme_end) = cs.find("://") {
133        let (scheme, rest) = cs.split_at(scheme_end + 3);
134        if let Some(at) = rest.find('@') {
135            let (userinfo, host_and_rest) = rest.split_at(at);
136            let redacted_user = match userinfo.find(':') {
137                Some(colon) => &userinfo[..colon],
138                None => userinfo,
139            };
140            return format!("{}{}***@{}", scheme, redacted_user, &host_and_rest[1..]);
141        }
142        return cs.to_string();
143    }
144    // Key=value form: redact any token whose key mentions password/pwd.
145    if cs.contains('=') {
146        return cs
147            .split(';')
148            .map(|pair| {
149                let eq = match pair.find('=') {
150                    Some(e) => e,
151                    None => return pair.to_string(),
152                };
153                let key = pair[..eq].trim().to_lowercase();
154                if key.contains("password") || key.contains("pwd") {
155                    format!("{}=***", &pair[..eq])
156                } else {
157                    pair.to_string()
158                }
159            })
160            .collect::<Vec<_>>()
161            .join(";");
162    }
163    cs.to_string()
164}
165
166impl DbContextOptions {
167    pub fn connection_string(&self) -> &str {
168        &self.connection_string
169    }
170    pub fn provider_tag(&self) -> Option<&str> {
171        self.provider_tag.as_deref()
172    }
173    pub fn lazy_loading_enabled(&self) -> bool {
174        self.lazy_loading_enabled
175    }
176    pub fn context_key(&self) -> Option<&str> {
177        self.context_key.as_deref()
178    }
179    pub fn create_provider(&self) -> EFResult<Arc<dyn IDatabaseProvider>> {
180        // Recover from a poisoned lock rather than panicking (consistent with
181        // `MetadataCache`): if a previous build panicked, `into_inner()` yields
182        // the still-`None` cache and we retry the build below.
183        let mut guard = self
184            .provider_cache
185            .lock()
186            .unwrap_or_else(|p| p.into_inner());
187        if let Some(provider) = guard.as_ref() {
188            return Ok(Arc::clone(provider));
189        }
190        let factory = self.provider_factory.as_ref().ok_or_else(|| {
191            crate::error::EFError::configuration(
192                "No provider configured. Call use_sqlite / use_postgres / use_mysql first.",
193            )
194        })?;
195        let provider = factory(self.connection_string())?;
196        #[cfg(feature = "tracing")]
197        if let Some(threshold) = self.slow_query_threshold {
198            provider.set_slow_query_threshold(threshold);
199        }
200        *guard = Some(Arc::clone(&provider));
201        Ok(provider)
202    }
203}
204
205#[allow(clippy::derivable_impls)]
206impl Default for DbContextOptions {
207    fn default() -> Self {
208        Self {
209            connection_string: String::new(),
210            provider_tag: None,
211            provider_factory: None,
212            provider_cache: Arc::new(std::sync::Mutex::new(None)),
213            interceptors: Vec::new(),
214            lazy_loading_enabled: false,
215            context_key: None,
216            metadata_cache: Arc::new(crate::metadata_cache::MetadataCache::new()),
217            #[cfg(feature = "tracing")]
218            slow_query_threshold: None,
219        }
220    }
221}
222
223pub struct DbContextOptionsBuilder {
224    inner: DbContextOptions,
225}
226
227impl DbContextOptionsBuilder {
228    pub fn new() -> Self {
229        Self {
230            inner: DbContextOptions::default(),
231        }
232    }
233    pub fn connection_string(&mut self, cs: impl Into<String>) -> &mut Self {
234        self.inner.connection_string = cs.into();
235        self
236    }
237    pub fn set_provider(&mut self, tag: &str, cs: impl Into<String>) -> &mut Self {
238        self.inner.provider_tag = Some(tag.to_string());
239        self.inner.connection_string = cs.into();
240        self
241    }
242    #[allow(clippy::type_complexity)]
243    pub fn set_provider_factory(
244        &mut self,
245        tag: &str,
246        cs: impl Into<String>,
247        factory: Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>,
248    ) -> &mut Self {
249        self.inner.provider_tag = Some(tag.to_string());
250        self.inner.connection_string = cs.into();
251        self.inner.provider_factory = Some(factory);
252        self
253    }
254    /// Registers a `SaveChanges` interceptor.
255    ///
256    /// Interceptors are called in registration order during
257    /// `save_changes()`. Use this for auditing, soft-delete,
258    /// validation, and other cross-cutting concerns.
259    ///
260    /// # Example
261    ///
262    /// ```rust,ignore
263    /// options
264    ///     .use_sqlite("app.db")
265    ///     .add_interceptor(AuditInterceptor::new());
266    /// ```
267    pub fn add_interceptor(
268        &mut self,
269        interceptor: impl crate::interceptor::ISaveChangesInterceptor + 'static,
270    ) -> &mut Self {
271        self.inner.interceptors.push(Arc::new(interceptor));
272        self
273    }
274
275    /// Enables or disables lazy loading of navigation properties.
276    ///
277    /// When enabled (`true`), `to_list()` attaches a `LazyContext` to every
278    /// navigation container on each materialized entity. The user can then
279    /// call `nav.load().await` to trigger a single-entity query on first
280    /// access; subsequent accesses read from the in-memory cache.
281    ///
282    /// When disabled (`false`, the default), navigation properties are
283    /// empty unless explicitly loaded via `Include` — matching v1.0
284    /// eager-only behavior.
285    ///
286    /// # Example
287    ///
288    /// ```rust,ignore
289    /// let mut options = DbContextOptionsBuilder::new();
290    /// options.use_sqlite_in_memory().use_lazy_loading(true);
291    /// ```
292    pub fn use_lazy_loading(&mut self, enabled: bool) -> &mut Self {
293        self.inner.lazy_loading_enabled = enabled;
294        self
295    }
296
297    /// Sets the context key used to filter entities and configurations
298    /// during `DbContext::discover_entities()`. Set automatically by
299    /// `add_dbcontext_keyed`; `None` (the default) selects the default
300    /// context.
301    pub fn context_key(&mut self, key: impl Into<String>) -> &mut Self {
302        self.inner.context_key = Some(key.into());
303        self
304    }
305
306    /// Sets the slow query threshold. Queries exceeding this duration
307    /// emit a `tracing::warn!` event with SQL and elapsed time.
308    ///
309    /// Only available when the `tracing` feature is enabled.
310    #[cfg(feature = "tracing")]
311    pub fn slow_query_threshold(&mut self, threshold: std::time::Duration) -> &mut Self {
312        self.inner.slow_query_threshold = Some(threshold);
313        self
314    }
315
316    pub fn build(self) -> DbContextOptions {
317        self.inner
318    }
319}
320
321impl Default for DbContextOptionsBuilder {
322    fn default() -> Self {
323        Self::new()
324    }
325}
326
327// ---------------------------------------------------------------------------
328// Type-erased set operations
329// ---------------------------------------------------------------------------
330
331#[async_trait::async_trait]
332trait ErasedSetOps: Send + Sync {
333    async fn save(
334        &self,
335        conn: &mut (dyn IAsyncConnection + Send),
336        provider: &dyn IDatabaseProvider,
337        raw_set: &mut (dyn Any + Send + Sync),
338        meta: &EntityTypeMeta,
339    ) -> EFResult<(usize, usize, usize)>;
340    fn detect_changes(&self, raw_set: &mut (dyn Any + Send + Sync));
341    fn clear(&self, raw_set: &mut (dyn Any + Send + Sync + 'static));
342    /// Collects type-erased views of all pending entries in the set, used to
343    /// build `SaveChangesContext` from the real save data source (`DbSet.entries`)
344    /// rather than the legacy (empty) `change_tracker`.
345    fn collect_entries(&self, raw_set: &(dyn Any + Send + Sync)) -> Vec<EntityEntryView>;
346}
347
348struct SetOps<E> {
349    _phantom: std::marker::PhantomData<E>,
350}
351impl<E> SetOps<E> {
352    fn new() -> Self {
353        Self {
354            _phantom: std::marker::PhantomData,
355        }
356    }
357}
358
359#[async_trait::async_trait]
360impl<E> ErasedSetOps for SetOps<E>
361where
362    E: IEntityType
363        + IEntitySnapshot
364        + IGetKeyValues
365        + IFromRow
366        + INavigationSetter
367        + Send
368        + Sync
369        + 'static,
370{
371    async fn save(
372        &self,
373        conn: &mut (dyn IAsyncConnection + Send),
374        provider: &dyn IDatabaseProvider,
375        raw_set: &mut (dyn Any + Send + Sync),
376        meta: &EntityTypeMeta,
377    ) -> EFResult<(usize, usize, usize)> {
378        let db_set = raw_set
379            .downcast_mut::<DbSet<E>>()
380            .expect("SetOps type mismatch");
381        save_one_set(conn, provider, db_set, meta).await
382    }
383    fn detect_changes(&self, raw_set: &mut (dyn Any + Send + Sync)) {
384        if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
385            db_set.detect_changes();
386        }
387    }
388    fn clear(&self, raw_set: &mut (dyn Any + Send + Sync + 'static)) {
389        if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
390            db_set.clear_entries();
391        }
392    }
393    fn collect_entries(&self, raw_set: &(dyn Any + Send + Sync)) -> Vec<EntityEntryView> {
394        let Some(db_set) = raw_set.downcast_ref::<DbSet<E>>() else {
395            return Vec::new();
396        };
397        let type_name = E::entity_meta().type_name.to_string();
398        db_set
399            .entries
400            .iter()
401            .map(|e| EntityEntryView {
402                type_id: TypeId::of::<E>(),
403                type_name: type_name.clone(),
404                state: e.state,
405            })
406            .collect()
407    }
408}
409
410// ---------------------------------------------------------------------------
411// DbContext
412// ---------------------------------------------------------------------------
413
414pub struct DbContext {
415    sets: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
416    savers: HashMap<TypeId, Box<dyn ErasedSetOps>>,
417    entity_metas: HashMap<TypeId, EntityTypeMeta>,
418    model_builder: ModelBuilder,
419    change_tracker: ChangeTracker,
420    provider: Arc<dyn IDatabaseProvider>,
421    interceptor_pipeline: InterceptorPipeline,
422    lazy_loading_enabled: bool,
423    /// Ambient transaction: registered by `use_transaction()`. When present,
424    /// `save_changes()` reuses this transaction's connection and does not
425    /// begin/commit/rollback on its own. Uses `take()`/restore pattern to
426    /// avoid `&mut self` borrow conflicts with `self.sets` during save.
427    ///
428    /// Note: `begin_transaction()` returns a handle without registering it
429    /// here — only `use_transaction()` registers an ambient. This separates
430    /// manual handle-based control from scoped ambient control.
431    ambient_transaction: Option<Box<dyn ITransaction>>,
432}
433
434impl DbContext {
435    /// Creates the context from options (uses the provider factory stored in options).
436    ///
437    /// Entity metadata is loaded from the process-level `MetadataCache` on
438    /// `DbContextOptions` — `inventory::iter` + `IEntityTypeConfiguration::configure()`
439    /// run once per `context_key` (first call), then the result is `Arc`-shared
440    /// across all `DbContext` instances. Per-instance `ModelBuilder` mutations
441    /// (`has_query_filter`, etc.) after construction only affect this instance.
442    pub fn from_options(options: &DbContextOptions) -> EFResult<Self> {
443        let provider = options.create_provider()?;
444        let built = options
445            .metadata_cache
446            .get_or_build(options.context_key.as_deref());
447        let ctx = Self {
448            sets: HashMap::new(),
449            savers: HashMap::new(),
450            entity_metas: built.entity_metas.clone(),
451            model_builder: ModelBuilder::from_built(&built),
452            change_tracker: ChangeTracker::new(),
453            provider,
454            interceptor_pipeline: InterceptorPipeline::new(options.interceptors.clone()),
455            lazy_loading_enabled: options.lazy_loading_enabled,
456            ambient_transaction: None,
457        };
458        Ok(ctx)
459    }
460
461    pub fn set<T>(&mut self) -> &mut DbSet<T>
462    where
463        T: IEntityType
464            + IEntitySnapshot
465            + IGetKeyValues
466            + IFromRow
467            + INavigationSetter
468            + Send
469            + Sync
470            + 'static,
471    {
472        let type_id = TypeId::of::<T>();
473        self.savers
474            .entry(type_id)
475            .or_insert_with(|| Box::new(SetOps::<T>::new()));
476        self.entity_metas
477            .entry(type_id)
478            .or_insert_with(T::entity_meta);
479        if !self.model_builder.has_entity(type_id) {
480            self.model_builder.register_entity_meta(T::entity_meta());
481        }
482        self.sets.entry(type_id).or_insert_with(|| {
483            let table_name = self
484                .model_builder
485                .build()
486                .into_iter()
487                .find(|m| m.type_id == type_id)
488                .map(|m| m.table_name.to_string())
489                .unwrap_or_else(|| T::entity_meta().table_name.to_string());
490            let mut db_set = DbSet::<T>::with_provider(table_name, Arc::clone(&self.provider));
491            if let Some(filter) = self.model_builder.get_query_filter(&type_id) {
492                db_set.set_query_filter(filter.clone());
493            }
494            db_set.set_filter_map(self.model_builder.filters_by_table());
495            db_set.set_lazy_loading_enabled(self.lazy_loading_enabled);
496            Box::new(db_set)
497        });
498        self.sets
499            .get_mut(&type_id)
500            .and_then(|b| b.downcast_mut::<DbSet<T>>())
501            .expect("DbSet type mismatch")
502    }
503
504    /// Returns the model builder for Fluent API configuration.
505    pub fn model(&mut self) -> &mut ModelBuilder {
506        &mut self.model_builder
507    }
508
509    /// Returns a read-only reference to the model builder.
510    pub fn model_builder(&self) -> &ModelBuilder {
511        &self.model_builder
512    }
513
514    /// Returns `true` if an entity of type `T` has been discovered and
515    /// registered in the entity metadata map.
516    pub fn entity_metas_contains<T: IEntityType>(&self) -> bool {
517        self.entity_metas.contains_key(&TypeId::of::<T>())
518    }
519
520    /// No-op since metadata caching was introduced.
521    ///
522    /// Historically, this method iterated `inventory::iter` to register entity
523    /// metadata and apply `#[entity(T)]` configurations. That work now happens
524    /// in `MetadataCache::build()`, invoked lazily by `from_options()` via
525    /// `MetadataCache::get_or_build()`. The result is `Arc`-shared across all
526    /// `DbContext` instances with the same `context_key`.
527    ///
528    /// Retained as a public method for backward compatibility — existing code
529    /// calling `ctx.discover_entities()?` after `from_options()` continues to
530    /// compile and run (as a no-op). The metadata is already populated.
531    pub fn discover_entities(&mut self) -> EFResult<()> {
532        Ok(())
533    }
534
535    /// Detects changes on all tracked DbSets by comparing property snapshots.
536    pub fn detect_changes(&mut self) {
537        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
538        for type_id in type_ids {
539            if let Some(set) = self.sets.get_mut(&type_id) {
540                if let Some(saver) = self.savers.get(&type_id) {
541                    saver.detect_changes(set.as_mut());
542                }
543            }
544        }
545    }
546
547    /// Builds the interceptor `SaveChangesContext` from the actual pending
548    /// entries across all `DbSet`s (the real save data source), instead of
549    /// the legacy `change_tracker` which is never populated by `DbSet::add`.
550    /// This keeps interceptor snapshots consistent with what will be committed.
551    fn build_save_context(&self) -> SaveChangesContext {
552        let mut views: Vec<EntityEntryView> = Vec::new();
553        for (type_id, set) in &self.sets {
554            if let Some(saver) = self.savers.get(type_id) {
555                views.extend(saver.collect_entries(set.as_ref()));
556            }
557        }
558        SaveChangesContext::from_views(views)
559    }
560
561    /// Creates all tables for registered entity types.
562    ///
563    /// Sources metas from `model_builder.build()`, which applies all Fluent
564    /// API configurations and `#[entity(T)]` overrides. Entities are
565    /// discovered automatically via `#[derive(EntityType)]`; call
566    /// `discover_entities()` first, or use `set::<T>()` to register manually.
567    pub async fn ensure_created(&self) -> EFResult<()> {
568        let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
569        if metas.is_empty() {
570            metas = self.entity_metas.values().cloned().collect();
571        }
572        if metas.is_empty() {
573            return Err(EFError::configuration(
574                "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_created().",
575            ));
576        }
577        let dialect = self.provider.migration_dialect();
578        MigrationEngine::new(dialect)
579            .ensure_created(&*self.provider, &metas)
580            .await?;
581
582        for meta in &metas {
583            let rows = self.model_builder.seed_rows_for(&meta.type_id);
584            if !rows.is_empty() {
585                MigrationEngine::new(dialect)
586                    .apply_seed_data(&*self.provider, meta, rows)
587                    .await?;
588            }
589        }
590        Ok(())
591    }
592
593    /// Drops all tables for registered entity types.
594    pub async fn ensure_deleted(&self) -> EFResult<()> {
595        let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
596        if metas.is_empty() {
597            metas = self.entity_metas.values().cloned().collect();
598        }
599        if metas.is_empty() {
600            return Err(EFError::configuration(
601                "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_deleted().",
602            ));
603        }
604        let dialect = self.provider.migration_dialect();
605        MigrationEngine::new(dialect)
606            .ensure_deleted(&*self.provider, &metas)
607            .await
608    }
609}
610
611// ---------------------------------------------------------------------------
612// DbContext inherent methods (formerly on IDbContext / IDbContextExt traits)
613// ---------------------------------------------------------------------------
614
615impl DbContext {
616    /// Returns the database provider.
617    pub fn provider(&self) -> &dyn IDatabaseProvider {
618        &*self.provider
619    }
620
621    /// Returns a read-only reference to the change tracker.
622    pub fn change_tracker(&self) -> &ChangeTracker {
623        &self.change_tracker
624    }
625
626    /// Returns a mutable reference to the change tracker.
627    pub fn change_tracker_mut(&mut self) -> &mut ChangeTracker {
628        &mut self.change_tracker
629    }
630
631    /// Returns a mutable reference to the ambient transaction handle, if one
632    /// is active (registered by `use_transaction()`).
633    ///
634    /// Inside a `use_transaction()` closure, use this to access the ambient
635    /// handle for savepoint and isolation operations. The borrow must be
636    /// released (by scoping) before calling `save_changes()` or other `&mut
637    /// self` methods.
638    pub fn transaction_mut(&mut self) -> Option<&mut Box<dyn ITransaction>> {
639        self.ambient_transaction.as_mut()
640    }
641
642    /// Begins a transaction and returns a typed handle.
643    ///
644    /// The returned `ITransaction` handle is **not** registered as ambient —
645    /// `save_changes()` calls will continue to self-manage their own
646    /// transactions. Use this when you need explicit control via
647    /// `txn.commit()` / `txn.rollback()` / `txn.create_point()` etc.
648    ///
649    /// For scoped ambient transactions where `save_changes()` should reuse
650    /// the same transaction, use [`DbContext::use_transaction`] instead.
651    ///
652    /// `commit` / `rollback` consume the handle by value, preventing
653    /// use-after-commit at the type level.
654    pub async fn begin_transaction(&mut self) -> EFResult<Box<dyn ITransaction>> {
655        let mut conn = self.provider.get_connection().await?;
656        conn.begin_transaction().await?;
657        Ok(Box::new(DbTransaction::new(conn)))
658    }
659
660    /// Saves all pending changes across all DbSets.
661    ///
662    /// Detects changes, runs interceptors, executes INSERT/UPDATE/DELETE in a
663    /// transaction, and clears tracked entries on success.
664    pub async fn save_changes(&mut self) -> EFResult<SaveChangesResult> {
665        let _save_guard = crate::observability::SaveChangesGuard::new();
666        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
667        for type_id in &type_ids {
668            let set = self.sets.get_mut(type_id).unwrap();
669            self.savers
670                .get(type_id)
671                .unwrap()
672                .detect_changes(set.as_mut());
673        }
674
675        // Build configured metas from model_builder so that Fluent API overrides
676        // (to_table, has_column_name, etc.) are respected during save operations.
677        let configured_metas: HashMap<TypeId, EntityTypeMeta> = self
678            .model_builder
679            .build()
680            .into_iter()
681            .map(|m| (m.type_id, m))
682            .collect();
683
684        // --- Interceptor: on_saving (pre-commit) ---
685        // Build the context from the actual pending entries across all DbSets
686        // (the real save data source), not the legacy (empty) change_tracker.
687        let save_ctx = self.build_save_context();
688        self.interceptor_pipeline.on_saving(&save_ctx).await?;
689
690        // === Transaction connection acquisition ===
691        // If an ambient transaction exists (registered by `use_transaction`),
692        // take it out and reuse its connection (no begin/commit/rollback —
693        // the outer scope manages that). Otherwise, self-manage a fresh
694        // transaction (original behavior).
695        enum TxnSource {
696            Ambient(Box<dyn ITransaction>),
697            Managed(Box<dyn IAsyncConnection>),
698        }
699        let mut txn = match self.ambient_transaction.take() {
700            Some(t) => TxnSource::Ambient(t),
701            None => {
702                let mut c = self.provider.get_connection().await?;
703                c.begin_transaction().await?;
704                TxnSource::Managed(c)
705            }
706        };
707
708        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
709        let mut total_added = 0usize;
710        let mut total_updated = 0usize;
711        let mut total_deleted = 0usize;
712        for type_id in &type_ids {
713            let saver = self.savers.get(type_id).expect("saver not registered");
714            let set = self.sets.get_mut(type_id).unwrap();
715            let meta = configured_metas
716                .get(type_id)
717                .or_else(|| self.entity_metas.get(type_id))
718                .expect("meta not found for entity type");
719            let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
720                TxnSource::Ambient(t) => t.connection(),
721                TxnSource::Managed(c) => c.as_mut(),
722            };
723            let (a, u, d) = match saver
724                .save(conn_ref, &*self.provider, set.as_mut(), meta)
725                .await
726            {
727                Ok(r) => r,
728                Err(e) => {
729                    if let TxnSource::Managed(mut conn) = txn {
730                        let _ = conn.rollback_transaction().await;
731                    } else if let TxnSource::Ambient(t) = txn {
732                        self.ambient_transaction = Some(t);
733                    }
734                    self.interceptor_pipeline
735                        .on_save_failed(&save_ctx, &e)
736                        .await;
737                    return Err(e);
738                }
739            };
740            total_added += a;
741            total_updated += u;
742            total_deleted += d;
743        }
744        match txn {
745            TxnSource::Ambient(t) => {
746                self.ambient_transaction = Some(t);
747            }
748            TxnSource::Managed(mut conn) => {
749                if let Err(e) = conn.commit_transaction().await {
750                    self.interceptor_pipeline
751                        .on_save_failed(&save_ctx, &e)
752                        .await;
753                    return Err(e);
754                }
755            }
756        }
757        self.change_tracker.accept_all_changes();
758        for type_id in &type_ids {
759            let saver = self.savers.get(type_id).unwrap();
760            let set = self.sets.get_mut(type_id).unwrap();
761            saver.clear(set.as_mut());
762        }
763
764        // --- Interceptor: on_saved (post-commit) ---
765        let result_ctx = SaveChangesResultContext {
766            added: total_added,
767            updated: total_updated,
768            deleted: total_deleted,
769        };
770        self.interceptor_pipeline
771            .on_saved(&save_ctx, &result_ctx)
772            .await?;
773
774        Ok(SaveChangesResult {
775            added: total_added,
776            updated: total_updated,
777            deleted: total_deleted,
778        })
779    }
780
781    /// Executes a closure within an ambient transaction.
782    ///
783    /// Registers the transaction as ambient for the duration of `f`, so that
784    /// `save_changes()` calls inside `f` reuse the same transaction. Commits
785    /// on `Ok`, rolls back on `Err`.
786    ///
787    /// The closure receives `&mut DbContext` and must return a pinned boxed
788    /// future. This signature works around Rust's async borrow checker by
789    /// letting the closure capture `ctx` by mutable reference while still
790    /// producing a `Send` future.
791    ///
792    /// # Example
793    ///
794    /// ```rust,ignore
795    /// ctx.use_transaction(|ctx| Box::pin(async move {
796    ///     ctx.set::<Blog>().add(blog);
797    ///     ctx.save_changes().await?;
798    ///     Ok(())
799    /// })).await?;
800    /// ```
801    pub async fn use_transaction<F, R>(&mut self, f: F) -> EFResult<R>
802    where
803        for<'a> F: FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output = EFResult<R>> + Send + 'a>>,
804        R: Send + 'static,
805    {
806        if self.ambient_transaction.is_some() {
807            return Err(EFError::transaction(
808                "ambient transaction already active; nested use_transaction is not supported",
809            ));
810        }
811        let mut conn = self.provider.get_connection().await?;
812        conn.begin_transaction().await?;
813        self.ambient_transaction = Some(Box::new(DbTransaction::new(conn)));
814        let result = f(self).await;
815        let txn = self
816            .ambient_transaction
817            .take()
818            .expect("ambient_transaction set above");
819        match result {
820            Ok(r) => {
821                txn.commit().await?;
822                Ok(r)
823            }
824            Err(e) => {
825                let _ = txn.rollback().await;
826                Err(e)
827            }
828        }
829    }
830}
831
832// ---------------------------------------------------------------------------
833// save_one_set
834// ---------------------------------------------------------------------------
835
836#[allow(clippy::type_complexity)]
837pub async fn save_one_set<E>(
838    conn: &mut dyn IAsyncConnection,
839    provider: &dyn IDatabaseProvider,
840    db_set: &mut DbSet<E>,
841    meta: &EntityTypeMeta,
842) -> EFResult<(usize, usize, usize)>
843where
844    E: IEntityType + IEntitySnapshot + IGetKeyValues + IFromRow,
845{
846    let query_filter = db_set.query_filter();
847
848    let added: Vec<(&E, &EntityTypeMeta)> = db_set
849        .tracked_by_state(crate::entity::EntityState::Added)
850        .into_iter()
851        .map(|(e, _)| (e, meta))
852        .collect();
853    let modified: Vec<(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)> = db_set
854        .tracked_by_state(crate::entity::EntityState::Modified)
855        .into_iter()
856        .map(|(e, orig)| (e, meta, orig))
857        .collect();
858    let deleted: Vec<(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)> = db_set
859        .tracked_by_state(crate::entity::EntityState::Deleted)
860        .into_iter()
861        .map(|(e, orig)| (e, meta, orig))
862        .collect();
863    let mut ac = 0usize;
864    let mut uc = 0usize;
865    let mut dc = 0usize;
866    if !added.is_empty() {
867        ac = ChangeExecutor::execute_inserts(conn, provider, &added, |_, _| {}).await?;
868    }
869    if !modified.is_empty() {
870        uc = ChangeExecutor::execute_updates(conn, provider, &modified, query_filter).await?;
871    }
872    if !deleted.is_empty() {
873        dc = ChangeExecutor::execute_deletes(conn, provider, &deleted, query_filter).await?;
874    }
875    Ok((ac, uc, dc))
876}
877
878// ---------------------------------------------------------------------------
879// SaveChangesResult
880// ---------------------------------------------------------------------------
881
882#[derive(Debug, Clone)]
883pub struct SaveChangesResult {
884    pub added: usize,
885    pub updated: usize,
886    pub deleted: usize,
887}
888impl SaveChangesResult {
889    pub fn total(&self) -> usize {
890        self.added + self.updated + self.deleted
891    }
892}
893impl std::fmt::Display for SaveChangesResult {
894    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
895        write!(
896            f,
897            "{} entities modified ({} added, {} updated, {} deleted)",
898            self.total(),
899            self.added,
900            self.updated,
901            self.deleted
902        )
903    }
904}