1use crate::config::{DbConnConfig, GlobalDatabaseConfig};
9use crate::options::build_db_handle;
10use crate::{DbError, DbHandle, Result};
11use dashmap::DashMap;
12use figment::Figment;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16pub struct DbManager {
18 global: Option<GlobalDatabaseConfig>,
20 figment: Figment,
22 home_dir: PathBuf,
24 cache: DashMap<String, Arc<DbHandle>>,
26}
27
28impl DbManager {
29 pub fn from_figment(figment: Figment, home_dir: PathBuf) -> Result<Self> {
34 let all_data: serde_json::Value = figment
36 .extract()
37 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
38
39 let global: Option<GlobalDatabaseConfig> = all_data
40 .get("database")
41 .and_then(|db| serde_json::from_value(db.clone()).ok());
42
43 Ok(Self {
44 global,
45 figment,
46 home_dir,
47 cache: DashMap::new(),
48 })
49 }
50
51 pub async fn get(&self, module: &str) -> Result<Option<Arc<DbHandle>>> {
57 if let Some(handle) = self.cache.get(module) {
59 return Ok(Some(handle.clone()));
60 }
61
62 match self.build_for_module(module).await? {
64 Some(handle) => {
65 match self.cache.entry(module.to_owned()) {
67 dashmap::mapref::entry::Entry::Occupied(entry) => {
68 Ok(Some(entry.get().clone()))
70 }
71 dashmap::mapref::entry::Entry::Vacant(entry) => {
72 entry.insert(handle.clone());
74 Ok(Some(handle))
75 }
76 }
77 }
78 _ => Ok(None),
79 }
80 }
81
82 async fn build_for_module(&self, module: &str) -> Result<Option<Arc<DbHandle>>> {
84 let module_data: serde_json::Value = self
86 .figment
87 .extract()
88 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
89
90 let module_cfg: Option<DbConnConfig> = module_data
91 .get("modules")
92 .and_then(|modules| modules.get(module))
93 .and_then(|m| m.get("database"))
94 .and_then(|db| serde_json::from_value(db.clone()).ok());
95
96 let Some(mut cfg) = module_cfg else {
97 tracing::debug!(
98 module = %module,
99 "Module has no database configuration; skipping"
100 );
101 return Ok(None);
102 };
103
104 if let Some(server_name) = &cfg.server {
106 let server_cfg = self
107 .global
108 .as_ref()
109 .and_then(|g| g.servers.get(server_name))
110 .ok_or_else(|| {
111 DbError::InvalidConfig(format!(
112 "Referenced server '{server_name}' not found in global database configuration"
113 ))
114 })?;
115
116 cfg = Self::merge_server_into_module(cfg, server_cfg.clone());
117 }
118
119 let module_home_dir = self.home_dir.join(module);
121 cfg = self.finalize_sqlite_paths(cfg, &module_home_dir)?;
122
123 let handle = build_db_handle(cfg, self.global.as_ref()).await?;
125
126 tracing::info!(
127 module = %module,
128 engine = ?handle.engine(),
129 dsn = %crate::options::redact_credentials_in_dsn(Some(handle.dsn())),
130 "Built database handle for module"
131 );
132
133 Ok(Some(Arc::new(handle)))
134 }
135
136 fn merge_server_into_module(
139 mut module_cfg: DbConnConfig,
140 server_cfg: DbConnConfig,
141 ) -> DbConnConfig {
142 if module_cfg.dsn.is_none() {
146 module_cfg.dsn = server_cfg.dsn;
147 }
148
149 if module_cfg.host.is_none() {
151 module_cfg.host = server_cfg.host;
152 }
153 if module_cfg.port.is_none() {
154 module_cfg.port = server_cfg.port;
155 }
156 if module_cfg.user.is_none() {
157 module_cfg.user = server_cfg.user;
158 }
159 if module_cfg.password.is_none() {
160 module_cfg.password = server_cfg.password;
161 }
162 if module_cfg.dbname.is_none() {
163 module_cfg.dbname = server_cfg.dbname;
164 }
165
166 match (&mut module_cfg.params, server_cfg.params) {
168 (Some(module_params), Some(server_params)) => {
169 for (key, value) in server_params {
171 module_params.entry(key).or_insert(value);
172 }
173 }
174 (None, Some(server_params)) => {
175 module_cfg.params = Some(server_params);
176 }
177 _ => {} }
179
180 if module_cfg.pool.is_none() {
182 module_cfg.pool = server_cfg.pool;
183 }
184
185 module_cfg
188 }
189
190 fn finalize_sqlite_paths(
192 &self,
193 mut cfg: DbConnConfig,
194 module_home: &Path,
195 ) -> Result<DbConnConfig> {
196 if let Some(file) = &cfg.file {
198 let absolute_path = module_home.join(file);
199
200 let auto_provision = self
202 .global
203 .as_ref()
204 .and_then(|g| g.auto_provision)
205 .unwrap_or(true);
206
207 if auto_provision {
208 if let Some(parent) = absolute_path.parent() {
210 std::fs::create_dir_all(parent).map_err(DbError::Io)?;
211 }
212 } else if let Some(parent) = absolute_path.parent() {
213 if !parent.exists() {
215 return Err(DbError::Io(std::io::Error::new(
216 std::io::ErrorKind::NotFound,
217 format!(
218 "Directory does not exist and auto_provision is disabled: {}",
219 parent.display()
220 ),
221 )));
222 }
223 }
224
225 cfg.path = Some(absolute_path);
226 cfg.file = None; }
228
229 if let Some(path) = &cfg.path
231 && path.is_relative()
232 {
233 cfg.path = Some(module_home.join(path));
234 }
235
236 Ok(cfg)
237 }
238}