Skip to main content

pharos_runtime_database/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Backend-neutral primary database identity, configuration, and registry.
4//! Vendor-specific database drivers live in their respective adapter crates.
5
6use std::sync::{Arc, OnceLock};
7
8pub mod account_tokens;
9pub mod backend;
10pub mod backend_registry;
11pub mod contract;
12pub mod conversion;
13pub mod document;
14pub mod effects;
15pub mod historical;
16pub mod index_values;
17pub mod integration;
18pub mod operations;
19pub mod payment;
20pub mod queue;
21pub mod relational;
22pub mod seed_from;
23pub mod sequence;
24pub mod snapshot;
25pub mod state_delta;
26pub mod webhook;
27
28pub use backend::{
29    BackendCapabilities, BackendConnection, BackendError, BackendKind, DbBackendConfig,
30    PrimaryBackend,
31};
32pub use backend_registry::BackendRegistry;
33struct DefaultPrimaryBackend {
34    kind: BackendKind,
35    capabilities: BackendCapabilities,
36}
37
38impl PrimaryBackend for DefaultPrimaryBackend {
39    /// Performs kind.
40    fn kind(&self) -> BackendKind {
41        self.kind
42    }
43
44    /// Performs capabilities.
45    fn capabilities(&self) -> BackendCapabilities {
46        self.capabilities
47    }
48}
49
50/// Builds default primary backend registry.
51pub fn build_default_primary_backend_registry() -> Result<BackendRegistry, BackendError> {
52    let mut registry = BackendRegistry::new();
53    registry.register(Arc::new(DefaultPrimaryBackend {
54        kind: BackendKind::Sqlite,
55        capabilities: BackendCapabilities {
56            readiness: true,
57            migrations: true,
58            prepared_execution: true,
59            transactions: true,
60            export: true,
61            import: true,
62            pooling: false,
63        },
64    }))?;
65    registry.register(Arc::new(DefaultPrimaryBackend {
66        kind: BackendKind::Mysql,
67        capabilities: BackendCapabilities {
68            readiness: true,
69            migrations: true,
70            prepared_execution: true,
71            transactions: true,
72            export: true,
73            import: true,
74            pooling: false,
75        },
76    }))?;
77    registry.register(Arc::new(DefaultPrimaryBackend {
78        kind: BackendKind::Mongodb,
79        capabilities: BackendCapabilities {
80            readiness: true,
81            migrations: true,
82            prepared_execution: false,
83            transactions: true,
84            export: true,
85            import: true,
86            pooling: true,
87        },
88    }))?;
89    registry.register(Arc::new(DefaultPrimaryBackend {
90        kind: BackendKind::Postgresql,
91        capabilities: BackendCapabilities {
92            readiness: true,
93            migrations: true,
94            prepared_execution: true,
95            transactions: true,
96            export: true,
97            import: true,
98            pooling: true,
99        },
100    }))?;
101    Ok(registry)
102}
103
104/// Returns the immutable default primary-backend registry for shipped backends.
105///
106/// # Returns
107///
108/// Returns the successful value produced by [`crate::default_primary_backend_registry`] or an error when the operation fails.
109///
110/// # Errors
111///
112/// Returns an error value when [`crate::default_primary_backend_registry`] cannot complete successfully.
113///
114/// # Examples
115///
116/// ```rust,ignore
117/// let _result = pharos_runtime_integrations::default_primary_backend_registry();
118/// ```
119///
120/// # Related
121///
122/// - [`crate`] for adjacent helpers that share the same runtime surface.
123pub fn default_primary_backend_registry() -> Result<&'static BackendRegistry, BackendError> {
124    static REGISTRY: OnceLock<Result<BackendRegistry, BackendError>> = OnceLock::new();
125    REGISTRY
126        .get_or_init(build_default_primary_backend_registry)
127        .as_ref()
128        .map_err(Clone::clone)
129}
130
131/// Resolves the typed primary backend kind from one runtime config value.
132///
133/// # Parameters
134///
135/// - `name`: Input consumed by [`crate::backend_kind`].
136///
137/// # Returns
138///
139/// Returns the successful value produced by [`crate::backend_kind`] or an error when the operation fails.
140///
141/// # Errors
142///
143/// Returns an error value when [`crate::backend_kind`] cannot complete successfully.
144///
145/// # Examples
146///
147/// ```rust,ignore
148/// let _result = pharos_runtime_integrations::backend_kind("name");
149/// ```
150///
151/// # Related
152///
153/// - [`crate`] for adjacent helpers that share the same runtime surface.
154pub fn backend_kind(name: &str) -> Result<BackendKind, BackendError> {
155    BackendKind::parse(name)
156}
157
158/// Performs valid env var name.
159fn valid_env_var_name(name: &str) -> bool {
160    let mut chars = name.chars();
161    match chars.next() {
162        Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
163        _ => return false,
164    }
165    chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
166}
167
168/// Validates one shared backend config before backend-specific connection setup.
169///
170/// # Parameters
171///
172/// - `config`: Input consumed by [`crate::validate_backend_config`].
173///
174/// # Returns
175///
176/// Returns the successful value produced by [`crate::validate_backend_config`] or an error when the operation fails.
177///
178/// # Errors
179///
180/// Returns an error value when [`crate::validate_backend_config`] cannot complete successfully.
181///
182/// # Examples
183///
184/// ```rust,ignore
185/// let _result = pharos_runtime_integrations::validate_backend_config(todo!());
186/// ```
187///
188/// # Related
189///
190/// - [`crate`] for adjacent helpers that share the same runtime surface.
191pub fn validate_backend_config(config: &DbBackendConfig) -> Result<(), BackendError> {
192    if !operations::valid_identifier(&config.backend) {
193        return Err(BackendError::InvalidConfig("db_backend"));
194    }
195    let backend = backend_kind(&config.backend).ok();
196    if !config.password_env_name.is_empty() && !valid_env_var_name(&config.password_env_name) {
197        return Err(BackendError::InvalidConfig("db_password_env"));
198    }
199    if !config.port.is_empty()
200        && config
201            .port
202            .parse::<u16>()
203            .ok()
204            .filter(|value| *value > 0)
205            .is_none()
206    {
207        return Err(BackendError::InvalidConfig("db_port"));
208    }
209    if !config.tls_mode.is_empty()
210        && !matches!(
211            config.tls_mode.as_str(),
212            "disable" | "allow" | "prefer" | "require" | "verify-ca" | "verify-full"
213        )
214    {
215        return Err(BackendError::InvalidConfig("db_tls_mode"));
216    }
217    match backend {
218        Some(BackendKind::Sqlite) => {
219            if config
220                .sqlite_path
221                .as_ref()
222                .is_none_or(|path| path.as_os_str().is_empty())
223            {
224                return Err(BackendError::InvalidConfig("db_path"));
225            }
226        }
227        Some(BackendKind::Mysql) => {
228            if config.host.is_empty() && config.socket_path.is_none() {
229                return Err(BackendError::InvalidConfig("db_host"));
230            }
231            if config.port.is_empty() && config.socket_path.is_none() {
232                return Err(BackendError::InvalidConfig("db_port"));
233            }
234            if config.database.is_empty() {
235                return Err(BackendError::InvalidConfig("db_name"));
236            }
237            if config.user.is_empty() {
238                return Err(BackendError::InvalidConfig("db_user"));
239            }
240        }
241        Some(BackendKind::Mongodb) => {
242            if config.host.is_empty() && config.socket_path.is_none() {
243                return Err(BackendError::InvalidConfig("db_host"));
244            }
245            if config.database.is_empty() {
246                return Err(BackendError::InvalidConfig("db_name"));
247            }
248        }
249        Some(BackendKind::Postgresql) => {
250            if config.host.is_empty() && config.socket_path.is_none() {
251                return Err(BackendError::InvalidConfig("db_host"));
252            }
253            if config.port.is_empty() && config.socket_path.is_none() {
254                return Err(BackendError::InvalidConfig("db_port"));
255            }
256            if config.database.is_empty() {
257                return Err(BackendError::InvalidConfig("db_name"));
258            }
259            if config.user.is_empty() {
260                return Err(BackendError::InvalidConfig("db_user"));
261            }
262        }
263        // Additional adapters own their transport validation in the required
264        // operation methods. Execution still requires exact registry resolution.
265        None => {}
266    }
267    Ok(())
268}
269
270/// Resolves one shipped primary backend through the shared immutable registry.
271///
272/// # Parameters
273///
274/// - `name`: Input consumed by [`crate::resolve_primary_backend`].
275///
276/// # Returns
277///
278/// Returns the successful value produced by [`crate::resolve_primary_backend`] or an error when the operation fails.
279///
280/// # Errors
281///
282/// Returns an error value when [`crate::resolve_primary_backend`] cannot complete successfully.
283///
284/// # Examples
285///
286/// ```rust,ignore
287/// let _result = pharos_runtime_integrations::resolve_primary_backend("name");
288/// ```
289///
290/// # Related
291///
292/// - [`crate`] for adjacent helpers that share the same runtime surface.
293pub fn resolve_primary_backend(name: &str) -> Result<Arc<dyn PrimaryBackend>, BackendError> {
294    default_primary_backend_registry()?.resolve(name)
295}