pharos_runtime_database/
lib.rs1#![forbid(unsafe_code)]
2
3use 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 fn kind(&self) -> BackendKind {
41 self.kind
42 }
43
44 fn capabilities(&self) -> BackendCapabilities {
46 self.capabilities
47 }
48}
49
50pub 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
104pub 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
131pub fn backend_kind(name: &str) -> Result<BackendKind, BackendError> {
155 BackendKind::parse(name)
156}
157
158fn 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
168pub 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 None => {}
266 }
267 Ok(())
268}
269
270pub fn resolve_primary_backend(name: &str) -> Result<Arc<dyn PrimaryBackend>, BackendError> {
294 default_primary_backend_registry()?.resolve(name)
295}