1use crate::config::{DbConnConfig, GlobalDatabaseConfig};
9use crate::options::build_db_handle;
10use crate::{Db, DbError, Result};
11use dashmap::DashMap;
12use figment::Figment;
13use std::path::{Path, PathBuf};
14
15pub struct DbManager {
17 global: Option<GlobalDatabaseConfig>,
19 figment: Figment,
21 home_dir: PathBuf,
23 cache: DashMap<String, Db>,
25}
26
27impl DbManager {
28 pub fn from_figment(figment: Figment, home_dir: PathBuf) -> Result<Self> {
33 let all_data: serde_json::Value = figment
35 .extract()
36 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
37
38 let global: Option<GlobalDatabaseConfig> = match all_data.get("database") {
39 None => None,
40 Some(db) => match serde_json::from_value(db.clone()) {
41 Ok(cfg) => Some(cfg),
42 Err(e) => {
43 tracing::warn!(
44 error = %e,
45 "Global 'database' key is present but failed to deserialize; ignoring"
46 );
47 None
48 }
49 },
50 };
51
52 Ok(Self {
53 global,
54 figment,
55 home_dir,
56 cache: DashMap::new(),
57 })
58 }
59
60 pub async fn get(&self, module: &str) -> Result<Option<Db>> {
66 if let Some(db) = self.cache.get(module) {
68 return Ok(Some(db.clone()));
69 }
70
71 match self.build_for_module(module).await? {
73 Some(db) => {
74 match self.cache.entry(module.to_owned()) {
76 dashmap::mapref::entry::Entry::Occupied(entry) => {
77 Ok(Some(entry.get().clone()))
79 }
80 dashmap::mapref::entry::Entry::Vacant(entry) => {
81 entry.insert(db.clone());
83 Ok(Some(db))
84 }
85 }
86 }
87 _ => Ok(None),
88 }
89 }
90
91 async fn build_for_module(&self, module: &str) -> Result<Option<Db>> {
93 let module_data: serde_json::Value = self
95 .figment
96 .extract()
97 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
98
99 let Some(db_value) = module_data
100 .get("modules")
101 .and_then(|modules| modules.get(module))
102 .and_then(|m| m.get("database"))
103 else {
104 tracing::debug!(module = %module, "Module has no database configuration; skipping");
105 return Ok(None);
106 };
107
108 let mut cfg: DbConnConfig = match serde_json::from_value(db_value.clone()) {
109 Ok(cfg) => cfg,
110 Err(e) => {
111 tracing::warn!(
112 module = %module,
113 error = %e,
114 "Module 'database' key is present but failed to deserialize; ignoring"
115 );
116 return Ok(None);
117 }
118 };
119
120 if let Some(server_name) = &cfg.server {
122 let server_cfg = self
123 .global
124 .as_ref()
125 .and_then(|g| g.servers.get(server_name))
126 .ok_or_else(|| {
127 DbError::InvalidConfig(format!(
128 "Referenced server '{server_name}' not found in global database configuration"
129 ))
130 })?;
131
132 cfg = Self::merge_server_into_module(cfg, server_cfg.clone());
133 }
134
135 let module_home_dir = self.home_dir.join(module);
137 cfg = self.finalize_sqlite_paths(cfg, &module_home_dir)?;
138
139 let handle = build_db_handle(cfg, self.global.as_ref()).await?;
141
142 tracing::info!(
143 module = %module,
144 engine = ?handle.engine(),
145 dsn = %crate::options::redact_credentials_in_dsn(Some(handle.dsn())),
146 "Built database handle for module"
147 );
148
149 Ok(Some(Db::new(handle)))
150 }
151
152 fn merge_server_into_module(
155 mut module_cfg: DbConnConfig,
156 server_cfg: DbConnConfig,
157 ) -> DbConnConfig {
158 if module_cfg.engine.is_none() {
162 module_cfg.engine = server_cfg.engine;
163 }
164
165 if module_cfg.dsn.is_none() {
167 module_cfg.dsn = server_cfg.dsn;
168 }
169
170 if module_cfg.host.is_none() {
172 module_cfg.host = server_cfg.host;
173 }
174 if module_cfg.port.is_none() {
175 module_cfg.port = server_cfg.port;
176 }
177 if module_cfg.user.is_none() {
178 module_cfg.user = server_cfg.user;
179 }
180 if module_cfg.password.is_none() {
181 module_cfg.password = server_cfg.password;
182 }
183 if module_cfg.dbname.is_none() {
184 module_cfg.dbname = server_cfg.dbname;
185 }
186
187 match (&mut module_cfg.params, server_cfg.params) {
189 (Some(module_params), Some(server_params)) => {
190 for (key, value) in server_params {
192 module_params.entry(key).or_insert(value);
193 }
194 }
195 (None, Some(server_params)) => {
196 module_cfg.params = Some(server_params);
197 }
198 _ => {} }
200
201 if module_cfg.pool.is_none() {
203 module_cfg.pool = server_cfg.pool;
204 }
205
206 module_cfg
209 }
210
211 fn finalize_sqlite_paths(
213 &self,
214 mut cfg: DbConnConfig,
215 module_home: &Path,
216 ) -> Result<DbConnConfig> {
217 if let Some(file) = &cfg.file {
219 let absolute_path = module_home.join(file);
220
221 let auto_provision = self
223 .global
224 .as_ref()
225 .and_then(|g| g.auto_provision)
226 .unwrap_or(true);
227
228 if auto_provision {
229 if let Some(parent) = absolute_path.parent() {
231 std::fs::create_dir_all(parent).map_err(DbError::Io)?;
232 }
233 } else if let Some(parent) = absolute_path.parent() {
234 if !parent.exists() {
236 return Err(DbError::Io(std::io::Error::new(
237 std::io::ErrorKind::NotFound,
238 format!(
239 "Directory does not exist and auto_provision is disabled: {}",
240 parent.display()
241 ),
242 )));
243 }
244 }
245
246 cfg.path = Some(absolute_path);
247 cfg.file = None; }
249
250 if let Some(path) = &cfg.path
252 && path.is_relative()
253 {
254 cfg.path = Some(module_home.join(path));
255 }
256
257 Ok(cfg)
258 }
259}