1use crate::config::{Backend, DbkitConfig};
2use crate::DbkitError;
3use sqlx::migrate::MigrateDatabase;
4use sqlx::{Any, AnyPool, any::AnyPoolOptions};
5use std::time::Duration;
6use tracing::{info, warn};
7
8pub struct ConnectionManager {
14 pool: AnyPool,
15 backend: Backend,
16 db_name: String,
17 connection_string: String,
18 config: DbkitConfig,
19}
20
21impl ConnectionManager {
22 pub async fn connect(config: DbkitConfig) -> Result<Self, DbkitError> {
24 sqlx::any::install_default_drivers();
26
27 let backend = Backend::from_url(&config.url)?;
28 let db_name = Self::extract_db_name(&config.url, backend);
29 let connection_string = config.url.clone();
30
31 if config.auto_create_db {
32 Self::ensure_database(&config.url, &db_name).await?;
33 }
34
35 let pool = Self::build_pool(&config)
36 .await
37 .map_err(|e| Self::map_connect_error(e, &db_name))?;
38
39 info!("connected to database '{}' ({:?})", db_name, backend);
40
41 Ok(Self {
42 pool,
43 backend,
44 db_name,
45 connection_string,
46 config,
47 })
48 }
49
50 pub async fn new(url: &str) -> Result<Self, DbkitError> {
54 Self::connect(DbkitConfig::from_url(url)).await
55 }
56
57 pub fn pool(&self) -> &AnyPool {
59 &self.pool
60 }
61
62 pub fn backend(&self) -> Backend {
64 self.backend
65 }
66
67 pub async fn get_connection(&self) -> Result<sqlx::pool::PoolConnection<Any>, DbkitError> {
69 self.pool
70 .acquire()
71 .await
72 .map_err(|e| DbkitError::Pool(e.to_string()))
73 }
74
75 pub async fn is_connected(&self) -> bool {
77 self.pool.acquire().await.is_ok()
78 }
79
80 pub fn db_name(&self) -> &str {
82 &self.db_name
83 }
84
85 pub fn connection_string(&self) -> &str {
87 &self.connection_string
88 }
89
90 pub fn config(&self) -> &DbkitConfig {
92 &self.config
93 }
94
95 #[cfg(feature = "postgres-native")]
114 pub async fn pg_native_pool(&self) -> Result<sqlx::PgPool, DbkitError> {
115 if self.backend != Backend::Postgres {
116 return Err(DbkitError::UnsupportedBackend(format!(
117 "pg_native_pool requires a Postgres connection, got {:?}",
118 self.backend
119 )));
120 }
121 sqlx::postgres::PgPoolOptions::new()
122 .max_connections(self.config.pool_size as u32)
123 .acquire_timeout(Duration::from_secs(self.config.connect_timeout_secs))
124 .idle_timeout(Duration::from_secs(self.config.idle_timeout_secs))
125 .connect(&self.connection_string)
126 .await
127 .map_err(|e| DbkitError::Pool(e.to_string()))
128 }
129
130 pub fn pool_status(&self) -> PoolStatus {
132 PoolStatus {
133 max_size: self.pool.options().get_max_connections() as usize,
134 size: self.pool.size() as usize,
135 idle: self.pool.num_idle(),
136 }
137 }
138
139 async fn build_pool(config: &DbkitConfig) -> Result<AnyPool, sqlx::Error> {
140 AnyPoolOptions::new()
141 .max_connections(config.pool_size as u32)
142 .acquire_timeout(Duration::from_secs(config.connect_timeout_secs))
143 .idle_timeout(Duration::from_secs(config.idle_timeout_secs))
144 .connect(&config.url)
145 .await
146 }
147
148 async fn ensure_database(url: &str, db_name: &str) -> Result<(), DbkitError> {
153 let exists = Any::database_exists(url).await.map_err(|e| {
154 DbkitError::DatabaseCreation {
155 name: db_name.to_string(),
156 reason: e.to_string(),
157 }
158 })?;
159
160 if !exists {
161 warn!("database '{}' does not exist, creating...", db_name);
162 Any::create_database(url)
163 .await
164 .map_err(|e| DbkitError::DatabaseCreation {
165 name: db_name.to_string(),
166 reason: e.to_string(),
167 })?;
168 info!("database '{}' created", db_name);
169 }
170
171 Ok(())
172 }
173
174 fn map_connect_error(e: sqlx::Error, db_name: &str) -> DbkitError {
177 if let sqlx::Error::Database(ref db) = e {
178 match db.code().as_deref() {
179 Some("28P01") | Some("28000") => return DbkitError::AuthFailed,
181 Some("53300") => return DbkitError::TooManyConnections,
183 _ => {}
184 }
185 }
186 DbkitError::Connection(format!("could not connect to '{}': {}", db_name, e))
187 }
188
189 fn extract_db_name(url: &str, backend: Backend) -> String {
190 match backend {
191 Backend::Sqlite => url
193 .splitn(2, ':')
194 .nth(1)
195 .unwrap_or("")
196 .trim_start_matches("//")
197 .split('?')
198 .next()
199 .unwrap_or("")
200 .to_string(),
201 _ => url
203 .rsplit('/')
204 .next()
205 .unwrap_or("")
206 .split('?')
207 .next()
208 .unwrap_or("")
209 .to_string(),
210 }
211 }
212}
213
214#[derive(Debug, Clone)]
216pub struct PoolStatus {
217 pub max_size: usize,
219 pub size: usize,
221 pub idle: usize,
223}
224
225impl std::fmt::Display for PoolStatus {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 write!(
228 f,
229 "pool: {}/{} connections, {} idle",
230 self.size, self.max_size, self.idle
231 )
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn extract_db_name_server() {
241 assert_eq!(
242 ConnectionManager::extract_db_name("postgres://u:p@host:5432/myapp", Backend::Postgres),
243 "myapp"
244 );
245 assert_eq!(
246 ConnectionManager::extract_db_name(
247 "mysql://host:3306/app?ssl-mode=required",
248 Backend::MySql
249 ),
250 "app"
251 );
252 }
253
254 #[test]
255 fn extract_db_name_sqlite() {
256 assert_eq!(
257 ConnectionManager::extract_db_name("sqlite://data/app.db", Backend::Sqlite),
258 "data/app.db"
259 );
260 }
261}