Skip to main content

rust_ef/db_context/
options.rs

1//! `DbContextOptions` and `DbContextOptionsBuilder` — provider factory and
2//! context-level configuration.
3
4use crate::error::EFResult;
5use crate::provider::IDatabaseProvider;
6use std::sync::Arc;
7
8#[derive(Clone)]
9pub struct DbContextOptions {
10    pub(crate) connection_string: String,
11    pub(crate) provider_tag: Option<String>,
12    #[allow(clippy::type_complexity)]
13    pub(crate) provider_factory:
14        Option<Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>>,
15    /// Process-level cache of the built provider (which owns the connection
16    /// pool). Built once on the first `create_provider()` call and shared
17    /// across every `DbContext` created from the same `Arc<DbContextOptions>`
18    /// (i.e. the same `add_dbcontext` registration). Keeping the provider
19    /// alive for the application lifetime means the connection pool is reused
20    /// across requests instead of being recreated per request.
21    pub(crate) provider_cache: Arc<std::sync::Mutex<Option<Arc<dyn IDatabaseProvider>>>>,
22    pub(crate) interceptors: Vec<Arc<dyn crate::interceptor::ISaveChangesInterceptor>>,
23    /// When `true`, `QueryBuilder::to_list()` attaches `LazyContext` to every
24    /// navigation container on materialized entities, enabling on-demand
25    /// loading via `BelongsTo::load()` / `HasMany::load()` / `HasOne::load()`.
26    ///
27    /// Defaults to `false` (opt-in) to preserve v1.0 eager-only behavior.
28    pub(crate) lazy_loading_enabled: bool,
29    pub(crate) context_key: Option<String>,
30    /// Process-level cache of `discover_entities()` output, keyed by
31    /// `context_key`. Shared across all `DbContext` instances created from
32    /// the same `DbContextOptions` (which is `Arc`-shared per `add_dbcontext`
33    /// registration). The first `from_options()` call builds the metadata;
34    /// subsequent calls `Arc::clone` it.
35    pub(crate) metadata_cache: Arc<crate::metadata_cache::MetadataCache>,
36    /// Slow query threshold for tracing. When set, queries exceeding this
37    /// duration emit a `tracing::warn!` event.
38    #[cfg(feature = "tracing")]
39    pub(crate) slow_query_threshold: Option<std::time::Duration>,
40}
41
42impl std::fmt::Debug for DbContextOptions {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("DbContextOptions")
45            .field(
46                "connection_string",
47                &redact_connection_string(&self.connection_string),
48            )
49            .field("provider_tag", &self.provider_tag)
50            .finish()
51    }
52}
53
54/// Redacts credentials from a connection string so `Debug` output never leaks
55/// passwords. Handles URL form (`scheme://user:pass@host`) and key=value form
56/// (`...;Password=...;...`). SQLite file paths and other credential-free
57/// strings are returned unchanged for debuggability.
58fn redact_connection_string(cs: &str) -> String {
59    // URL form: scheme://[user[:pass]@]host...
60    if let Some(scheme_end) = cs.find("://") {
61        let (scheme, rest) = cs.split_at(scheme_end + 3);
62        if let Some(at) = rest.find('@') {
63            let (userinfo, host_and_rest) = rest.split_at(at);
64            let redacted_user = match userinfo.find(':') {
65                Some(colon) => &userinfo[..colon],
66                None => userinfo,
67            };
68            return format!("{}{}***@{}", scheme, redacted_user, &host_and_rest[1..]);
69        }
70        return cs.to_string();
71    }
72    // Key=value form: redact any token whose key mentions password/pwd.
73    if cs.contains('=') {
74        return cs
75            .split(';')
76            .map(|pair| {
77                let eq = match pair.find('=') {
78                    Some(e) => e,
79                    None => return pair.to_string(),
80                };
81                let key = pair[..eq].trim().to_lowercase();
82                if key.contains("password") || key.contains("pwd") {
83                    format!("{}=***", &pair[..eq])
84                } else {
85                    pair.to_string()
86                }
87            })
88            .collect::<Vec<_>>()
89            .join(";");
90    }
91    cs.to_string()
92}
93
94impl DbContextOptions {
95    pub fn connection_string(&self) -> &str {
96        &self.connection_string
97    }
98    pub fn provider_tag(&self) -> Option<&str> {
99        self.provider_tag.as_deref()
100    }
101    pub fn lazy_loading_enabled(&self) -> bool {
102        self.lazy_loading_enabled
103    }
104    pub fn context_key(&self) -> Option<&str> {
105        self.context_key.as_deref()
106    }
107    pub fn create_provider(&self) -> EFResult<Arc<dyn IDatabaseProvider>> {
108        // Recover from a poisoned lock rather than panicking (consistent with
109        // `MetadataCache`): if a previous build panicked, `into_inner()` yields
110        // the still-`None` cache and we retry the build below.
111        let mut guard = self
112            .provider_cache
113            .lock()
114            .unwrap_or_else(|p| p.into_inner());
115        if let Some(provider) = guard.as_ref() {
116            return Ok(Arc::clone(provider));
117        }
118        let factory = self.provider_factory.as_ref().ok_or_else(|| {
119            crate::error::EFError::configuration(
120                "No provider configured. Call use_sqlite / use_postgres / use_mysql first.",
121            )
122        })?;
123        let provider = factory(self.connection_string())?;
124        #[cfg(feature = "tracing")]
125        if let Some(threshold) = self.slow_query_threshold {
126            provider.set_slow_query_threshold(threshold);
127        }
128        *guard = Some(Arc::clone(&provider));
129        Ok(provider)
130    }
131}
132
133#[allow(clippy::derivable_impls)]
134impl Default for DbContextOptions {
135    fn default() -> Self {
136        Self {
137            connection_string: String::new(),
138            provider_tag: None,
139            provider_factory: None,
140            provider_cache: Arc::new(std::sync::Mutex::new(None)),
141            interceptors: Vec::new(),
142            lazy_loading_enabled: false,
143            context_key: None,
144            metadata_cache: Arc::new(crate::metadata_cache::MetadataCache::new()),
145            #[cfg(feature = "tracing")]
146            slow_query_threshold: None,
147        }
148    }
149}
150
151pub struct DbContextOptionsBuilder {
152    inner: DbContextOptions,
153}
154
155impl DbContextOptionsBuilder {
156    pub fn new() -> Self {
157        Self {
158            inner: DbContextOptions::default(),
159        }
160    }
161    pub fn connection_string(&mut self, cs: impl Into<String>) -> &mut Self {
162        self.inner.connection_string = cs.into();
163        self
164    }
165    pub fn set_provider(&mut self, tag: &str, cs: impl Into<String>) -> &mut Self {
166        self.inner.provider_tag = Some(tag.to_string());
167        self.inner.connection_string = cs.into();
168        self
169    }
170    #[allow(clippy::type_complexity)]
171    pub fn set_provider_factory(
172        &mut self,
173        tag: &str,
174        cs: impl Into<String>,
175        factory: Arc<dyn Fn(&str) -> EFResult<Arc<dyn IDatabaseProvider>> + Send + Sync>,
176    ) -> &mut Self {
177        self.inner.provider_tag = Some(tag.to_string());
178        self.inner.connection_string = cs.into();
179        self.inner.provider_factory = Some(factory);
180        self
181    }
182    /// Registers a `SaveChanges` interceptor.
183    ///
184    /// Interceptors are called in registration order during
185    /// `save_changes()`. Use this for auditing, soft-delete,
186    /// validation, and other cross-cutting concerns.
187    ///
188    /// # Example
189    ///
190    /// ```rust,ignore
191    /// options
192    ///     .use_sqlite("app.db")
193    ///     .add_interceptor(AuditInterceptor::new());
194    /// ```
195    pub fn add_interceptor(
196        &mut self,
197        interceptor: impl crate::interceptor::ISaveChangesInterceptor + 'static,
198    ) -> &mut Self {
199        self.inner.interceptors.push(Arc::new(interceptor));
200        self
201    }
202
203    /// Enables or disables lazy loading of navigation properties.
204    ///
205    /// When enabled (`true`), `to_list()` attaches a `LazyContext` to every
206    /// navigation container on each materialized entity. The user can then
207    /// call `nav.load().await` to trigger a single-entity query on first
208    /// access; subsequent accesses read from the in-memory cache.
209    ///
210    /// When disabled (`false`, the default), navigation properties are
211    /// empty unless explicitly loaded via `Include` — matching v1.0
212    /// eager-only behavior.
213    ///
214    /// # Example
215    ///
216    /// ```rust,ignore
217    /// let mut options = DbContextOptionsBuilder::new();
218    /// options.use_sqlite_in_memory().use_lazy_loading(true);
219    /// ```
220    pub fn use_lazy_loading(&mut self, enabled: bool) -> &mut Self {
221        self.inner.lazy_loading_enabled = enabled;
222        self
223    }
224
225    /// Sets the context key used to filter entities and configurations
226    /// during `DbContext::discover_entities()`. Set automatically by
227    /// `add_dbcontext_keyed`; `None` (the default) selects the default
228    /// context.
229    pub fn context_key(&mut self, key: impl Into<String>) -> &mut Self {
230        self.inner.context_key = Some(key.into());
231        self
232    }
233
234    /// Sets the slow query threshold. Queries exceeding this duration
235    /// emit a `tracing::warn!` event with SQL and elapsed time.
236    ///
237    /// Only available when the `tracing` feature is enabled.
238    #[cfg(feature = "tracing")]
239    pub fn slow_query_threshold(&mut self, threshold: std::time::Duration) -> &mut Self {
240        self.inner.slow_query_threshold = Some(threshold);
241        self
242    }
243
244    pub fn build(self) -> DbContextOptions {
245        self.inner
246    }
247}
248
249impl Default for DbContextOptionsBuilder {
250    fn default() -> Self {
251        Self::new()
252    }
253}