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::cascade::{self, CascadeDeleteAction, CascadeDeleteDirective, DrainedChild, FixupLink};
61use crate::db_set::DbSet;
62use crate::dependency_graph::DependencyGraph;
63use crate::entity::{
64    EntityState, IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter,
65};
66use crate::error::{EFError, EFResult};
67use crate::interceptor::{InterceptorPipeline, SaveChangesContext, SaveChangesResultContext};
68use crate::metadata::{EntityTypeMeta, NavigationKind};
69use crate::migration::MigrationEngine;
70use crate::model_builder::ModelBuilder;
71use crate::provider::{DbValue, IAsyncConnection, IDatabaseProvider};
72use crate::tracking::{ChangeTracker, EntityEntryView};
73use crate::transaction::{DbTransaction, ITransaction};
74use std::any::{Any, TypeId};
75use std::collections::HashMap;
76use std::future::Future;
77use std::pin::Pin;
78use std::sync::Arc;
79
80// ---------------------------------------------------------------------------
81// DbContextOptions / DbContextOptionsBuilder
82// ---------------------------------------------------------------------------
83
84#[derive(Clone)]
85pub struct DbContextOptions {
86    pub(crate) connection_string: String,
87    pub(crate) provider_tag: Option<String>,
88    #[allow(clippy::type_complexity)]
89    pub(crate) provider_factory:
90        Option<Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>>,
91    /// Process-level cache of the built provider (which owns the connection
92    /// pool). Built once on the first `create_provider()` call and shared
93    /// across every `DbContext` created from the same `Arc<DbContextOptions>`
94    /// (i.e. the same `add_dbcontext` registration). Keeping the provider
95    /// alive for the application lifetime means the connection pool is reused
96    /// across requests instead of being recreated per request.
97    pub(crate) provider_cache: Arc<std::sync::Mutex<Option<Arc<dyn IDatabaseProvider>>>>,
98    pub(crate) interceptors: Vec<Arc<dyn crate::interceptor::ISaveChangesInterceptor>>,
99    /// When `true`, `QueryBuilder::to_list()` attaches `LazyContext` to every
100    /// navigation container on materialized entities, enabling on-demand
101    /// loading via `BelongsTo::load()` / `HasMany::load()` / `HasOne::load()`.
102    ///
103    /// Defaults to `false` (opt-in) to preserve v1.0 eager-only behavior.
104    pub(crate) lazy_loading_enabled: bool,
105    pub(crate) context_key: Option<String>,
106    /// Process-level cache of `discover_entities()` output, keyed by
107    /// `context_key`. Shared across all `DbContext` instances created from
108    /// the same `DbContextOptions` (which is `Arc`-shared per `add_dbcontext`
109    /// registration). The first `from_options()` call builds the metadata;
110    /// subsequent calls `Arc::clone` it.
111    pub(crate) metadata_cache: Arc<crate::metadata_cache::MetadataCache>,
112    /// Slow query threshold for tracing. When set, queries exceeding this
113    /// duration emit a `tracing::warn!` event.
114    #[cfg(feature = "tracing")]
115    pub(crate) slow_query_threshold: Option<std::time::Duration>,
116}
117
118impl std::fmt::Debug for DbContextOptions {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("DbContextOptions")
121            .field(
122                "connection_string",
123                &redact_connection_string(&self.connection_string),
124            )
125            .field("provider_tag", &self.provider_tag)
126            .finish()
127    }
128}
129
130/// Redacts credentials from a connection string so `Debug` output never leaks
131/// passwords. Handles URL form (`scheme://user:pass@host`) and key=value form
132/// (`...;Password=...;...`). SQLite file paths and other credential-free
133/// strings are returned unchanged for debuggability.
134fn redact_connection_string(cs: &str) -> String {
135    // URL form: scheme://[user[:pass]@]host...
136    if let Some(scheme_end) = cs.find("://") {
137        let (scheme, rest) = cs.split_at(scheme_end + 3);
138        if let Some(at) = rest.find('@') {
139            let (userinfo, host_and_rest) = rest.split_at(at);
140            let redacted_user = match userinfo.find(':') {
141                Some(colon) => &userinfo[..colon],
142                None => userinfo,
143            };
144            return format!("{}{}***@{}", scheme, redacted_user, &host_and_rest[1..]);
145        }
146        return cs.to_string();
147    }
148    // Key=value form: redact any token whose key mentions password/pwd.
149    if cs.contains('=') {
150        return cs
151            .split(';')
152            .map(|pair| {
153                let eq = match pair.find('=') {
154                    Some(e) => e,
155                    None => return pair.to_string(),
156                };
157                let key = pair[..eq].trim().to_lowercase();
158                if key.contains("password") || key.contains("pwd") {
159                    format!("{}=***", &pair[..eq])
160                } else {
161                    pair.to_string()
162                }
163            })
164            .collect::<Vec<_>>()
165            .join(";");
166    }
167    cs.to_string()
168}
169
170impl DbContextOptions {
171    pub fn connection_string(&self) -> &str {
172        &self.connection_string
173    }
174    pub fn provider_tag(&self) -> Option<&str> {
175        self.provider_tag.as_deref()
176    }
177    pub fn lazy_loading_enabled(&self) -> bool {
178        self.lazy_loading_enabled
179    }
180    pub fn context_key(&self) -> Option<&str> {
181        self.context_key.as_deref()
182    }
183    pub fn create_provider(&self) -> EFResult<Arc<dyn IDatabaseProvider>> {
184        // Recover from a poisoned lock rather than panicking (consistent with
185        // `MetadataCache`): if a previous build panicked, `into_inner()` yields
186        // the still-`None` cache and we retry the build below.
187        let mut guard = self
188            .provider_cache
189            .lock()
190            .unwrap_or_else(|p| p.into_inner());
191        if let Some(provider) = guard.as_ref() {
192            return Ok(Arc::clone(provider));
193        }
194        let factory = self.provider_factory.as_ref().ok_or_else(|| {
195            crate::error::EFError::configuration(
196                "No provider configured. Call use_sqlite / use_postgres / use_mysql first.",
197            )
198        })?;
199        let provider = factory(self.connection_string())?;
200        #[cfg(feature = "tracing")]
201        if let Some(threshold) = self.slow_query_threshold {
202            provider.set_slow_query_threshold(threshold);
203        }
204        *guard = Some(Arc::clone(&provider));
205        Ok(provider)
206    }
207}
208
209#[allow(clippy::derivable_impls)]
210impl Default for DbContextOptions {
211    fn default() -> Self {
212        Self {
213            connection_string: String::new(),
214            provider_tag: None,
215            provider_factory: None,
216            provider_cache: Arc::new(std::sync::Mutex::new(None)),
217            interceptors: Vec::new(),
218            lazy_loading_enabled: false,
219            context_key: None,
220            metadata_cache: Arc::new(crate::metadata_cache::MetadataCache::new()),
221            #[cfg(feature = "tracing")]
222            slow_query_threshold: None,
223        }
224    }
225}
226
227pub struct DbContextOptionsBuilder {
228    inner: DbContextOptions,
229}
230
231impl DbContextOptionsBuilder {
232    pub fn new() -> Self {
233        Self {
234            inner: DbContextOptions::default(),
235        }
236    }
237    pub fn connection_string(&mut self, cs: impl Into<String>) -> &mut Self {
238        self.inner.connection_string = cs.into();
239        self
240    }
241    pub fn set_provider(&mut self, tag: &str, cs: impl Into<String>) -> &mut Self {
242        self.inner.provider_tag = Some(tag.to_string());
243        self.inner.connection_string = cs.into();
244        self
245    }
246    #[allow(clippy::type_complexity)]
247    pub fn set_provider_factory(
248        &mut self,
249        tag: &str,
250        cs: impl Into<String>,
251        factory: Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>,
252    ) -> &mut Self {
253        self.inner.provider_tag = Some(tag.to_string());
254        self.inner.connection_string = cs.into();
255        self.inner.provider_factory = Some(factory);
256        self
257    }
258    /// Registers a `SaveChanges` interceptor.
259    ///
260    /// Interceptors are called in registration order during
261    /// `save_changes()`. Use this for auditing, soft-delete,
262    /// validation, and other cross-cutting concerns.
263    ///
264    /// # Example
265    ///
266    /// ```rust,ignore
267    /// options
268    ///     .use_sqlite("app.db")
269    ///     .add_interceptor(AuditInterceptor::new());
270    /// ```
271    pub fn add_interceptor(
272        &mut self,
273        interceptor: impl crate::interceptor::ISaveChangesInterceptor + 'static,
274    ) -> &mut Self {
275        self.inner.interceptors.push(Arc::new(interceptor));
276        self
277    }
278
279    /// Enables or disables lazy loading of navigation properties.
280    ///
281    /// When enabled (`true`), `to_list()` attaches a `LazyContext` to every
282    /// navigation container on each materialized entity. The user can then
283    /// call `nav.load().await` to trigger a single-entity query on first
284    /// access; subsequent accesses read from the in-memory cache.
285    ///
286    /// When disabled (`false`, the default), navigation properties are
287    /// empty unless explicitly loaded via `Include` — matching v1.0
288    /// eager-only behavior.
289    ///
290    /// # Example
291    ///
292    /// ```rust,ignore
293    /// let mut options = DbContextOptionsBuilder::new();
294    /// options.use_sqlite_in_memory().use_lazy_loading(true);
295    /// ```
296    pub fn use_lazy_loading(&mut self, enabled: bool) -> &mut Self {
297        self.inner.lazy_loading_enabled = enabled;
298        self
299    }
300
301    /// Sets the context key used to filter entities and configurations
302    /// during `DbContext::discover_entities()`. Set automatically by
303    /// `add_dbcontext_keyed`; `None` (the default) selects the default
304    /// context.
305    pub fn context_key(&mut self, key: impl Into<String>) -> &mut Self {
306        self.inner.context_key = Some(key.into());
307        self
308    }
309
310    /// Sets the slow query threshold. Queries exceeding this duration
311    /// emit a `tracing::warn!` event with SQL and elapsed time.
312    ///
313    /// Only available when the `tracing` feature is enabled.
314    #[cfg(feature = "tracing")]
315    pub fn slow_query_threshold(&mut self, threshold: std::time::Duration) -> &mut Self {
316        self.inner.slow_query_threshold = Some(threshold);
317        self
318    }
319
320    pub fn build(self) -> DbContextOptions {
321        self.inner
322    }
323}
324
325impl Default for DbContextOptionsBuilder {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331// ---------------------------------------------------------------------------
332// Type-erased set operations
333// ---------------------------------------------------------------------------
334
335#[async_trait::async_trait]
336trait ErasedSetOps: Send + Sync {
337    async fn save(
338        &self,
339        conn: &mut (dyn IAsyncConnection + Send),
340        provider: &dyn IDatabaseProvider,
341        raw_set: &mut (dyn Any + Send + Sync),
342        meta: &EntityTypeMeta,
343    ) -> EFResult<(usize, usize, usize)>;
344    fn detect_changes(&self, raw_set: &mut (dyn Any + Send + Sync));
345    /// Accepts all pending changes in the set: Added/Modified → Unchanged
346    /// (with refreshed snapshots), Deleted entries removed. Called after a
347    /// successful `save_changes` commit so tracked entities retain their
348    /// DB-generated PKs and can be compared against future modifications.
349    fn accept_all_changes(&self, raw_set: &mut (dyn Any + Send + Sync + 'static));
350    /// Collects type-erased views of all pending entries in the set, used to
351    /// build `SaveChangesContext` from the real save data source (`DbSet.entries`)
352    /// rather than the legacy (empty) `change_tracker`.
353    fn collect_entries(&self, raw_set: &(dyn Any + Send + Sync)) -> Vec<EntityEntryView>;
354
355    // ── Cascade pipeline methods ──
356
357    /// Drains HasMany/ManyToMany children from all Added entries. Returns
358    /// type-erased children with parent linkage info for FK fixup.
359    fn drain_cascade_children(
360        &self,
361        raw_set: &mut (dyn Any + Send + Sync),
362        meta: &EntityTypeMeta,
363    ) -> Vec<DrainedChild>;
364
365    /// Adds a cascade-drained child (type-erased) to this set as Added.
366    /// Returns the new entry index, or `None` if the type doesn't match.
367    fn add_cascade_child(
368        &self,
369        raw_set: &mut (dyn Any + Send + Sync),
370        child: Box<dyn Any + Send + Sync>,
371    ) -> Option<usize>;
372
373    /// Returns the number of tracked entries.
374    fn entry_count(&self, raw_set: &(dyn Any + Send + Sync)) -> usize;
375
376    /// Reads the first PK value (as i64) of the entry at `idx`. Used after
377    /// INSERT + backfill to read the principal PK for FK fixup.
378    fn get_pk_at(&self, raw_set: &(dyn Any + Send + Sync), idx: usize) -> Option<i64>;
379
380    /// Sets the FK field on the entry at `idx` pointing to `target_type`.
381    fn set_fk_at(
382        &self,
383        raw_set: &mut (dyn Any + Send + Sync),
384        idx: usize,
385        target_type: TypeId,
386        key: i64,
387    );
388
389    /// Phase 1a: INSERT Added (non-upsert), backfill PKs.
390    async fn insert_added(
391        &self,
392        conn: &mut (dyn IAsyncConnection + Send),
393        provider: &dyn IDatabaseProvider,
394        raw_set: &mut (dyn Any + Send + Sync),
395        meta: &EntityTypeMeta,
396    ) -> EFResult<usize>;
397
398    /// Phase 1b: UPSERT Added (is_upsert = true).
399    async fn upsert_added(
400        &self,
401        conn: &mut (dyn IAsyncConnection + Send),
402        provider: &dyn IDatabaseProvider,
403        raw_set: &mut (dyn Any + Send + Sync),
404        meta: &EntityTypeMeta,
405    ) -> EFResult<usize>;
406
407    /// Phase 2: UPDATE Modified.
408    async fn update_modified(
409        &self,
410        conn: &mut (dyn IAsyncConnection + Send),
411        provider: &dyn IDatabaseProvider,
412        raw_set: &mut (dyn Any + Send + Sync),
413        meta: &EntityTypeMeta,
414    ) -> EFResult<usize>;
415
416    /// Phase 3: DELETE Deleted.
417    async fn delete_deleted(
418        &self,
419        conn: &mut (dyn IAsyncConnection + Send),
420        provider: &dyn IDatabaseProvider,
421        raw_set: &mut (dyn Any + Send + Sync),
422        meta: &EntityTypeMeta,
423    ) -> EFResult<usize>;
424
425    /// Drains HasMany children from Deleted entries and collects direct DELETE
426    /// directives for untracked dependents. `processed` tracks already-handled
427    /// entries to avoid duplicate directives across drain loop iterations.
428    fn drain_cascade_deleted_children(
429        &self,
430        raw_set: &mut (dyn Any + Send + Sync),
431        meta: &EntityTypeMeta,
432        processed: &mut std::collections::HashSet<(TypeId, usize)>,
433    ) -> (
434        Vec<DrainedChild>,
435        Vec<crate::cascade::CascadeDeleteDirective>,
436    );
437
438    /// Adds a cascade-drained child to this set as Deleted. Returns the new
439    /// entry index, or `None` if the type doesn't match or the child has no PK.
440    fn add_cascade_deleted_child(
441        &self,
442        raw_set: &mut (dyn Any + Send + Sync),
443        child: Box<dyn Any + Send + Sync>,
444    ) -> Option<usize>;
445}
446
447struct SetOps<E> {
448    _phantom: std::marker::PhantomData<E>,
449}
450impl<E> SetOps<E> {
451    fn new() -> Self {
452        Self {
453            _phantom: std::marker::PhantomData,
454        }
455    }
456}
457
458#[async_trait::async_trait]
459impl<E> ErasedSetOps for SetOps<E>
460where
461    E: IEntityType
462        + IEntitySnapshot
463        + IGetKeyValues
464        + IFromRow
465        + INavigationSetter
466        + Send
467        + Sync
468        + 'static,
469{
470    async fn save(
471        &self,
472        conn: &mut (dyn IAsyncConnection + Send),
473        provider: &dyn IDatabaseProvider,
474        raw_set: &mut (dyn Any + Send + Sync),
475        meta: &EntityTypeMeta,
476    ) -> EFResult<(usize, usize, usize)> {
477        let db_set = raw_set
478            .downcast_mut::<DbSet<E>>()
479            .expect("SetOps type mismatch");
480        save_one_set(conn, provider, db_set, meta).await
481    }
482    fn detect_changes(&self, raw_set: &mut (dyn Any + Send + Sync)) {
483        if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
484            db_set.detect_changes();
485        }
486    }
487    fn accept_all_changes(&self, raw_set: &mut (dyn Any + Send + Sync + 'static)) {
488        if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
489            db_set.accept_all_changes();
490        }
491    }
492    fn collect_entries(&self, raw_set: &(dyn Any + Send + Sync)) -> Vec<EntityEntryView> {
493        let Some(db_set) = raw_set.downcast_ref::<DbSet<E>>() else {
494            return Vec::new();
495        };
496        let type_name = E::entity_meta().type_name.to_string();
497        db_set
498            .entries
499            .iter()
500            .map(|e| EntityEntryView {
501                type_id: TypeId::of::<E>(),
502                type_name: type_name.clone(),
503                state: e.state,
504            })
505            .collect()
506    }
507
508    fn drain_cascade_children(
509        &self,
510        raw_set: &mut (dyn Any + Send + Sync),
511        meta: &EntityTypeMeta,
512    ) -> Vec<DrainedChild> {
513        let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() else {
514            return Vec::new();
515        };
516        let mut result = Vec::new();
517        for (entry_idx, entry) in db_set.entries.iter_mut().enumerate() {
518            if entry.state != EntityState::Added || entry.is_upsert {
519                continue;
520            }
521            for nav in &meta.navigations {
522                if !matches!(nav.kind, NavigationKind::HasMany | NavigationKind::ManyToMany) {
523                    continue;
524                }
525                if let Some(items) = entry.entity.drain_has_many(nav.field_name.as_ref()) {
526                    for item in items {
527                        result.push(DrainedChild {
528                            parent_type_id: TypeId::of::<E>(),
529                            parent_entry_idx: entry_idx,
530                            child: item,
531                            child_type_id: nav.related_type_id,
532                            fk_target_type_id: TypeId::of::<E>(),
533                            through_table: nav.through_table.as_ref().map(|s| s.to_string()),
534                            through_parent_fk_col: nav
535                                .through_parent_fk
536                                .as_ref()
537                                .map(|s| s.to_string()),
538                            through_child_fk_col: nav
539                                .through_related_fk
540                                .as_ref()
541                                .map(|s| s.to_string()),
542                        });
543                    }
544                }
545            }
546        }
547        result
548    }
549
550    fn add_cascade_child(
551        &self,
552        raw_set: &mut (dyn Any + Send + Sync),
553        child: Box<dyn Any + Send + Sync>,
554    ) -> Option<usize> {
555        let db_set = raw_set.downcast_mut::<DbSet<E>>()?;
556        let child = child.downcast::<E>().ok()?;
557        let pk: i64 = child
558            .key_values()
559            .into_values()
560            .next()
561            .and_then(|v| v.try_into().ok())
562            .unwrap_or(0);
563        let entity = *child;
564        if pk > 0 {
565            db_set.attach(entity);
566        } else {
567            db_set.add(entity);
568        }
569        Some(db_set.entries.len() - 1)
570    }
571
572    fn entry_count(&self, raw_set: &(dyn Any + Send + Sync)) -> usize {
573        raw_set
574            .downcast_ref::<DbSet<E>>()
575            .map(|s| s.entries.len())
576            .unwrap_or(0)
577    }
578
579    fn get_pk_at(&self, raw_set: &(dyn Any + Send + Sync), idx: usize) -> Option<i64> {
580        let db_set = raw_set.downcast_ref::<DbSet<E>>()?;
581        let entry = db_set.entries.get(idx)?;
582        entry
583            .entity
584            .key_values()
585            .into_values()
586            .next()
587            .and_then(|v| v.try_into().ok())
588    }
589
590    fn set_fk_at(
591        &self,
592        raw_set: &mut (dyn Any + Send + Sync),
593        idx: usize,
594        target_type: TypeId,
595        key: i64,
596    ) {
597        if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
598            if let Some(entry) = db_set.entries.get_mut(idx) {
599                entry.entity.set_foreign_key(target_type, key);
600            }
601        }
602    }
603
604    async fn insert_added(
605        &self,
606        conn: &mut (dyn IAsyncConnection + Send),
607        provider: &dyn IDatabaseProvider,
608        raw_set: &mut (dyn Any + Send + Sync),
609        meta: &EntityTypeMeta,
610    ) -> EFResult<usize> {
611        let db_set = raw_set
612            .downcast_mut::<DbSet<E>>()
613            .expect("SetOps type mismatch");
614        insert_added_phase(conn, provider, db_set, meta).await
615    }
616
617    async fn upsert_added(
618        &self,
619        conn: &mut (dyn IAsyncConnection + Send),
620        provider: &dyn IDatabaseProvider,
621        raw_set: &mut (dyn Any + Send + Sync),
622        meta: &EntityTypeMeta,
623    ) -> EFResult<usize> {
624        let db_set = raw_set
625            .downcast_mut::<DbSet<E>>()
626            .expect("SetOps type mismatch");
627        upsert_added_phase(conn, provider, db_set, meta).await
628    }
629
630    async fn update_modified(
631        &self,
632        conn: &mut (dyn IAsyncConnection + Send),
633        provider: &dyn IDatabaseProvider,
634        raw_set: &mut (dyn Any + Send + Sync),
635        meta: &EntityTypeMeta,
636    ) -> EFResult<usize> {
637        let db_set = raw_set
638            .downcast_mut::<DbSet<E>>()
639            .expect("SetOps type mismatch");
640        let query_filter = db_set.query_filter().cloned();
641        update_modified_phase(conn, provider, db_set, meta, query_filter.as_ref()).await
642    }
643
644    async fn delete_deleted(
645        &self,
646        conn: &mut (dyn IAsyncConnection + Send),
647        provider: &dyn IDatabaseProvider,
648        raw_set: &mut (dyn Any + Send + Sync),
649        meta: &EntityTypeMeta,
650    ) -> EFResult<usize> {
651        let db_set = raw_set
652            .downcast_mut::<DbSet<E>>()
653            .expect("SetOps type mismatch");
654        let query_filter = db_set.query_filter().cloned();
655        delete_deleted_phase(conn, provider, db_set, meta, query_filter.as_ref()).await
656    }
657
658    fn drain_cascade_deleted_children(
659        &self,
660        raw_set: &mut (dyn Any + Send + Sync),
661        meta: &EntityTypeMeta,
662        processed: &mut std::collections::HashSet<(TypeId, usize)>,
663    ) -> (Vec<DrainedChild>, Vec<CascadeDeleteDirective>) {
664        use crate::relations::DeleteBehavior;
665
666        let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() else {
667            return (Vec::new(), Vec::new());
668        };
669        let mut drained_children = Vec::new();
670        let mut directives = Vec::new();
671
672        for (entry_idx, entry) in db_set.entries.iter_mut().enumerate() {
673            if entry.state != EntityState::Deleted {
674                continue;
675            }
676            // Skip already-processed entries (prevents duplicate directives)
677            if !processed.insert((TypeId::of::<E>(), entry_idx)) {
678                continue;
679            }
680
681            // Get principal PK for directives
682            let principal_pk: i64 = entry
683                .entity
684                .key_values()
685                .into_values()
686                .next()
687                .and_then(|v| v.try_into().ok())
688                .unwrap_or(0);
689
690            for nav in &meta.navigations {
691                match nav.kind {
692                    NavigationKind::ManyToMany => {
693                        // M2M: always delete join table rows, don't delete related entities
694                        if let (Some(table), Some(fk_col), Some(pk)) = (
695                            nav.through_table.as_ref(),
696                            nav.through_parent_fk.as_ref(),
697                            (principal_pk > 0).then_some(principal_pk),
698                        ) {
699                            directives.push(CascadeDeleteDirective {
700                                table: table.to_string(),
701                                fk_column: fk_col.to_string(),
702                                principal_pk: pk,
703                                action: CascadeDeleteAction::Delete,
704                            });
705                        }
706                    }
707                    NavigationKind::HasMany => {
708                        let behavior = resolve_delete_behavior(nav);
709                        match behavior {
710                            DeleteBehavior::Cascade => {
711                                // Drain loaded children + collect DELETE directive for untracked
712                                if let Some(items) = entry.entity.drain_has_many(nav.field_name.as_ref()) {
713                                    for item in items {
714                                        drained_children.push(DrainedChild {
715                                            parent_type_id: TypeId::of::<E>(),
716                                            parent_entry_idx: entry_idx,
717                                            child: item,
718                                            child_type_id: nav.related_type_id,
719                                            fk_target_type_id: TypeId::of::<E>(),
720                                            through_table: None,
721                                            through_parent_fk_col: None,
722                                            through_child_fk_col: None,
723                                        });
724                                    }
725                                }
726                                // Also collect direct DELETE for untracked dependents
727                                if let (Some(table), Some(fk_col), Some(pk)) = (
728                                    nav.related_table.as_ref(),
729                                    nav.fk_column.as_ref(),
730                                    (principal_pk > 0).then_some(principal_pk),
731                                ) {
732                                    directives.push(CascadeDeleteDirective {
733                                        table: table.to_string(),
734                                        fk_column: fk_col.to_string(),
735                                        principal_pk: pk,
736                                        action: CascadeDeleteAction::Delete,
737                                    });
738                                }
739                            }
740                            DeleteBehavior::SetNull => {
741                                // Don't drain children; just set FK to NULL
742                                if let (Some(table), Some(fk_col), Some(pk)) = (
743                                    nav.related_table.as_ref(),
744                                    nav.fk_column.as_ref(),
745                                    (principal_pk > 0).then_some(principal_pk),
746                                ) {
747                                    directives.push(CascadeDeleteDirective {
748                                        table: table.to_string(),
749                                        fk_column: fk_col.to_string(),
750                                        principal_pk: pk,
751                                        action: CascadeDeleteAction::SetNull,
752                                    });
753                                }
754                            }
755                            DeleteBehavior::Restrict | DeleteBehavior::NoAction => {
756                                // No cascade — skip
757                            }
758                        }
759                    }
760                    NavigationKind::BelongsTo | NavigationKind::HasOne => {
761                        // Not applicable for cascade delete
762                    }
763                }
764            }
765        }
766        (drained_children, directives)
767    }
768
769    fn add_cascade_deleted_child(
770        &self,
771        raw_set: &mut (dyn Any + Send + Sync),
772        child: Box<dyn Any + Send + Sync>,
773    ) -> Option<usize> {
774        let db_set = raw_set.downcast_mut::<DbSet<E>>()?;
775        let child = child.downcast::<E>().ok()?;
776        let pk: i64 = child
777            .key_values()
778            .into_values()
779            .next()
780            .and_then(|v| v.try_into().ok())
781            .unwrap_or(0);
782        if pk == 0 {
783            return None;
784        }
785        let original = child.snapshot();
786        db_set.entries.push(crate::db_set::TrackedEntry {
787            entity: *child,
788            state: EntityState::Deleted,
789            original: Some(original),
790            modified_properties: Vec::new(),
791            is_upsert: false,
792        });
793        Some(db_set.entries.len() - 1)
794    }
795}
796
797/// Resolves the effective `DeleteBehavior` for a navigation, applying
798/// EFCore-style defaults when `delete_behavior` is `None`:
799/// - ManyToMany → Cascade (join rows are always pruned)
800/// - required FK (non-nullable type, e.g. `i32`) → Cascade
801/// - optional FK (nullable type, e.g. `Option<i32>`) → Restrict
802pub(crate) fn resolve_delete_behavior(
803    nav: &crate::metadata::NavigationMeta,
804) -> crate::relations::DeleteBehavior {
805    use crate::relations::DeleteBehavior;
806    if let Some(b) = nav.delete_behavior {
807        return b;
808    }
809    if nav.kind == NavigationKind::ManyToMany {
810        return DeleteBehavior::Cascade;
811    }
812    if let Some(meta_fn) = nav.related_entity_meta {
813        let child_meta = meta_fn();
814        if let Some(fk_prop) = child_meta.properties.iter().find(|p| p.is_foreign_key) {
815            // Determine nullability from the Rust type name: `Option<T>` is
816            // nullable, everything else (i32, i64, String, ...) is not.
817            let is_nullable = fk_prop.type_name.contains("Option");
818            return if is_nullable {
819                DeleteBehavior::Restrict
820            } else {
821                DeleteBehavior::Cascade
822            };
823        }
824    }
825    DeleteBehavior::Cascade
826}
827
828// ---------------------------------------------------------------------------
829// DbContext
830// ---------------------------------------------------------------------------
831
832pub struct DbContext {
833    sets: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
834    savers: HashMap<TypeId, Box<dyn ErasedSetOps>>,
835    entity_metas: HashMap<TypeId, EntityTypeMeta>,
836    model_builder: ModelBuilder,
837    change_tracker: ChangeTracker,
838    provider: Arc<dyn IDatabaseProvider>,
839    interceptor_pipeline: InterceptorPipeline,
840    lazy_loading_enabled: bool,
841    /// Ambient transaction: registered by `use_transaction()`. When present,
842    /// `save_changes()` reuses this transaction's connection and does not
843    /// begin/commit/rollback on its own. Uses `take()`/restore pattern to
844    /// avoid `&mut self` borrow conflicts with `self.sets` during save.
845    ///
846    /// Note: `begin_transaction()` returns a handle without registering it
847    /// here — only `use_transaction()` registers an ambient. This separates
848    /// manual handle-based control from scoped ambient control.
849    ambient_transaction: Option<Box<dyn ITransaction>>,
850}
851
852impl DbContext {
853    /// Creates the context from options (uses the provider factory stored in options).
854    ///
855    /// Entity metadata is loaded from the process-level `MetadataCache` on
856    /// `DbContextOptions` — `inventory::iter` + `IEntityTypeConfiguration::configure()`
857    /// run once per `context_key` (first call), then the result is `Arc`-shared
858    /// across all `DbContext` instances. Per-instance `ModelBuilder` mutations
859    /// (`has_query_filter`, etc.) after construction only affect this instance.
860    pub fn from_options(options: &DbContextOptions) -> EFResult<Self> {
861        let provider = options.create_provider()?;
862        let built = options
863            .metadata_cache
864            .get_or_build(options.context_key.as_deref());
865        let ctx = Self {
866            sets: HashMap::new(),
867            savers: HashMap::new(),
868            entity_metas: built.entity_metas.clone(),
869            model_builder: ModelBuilder::from_built(&built),
870            change_tracker: ChangeTracker::new(),
871            provider,
872            interceptor_pipeline: InterceptorPipeline::new(options.interceptors.clone()),
873            lazy_loading_enabled: options.lazy_loading_enabled,
874            ambient_transaction: None,
875        };
876        Ok(ctx)
877    }
878
879    pub fn set<T>(&mut self) -> &mut DbSet<T>
880    where
881        T: IEntityType
882            + IEntitySnapshot
883            + IGetKeyValues
884            + IFromRow
885            + INavigationSetter
886            + Send
887            + Sync
888            + 'static,
889    {
890        let type_id = TypeId::of::<T>();
891        self.savers
892            .entry(type_id)
893            .or_insert_with(|| Box::new(SetOps::<T>::new()));
894        self.entity_metas
895            .entry(type_id)
896            .or_insert_with(T::entity_meta);
897        if !self.model_builder.has_entity(type_id) {
898            self.model_builder.register_entity_meta(T::entity_meta());
899        }
900        self.sets.entry(type_id).or_insert_with(|| {
901            let table_name = self
902                .model_builder
903                .build()
904                .into_iter()
905                .find(|m| m.type_id == type_id)
906                .map(|m| m.table_name.to_string())
907                .unwrap_or_else(|| T::entity_meta().table_name.to_string());
908            let mut db_set = DbSet::<T>::with_provider(table_name, Arc::clone(&self.provider));
909            if let Some(filter) = self.model_builder.get_query_filter(&type_id) {
910                db_set.set_query_filter(filter.clone());
911            }
912            db_set.set_filter_map(self.model_builder.filters_by_table());
913            db_set.set_lazy_loading_enabled(self.lazy_loading_enabled);
914            Box::new(db_set)
915        });
916        self.sets
917            .get_mut(&type_id)
918            .and_then(|b| b.downcast_mut::<DbSet<T>>())
919            .expect("DbSet type mismatch")
920    }
921
922    /// Returns the model builder for Fluent API configuration.
923    pub fn model(&mut self) -> &mut ModelBuilder {
924        &mut self.model_builder
925    }
926
927    /// Returns a read-only reference to the model builder.
928    pub fn model_builder(&self) -> &ModelBuilder {
929        &self.model_builder
930    }
931
932    /// Returns `true` if an entity of type `T` has been discovered and
933    /// registered in the entity metadata map.
934    pub fn entity_metas_contains<T: IEntityType>(&self) -> bool {
935        self.entity_metas.contains_key(&TypeId::of::<T>())
936    }
937
938    /// No-op since metadata caching was introduced.
939    ///
940    /// Historically, this method iterated `inventory::iter` to register entity
941    /// metadata and apply `#[entity(T)]` configurations. That work now happens
942    /// in `MetadataCache::build()`, invoked lazily by `from_options()` via
943    /// `MetadataCache::get_or_build()`. The result is `Arc`-shared across all
944    /// `DbContext` instances with the same `context_key`.
945    ///
946    /// Retained as a public method for backward compatibility — existing code
947    /// calling `ctx.discover_entities()?` after `from_options()` continues to
948    /// compile and run (as a no-op). The metadata is already populated.
949    pub fn discover_entities(&mut self) -> EFResult<()> {
950        Ok(())
951    }
952
953    /// Detects changes on all tracked DbSets by comparing property snapshots.
954    pub fn detect_changes(&mut self) {
955        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
956        for type_id in type_ids {
957            if let Some(set) = self.sets.get_mut(&type_id) {
958                if let Some(saver) = self.savers.get(&type_id) {
959                    saver.detect_changes(set.as_mut());
960                }
961            }
962        }
963    }
964
965    /// Builds the interceptor `SaveChangesContext` from the actual pending
966    /// entries across all `DbSet`s (the real save data source), instead of
967    /// the legacy `change_tracker` which is never populated by `DbSet::add`.
968    /// This keeps interceptor snapshots consistent with what will be committed.
969    fn build_save_context(&self) -> SaveChangesContext {
970        let mut views: Vec<EntityEntryView> = Vec::new();
971        for (type_id, set) in &self.sets {
972            if let Some(saver) = self.savers.get(type_id) {
973                views.extend(saver.collect_entries(set.as_ref()));
974            }
975        }
976        SaveChangesContext::from_views(views)
977    }
978
979    /// Creates all tables for registered entity types.
980    ///
981    /// Sources metas from `model_builder.build()`, which applies all Fluent
982    /// API configurations and `#[entity(T)]` overrides. Entities are
983    /// discovered automatically via `#[derive(EntityType)]`; call
984    /// `discover_entities()` first, or use `set::<T>()` to register manually.
985    pub async fn ensure_created(&self) -> EFResult<()> {
986        let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
987        if metas.is_empty() {
988            metas = self.entity_metas.values().cloned().collect();
989        }
990        if metas.is_empty() {
991            return Err(EFError::configuration(
992                "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_created().",
993            ));
994        }
995        let dialect = self.provider.migration_dialect();
996        MigrationEngine::new(dialect)
997            .ensure_created(&*self.provider, &metas)
998            .await?;
999
1000        for meta in &metas {
1001            let rows = self.model_builder.seed_rows_for(&meta.type_id);
1002            if !rows.is_empty() {
1003                MigrationEngine::new(dialect)
1004                    .apply_seed_data(&*self.provider, meta, rows)
1005                    .await?;
1006            }
1007        }
1008        Ok(())
1009    }
1010
1011    /// Drops all tables for registered entity types.
1012    pub async fn ensure_deleted(&self) -> EFResult<()> {
1013        let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
1014        if metas.is_empty() {
1015            metas = self.entity_metas.values().cloned().collect();
1016        }
1017        if metas.is_empty() {
1018            return Err(EFError::configuration(
1019                "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_deleted().",
1020            ));
1021        }
1022        let dialect = self.provider.migration_dialect();
1023        MigrationEngine::new(dialect)
1024            .ensure_deleted(&*self.provider, &metas)
1025            .await
1026    }
1027}
1028
1029// ---------------------------------------------------------------------------
1030// DbContext inherent methods (formerly on IDbContext / IDbContextExt traits)
1031// ---------------------------------------------------------------------------
1032
1033impl DbContext {
1034    /// Returns the database provider.
1035    pub fn provider(&self) -> &dyn IDatabaseProvider {
1036        &*self.provider
1037    }
1038
1039    /// Returns a read-only reference to the change tracker.
1040    pub fn change_tracker(&self) -> &ChangeTracker {
1041        &self.change_tracker
1042    }
1043
1044    /// Returns a mutable reference to the change tracker.
1045    pub fn change_tracker_mut(&mut self) -> &mut ChangeTracker {
1046        &mut self.change_tracker
1047    }
1048
1049    /// Executes a raw SQL query and materializes the result rows into entities.
1050    ///
1051    /// This is the escape hatch for complex queries (multi-table JOINs, CTEs,
1052    /// window functions) that are hard to express via LINQ. The caller is
1053    /// responsible for SQL correctness and parameterization.
1054    ///
1055    /// # Example
1056    /// ```rust,ignore
1057    /// let blogs: Vec<Blog> = ctx
1058    ///     .sql_query("SELECT * FROM blogs WHERE id = ?", &[DbValue::I32(1)])
1059    ///     .await?;
1060    /// ```
1061    pub async fn sql_query<T: IFromRow + IEntityType>(
1062        &self,
1063        sql: &str,
1064        params: &[DbValue],
1065    ) -> EFResult<Vec<T>> {
1066        let mut conn = self.provider.get_connection().await?;
1067        let rows = conn.query(sql, params).await?;
1068        crate::entity::materialize_entities(&rows)
1069    }
1070
1071    /// Returns a mutable reference to the ambient transaction handle, if one
1072    /// is active (registered by `use_transaction()`).
1073    ///
1074    /// Inside a `use_transaction()` closure, use this to access the ambient
1075    /// handle for savepoint and isolation operations. The borrow must be
1076    /// released (by scoping) before calling `save_changes()` or other `&mut
1077    /// self` methods.
1078    pub fn transaction_mut(&mut self) -> Option<&mut Box<dyn ITransaction>> {
1079        self.ambient_transaction.as_mut()
1080    }
1081
1082    /// Begins a transaction and returns a typed handle.
1083    ///
1084    /// The returned `ITransaction` handle is **not** registered as ambient —
1085    /// `save_changes()` calls will continue to self-manage their own
1086    /// transactions. Use this when you need explicit control via
1087    /// `txn.commit()` / `txn.rollback()` / `txn.create_point()` etc.
1088    ///
1089    /// For scoped ambient transactions where `save_changes()` should reuse
1090    /// the same transaction, use [`DbContext::use_transaction`] instead.
1091    ///
1092    /// `commit` / `rollback` consume the handle by value, preventing
1093    /// use-after-commit at the type level.
1094    pub async fn begin_transaction(&mut self) -> EFResult<Box<dyn ITransaction>> {
1095        let mut conn = self.provider.get_connection().await?;
1096        conn.begin_transaction().await?;
1097        Ok(Box::new(DbTransaction::new(conn)))
1098    }
1099
1100    /// Saves all pending changes across all DbSets.
1101    ///
1102    /// Detects changes, runs interceptors, executes INSERT/UPDATE/DELETE in a
1103    /// transaction, and clears tracked entries on success.
1104    pub async fn save_changes(&mut self) -> EFResult<SaveChangesResult> {
1105        let _save_guard = crate::observability::SaveChangesGuard::new();
1106        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
1107        for type_id in &type_ids {
1108            let set = self.sets.get_mut(type_id).unwrap();
1109            self.savers
1110                .get(type_id)
1111                .unwrap()
1112                .detect_changes(set.as_mut());
1113        }
1114
1115        // Build configured metas from model_builder so that Fluent API overrides
1116        // (to_table, has_column_name, etc.) are respected during save operations.
1117        let configured_metas: HashMap<TypeId, EntityTypeMeta> = self
1118            .model_builder
1119            .build()
1120            .into_iter()
1121            .map(|m| (m.type_id, m))
1122            .collect();
1123
1124        // --- Interceptor: on_saving (pre-commit) ---
1125        // Build the context from the actual pending entries across all DbSets
1126        // (the real save data source), not the legacy (empty) change_tracker.
1127        let save_ctx = self.build_save_context();
1128        self.interceptor_pipeline.on_saving(&save_ctx).await?;
1129
1130        // === Transaction connection acquisition ===
1131        // If an ambient transaction exists (registered by `use_transaction`),
1132        // take it out and reuse its connection (no begin/commit/rollback —
1133        // the outer scope manages that). Otherwise, self-manage a fresh
1134        // transaction (original behavior).
1135        enum TxnSource {
1136            Ambient(Box<dyn ITransaction>),
1137            Managed(Box<dyn IAsyncConnection>),
1138        }
1139        let mut txn = match self.ambient_transaction.take() {
1140            Some(t) => TxnSource::Ambient(t),
1141            None => {
1142                let mut c = self.provider.get_connection().await?;
1143                c.begin_transaction().await?;
1144                TxnSource::Managed(c)
1145            }
1146        };
1147
1148        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
1149
1150        // --- Cascade drain loop ---
1151        // Iteratively drain HasMany/M2M children from Added principals. Drained
1152        // children are added to their target DbSet as Added (if new) or attached
1153        // as Unchanged (if existing). Repeats until no new children are
1154        // extracted (handles arbitrary depth).
1155        let mut fixup_links: Vec<FixupLink> = Vec::new();
1156        loop {
1157            let mut all_drained: Vec<DrainedChild> = Vec::new();
1158            for type_id in &type_ids {
1159                let saver = self.savers.get(type_id).expect("saver not registered");
1160                let set = self.sets.get_mut(type_id).unwrap();
1161                let meta = configured_metas
1162                    .get(type_id)
1163                    .or_else(|| self.entity_metas.get(type_id))
1164                    .expect("meta not found");
1165                all_drained.extend(saver.drain_cascade_children(set.as_mut(), meta));
1166            }
1167            if all_drained.is_empty() {
1168                break;
1169            }
1170            for child in all_drained {
1171                let child_saver = self.savers.get(&child.child_type_id).ok_or_else(|| {
1172                    EFError::configuration(format!(
1173                        "Cannot cascade-save child type {:?}: no DbSet registered. \
1174                         Call ctx.set::<ChildType>() before save_changes.",
1175                        child.child_type_id
1176                    ))
1177                })?;
1178                let child_set = self
1179                    .sets
1180                    .get_mut(&child.child_type_id)
1181                    .expect("set not found for registered saver");
1182                if let Some(child_idx) =
1183                    child_saver.add_cascade_child(child_set.as_mut(), child.child)
1184                {
1185                    if let Some(link) = fixup_links.iter_mut().find(|l| {
1186                        l.parent_type_id == child.parent_type_id
1187                            && l.parent_entry_idx == child.parent_entry_idx
1188                            && l.child_type_id == child.child_type_id
1189                            && l.through_table == child.through_table
1190                    }) {
1191                        link.child_entry_indices.push(child_idx);
1192                    } else {
1193                        fixup_links.push(FixupLink {
1194                            parent_type_id: child.parent_type_id,
1195                            parent_entry_idx: child.parent_entry_idx,
1196                            child_type_id: child.child_type_id,
1197                            child_entry_indices: vec![child_idx],
1198                            fk_target_type_id: child.fk_target_type_id,
1199                            through_table: child.through_table,
1200                            through_parent_fk_col: child.through_parent_fk_col,
1201                            through_child_fk_col: child.through_child_fk_col,
1202                        });
1203                    }
1204                }
1205            }
1206        }
1207
1208        // --- Cascade DELETE drain loop ---
1209        // Iteratively drain HasMany children from Deleted principals. Drained
1210        // children are added to their target DbSet as Deleted. Also collects
1211        // direct DELETE/SET NULL directives for untracked dependents, executed
1212        // before the PK-based DELETE phase.
1213        let mut delete_directives: Vec<CascadeDeleteDirective> = Vec::new();
1214        let mut processed: std::collections::HashSet<(TypeId, usize)> =
1215            std::collections::HashSet::new();
1216        loop {
1217            let mut all_drained_deleted: Vec<DrainedChild> = Vec::new();
1218            for type_id in &type_ids {
1219                let saver = self.savers.get(type_id).expect("saver not registered");
1220                let set = self.sets.get_mut(type_id).unwrap();
1221                let meta = configured_metas
1222                    .get(type_id)
1223                    .or_else(|| self.entity_metas.get(type_id))
1224                    .expect("meta not found");
1225                let (drained, directives) =
1226                    saver.drain_cascade_deleted_children(set.as_mut(), meta, &mut processed);
1227                all_drained_deleted.extend(drained);
1228                delete_directives.extend(directives);
1229            }
1230            if all_drained_deleted.is_empty() {
1231                break;
1232            }
1233            for child in all_drained_deleted {
1234                let child_saver = self.savers.get(&child.child_type_id).ok_or_else(|| {
1235                    EFError::configuration(format!(
1236                        "Cannot cascade-delete child type {:?}: no DbSet registered. \
1237                         Call ctx.set::<ChildType>() before save_changes.",
1238                        child.child_type_id
1239                    ))
1240                })?;
1241                let child_set = self
1242                    .sets
1243                    .get_mut(&child.child_type_id)
1244                    .expect("set not found for registered saver");
1245                child_saver.add_cascade_deleted_child(child_set.as_mut(), child.child);
1246            }
1247        }
1248
1249        // --- Topological sort ---
1250        let graph = DependencyGraph::build(&configured_metas);
1251        let insert_order = graph.topological_sort();
1252        let delete_order = graph.deletion_order();
1253
1254        let mut total_added = 0usize;
1255        let mut total_updated = 0usize;
1256        let mut total_deleted = 0usize;
1257
1258        // --- INSERT phase (topological order) + FK fixup ---
1259        for type_id in &insert_order {
1260            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
1261                continue;
1262            }
1263            let saver = self.savers.get(type_id).expect("saver not registered");
1264            let set = self.sets.get_mut(type_id).unwrap();
1265            let meta = configured_metas
1266                .get(type_id)
1267                .or_else(|| self.entity_metas.get(type_id))
1268                .expect("meta not found");
1269            let inserted = {
1270                let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1271                    TxnSource::Ambient(t) => t.connection(),
1272                    TxnSource::Managed(c) => c.as_mut(),
1273                };
1274                match saver
1275                    .insert_added(conn_ref, &*self.provider, set.as_mut(), meta)
1276                    .await
1277                {
1278                    Ok(n) => n,
1279                    Err(e) => {
1280                        if let TxnSource::Managed(mut conn) = txn {
1281                            let _ = conn.rollback_transaction().await;
1282                        } else if let TxnSource::Ambient(t) = txn {
1283                            self.ambient_transaction = Some(t);
1284                        }
1285                        self.interceptor_pipeline
1286                            .on_save_failed(&save_ctx, &e)
1287                            .await;
1288                        return Err(e);
1289                    }
1290                }
1291            };
1292            total_added += inserted;
1293
1294            // FK fixup: one-to-many links where parent == this type
1295            let link_indices: Vec<usize> = fixup_links
1296                .iter()
1297                .enumerate()
1298                .filter(|(_, l)| l.parent_type_id == *type_id && l.through_table.is_none())
1299                .map(|(i, _)| i)
1300                .collect();
1301
1302            // Collect self-referential UPDATEs (deferred to avoid borrow conflicts)
1303            let mut self_ref_updates: Vec<(String, i64, i64)> = Vec::new();
1304
1305            for link_idx in &link_indices {
1306                let link = &fixup_links[*link_idx];
1307                let parent_pk = {
1308                    let parent_saver = self.savers.get(&link.parent_type_id).unwrap();
1309                    let parent_set = self.sets.get(&link.parent_type_id).unwrap();
1310                    parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
1311                };
1312                let Some(pk) = parent_pk else {
1313                    continue;
1314                };
1315
1316                // set_fk_at on children (in-memory)
1317                {
1318                    let child_saver = self.savers.get(&link.child_type_id).unwrap();
1319                    let child_set = self.sets.get_mut(&link.child_type_id).unwrap();
1320                    for &child_idx in &link.child_entry_indices {
1321                        child_saver.set_fk_at(
1322                            child_set.as_mut(),
1323                            child_idx,
1324                            link.fk_target_type_id,
1325                            pk,
1326                        );
1327                    }
1328                }
1329
1330                // Self-referential: child already inserted with FK=0
1331                if link.child_type_id == link.parent_type_id {
1332                    let child_meta = configured_metas
1333                        .get(&link.child_type_id)
1334                        .or_else(|| self.entity_metas.get(&link.child_type_id))
1335                        .unwrap();
1336                    let fk_col = child_meta
1337                        .properties
1338                        .iter()
1339                        .find(|p| p.is_foreign_key)
1340                        .map(|p| p.column_name.as_ref());
1341                    let pk_col = child_meta
1342                        .properties
1343                        .iter()
1344                        .find(|p| p.is_primary_key)
1345                        .map(|p| p.column_name.as_ref())
1346                        .unwrap_or("id");
1347                    if let Some(fk_col) = fk_col {
1348                        for &child_idx in &link.child_entry_indices {
1349                            let child_pk = {
1350                                let child_saver = self.savers.get(&link.child_type_id).unwrap();
1351                                let child_set = self.sets.get(&link.child_type_id).unwrap();
1352                                child_saver.get_pk_at(child_set.as_ref(), child_idx)
1353                            };
1354                            if let Some(child_pk) = child_pk {
1355                                let sql = format!(
1356                                    "UPDATE {} SET {} = ? WHERE {} = ?",
1357                                    child_meta.table_name, fk_col, pk_col
1358                                );
1359                                self_ref_updates.push((sql, pk, child_pk));
1360                            }
1361                        }
1362                    }
1363                }
1364            }
1365
1366            // Execute deferred self-referential UPDATEs
1367            for (sql, fk_val, pk_val) in self_ref_updates {
1368                let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1369                    TxnSource::Ambient(t) => t.connection(),
1370                    TxnSource::Managed(c) => c.as_mut(),
1371                };
1372                if let Err(e) = conn_ref
1373                    .execute(&sql, &[DbValue::from(fk_val), DbValue::from(pk_val)])
1374                    .await
1375                {
1376                    if let TxnSource::Managed(mut conn) = txn {
1377                        let _ = conn.rollback_transaction().await;
1378                    } else if let TxnSource::Ambient(t) = txn {
1379                        self.ambient_transaction = Some(t);
1380                    }
1381                    self.interceptor_pipeline
1382                        .on_save_failed(&save_ctx, &e)
1383                        .await;
1384                    return Err(e);
1385                }
1386            }
1387        }
1388
1389        // --- M2M join row insertion (after all entity INSERTs) ---
1390        for link in &fixup_links {
1391            if link.through_table.is_none() {
1392                continue;
1393            }
1394            let table = link.through_table.as_ref().unwrap();
1395            let parent_col = link.through_parent_fk_col.as_ref().unwrap();
1396            let child_col = link.through_child_fk_col.as_ref().unwrap();
1397
1398            let parent_pk = {
1399                let parent_saver = self.savers.get(&link.parent_type_id).unwrap();
1400                let parent_set = self.sets.get(&link.parent_type_id).unwrap();
1401                parent_saver.get_pk_at(parent_set.as_ref(), link.parent_entry_idx)
1402            };
1403            let Some(parent_pk) = parent_pk else {
1404                continue;
1405            };
1406
1407            let mut child_pks: Vec<i64> = Vec::new();
1408            {
1409                let child_saver = self.savers.get(&link.child_type_id).unwrap();
1410                let child_set = self.sets.get(&link.child_type_id).unwrap();
1411                for &child_idx in &link.child_entry_indices {
1412                    if let Some(child_pk) = child_saver.get_pk_at(child_set.as_ref(), child_idx) {
1413                        child_pks.push(child_pk);
1414                    }
1415                }
1416            }
1417
1418            if !child_pks.is_empty() {
1419                let sql = cascade::m2m_insert_sql(table, parent_col, child_col, child_pks.len());
1420                let mut params: Vec<DbValue> = Vec::with_capacity(child_pks.len() * 2);
1421                for child_pk in &child_pks {
1422                    params.push(DbValue::from(parent_pk));
1423                    params.push(DbValue::from(*child_pk));
1424                }
1425                let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1426                    TxnSource::Ambient(t) => t.connection(),
1427                    TxnSource::Managed(c) => c.as_mut(),
1428                };
1429                if let Err(e) = conn_ref.execute(&sql, &params).await {
1430                    if let TxnSource::Managed(mut conn) = txn {
1431                        let _ = conn.rollback_transaction().await;
1432                    } else if let TxnSource::Ambient(t) = txn {
1433                        self.ambient_transaction = Some(t);
1434                    }
1435                    self.interceptor_pipeline
1436                        .on_save_failed(&save_ctx, &e)
1437                        .await;
1438                    return Err(e);
1439                }
1440                total_added += child_pks.len();
1441            }
1442        }
1443
1444        // --- UPSERT phase (topological order) ---
1445        for type_id in &insert_order {
1446            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
1447                continue;
1448            }
1449            let saver = self.savers.get(type_id).expect("saver not registered");
1450            let set = self.sets.get_mut(type_id).unwrap();
1451            let meta = configured_metas
1452                .get(type_id)
1453                .or_else(|| self.entity_metas.get(type_id))
1454                .expect("meta not found");
1455            let n = {
1456                let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1457                    TxnSource::Ambient(t) => t.connection(),
1458                    TxnSource::Managed(c) => c.as_mut(),
1459                };
1460                match saver
1461                    .upsert_added(conn_ref, &*self.provider, set.as_mut(), meta)
1462                    .await
1463                {
1464                    Ok(n) => n,
1465                    Err(e) => {
1466                        if let TxnSource::Managed(mut conn) = txn {
1467                            let _ = conn.rollback_transaction().await;
1468                        } else if let TxnSource::Ambient(t) = txn {
1469                            self.ambient_transaction = Some(t);
1470                        }
1471                        self.interceptor_pipeline
1472                            .on_save_failed(&save_ctx, &e)
1473                            .await;
1474                        return Err(e);
1475                    }
1476                }
1477            };
1478            total_added += n;
1479        }
1480
1481        // --- UPDATE phase (topological order) ---
1482        for type_id in &insert_order {
1483            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
1484                continue;
1485            }
1486            let saver = self.savers.get(type_id).expect("saver not registered");
1487            let set = self.sets.get_mut(type_id).unwrap();
1488            let meta = configured_metas
1489                .get(type_id)
1490                .or_else(|| self.entity_metas.get(type_id))
1491                .expect("meta not found");
1492            let n = {
1493                let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1494                    TxnSource::Ambient(t) => t.connection(),
1495                    TxnSource::Managed(c) => c.as_mut(),
1496                };
1497                match saver
1498                    .update_modified(conn_ref, &*self.provider, set.as_mut(), meta)
1499                    .await
1500                {
1501                    Ok(n) => n,
1502                    Err(e) => {
1503                        if let TxnSource::Managed(mut conn) = txn {
1504                            let _ = conn.rollback_transaction().await;
1505                        } else if let TxnSource::Ambient(t) = txn {
1506                            self.ambient_transaction = Some(t);
1507                        }
1508                        self.interceptor_pipeline
1509                            .on_save_failed(&save_ctx, &e)
1510                            .await;
1511                        return Err(e);
1512                    }
1513                }
1514            };
1515            total_updated += n;
1516        }
1517
1518        // --- Direct cascade SET NULL SQL (before PK-based deletes) ---
1519        // SetNull directives must run before the principal is deleted to avoid
1520        // FK constraint violations (the FK is nullified while the principal
1521        // still exists).
1522        for directive in &delete_directives {
1523            if directive.action != CascadeDeleteAction::SetNull {
1524                continue;
1525            }
1526            let sql = format!(
1527                "UPDATE {} SET {} = NULL WHERE {} = ?",
1528                directive.table, directive.fk_column, directive.fk_column
1529            );
1530            let params = vec![DbValue::from(directive.principal_pk)];
1531            let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1532                TxnSource::Ambient(t) => t.connection(),
1533                TxnSource::Managed(c) => c.as_mut(),
1534            };
1535            if let Err(e) = conn_ref.execute(&sql, &params).await {
1536                if let TxnSource::Managed(mut conn) = txn {
1537                    let _ = conn.rollback_transaction().await;
1538                } else if let TxnSource::Ambient(t) = txn {
1539                    self.ambient_transaction = Some(t);
1540                }
1541                self.interceptor_pipeline
1542                    .on_save_failed(&save_ctx, &e)
1543                    .await;
1544                return Err(e);
1545            }
1546        }
1547
1548        // --- DELETE phase (reverse topological order: dependents first) ---
1549        for type_id in &delete_order {
1550            if !self.sets.contains_key(type_id) || !self.savers.contains_key(type_id) {
1551                continue;
1552            }
1553            let saver = self.savers.get(type_id).expect("saver not registered");
1554            let set = self.sets.get_mut(type_id).unwrap();
1555            let meta = configured_metas
1556                .get(type_id)
1557                .or_else(|| self.entity_metas.get(type_id))
1558                .expect("meta not found");
1559            let n = {
1560                let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1561                    TxnSource::Ambient(t) => t.connection(),
1562                    TxnSource::Managed(c) => c.as_mut(),
1563                };
1564                match saver
1565                    .delete_deleted(conn_ref, &*self.provider, set.as_mut(), meta)
1566                    .await
1567                {
1568                    Ok(n) => n,
1569                    Err(e) => {
1570                        if let TxnSource::Managed(mut conn) = txn {
1571                            let _ = conn.rollback_transaction().await;
1572                        } else if let TxnSource::Ambient(t) = txn {
1573                            self.ambient_transaction = Some(t);
1574                        }
1575                        self.interceptor_pipeline
1576                            .on_save_failed(&save_ctx, &e)
1577                            .await;
1578                        return Err(e);
1579                    }
1580                }
1581            };
1582            total_deleted += n;
1583        }
1584
1585        // --- Direct cascade DELETE SQL (after PK-based deletes) ---
1586        // Cascade DELETE directives run after tracked entities are PK-deleted
1587        // to clean up untracked dependents. Running before would cause the
1588        // PK-based batch delete to find 0 rows and throw ConcurrencyConflict.
1589        for directive in &delete_directives {
1590            if directive.action != CascadeDeleteAction::Delete {
1591                continue;
1592            }
1593            let sql = format!(
1594                "DELETE FROM {} WHERE {} = ?",
1595                directive.table, directive.fk_column
1596            );
1597            let params = vec![DbValue::from(directive.principal_pk)];
1598            let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
1599                TxnSource::Ambient(t) => t.connection(),
1600                TxnSource::Managed(c) => c.as_mut(),
1601            };
1602            if let Err(e) = conn_ref.execute(&sql, &params).await {
1603                if let TxnSource::Managed(mut conn) = txn {
1604                    let _ = conn.rollback_transaction().await;
1605                } else if let TxnSource::Ambient(t) = txn {
1606                    self.ambient_transaction = Some(t);
1607                }
1608                self.interceptor_pipeline
1609                    .on_save_failed(&save_ctx, &e)
1610                    .await;
1611                return Err(e);
1612            }
1613        }
1614
1615        match txn {
1616            TxnSource::Ambient(t) => {
1617                self.ambient_transaction = Some(t);
1618            }
1619            TxnSource::Managed(mut conn) => {
1620                if let Err(e) = conn.commit_transaction().await {
1621                    self.interceptor_pipeline
1622                        .on_save_failed(&save_ctx, &e)
1623                        .await;
1624                    return Err(e);
1625                }
1626            }
1627        }
1628        self.change_tracker.accept_all_changes();
1629        for type_id in &type_ids {
1630            let saver = self.savers.get(type_id).unwrap();
1631            let set = self.sets.get_mut(type_id).unwrap();
1632            saver.accept_all_changes(set.as_mut());
1633        }
1634
1635        // --- Interceptor: on_saved (post-commit) ---
1636        let result_ctx = SaveChangesResultContext {
1637            added: total_added,
1638            updated: total_updated,
1639            deleted: total_deleted,
1640        };
1641        self.interceptor_pipeline
1642            .on_saved(&save_ctx, &result_ctx)
1643            .await?;
1644
1645        Ok(SaveChangesResult {
1646            added: total_added,
1647            updated: total_updated,
1648            deleted: total_deleted,
1649        })
1650    }
1651
1652    /// Executes a closure within an ambient transaction.
1653    ///
1654    /// Registers the transaction as ambient for the duration of `f`, so that
1655    /// `save_changes()` calls inside `f` reuse the same transaction. Commits
1656    /// on `Ok`, rolls back on `Err`.
1657    ///
1658    /// The closure receives `&mut DbContext` and must return a pinned boxed
1659    /// future. This signature works around Rust's async borrow checker by
1660    /// letting the closure capture `ctx` by mutable reference while still
1661    /// producing a `Send` future.
1662    ///
1663    /// # Example
1664    ///
1665    /// ```rust,ignore
1666    /// ctx.use_transaction(|ctx| Box::pin(async move {
1667    ///     ctx.set::<Blog>().add(blog);
1668    ///     ctx.save_changes().await?;
1669    ///     Ok(())
1670    /// })).await?;
1671    /// ```
1672    pub async fn use_transaction<F, R>(&mut self, f: F) -> EFResult<R>
1673    where
1674        for<'a> F: FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output = EFResult<R>> + Send + 'a>>,
1675        R: Send + 'static,
1676    {
1677        if self.ambient_transaction.is_some() {
1678            return Err(EFError::transaction(
1679                "ambient transaction already active; nested use_transaction is not supported",
1680            ));
1681        }
1682        let mut conn = self.provider.get_connection().await?;
1683        conn.begin_transaction().await?;
1684        self.ambient_transaction = Some(Box::new(DbTransaction::new(conn)));
1685        let result = f(self).await;
1686        let txn = self
1687            .ambient_transaction
1688            .take()
1689            .expect("ambient_transaction set above");
1690        match result {
1691            Ok(r) => {
1692                txn.commit().await?;
1693                Ok(r)
1694            }
1695            Err(e) => {
1696                let _ = txn.rollback().await;
1697                Err(e)
1698            }
1699        }
1700    }
1701}
1702
1703// ---------------------------------------------------------------------------
1704// save_one_set
1705// ---------------------------------------------------------------------------
1706
1707#[allow(clippy::type_complexity)]
1708pub async fn save_one_set<E>(
1709    conn: &mut dyn IAsyncConnection,
1710    provider: &dyn IDatabaseProvider,
1711    db_set: &mut DbSet<E>,
1712    meta: &EntityTypeMeta,
1713) -> EFResult<(usize, usize, usize)>
1714where
1715    E: IEntityType + IEntitySnapshot + IGetKeyValues + IFromRow,
1716{
1717    let query_filter = db_set.query_filter().cloned();
1718    let ac = insert_added_phase(conn, provider, db_set, meta).await?;
1719    let ac_upsert = upsert_added_phase(conn, provider, db_set, meta).await?;
1720    let uc = update_modified_phase(conn, provider, db_set, meta, query_filter.as_ref()).await?;
1721    let dc = delete_deleted_phase(conn, provider, db_set, meta, query_filter.as_ref()).await?;
1722    Ok((ac + ac_upsert, uc, dc))
1723}
1724
1725/// Phase 1a: INSERT Added (non-upsert) entities, then backfill generated PKs.
1726pub async fn insert_added_phase<E>(
1727    conn: &mut dyn IAsyncConnection,
1728    provider: &dyn IDatabaseProvider,
1729    db_set: &mut DbSet<E>,
1730    meta: &EntityTypeMeta,
1731) -> EFResult<usize>
1732where
1733    E: IEntityType + IEntitySnapshot + IGetKeyValues,
1734{
1735    let added: Vec<(&E, &EntityTypeMeta)> = db_set
1736        .tracked_by_state(EntityState::Added)
1737        .into_iter()
1738        .filter(|(_, _, _, is_upsert)| !*is_upsert)
1739        .map(|(e, _, _, _)| (e, meta))
1740        .collect();
1741    if added.is_empty() {
1742        return Ok(0);
1743    }
1744    let added_count = added.len();
1745    let mut generated_keys: Vec<i64> = vec![0; added_count];
1746    let inserted = ChangeExecutor::execute_inserts(conn, provider, &added, |idx, key| {
1747        if idx < generated_keys.len() {
1748            generated_keys[idx] = key;
1749        }
1750    })
1751    .await?;
1752    db_set.backfill_added_keys(&generated_keys);
1753    Ok(inserted)
1754}
1755
1756/// Phase 1b: UPSERT Added entities (is_upsert = true).
1757pub async fn upsert_added_phase<E>(
1758    conn: &mut dyn IAsyncConnection,
1759    provider: &dyn IDatabaseProvider,
1760    db_set: &mut DbSet<E>,
1761    meta: &EntityTypeMeta,
1762) -> EFResult<usize>
1763where
1764    E: IEntityType + IEntitySnapshot + IGetKeyValues,
1765{
1766    let upserts: Vec<(&E, &EntityTypeMeta)> = db_set
1767        .tracked_by_state(EntityState::Added)
1768        .into_iter()
1769        .filter(|(_, _, _, is_upsert)| *is_upsert)
1770        .map(|(e, _, _, _)| (e, meta))
1771        .collect();
1772    if upserts.is_empty() {
1773        return Ok(0);
1774    }
1775    ChangeExecutor::execute_upserts(conn, provider, &upserts).await
1776}
1777
1778/// Phase 2: UPDATE Modified entities (partial update via modified_properties).
1779pub async fn update_modified_phase<E>(
1780    conn: &mut dyn IAsyncConnection,
1781    provider: &dyn IDatabaseProvider,
1782    db_set: &mut DbSet<E>,
1783    meta: &EntityTypeMeta,
1784    query_filter: Option<&crate::query::BoolExpr>,
1785) -> EFResult<usize>
1786where
1787    E: IEntityType + IEntitySnapshot + IGetKeyValues,
1788{
1789    let modified: Vec<(
1790        &E,
1791        &EntityTypeMeta,
1792        Option<&HashMap<String, DbValue>>,
1793        &[String],
1794    )> = db_set
1795        .tracked_by_state(EntityState::Modified)
1796        .into_iter()
1797        .map(|(e, orig, mods, _)| (e, meta, orig, mods))
1798        .collect();
1799    if modified.is_empty() {
1800        return Ok(0);
1801    }
1802    ChangeExecutor::execute_updates(conn, provider, &modified, query_filter).await
1803}
1804
1805/// Phase 3: DELETE Deleted entities.
1806pub async fn delete_deleted_phase<E>(
1807    conn: &mut dyn IAsyncConnection,
1808    provider: &dyn IDatabaseProvider,
1809    db_set: &mut DbSet<E>,
1810    meta: &EntityTypeMeta,
1811    query_filter: Option<&crate::query::BoolExpr>,
1812) -> EFResult<usize>
1813where
1814    E: IEntityType + IEntitySnapshot + IGetKeyValues,
1815{
1816    let deleted: Vec<(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)> = db_set
1817        .tracked_by_state(EntityState::Deleted)
1818        .into_iter()
1819        .map(|(e, orig, _, _)| (e, meta, orig))
1820        .collect();
1821    if deleted.is_empty() {
1822        return Ok(0);
1823    }
1824    ChangeExecutor::execute_deletes(conn, provider, &deleted, query_filter).await
1825}
1826
1827// ---------------------------------------------------------------------------
1828// SaveChangesResult
1829// ---------------------------------------------------------------------------
1830
1831#[derive(Debug, Clone)]
1832pub struct SaveChangesResult {
1833    pub added: usize,
1834    pub updated: usize,
1835    pub deleted: usize,
1836}
1837impl SaveChangesResult {
1838    pub fn total(&self) -> usize {
1839        self.added + self.updated + self.deleted
1840    }
1841}
1842impl std::fmt::Display for SaveChangesResult {
1843    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1844        write!(
1845            f,
1846            "{} entities modified ({} added, {} updated, {} deleted)",
1847            self.total(),
1848            self.added,
1849            self.updated,
1850            self.deleted
1851        )
1852    }
1853}