1use crate::change_executor::ChangeExecutor;
60use crate::db_set::DbSet;
61use crate::entity::{IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter};
62use crate::error::{EFError, EFResult};
63use crate::interceptor::{InterceptorPipeline, SaveChangesContext, SaveChangesResultContext};
64use crate::metadata::EntityTypeMeta;
65use crate::migration::MigrationEngine;
66use crate::model_builder::ModelBuilder;
67use crate::provider::{DbValue, IAsyncConnection, IDatabaseProvider};
68use crate::tracking::{ChangeTracker, EntityEntryView};
69use crate::transaction::{DbTransaction, ITransaction};
70use std::any::{Any, TypeId};
71use std::collections::HashMap;
72use std::future::Future;
73use std::pin::Pin;
74use std::sync::Arc;
75
76#[derive(Clone)]
81pub struct DbContextOptions {
82 pub(crate) connection_string: String,
83 pub(crate) provider_tag: Option<String>,
84 #[allow(clippy::type_complexity)]
85 pub(crate) provider_factory:
86 Option<Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>>,
87 pub(crate) provider_cache: Arc<std::sync::Mutex<Option<Arc<dyn IDatabaseProvider>>>>,
94 pub(crate) interceptors: Vec<Arc<dyn crate::interceptor::ISaveChangesInterceptor>>,
95 pub(crate) lazy_loading_enabled: bool,
101 pub(crate) context_key: Option<String>,
102 pub(crate) metadata_cache: Arc<crate::metadata_cache::MetadataCache>,
108 #[cfg(feature = "tracing")]
111 pub(crate) slow_query_threshold: Option<std::time::Duration>,
112}
113
114impl std::fmt::Debug for DbContextOptions {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("DbContextOptions")
117 .field(
118 "connection_string",
119 &redact_connection_string(&self.connection_string),
120 )
121 .field("provider_tag", &self.provider_tag)
122 .finish()
123 }
124}
125
126fn redact_connection_string(cs: &str) -> String {
131 if let Some(scheme_end) = cs.find("://") {
133 let (scheme, rest) = cs.split_at(scheme_end + 3);
134 if let Some(at) = rest.find('@') {
135 let (userinfo, host_and_rest) = rest.split_at(at);
136 let redacted_user = match userinfo.find(':') {
137 Some(colon) => &userinfo[..colon],
138 None => userinfo,
139 };
140 return format!("{}{}***@{}", scheme, redacted_user, &host_and_rest[1..]);
141 }
142 return cs.to_string();
143 }
144 if cs.contains('=') {
146 return cs
147 .split(';')
148 .map(|pair| {
149 let eq = match pair.find('=') {
150 Some(e) => e,
151 None => return pair.to_string(),
152 };
153 let key = pair[..eq].trim().to_lowercase();
154 if key.contains("password") || key.contains("pwd") {
155 format!("{}=***", &pair[..eq])
156 } else {
157 pair.to_string()
158 }
159 })
160 .collect::<Vec<_>>()
161 .join(";");
162 }
163 cs.to_string()
164}
165
166impl DbContextOptions {
167 pub fn connection_string(&self) -> &str {
168 &self.connection_string
169 }
170 pub fn provider_tag(&self) -> Option<&str> {
171 self.provider_tag.as_deref()
172 }
173 pub fn lazy_loading_enabled(&self) -> bool {
174 self.lazy_loading_enabled
175 }
176 pub fn context_key(&self) -> Option<&str> {
177 self.context_key.as_deref()
178 }
179 pub fn create_provider(&self) -> EFResult<Arc<dyn IDatabaseProvider>> {
180 let mut guard = self
184 .provider_cache
185 .lock()
186 .unwrap_or_else(|p| p.into_inner());
187 if let Some(provider) = guard.as_ref() {
188 return Ok(Arc::clone(provider));
189 }
190 let factory = self.provider_factory.as_ref().ok_or_else(|| {
191 crate::error::EFError::configuration(
192 "No provider configured. Call use_sqlite / use_postgres / use_mysql first.",
193 )
194 })?;
195 let provider = factory(self.connection_string())?;
196 #[cfg(feature = "tracing")]
197 if let Some(threshold) = self.slow_query_threshold {
198 provider.set_slow_query_threshold(threshold);
199 }
200 *guard = Some(Arc::clone(&provider));
201 Ok(provider)
202 }
203}
204
205#[allow(clippy::derivable_impls)]
206impl Default for DbContextOptions {
207 fn default() -> Self {
208 Self {
209 connection_string: String::new(),
210 provider_tag: None,
211 provider_factory: None,
212 provider_cache: Arc::new(std::sync::Mutex::new(None)),
213 interceptors: Vec::new(),
214 lazy_loading_enabled: false,
215 context_key: None,
216 metadata_cache: Arc::new(crate::metadata_cache::MetadataCache::new()),
217 #[cfg(feature = "tracing")]
218 slow_query_threshold: None,
219 }
220 }
221}
222
223pub struct DbContextOptionsBuilder {
224 inner: DbContextOptions,
225}
226
227impl DbContextOptionsBuilder {
228 pub fn new() -> Self {
229 Self {
230 inner: DbContextOptions::default(),
231 }
232 }
233 pub fn connection_string(&mut self, cs: impl Into<String>) -> &mut Self {
234 self.inner.connection_string = cs.into();
235 self
236 }
237 pub fn set_provider(&mut self, tag: &str, cs: impl Into<String>) -> &mut Self {
238 self.inner.provider_tag = Some(tag.to_string());
239 self.inner.connection_string = cs.into();
240 self
241 }
242 #[allow(clippy::type_complexity)]
243 pub fn set_provider_factory(
244 &mut self,
245 tag: &str,
246 cs: impl Into<String>,
247 factory: Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>,
248 ) -> &mut Self {
249 self.inner.provider_tag = Some(tag.to_string());
250 self.inner.connection_string = cs.into();
251 self.inner.provider_factory = Some(factory);
252 self
253 }
254 pub fn add_interceptor(
268 &mut self,
269 interceptor: impl crate::interceptor::ISaveChangesInterceptor + 'static,
270 ) -> &mut Self {
271 self.inner.interceptors.push(Arc::new(interceptor));
272 self
273 }
274
275 pub fn use_lazy_loading(&mut self, enabled: bool) -> &mut Self {
293 self.inner.lazy_loading_enabled = enabled;
294 self
295 }
296
297 pub fn context_key(&mut self, key: impl Into<String>) -> &mut Self {
302 self.inner.context_key = Some(key.into());
303 self
304 }
305
306 #[cfg(feature = "tracing")]
311 pub fn slow_query_threshold(&mut self, threshold: std::time::Duration) -> &mut Self {
312 self.inner.slow_query_threshold = Some(threshold);
313 self
314 }
315
316 pub fn build(self) -> DbContextOptions {
317 self.inner
318 }
319}
320
321impl Default for DbContextOptionsBuilder {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327#[async_trait::async_trait]
332trait ErasedSetOps: Send + Sync {
333 async fn save(
334 &self,
335 conn: &mut (dyn IAsyncConnection + Send),
336 provider: &dyn IDatabaseProvider,
337 raw_set: &mut (dyn Any + Send + Sync),
338 meta: &EntityTypeMeta,
339 ) -> EFResult<(usize, usize, usize)>;
340 fn detect_changes(&self, raw_set: &mut (dyn Any + Send + Sync));
341 fn clear(&self, raw_set: &mut (dyn Any + Send + Sync + 'static));
342 fn collect_entries(&self, raw_set: &(dyn Any + Send + Sync)) -> Vec<EntityEntryView>;
346}
347
348struct SetOps<E> {
349 _phantom: std::marker::PhantomData<E>,
350}
351impl<E> SetOps<E> {
352 fn new() -> Self {
353 Self {
354 _phantom: std::marker::PhantomData,
355 }
356 }
357}
358
359#[async_trait::async_trait]
360impl<E> ErasedSetOps for SetOps<E>
361where
362 E: IEntityType
363 + IEntitySnapshot
364 + IGetKeyValues
365 + IFromRow
366 + INavigationSetter
367 + Send
368 + Sync
369 + 'static,
370{
371 async fn save(
372 &self,
373 conn: &mut (dyn IAsyncConnection + Send),
374 provider: &dyn IDatabaseProvider,
375 raw_set: &mut (dyn Any + Send + Sync),
376 meta: &EntityTypeMeta,
377 ) -> EFResult<(usize, usize, usize)> {
378 let db_set = raw_set
379 .downcast_mut::<DbSet<E>>()
380 .expect("SetOps type mismatch");
381 save_one_set(conn, provider, db_set, meta).await
382 }
383 fn detect_changes(&self, raw_set: &mut (dyn Any + Send + Sync)) {
384 if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
385 db_set.detect_changes();
386 }
387 }
388 fn clear(&self, raw_set: &mut (dyn Any + Send + Sync + 'static)) {
389 if let Some(db_set) = raw_set.downcast_mut::<DbSet<E>>() {
390 db_set.clear_entries();
391 }
392 }
393 fn collect_entries(&self, raw_set: &(dyn Any + Send + Sync)) -> Vec<EntityEntryView> {
394 let Some(db_set) = raw_set.downcast_ref::<DbSet<E>>() else {
395 return Vec::new();
396 };
397 let type_name = E::entity_meta().type_name.to_string();
398 db_set
399 .entries
400 .iter()
401 .map(|e| EntityEntryView {
402 type_id: TypeId::of::<E>(),
403 type_name: type_name.clone(),
404 state: e.state,
405 })
406 .collect()
407 }
408}
409
410pub struct DbContext {
415 sets: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
416 savers: HashMap<TypeId, Box<dyn ErasedSetOps>>,
417 entity_metas: HashMap<TypeId, EntityTypeMeta>,
418 model_builder: ModelBuilder,
419 change_tracker: ChangeTracker,
420 provider: Arc<dyn IDatabaseProvider>,
421 interceptor_pipeline: InterceptorPipeline,
422 lazy_loading_enabled: bool,
423 ambient_transaction: Option<Box<dyn ITransaction>>,
432}
433
434impl DbContext {
435 pub fn from_options(options: &DbContextOptions) -> EFResult<Self> {
443 let provider = options.create_provider()?;
444 let built = options
445 .metadata_cache
446 .get_or_build(options.context_key.as_deref());
447 let ctx = Self {
448 sets: HashMap::new(),
449 savers: HashMap::new(),
450 entity_metas: built.entity_metas.clone(),
451 model_builder: ModelBuilder::from_built(&built),
452 change_tracker: ChangeTracker::new(),
453 provider,
454 interceptor_pipeline: InterceptorPipeline::new(options.interceptors.clone()),
455 lazy_loading_enabled: options.lazy_loading_enabled,
456 ambient_transaction: None,
457 };
458 Ok(ctx)
459 }
460
461 pub fn set<T>(&mut self) -> &mut DbSet<T>
462 where
463 T: IEntityType
464 + IEntitySnapshot
465 + IGetKeyValues
466 + IFromRow
467 + INavigationSetter
468 + Send
469 + Sync
470 + 'static,
471 {
472 let type_id = TypeId::of::<T>();
473 self.savers
474 .entry(type_id)
475 .or_insert_with(|| Box::new(SetOps::<T>::new()));
476 self.entity_metas
477 .entry(type_id)
478 .or_insert_with(T::entity_meta);
479 if !self.model_builder.has_entity(type_id) {
480 self.model_builder.register_entity_meta(T::entity_meta());
481 }
482 self.sets.entry(type_id).or_insert_with(|| {
483 let table_name = self
484 .model_builder
485 .build()
486 .into_iter()
487 .find(|m| m.type_id == type_id)
488 .map(|m| m.table_name.to_string())
489 .unwrap_or_else(|| T::entity_meta().table_name.to_string());
490 let mut db_set = DbSet::<T>::with_provider(table_name, Arc::clone(&self.provider));
491 if let Some(filter) = self.model_builder.get_query_filter(&type_id) {
492 db_set.set_query_filter(filter.clone());
493 }
494 db_set.set_filter_map(self.model_builder.filters_by_table());
495 db_set.set_lazy_loading_enabled(self.lazy_loading_enabled);
496 Box::new(db_set)
497 });
498 self.sets
499 .get_mut(&type_id)
500 .and_then(|b| b.downcast_mut::<DbSet<T>>())
501 .expect("DbSet type mismatch")
502 }
503
504 pub fn model(&mut self) -> &mut ModelBuilder {
506 &mut self.model_builder
507 }
508
509 pub fn model_builder(&self) -> &ModelBuilder {
511 &self.model_builder
512 }
513
514 pub fn entity_metas_contains<T: IEntityType>(&self) -> bool {
517 self.entity_metas.contains_key(&TypeId::of::<T>())
518 }
519
520 pub fn discover_entities(&mut self) -> EFResult<()> {
532 Ok(())
533 }
534
535 pub fn detect_changes(&mut self) {
537 let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
538 for type_id in type_ids {
539 if let Some(set) = self.sets.get_mut(&type_id) {
540 if let Some(saver) = self.savers.get(&type_id) {
541 saver.detect_changes(set.as_mut());
542 }
543 }
544 }
545 }
546
547 fn build_save_context(&self) -> SaveChangesContext {
552 let mut views: Vec<EntityEntryView> = Vec::new();
553 for (type_id, set) in &self.sets {
554 if let Some(saver) = self.savers.get(type_id) {
555 views.extend(saver.collect_entries(set.as_ref()));
556 }
557 }
558 SaveChangesContext::from_views(views)
559 }
560
561 pub async fn ensure_created(&self) -> EFResult<()> {
568 let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
569 if metas.is_empty() {
570 metas = self.entity_metas.values().cloned().collect();
571 }
572 if metas.is_empty() {
573 return Err(EFError::configuration(
574 "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_created().",
575 ));
576 }
577 let dialect = self.provider.migration_dialect();
578 MigrationEngine::new(dialect)
579 .ensure_created(&*self.provider, &metas)
580 .await?;
581
582 for meta in &metas {
583 let rows = self.model_builder.seed_rows_for(&meta.type_id);
584 if !rows.is_empty() {
585 MigrationEngine::new(dialect)
586 .apply_seed_data(&*self.provider, meta, rows)
587 .await?;
588 }
589 }
590 Ok(())
591 }
592
593 pub async fn ensure_deleted(&self) -> EFResult<()> {
595 let mut metas: Vec<EntityTypeMeta> = self.model_builder.build();
596 if metas.is_empty() {
597 metas = self.entity_metas.values().cloned().collect();
598 }
599 if metas.is_empty() {
600 return Err(EFError::configuration(
601 "No entity types registered. Call ctx.discover_entities() or ctx.set::<T>() before ensure_deleted().",
602 ));
603 }
604 let dialect = self.provider.migration_dialect();
605 MigrationEngine::new(dialect)
606 .ensure_deleted(&*self.provider, &metas)
607 .await
608 }
609}
610
611impl DbContext {
616 pub fn provider(&self) -> &dyn IDatabaseProvider {
618 &*self.provider
619 }
620
621 pub fn change_tracker(&self) -> &ChangeTracker {
623 &self.change_tracker
624 }
625
626 pub fn change_tracker_mut(&mut self) -> &mut ChangeTracker {
628 &mut self.change_tracker
629 }
630
631 pub fn transaction_mut(&mut self) -> Option<&mut Box<dyn ITransaction>> {
639 self.ambient_transaction.as_mut()
640 }
641
642 pub async fn begin_transaction(&mut self) -> EFResult<Box<dyn ITransaction>> {
655 let mut conn = self.provider.get_connection().await?;
656 conn.begin_transaction().await?;
657 Ok(Box::new(DbTransaction::new(conn)))
658 }
659
660 pub async fn save_changes(&mut self) -> EFResult<SaveChangesResult> {
665 let _save_guard = crate::observability::SaveChangesGuard::new();
666 let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
667 for type_id in &type_ids {
668 let set = self.sets.get_mut(type_id).unwrap();
669 self.savers
670 .get(type_id)
671 .unwrap()
672 .detect_changes(set.as_mut());
673 }
674
675 let configured_metas: HashMap<TypeId, EntityTypeMeta> = self
678 .model_builder
679 .build()
680 .into_iter()
681 .map(|m| (m.type_id, m))
682 .collect();
683
684 let save_ctx = self.build_save_context();
688 self.interceptor_pipeline.on_saving(&save_ctx).await?;
689
690 enum TxnSource {
696 Ambient(Box<dyn ITransaction>),
697 Managed(Box<dyn IAsyncConnection>),
698 }
699 let mut txn = match self.ambient_transaction.take() {
700 Some(t) => TxnSource::Ambient(t),
701 None => {
702 let mut c = self.provider.get_connection().await?;
703 c.begin_transaction().await?;
704 TxnSource::Managed(c)
705 }
706 };
707
708 let type_ids: Vec<TypeId> = self.sets.keys().copied().collect();
709 let mut total_added = 0usize;
710 let mut total_updated = 0usize;
711 let mut total_deleted = 0usize;
712 for type_id in &type_ids {
713 let saver = self.savers.get(type_id).expect("saver not registered");
714 let set = self.sets.get_mut(type_id).unwrap();
715 let meta = configured_metas
716 .get(type_id)
717 .or_else(|| self.entity_metas.get(type_id))
718 .expect("meta not found for entity type");
719 let conn_ref: &mut dyn IAsyncConnection = match &mut txn {
720 TxnSource::Ambient(t) => t.connection(),
721 TxnSource::Managed(c) => c.as_mut(),
722 };
723 let (a, u, d) = match saver
724 .save(conn_ref, &*self.provider, set.as_mut(), meta)
725 .await
726 {
727 Ok(r) => r,
728 Err(e) => {
729 if let TxnSource::Managed(mut conn) = txn {
730 let _ = conn.rollback_transaction().await;
731 } else if let TxnSource::Ambient(t) = txn {
732 self.ambient_transaction = Some(t);
733 }
734 self.interceptor_pipeline
735 .on_save_failed(&save_ctx, &e)
736 .await;
737 return Err(e);
738 }
739 };
740 total_added += a;
741 total_updated += u;
742 total_deleted += d;
743 }
744 match txn {
745 TxnSource::Ambient(t) => {
746 self.ambient_transaction = Some(t);
747 }
748 TxnSource::Managed(mut conn) => {
749 if let Err(e) = conn.commit_transaction().await {
750 self.interceptor_pipeline
751 .on_save_failed(&save_ctx, &e)
752 .await;
753 return Err(e);
754 }
755 }
756 }
757 self.change_tracker.accept_all_changes();
758 for type_id in &type_ids {
759 let saver = self.savers.get(type_id).unwrap();
760 let set = self.sets.get_mut(type_id).unwrap();
761 saver.clear(set.as_mut());
762 }
763
764 let result_ctx = SaveChangesResultContext {
766 added: total_added,
767 updated: total_updated,
768 deleted: total_deleted,
769 };
770 self.interceptor_pipeline
771 .on_saved(&save_ctx, &result_ctx)
772 .await?;
773
774 Ok(SaveChangesResult {
775 added: total_added,
776 updated: total_updated,
777 deleted: total_deleted,
778 })
779 }
780
781 pub async fn use_transaction<F, R>(&mut self, f: F) -> EFResult<R>
802 where
803 for<'a> F: FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output = EFResult<R>> + Send + 'a>>,
804 R: Send + 'static,
805 {
806 if self.ambient_transaction.is_some() {
807 return Err(EFError::transaction(
808 "ambient transaction already active; nested use_transaction is not supported",
809 ));
810 }
811 let mut conn = self.provider.get_connection().await?;
812 conn.begin_transaction().await?;
813 self.ambient_transaction = Some(Box::new(DbTransaction::new(conn)));
814 let result = f(self).await;
815 let txn = self
816 .ambient_transaction
817 .take()
818 .expect("ambient_transaction set above");
819 match result {
820 Ok(r) => {
821 txn.commit().await?;
822 Ok(r)
823 }
824 Err(e) => {
825 let _ = txn.rollback().await;
826 Err(e)
827 }
828 }
829 }
830}
831
832#[allow(clippy::type_complexity)]
837pub async fn save_one_set<E>(
838 conn: &mut dyn IAsyncConnection,
839 provider: &dyn IDatabaseProvider,
840 db_set: &mut DbSet<E>,
841 meta: &EntityTypeMeta,
842) -> EFResult<(usize, usize, usize)>
843where
844 E: IEntityType + IEntitySnapshot + IGetKeyValues + IFromRow,
845{
846 let query_filter = db_set.query_filter();
847
848 let added: Vec<(&E, &EntityTypeMeta)> = db_set
849 .tracked_by_state(crate::entity::EntityState::Added)
850 .into_iter()
851 .map(|(e, _)| (e, meta))
852 .collect();
853 let modified: Vec<(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)> = db_set
854 .tracked_by_state(crate::entity::EntityState::Modified)
855 .into_iter()
856 .map(|(e, orig)| (e, meta, orig))
857 .collect();
858 let deleted: Vec<(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)> = db_set
859 .tracked_by_state(crate::entity::EntityState::Deleted)
860 .into_iter()
861 .map(|(e, orig)| (e, meta, orig))
862 .collect();
863 let mut ac = 0usize;
864 let mut uc = 0usize;
865 let mut dc = 0usize;
866 if !added.is_empty() {
867 ac = ChangeExecutor::execute_inserts(conn, provider, &added, |_, _| {}).await?;
868 }
869 if !modified.is_empty() {
870 uc = ChangeExecutor::execute_updates(conn, provider, &modified, query_filter).await?;
871 }
872 if !deleted.is_empty() {
873 dc = ChangeExecutor::execute_deletes(conn, provider, &deleted, query_filter).await?;
874 }
875 Ok((ac, uc, dc))
876}
877
878#[derive(Debug, Clone)]
883pub struct SaveChangesResult {
884 pub added: usize,
885 pub updated: usize,
886 pub deleted: usize,
887}
888impl SaveChangesResult {
889 pub fn total(&self) -> usize {
890 self.added + self.updated + self.deleted
891 }
892}
893impl std::fmt::Display for SaveChangesResult {
894 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
895 write!(
896 f,
897 "{} entities modified ({} added, {} updated, {} deleted)",
898 self.total(),
899 self.added,
900 self.updated,
901 self.deleted
902 )
903 }
904}