1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
//! Database manager for per-module database connections.
//!
//! The `DbManager` is responsible for:
//! - Loading global database configuration from Figment
//! - Building and caching database handles per module
//! - Merging global server configurations with module-specific settings
use crate::config::{DbConnConfig, GlobalDatabaseConfig};
use crate::options::build_db_handle;
use crate::{Db, DbError, Result};
use dashmap::DashMap;
use figment::Figment;
use std::path::{Path, PathBuf};
/// Central database manager that handles per-module database connections.
pub struct DbManager {
/// Global database configuration loaded from Figment
global: Option<GlobalDatabaseConfig>,
/// Figment instance for reading module configurations
figment: Figment,
/// Base home directory for modules
home_dir: PathBuf,
/// Cache of secure DB entrypoints per module
cache: DashMap<String, Db>,
}
impl DbManager {
/// Create a new `DbManager` from a Figment configuration.
///
/// # Errors
/// Returns an error if the configuration cannot be parsed.
pub fn from_figment(figment: Figment, home_dir: PathBuf) -> Result<Self> {
// Parse global database configuration from "db.*" section
let all_data: serde_json::Value = figment
.extract()
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
let global: Option<GlobalDatabaseConfig> = match all_data.get("database") {
None => None,
Some(db) => match serde_json::from_value(db.clone()) {
Ok(cfg) => Some(cfg),
Err(e) => {
tracing::warn!(
error = %e,
"Global 'database' key is present but failed to deserialize; ignoring"
);
None
}
},
};
Ok(Self {
global,
figment,
home_dir,
cache: DashMap::new(),
})
}
/// Get a database handle for the specified module.
/// Returns cached handle if available, otherwise builds a new one.
///
/// # Errors
/// Returns an error if the database connection cannot be established.
pub async fn get(&self, module: &str) -> Result<Option<Db>> {
// Check cache first
if let Some(db) = self.cache.get(module) {
return Ok(Some(db.clone()));
}
// Build new Db
match self.build_for_module(module).await? {
Some(db) => {
// Use entry API to handle race conditions properly
match self.cache.entry(module.to_owned()) {
dashmap::mapref::entry::Entry::Occupied(entry) => {
// Another thread beat us to it, return the cached version
Ok(Some(entry.get().clone()))
}
dashmap::mapref::entry::Entry::Vacant(entry) => {
// We're first, insert our Db
entry.insert(db.clone());
Ok(Some(db))
}
}
}
_ => Ok(None),
}
}
/// Build a database handle for the specified module.
async fn build_for_module(&self, module: &str) -> Result<Option<Db>> {
// Read module database configuration from Figment
let module_data: serde_json::Value = self
.figment
.extract()
.unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
let Some(db_value) = module_data
.get("modules")
.and_then(|modules| modules.get(module))
.and_then(|m| m.get("database"))
else {
tracing::debug!(module = %module, "Module has no database configuration; skipping");
return Ok(None);
};
let mut cfg: DbConnConfig = match serde_json::from_value(db_value.clone()) {
Ok(cfg) => cfg,
Err(e) => {
tracing::warn!(
module = %module,
error = %e,
"Module 'database' key is present but failed to deserialize; ignoring"
);
return Ok(None);
}
};
// If module references a global server, merge configurations
if let Some(server_name) = &cfg.server {
let server_cfg = self
.global
.as_ref()
.and_then(|g| g.servers.get(server_name))
.ok_or_else(|| {
DbError::InvalidConfig(format!(
"Referenced server '{server_name}' not found in global database configuration"
))
})?;
cfg = Self::merge_server_into_module(cfg, server_cfg.clone());
}
// Finalize SQLite paths if needed
let module_home_dir = self.home_dir.join(module);
cfg = self.finalize_sqlite_paths(cfg, &module_home_dir)?;
// Build the database handle
let handle = build_db_handle(cfg, self.global.as_ref()).await?;
tracing::info!(
module = %module,
engine = ?handle.engine(),
dsn = %crate::options::redact_credentials_in_dsn(Some(handle.dsn())),
"Built database handle for module"
);
Ok(Some(Db::new(handle)))
}
/// Merge global server configuration into module configuration.
/// Module fields override server fields. Params maps are merged with module taking precedence.
fn merge_server_into_module(
mut module_cfg: DbConnConfig,
server_cfg: DbConnConfig,
) -> DbConnConfig {
// Start with server config as base, then apply module overrides
// Engine: module takes precedence (important for field-based configs)
if module_cfg.engine.is_none() {
module_cfg.engine = server_cfg.engine;
}
// DSN: module takes precedence
if module_cfg.dsn.is_none() {
module_cfg.dsn = server_cfg.dsn;
}
// Individual fields: module takes precedence
if module_cfg.host.is_none() {
module_cfg.host = server_cfg.host;
}
if module_cfg.port.is_none() {
module_cfg.port = server_cfg.port;
}
if module_cfg.user.is_none() {
module_cfg.user = server_cfg.user;
}
if module_cfg.password.is_none() {
module_cfg.password = server_cfg.password;
}
if module_cfg.dbname.is_none() {
module_cfg.dbname = server_cfg.dbname;
}
// Params: merge maps with module taking precedence
match (&mut module_cfg.params, server_cfg.params) {
(Some(module_params), Some(server_params)) => {
// Merge server params first, then module params (module overrides)
for (key, value) in server_params {
module_params.entry(key).or_insert(value);
}
}
(None, Some(server_params)) => {
module_cfg.params = Some(server_params);
}
_ => {} // Module has params or server has none - keep module params
}
// Pool: module takes precedence
if module_cfg.pool.is_none() {
module_cfg.pool = server_cfg.pool;
}
// Note: file, path, and server fields are module-only and not merged
module_cfg
}
/// Finalize `SQLite` paths by resolving relative file paths to absolute paths.
fn finalize_sqlite_paths(
&self,
mut cfg: DbConnConfig,
module_home: &Path,
) -> Result<DbConnConfig> {
// If file is specified, convert to absolute path under module home
if let Some(file) = &cfg.file {
let absolute_path = module_home.join(file);
// Check auto_provision setting
let auto_provision = self
.global
.as_ref()
.and_then(|g| g.auto_provision)
.unwrap_or(true);
if auto_provision {
// Create all necessary directories
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent).map_err(DbError::Io)?;
}
} else if let Some(parent) = absolute_path.parent() {
// When auto_provision is false, check if the directory exists
if !parent.exists() {
return Err(DbError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"Directory does not exist and auto_provision is disabled: {}",
parent.display()
),
)));
}
}
cfg.path = Some(absolute_path);
cfg.file = None; // Clear file since path takes precedence and we can't have both
}
// If path is relative, make it absolute relative to module home
if let Some(path) = &cfg.path
&& path.is_relative()
{
cfg.path = Some(module_home.join(path));
}
Ok(cfg)
}
}