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> = all_data
39 .get("database")
40 .and_then(|db| serde_json::from_value(db.clone()).ok());
41
42 Ok(Self {
43 global,
44 figment,
45 home_dir,
46 cache: DashMap::new(),
47 })
48 }
49
50 pub async fn get(&self, module: &str) -> Result<Option<Db>> {
56 if let Some(db) = self.cache.get(module) {
58 return Ok(Some(db.clone()));
59 }
60
61 match self.build_for_module(module).await? {
63 Some(db) => {
64 match self.cache.entry(module.to_owned()) {
66 dashmap::mapref::entry::Entry::Occupied(entry) => {
67 Ok(Some(entry.get().clone()))
69 }
70 dashmap::mapref::entry::Entry::Vacant(entry) => {
71 entry.insert(db.clone());
73 Ok(Some(db))
74 }
75 }
76 }
77 _ => Ok(None),
78 }
79 }
80
81 async fn build_for_module(&self, module: &str) -> Result<Option<Db>> {
83 let module_data: serde_json::Value = self
85 .figment
86 .extract()
87 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
88
89 let module_cfg: Option<DbConnConfig> = module_data
90 .get("modules")
91 .and_then(|modules| modules.get(module))
92 .and_then(|m| m.get("database"))
93 .and_then(|db| serde_json::from_value(db.clone()).ok());
94
95 let Some(mut cfg) = module_cfg else {
96 tracing::debug!(
97 module = %module,
98 "Module has no database configuration; skipping"
99 );
100 return Ok(None);
101 };
102
103 if let Some(server_name) = &cfg.server {
105 let server_cfg = self
106 .global
107 .as_ref()
108 .and_then(|g| g.servers.get(server_name))
109 .ok_or_else(|| {
110 DbError::InvalidConfig(format!(
111 "Referenced server '{server_name}' not found in global database configuration"
112 ))
113 })?;
114
115 cfg = Self::merge_server_into_module(cfg, server_cfg.clone());
116 }
117
118 let module_home_dir = self.home_dir.join(module);
120 cfg = self.finalize_sqlite_paths(cfg, &module_home_dir)?;
121
122 let handle = build_db_handle(cfg, self.global.as_ref()).await?;
124
125 tracing::info!(
126 module = %module,
127 engine = ?handle.engine(),
128 dsn = %crate::options::redact_credentials_in_dsn(Some(handle.dsn())),
129 "Built database handle for module"
130 );
131
132 Ok(Some(Db::new(handle)))
133 }
134
135 fn merge_server_into_module(
138 mut module_cfg: DbConnConfig,
139 server_cfg: DbConnConfig,
140 ) -> DbConnConfig {
141 if module_cfg.engine.is_none() {
145 module_cfg.engine = server_cfg.engine;
146 }
147
148 if module_cfg.dsn.is_none() {
150 module_cfg.dsn = server_cfg.dsn;
151 }
152
153 if module_cfg.host.is_none() {
155 module_cfg.host = server_cfg.host;
156 }
157 if module_cfg.port.is_none() {
158 module_cfg.port = server_cfg.port;
159 }
160 if module_cfg.user.is_none() {
161 module_cfg.user = server_cfg.user;
162 }
163 if module_cfg.password.is_none() {
164 module_cfg.password = server_cfg.password;
165 }
166 if module_cfg.dbname.is_none() {
167 module_cfg.dbname = server_cfg.dbname;
168 }
169
170 match (&mut module_cfg.params, server_cfg.params) {
172 (Some(module_params), Some(server_params)) => {
173 for (key, value) in server_params {
175 module_params.entry(key).or_insert(value);
176 }
177 }
178 (None, Some(server_params)) => {
179 module_cfg.params = Some(server_params);
180 }
181 _ => {} }
183
184 if module_cfg.pool.is_none() {
186 module_cfg.pool = server_cfg.pool;
187 }
188
189 module_cfg
192 }
193
194 fn finalize_sqlite_paths(
196 &self,
197 mut cfg: DbConnConfig,
198 module_home: &Path,
199 ) -> Result<DbConnConfig> {
200 if let Some(file) = &cfg.file {
202 let absolute_path = module_home.join(file);
203
204 let auto_provision = self
206 .global
207 .as_ref()
208 .and_then(|g| g.auto_provision)
209 .unwrap_or(true);
210
211 if auto_provision {
212 if let Some(parent) = absolute_path.parent() {
214 std::fs::create_dir_all(parent).map_err(DbError::Io)?;
215 }
216 } else if let Some(parent) = absolute_path.parent() {
217 if !parent.exists() {
219 return Err(DbError::Io(std::io::Error::new(
220 std::io::ErrorKind::NotFound,
221 format!(
222 "Directory does not exist and auto_provision is disabled: {}",
223 parent.display()
224 ),
225 )));
226 }
227 }
228
229 cfg.path = Some(absolute_path);
230 cfg.file = None; }
232
233 if let Some(path) = &cfg.path
235 && path.is_relative()
236 {
237 cfg.path = Some(module_home.join(path));
238 }
239
240 Ok(cfg)
241 }
242}