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::{IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter};
19use crate::error::{EFError, EFResult};
20use crate::interceptor::InterceptorPipeline;
21use crate::metadata::EntityTypeMeta;
22use crate::migration::MigrationEngine;
23use crate::model_builder::ModelBuilder;
24use crate::provider::{DbValue, IDatabaseProvider};
25use crate::tracking::ChangeTracker;
26use crate::transaction::{DbTransaction, ITransaction};
27use std::any::{Any, TypeId};
28use std::collections::HashMap;
29use std::future::Future;
30use std::pin::Pin;
31use std::sync::Arc;
32
33use super::{DbContextOptions, ErasedSetOps, SetOps};
34
35/// The session / unit-of-work entry point.
36///
37/// Entity sets are lazy-created via `ctx.set::<T>()`. Metadata is loaded from
38/// the process-level `MetadataCache` on `DbContextOptions` (built once per
39/// `context_key`, then `Arc`-shared). `save_changes()` commits all pending
40/// changes in a transaction.
41pub struct DbContext {
42 pub(crate) sets: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
43 pub(crate) savers: HashMap<TypeId, Box<dyn ErasedSetOps>>,
44 pub(crate) entity_metas: HashMap<TypeId, EntityTypeMeta>,
45 pub(crate) model_builder: ModelBuilder,
46 pub(crate) change_tracker: ChangeTracker,
47 pub(crate) provider: Arc<dyn IDatabaseProvider>,
48 pub(crate) interceptor_pipeline: InterceptorPipeline,
49 pub(crate) lazy_loading_enabled: bool,
50 /// Ambient transaction: registered by `use_transaction()`. When present,
51 /// `save_changes()` reuses this transaction's connection and does not
52 /// begin/commit/rollback on its own. Uses `take()`/restore pattern to
53 /// avoid `&mut self` borrow conflicts with `self.sets` during save.
54 pub(crate) ambient_transaction: Option<Box<dyn ITransaction>>,
55}
56
57impl DbContext {
58 /// Creates the context from options (uses the provider factory stored in options).
59 ///
60 /// Entity metadata is loaded from the process-level `MetadataCache` on
61 /// `DbContextOptions` — `inventory::iter` + `IEntityTypeConfiguration::configure()`
62 /// run once per `context_key` (first call), then the result is `Arc`-shared
63 /// across all `DbContext` instances. Per-instance `ModelBuilder` mutations
64 /// (`has_query_filter`, etc.) after construction only affect this instance.
65 pub fn from_options(options: &DbContextOptions) -> EFResult<Self> {
66 let provider = options.create_provider()?;
67 let built = options
68 .metadata_cache
69 .get_or_build(options.context_key.as_deref());
70 let ctx = Self {
71 sets: HashMap::new(),
72 savers: HashMap::new(),
73 entity_metas: built.entity_metas.clone(),
74 model_builder: ModelBuilder::from_built(&built),
75 change_tracker: ChangeTracker::new(),
76 provider,
77 interceptor_pipeline: InterceptorPipeline::new(options.interceptors.clone()),
78 lazy_loading_enabled: options.lazy_loading_enabled,
79 ambient_transaction: None,
80 };
81 Ok(ctx)
82 }
83
84 pub fn set<T>(&mut self) -> &mut DbSet<T>
85 where
86 T: IEntityType
87 + IEntitySnapshot
88 + IGetKeyValues
89 + IFromRow
90 + INavigationSetter
91 + Send
92 + Sync
93 + 'static,
94 {
95 let type_id = TypeId::of::<T>();
96 self.savers
97 .entry(type_id)
98 .or_insert_with(|| Box::new(SetOps::<T>::new()));
99 self.entity_metas
100 .entry(type_id)
101 .or_insert_with(T::entity_meta);
102 if !self.model_builder.has_entity(type_id) {
103 self.model_builder.register_entity_meta(T::entity_meta());
104 }
105 self.sets.entry(type_id).or_insert_with(|| {
106 let table_name = self
107 .model_builder
108 .build()
109 .into_iter()
110 .find(|m| m.type_id == type_id)
111 .map(|m| m.table_name.to_string())
112 .unwrap_or_else(|| T::entity_meta().table_name.to_string());
113 let mut db_set = DbSet::<T>::with_provider(table_name, Arc::clone(&self.provider));
114 if let Some(filter) = self.model_builder.get_query_filter(&type_id) {
115 db_set.set_query_filter(filter.clone());
116 }
117 db_set.set_filter_map(self.model_builder.filters_by_table());
118 db_set.set_lazy_loading_enabled(self.lazy_loading_enabled);
119 Box::new(db_set)
120 });
121 self.sets
122 .get_mut(&type_id)
123 .and_then(|b| b.downcast_mut::<DbSet<T>>())
124 .expect("DbSet type mismatch")
125 }
126
127 /// Returns the model builder for Fluent API configuration.
128 pub fn model(&mut self) -> &mut ModelBuilder {
129 &mut self.model_builder
130 }
131
132 /// Returns a read-only reference to the model builder.
133 pub fn model_builder(&self) -> &ModelBuilder {
134 &self.model_builder
135 }
136
137 /// Returns `true` if an entity of type `T` has been discovered and
138 /// registered in the entity metadata map.
139 pub fn entity_metas_contains<T: IEntityType>(&self) -> bool {
140 self.entity_metas.contains_key(&TypeId::of::<T>())
141 }
142
143 /// No-op since metadata caching was introduced.
144 ///
145 /// Historically, this method iterated `inventory::iter` to register entity
146 /// metadata and apply `#[entity(T)]` configurations. That work now happens
147 /// in `MetadataCache::build()`, invoked lazily by `from_options()` via
148 /// `MetadataCache::get_or_build()`. The result is `Arc`-shared across all
149 /// `DbContext` instances with the same `context_key`.
150 ///
151 /// Retained as a public method for backward compatibility — existing code
152 /// calling `ctx.discover_entities()?` after `from_options()` continues to
153 /// compile and run (as a no-op). The metadata is already populated.
154 pub fn discover_entities(&mut self) -> EFResult<()> {
155 Ok(())
156 }
157
158 /// Detects changes on all tracked DbSets by comparing property snapshots.
159 pub fn detect_changes(&mut self) {
160 let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
161 for type_id in type_ids {
162 if let Some(set) = self.sets.get_mut(&type_id) {
163 if let Some(saver) = self.savers.get(&type_id) {
164 saver.detect_changes(set.as_mut());
165 }
166 }
167 }
168 }
169
170 /// Creates all tables for registered entity types.
171 ///
172 /// Sources metas from `model_builder.build()`, which applies all Fluent
173 /// API configurations and `#[entity(T)]` overrides. Entities are
174 /// discovered automatically via `#[derive(EntityType)]`; call
175 /// `discover_entities()` first, or use `set::<T>()` to register manually.
176 pub async fn ensure_created(&self) -> EFResult<()> {
177 let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
178 if metas.is_empty() {
179 metas = self.entity_metas.values().cloned().collect();
180 }
181 if metas.is_empty() {
182 return Err(EFError::configuration(
183 "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_created().",
184 ));
185 }
186 let dialect = self.provider.migration_dialect();
187 MigrationEngine::new(dialect)
188 .ensure_created(&*self.provider, &metas)
189 .await?;
190
191 for meta in &metas {
192 let rows = self.model_builder.seed_rows_for(&meta.type_id);
193 if !rows.is_empty() {
194 MigrationEngine::new(dialect)
195 .apply_seed_data(&*self.provider, meta, rows)
196 .await?;
197 }
198 }
199 Ok(())
200 }
201
202 /// Drops all tables for registered entity types.
203 pub async fn ensure_deleted(&self) -> EFResult<()> {
204 let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
205 if metas.is_empty() {
206 metas = self.entity_metas.values().cloned().collect();
207 }
208 if metas.is_empty() {
209 return Err(EFError::configuration(
210 "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_deleted().",
211 ));
212 }
213 let dialect = self.provider.migration_dialect();
214 MigrationEngine::new(dialect)
215 .ensure_deleted(&*self.provider, &metas)
216 .await
217 }
218}
219
220// ---------------------------------------------------------------------------
221// DbContext inherent methods (formerly on IDbContext / IDbContextExt traits)
222// ---------------------------------------------------------------------------
223
224impl DbContext {
225 /// Returns the database provider.
226 pub fn provider(&self) -> &dyn IDatabaseProvider {
227 &*self.provider
228 }
229
230 /// Returns a read-only reference to the change tracker.
231 pub fn change_tracker(&self) -> &ChangeTracker {
232 &self.change_tracker
233 }
234
235 /// Returns a mutable reference to the change tracker.
236 pub fn change_tracker_mut(&mut self) -> &mut ChangeTracker {
237 &mut self.change_tracker
238 }
239
240 /// Executes a raw SQL query and materializes the result rows into entities.
241 ///
242 /// This is the escape hatch for complex queries (multi-table JOINs, CTEs,
243 /// window functions) that are hard to express via LINQ. The caller is
244 /// responsible for SQL correctness and parameterization.
245 ///
246 /// # Example
247 /// ```rust,ignore
248 /// let blogs: Vec<Blog> = ctx
249 /// .sql_query("SELECT * FROM blogs WHERE id = ?", &[DbValue::I32(1)])
250 /// .await?;
251 /// ```
252 pub async fn sql_query<T: IFromRow + IEntityType>(
253 &self,
254 sql: &str,
255 params: &[DbValue],
256 ) -> EFResult<Vec<T>> {
257 let mut conn = self.provider.get_connection().await?;
258 let rows = conn.query(sql, params).await?;
259 crate::entity::materialize_entities(&rows)
260 }
261
262 /// Returns a mutable reference to the ambient transaction handle, if one
263 /// is active (registered by `use_transaction()`).
264 ///
265 /// Inside a `use_transaction()` closure, use this to access the ambient
266 /// handle for savepoint and isolation operations. The borrow must be
267 /// released (by scoping) before calling `save_changes()` or other `&mut
268 /// self` methods.
269 pub fn transaction_mut(&mut self) -> Option<&mut Box<dyn ITransaction>> {
270 self.ambient_transaction.as_mut()
271 }
272
273 /// Begins a transaction and returns a typed handle.
274 ///
275 /// The returned `ITransaction` handle is **not** registered as ambient —
276 /// `save_changes()` calls will continue to self-manage their own
277 /// transactions. Use this when you need explicit control via
278 /// `txn.commit()` / `txn.rollback()` / `txn.create_point()` etc.
279 ///
280 /// For scoped ambient transactions where `save_changes()` should reuse
281 /// the same transaction, use [`DbContext::use_transaction`] instead.
282 ///
283 /// `commit` / `rollback` consume the handle by value, preventing
284 /// use-after-commit at the type level.
285 pub async fn begin_transaction(&mut self) -> EFResult<Box<dyn ITransaction>> {
286 let mut conn = self.provider.get_connection().await?;
287 conn.begin_transaction().await?;
288 Ok(Box::new(DbTransaction::new(conn)))
289 }
290
291 /// Executes a closure within an ambient transaction.
292 ///
293 /// Registers the transaction as ambient for the duration of `f`, so that
294 /// `save_changes()` calls inside `f` reuse the same transaction. Commits
295 /// on `Ok`, rolls back on `Err`.
296 ///
297 /// The closure receives `&mut DbContext` and must return a pinned boxed
298 /// future. This signature works around Rust's async borrow checker by
299 /// letting the closure capture `ctx` by mutable reference while still
300 /// producing a `Send` future.
301 ///
302 /// # Example
303 ///
304 /// ```rust,ignore
305 /// ctx.use_transaction(|ctx| Box::pin(async move {
306 /// ctx.set::<Blog>().add(blog);
307 /// ctx.save_changes().await?;
308 /// Ok(())
309 /// })).await?;
310 /// ```
311 pub async fn use_transaction<F, R>(&mut self, f: F) -> EFResult<R>
312 where
313 for<'a> F: FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output = EFResult<R>> + Send + 'a>>,
314 R: Send + 'static,
315 {
316 if self.ambient_transaction.is_some() {
317 return Err(EFError::transaction(
318 "ambient transaction already active; nested use_transaction is not supported",
319 ));
320 }
321 let mut conn = self.provider.get_connection().await?;
322 conn.begin_transaction().await?;
323 self.ambient_transaction = Some(Box::new(DbTransaction::new(conn)));
324 let result = f(self).await;
325 let txn = self.ambient_transaction.take().ok_or_else(|| {
326 EFError::transaction("ambient_transaction was consumed during use_transaction closure")
327 })?;
328 match result {
329 Ok(r) => {
330 txn.commit().await?;
331 Ok(r)
332 }
333 Err(e) => {
334 let _ = txn.rollback().await;
335 Err(e)
336 }
337 }
338 }
339}