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