Skip to main content

rust_ef/db_context/
context.rs

1//! `DbContext` struct and non-save methods.
2//!
3//! `DbContext` is the concrete context type. Entity sets use a type-map:
4//! `ctx.set::<Blog>()` lazy-creates `DbSet<Blog>`. `SetOps<T>` dispatchers
5//! enable `save_changes()` to iterate all entity types.
6//!
7//! ## Ownership and Mutation
8//!
9//! `DbContext` methods (`set::<T>()`, `save_changes()`, `detect_changes()`)
10//! require `&mut self` — this is idiomatic Rust, not a limitation. The DI
11//! integration (`add_dbcontext`) registers the context as **Scoped** and
12//! supports owned resolution via `get_owned::<DbContext>()`.
13//!
14//! `DbContext` is **not** thread-safe — a single instance must not be shared
15//! across threads (aligned with EFCore).
16
17use crate::db_set::DbSet;
18use crate::entity::{
19    EntityState, IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter,
20};
21use crate::error::{EFError, EFResult};
22use crate::interceptor::InterceptorPipeline;
23use crate::metadata::EntityTypeMeta;
24use crate::migration::MigrationEngine;
25use crate::model_builder::ModelBuilder;
26use crate::provider::{DbValue, IDatabaseProvider};
27use crate::tracking::ChangeTracker;
28use crate::transaction::{DbTransaction, ITransaction};
29use std::any::{Any, TypeId};
30use std::collections::HashMap;
31use std::future::Future;
32use std::pin::Pin;
33use std::sync::Arc;
34
35use super::{DbContextOptions, ErasedSetOps, SetOps};
36
37/// The session / unit-of-work entry point.
38///
39/// Entity sets are lazy-created via `ctx.set::<T>()`. Metadata is loaded from
40/// the process-level `MetadataCache` on `DbContextOptions` (built once per
41/// `context_key`, then `Arc`-shared). `save_changes()` commits all pending
42/// changes in a transaction.
43pub struct DbContext {
44    pub(crate) sets: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
45    pub(crate) savers: HashMap<TypeId, Box<dyn ErasedSetOps>>,
46    pub(crate) entity_metas: HashMap<TypeId, EntityTypeMeta>,
47    pub(crate) model_builder: ModelBuilder,
48    pub(crate) change_tracker: ChangeTracker,
49    pub(crate) provider: Arc<dyn IDatabaseProvider>,
50    pub(crate) interceptor_pipeline: InterceptorPipeline,
51    pub(crate) lazy_loading_enabled: bool,
52    /// Ambient transaction: registered by `use_transaction()`. When present,
53    /// `save_changes()` reuses this transaction's connection and does not
54    /// begin/commit/rollback on its own. Uses `take()`/restore pattern to
55    /// avoid `&mut self` borrow conflicts with `self.sets` during save.
56    pub(crate) ambient_transaction: Option<Box<dyn ITransaction>>,
57}
58
59impl DbContext {
60    /// Creates the context from options (uses the provider factory stored in options).
61    ///
62    /// Entity metadata is loaded from the process-level `MetadataCache` on
63    /// `DbContextOptions` — `inventory::iter` + `IEntityTypeConfiguration::configure()`
64    /// run once per `context_key` (first call), then the result is `Arc`-shared
65    /// across all `DbContext` instances. Per-instance `ModelBuilder` mutations
66    /// (`has_query_filter`, etc.) after construction only affect this instance.
67    pub fn from_options(options: &DbContextOptions) -> EFResult<Self> {
68        let provider = options.create_provider()?;
69        let built = options
70            .metadata_cache
71            .get_or_build(options.context_key.as_deref());
72        let ctx = Self {
73            sets: HashMap::new(),
74            savers: HashMap::new(),
75            entity_metas: built.entity_metas.clone(),
76            model_builder: ModelBuilder::from_built(&built),
77            change_tracker: ChangeTracker::new(),
78            provider,
79            interceptor_pipeline: InterceptorPipeline::new(options.interceptors.clone()),
80            lazy_loading_enabled: options.lazy_loading_enabled,
81            ambient_transaction: None,
82        };
83        Ok(ctx)
84    }
85
86    pub fn set<T>(&mut self) -> &mut DbSet<T>
87    where
88        T: IEntityType
89            + IEntitySnapshot
90            + IGetKeyValues
91            + IFromRow
92            + INavigationSetter
93            + Send
94            + Sync
95            + 'static,
96    {
97        let type_id = TypeId::of::<T>();
98        self.savers
99            .entry(type_id)
100            .or_insert_with(|| Box::new(SetOps::<T>::new()));
101        self.entity_metas
102            .entry(type_id)
103            .or_insert_with(T::entity_meta);
104        if !self.model_builder.has_entity(type_id) {
105            self.model_builder.register_entity_meta(T::entity_meta());
106        }
107        self.sets.entry(type_id).or_insert_with(|| {
108            let table_name = self
109                .model_builder
110                .build()
111                .into_iter()
112                .find(|m| m.type_id == type_id)
113                .map(|m| m.table_name.to_string())
114                .unwrap_or_else(|| T::entity_meta().table_name.to_string());
115            let mut db_set = DbSet::<T>::with_provider(table_name, Arc::clone(&self.provider));
116            if let Some(filter) = self.model_builder.get_query_filter(&type_id) {
117                db_set.set_query_filter(filter.clone());
118            }
119            db_set.set_filter_map(self.model_builder.filters_by_table());
120            db_set.set_lazy_loading_enabled(self.lazy_loading_enabled);
121            Box::new(db_set)
122        });
123        self.sets
124            .get_mut(&type_id)
125            .and_then(|b| b.downcast_mut::<DbSet<T>>())
126            .expect("DbSet type mismatch")
127    }
128
129    /// Returns the model builder for Fluent API configuration.
130    pub fn model(&mut self) -> &mut ModelBuilder {
131        &mut self.model_builder
132    }
133
134    /// Returns a read-only reference to the model builder.
135    pub fn model_builder(&self) -> &ModelBuilder {
136        &self.model_builder
137    }
138
139    /// Returns `true` if an entity of type `T` has been discovered and
140    /// registered in the entity metadata map.
141    pub fn entity_metas_contains<T: IEntityType>(&self) -> bool {
142        self.entity_metas.contains_key(&TypeId::of::<T>())
143    }
144
145    /// No-op since metadata caching was introduced.
146    ///
147    /// Historically, this method iterated `inventory::iter` to register entity
148    /// metadata and apply `#[entity(T)]` configurations. That work now happens
149    /// in `MetadataCache::build()`, invoked lazily by `from_options()` via
150    /// `MetadataCache::get_or_build()`. The result is `Arc`-shared across all
151    /// `DbContext` instances with the same `context_key`.
152    ///
153    /// Retained as a public method for backward compatibility — existing code
154    /// calling `ctx.discover_entities()?` after `from_options()` continues to
155    /// compile and run (as a no-op). The metadata is already populated.
156    pub fn discover_entities(&mut self) -> EFResult<()> {
157        Ok(())
158    }
159
160    /// Detects changes on all tracked DbSets by comparing property snapshots.
161    pub fn detect_changes(&mut self) {
162        let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
163        for type_id in type_ids {
164            let Some(set) = self.sets.get_mut(&type_id) else {
165                continue;
166            };
167            let Some(saver) = self.savers.get(&type_id) else {
168                continue;
169            };
170            saver.detect_changes(set.as_mut(), &mut self.change_tracker);
171        }
172    }
173
174    /// Creates all tables for registered entity types.
175    ///
176    /// Sources metas from `model_builder.build()`, which applies all Fluent
177    /// API configurations and `#[entity(T)]` overrides. Entities are
178    /// discovered automatically via `#[derive(EntityType)]`; call
179    /// `discover_entities()` first, or use `set::<T>()` to register manually.
180    pub async fn ensure_created(&self) -> EFResult<()> {
181        let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
182        if metas.is_empty() {
183            metas = self.entity_metas.values().cloned().collect();
184        }
185        if metas.is_empty() {
186            return Err(EFError::configuration(
187                "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_created().",
188            ));
189        }
190        let dialect = self.provider.migration_dialect();
191        MigrationEngine::new(dialect)
192            .ensure_created(&*self.provider, &metas)
193            .await?;
194
195        for meta in &metas {
196            let rows = self.model_builder.seed_rows_for(&meta.type_id);
197            if !rows.is_empty() {
198                MigrationEngine::new(dialect)
199                    .apply_seed_data(&*self.provider, meta, rows)
200                    .await?;
201            }
202        }
203        Ok(())
204    }
205
206    /// Drops all tables for registered entity types.
207    pub async fn ensure_deleted(&self) -> EFResult<()> {
208        let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
209        if metas.is_empty() {
210            metas = self.entity_metas.values().cloned().collect();
211        }
212        if metas.is_empty() {
213            return Err(EFError::configuration(
214                "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_deleted().",
215            ));
216        }
217        let dialect = self.provider.migration_dialect();
218        MigrationEngine::new(dialect)
219            .ensure_deleted(&*self.provider, &metas)
220            .await
221    }
222}
223
224// ---------------------------------------------------------------------------
225// DbContext inherent methods (formerly on IDbContext / IDbContextExt traits)
226// ---------------------------------------------------------------------------
227
228impl DbContext {
229    /// Returns the database provider.
230    pub fn provider(&self) -> &dyn IDatabaseProvider {
231        &*self.provider
232    }
233
234    /// Returns a read-only reference to the change tracker.
235    pub fn change_tracker(&self) -> &ChangeTracker {
236        &self.change_tracker
237    }
238
239    /// Returns a mutable reference to the change tracker.
240    pub fn change_tracker_mut(&mut self) -> &mut ChangeTracker {
241        &mut self.change_tracker
242    }
243
244    /// Executes a raw SQL query and materializes the result rows into entities.
245    ///
246    /// This is the escape hatch for complex queries (multi-table JOINs, CTEs,
247    /// window functions) that are hard to express via LINQ. The caller is
248    /// responsible for SQL correctness and parameterization.
249    ///
250    /// # Example
251    /// ```rust,ignore
252    /// let blogs: Vec<Blog> = ctx
253    ///     .sql_query("SELECT * FROM blogs WHERE id = ?", &[DbValue::I32(1)])
254    ///     .await?;
255    /// ```
256    pub async fn sql_query<T: IFromRow + IEntityType>(
257        &self,
258        sql: &str,
259        params: &[DbValue],
260    ) -> EFResult<Vec<T>> {
261        let mut conn = self.provider.get_connection().await?;
262        let rows = conn.query(sql, params).await?;
263        crate::entity::materialize_entities(&rows)
264    }
265
266    /// Returns a mutable reference to the ambient transaction handle, if one
267    /// is active (registered by `use_transaction()`).
268    ///
269    /// Inside a `use_transaction()` closure, use this to access the ambient
270    /// handle for savepoint and isolation operations. The borrow must be
271    /// released (by scoping) before calling `save_changes()` or other `&mut
272    /// self` methods.
273    pub fn transaction_mut(&mut self) -> Option<&mut Box<dyn ITransaction>> {
274        self.ambient_transaction.as_mut()
275    }
276
277    /// Begins a transaction and returns a typed handle.
278    ///
279    /// The returned `ITransaction` handle is **not** registered as ambient —
280    /// `save_changes()` calls will continue to self-manage their own
281    /// transactions. Use this when you need explicit control via
282    /// `txn.commit()` / `txn.rollback()` / `txn.create_point()` etc.
283    ///
284    /// For scoped ambient transactions where `save_changes()` should reuse
285    /// the same transaction, use [`DbContext::use_transaction`] instead.
286    ///
287    /// `commit` / `rollback` consume the handle by value, preventing
288    /// use-after-commit at the type level.
289    pub async fn begin_transaction(&mut self) -> EFResult<Box<dyn ITransaction>> {
290        let mut conn = self.provider.get_connection().await?;
291        conn.begin_transaction().await?;
292        Ok(Box::new(DbTransaction::new(conn)))
293    }
294
295    /// Executes a closure within an ambient transaction.
296    ///
297    /// Registers the transaction as ambient for the duration of `f`, so that
298    /// `save_changes()` calls inside `f` reuse the same transaction. Commits
299    /// on `Ok`, rolls back on `Err`.
300    ///
301    /// The closure receives `&mut DbContext` and must return a pinned boxed
302    /// future. This signature works around Rust's async borrow checker by
303    /// letting the closure capture `ctx` by mutable reference while still
304    /// producing a `Send` future.
305    ///
306    /// # Example
307    ///
308    /// ```rust,ignore
309    /// ctx.use_transaction(|ctx| Box::pin(async move {
310    ///     ctx.add::<Blog>(blog);
311    ///     ctx.save_changes().await?;
312    ///     Ok(())
313    /// })).await?;
314    /// ```
315    pub async fn use_transaction<F, R>(&mut self, f: F) -> EFResult<R>
316    where
317        for<'a> F: FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output = EFResult<R>> + Send + 'a>>,
318        R: Send + 'static,
319    {
320        if self.ambient_transaction.is_some() {
321            return Err(EFError::transaction(
322                "ambient transaction already active; nested use_transaction is not supported",
323            ));
324        }
325        let mut conn = self.provider.get_connection().await?;
326        conn.begin_transaction().await?;
327        self.ambient_transaction = Some(Box::new(DbTransaction::new(conn)));
328        let result = f(self).await;
329        let txn = self.ambient_transaction.take().ok_or_else(|| {
330            EFError::transaction("ambient_transaction was consumed during use_transaction closure")
331        })?;
332        match result {
333            Ok(r) => {
334                txn.commit().await?;
335                Ok(r)
336            }
337            Err(e) => {
338                let _ = txn.rollback().await;
339                Err(e)
340            }
341        }
342    }
343}
344
345// ---------------------------------------------------------------------------
346// Entity mutation methods — coordinate DbSet + ChangeTracker
347// ---------------------------------------------------------------------------
348
349impl DbContext {
350    /// Ensures the entity type is registered (saver, metadata, model builder).
351    /// Returns `(type_id, type_name)`.
352    fn ensure_registered<T>(&mut self) -> (TypeId, String)
353    where
354        T: IEntityType
355            + IEntitySnapshot
356            + IGetKeyValues
357            + IFromRow
358            + INavigationSetter
359            + Send
360            + Sync
361            + 'static,
362    {
363        let type_id = TypeId::of::<T>();
364        self.savers
365            .entry(type_id)
366            .or_insert_with(|| Box::new(SetOps::<T>::new()));
367        self.entity_metas
368            .entry(type_id)
369            .or_insert_with(T::entity_meta);
370        if !self.model_builder.has_entity(type_id) {
371            self.model_builder.register_entity_meta(T::entity_meta());
372        }
373        let type_name = self.entity_metas[&type_id].type_name.to_string();
374        (type_id, type_name)
375    }
376
377    /// Adds a new entity in `Added` state. The entity will be INSERTed during
378    /// `save_changes()`.
379    pub fn add<T>(&mut self, entity: T)
380    where
381        T: IEntityType
382            + IEntitySnapshot
383            + IGetKeyValues
384            + IFromRow
385            + INavigationSetter
386            + Send
387            + Sync
388            + 'static,
389    {
390        let (type_id, type_name) = self.ensure_registered::<T>();
391        let entry_id =
392            self.change_tracker
393                .track(type_id, &type_name, EntityState::Added, None, false);
394        self.set::<T>().push_entry(entity, entry_id);
395    }
396
397    /// Attaches an existing entity in `Unchanged` state with a snapshot for
398    /// future change detection.
399    pub fn attach<T>(&mut self, entity: T)
400    where
401        T: IEntityType
402            + IEntitySnapshot
403            + IGetKeyValues
404            + IFromRow
405            + INavigationSetter
406            + Send
407            + Sync
408            + 'static,
409    {
410        let (type_id, type_name) = self.ensure_registered::<T>();
411        let snapshot = entity.snapshot();
412        let entry_id = self.change_tracker.track(
413            type_id,
414            &type_name,
415            EntityState::Unchanged,
416            Some(snapshot),
417            false,
418        );
419        self.set::<T>().push_entry(entity, entry_id);
420    }
421
422    /// Marks an entity as `Modified` — all fields will be UPDATEd during
423    /// `save_changes()`.
424    pub fn update<T>(&mut self, entity: T)
425    where
426        T: IEntityType
427            + IEntitySnapshot
428            + IGetKeyValues
429            + IFromRow
430            + INavigationSetter
431            + Send
432            + Sync
433            + 'static,
434    {
435        let (type_id, type_name) = self.ensure_registered::<T>();
436        let entry_id =
437            self.change_tracker
438                .track(type_id, &type_name, EntityState::Modified, None, false);
439        self.set::<T>().push_entry(entity, entry_id);
440    }
441
442    /// Marks an entity for upsert (INSERT ... ON CONFLICT DO UPDATE).
443    pub fn upsert<T>(&mut self, entity: T)
444    where
445        T: IEntityType
446            + IEntitySnapshot
447            + IGetKeyValues
448            + IFromRow
449            + INavigationSetter
450            + Send
451            + Sync
452            + 'static,
453    {
454        let (type_id, type_name) = self.ensure_registered::<T>();
455        let entry_id =
456            self.change_tracker
457                .track(type_id, &type_name, EntityState::Added, None, true);
458        self.set::<T>().push_entry(entity, entry_id);
459    }
460
461    /// Marks the entity at the given index as `Deleted`.
462    pub fn remove_at<T>(&mut self, index: usize) -> EFResult<()>
463    where
464        T: IEntityType
465            + IEntitySnapshot
466            + IGetKeyValues
467            + IFromRow
468            + INavigationSetter
469            + Send
470            + Sync
471            + 'static,
472    {
473        let entry_id = self
474            .set::<T>()
475            .entry_id_at(index)
476            .ok_or_else(|| EFError::not_found("Entity not found at the given index".to_string()))?;
477        self.change_tracker
478            .set_state(entry_id, EntityState::Deleted);
479        Ok(())
480    }
481
482    /// Marks all entities of type `T` as `Deleted`.
483    pub fn remove_all<T>(&mut self)
484    where
485        T: IEntityType
486            + IEntitySnapshot
487            + IGetKeyValues
488            + IFromRow
489            + INavigationSetter
490            + Send
491            + Sync
492            + 'static,
493    {
494        let entry_ids: Vec<u64> = self.set::<T>().entries.iter().map(|e| e.entry_id).collect();
495        for entry_id in entry_ids {
496            self.change_tracker
497                .set_state(entry_id, EntityState::Deleted);
498        }
499    }
500
501    /// Loads all rows from the database into the DbSet as `Unchanged`.
502    /// Clears existing entries and tracker state for type `T` first.
503    pub async fn load_all<T>(&mut self) -> EFResult<()>
504    where
505        T: IEntityType
506            + IEntitySnapshot
507            + IGetKeyValues
508            + IFromRow
509            + INavigationSetter
510            + crate::entity::ILazyInit
511            + Send
512            + Sync
513            + 'static,
514    {
515        let type_id = TypeId::of::<T>();
516        let items = self.set::<T>().query().to_list().await?;
517        self.set::<T>().clear_entries();
518        self.change_tracker.clear_by_type(type_id);
519        for item in items {
520            self.attach::<T>(item);
521        }
522        Ok(())
523    }
524}