1use crate::error::{DbError, DbResult};
3use serde::{Deserialize, Serialize};
4use sqlx::any::AnyConnectOptions;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
8pub enum DatabaseType {
9 Postgres,
10 Mysql,
11 Sqlite,
12}
13
14impl Default for DatabaseType {
15 fn default() -> Self {
16 DatabaseType::Postgres }
18}
19
20impl FromStr for DatabaseType {
21 type Err = DbError;
22
23 fn from_str(s: &str) -> Result<Self, Self::Err> {
24 match s.to_lowercase().as_str() {
25 "postgres" | "postgresql" => Ok(DatabaseType::Postgres),
26 "mysql" | "mariadb" => Ok(DatabaseType::Mysql),
27 "sqlite" => Ok(DatabaseType::Sqlite),
28 _ => Err(DbError::Config(format!("Unsupported database type: {}", s))),
29 }
30 }
31}
32
33impl std::fmt::Display for DatabaseType {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 DatabaseType::Postgres => write!(f, "PostgreSQL"),
37 DatabaseType::Mysql => write!(f, "MySQL/MariaDB"),
38 DatabaseType::Sqlite => write!(f, "SQLite"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44pub struct PoolOptionsConfig {
45 pub max_connections: Option<u32>,
46 pub min_connections: Option<u32>,
47 pub connect_timeout_seconds: Option<u64>,
48 pub idle_timeout_seconds: Option<u64>,
49 pub max_lifetime_seconds: Option<u64>,
50 pub acquire_timeout_seconds: Option<u64>,
51 pub test_before_acquire: Option<bool>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct DbConfig {
56 pub db_type: DatabaseType,
57 pub host: Option<String>,
58 pub port: Option<u16>,
59 pub username: Option<String>,
60 pub password: Option<String>,
61 pub database_name: Option<String>,
62 pub schema: Option<String>, pub connection_string: Option<String>,
64 pub pool_options: Option<PoolOptionsConfig>,
65 pub sqlite_path: Option<String>,
67}
68
69impl DbConfig {
70 pub fn new(db_type: DatabaseType) -> Self {
71 Self {
72 db_type,
73 ..Default::default()
74 }
75 }
76
77 pub fn host(mut self, host: impl Into<String>) -> Self {
78 self.host = Some(host.into());
79 self
80 }
81
82 pub fn port(mut self, port: u16) -> Self {
83 self.port = Some(port);
84 self
85 }
86
87 pub fn username(mut self, username: impl Into<String>) -> Self {
88 self.username = Some(username.into());
89 self
90 }
91
92 pub fn password(mut self, password: impl Into<String>) -> Self {
93 self.password = Some(password.into());
94 self
95 }
96
97 pub fn database_name(mut self, database_name: impl Into<String>) -> Self {
98 self.database_name = Some(database_name.into());
99 self
100 }
101
102 pub fn schema(mut self, schema: impl Into<String>) -> Self {
103 self.schema = Some(schema.into());
104 self
105 }
106
107 pub fn connection_string(mut self, cs: impl Into<String>) -> Self {
108 self.connection_string = Some(cs.into());
109 self
110 }
111
112 pub fn pool_options(mut self, pool_opts: PoolOptionsConfig) -> Self {
113 self.pool_options = Some(pool_opts);
114 self
115 }
116
117 pub fn build_connection_string(&self) -> DbResult<String> {
119 if let Some(cs) = &self.connection_string {
120 return Ok(cs.clone());
121 }
122
123 match self.db_type {
124 DatabaseType::Postgres => Ok(format!(
125 "postgresql://{}:{}@{}:{}/{}",
126 self.username
127 .as_deref()
128 .ok_or_else(|| DbError::Config("Missing username for Postgres".to_string()))?,
129 self.password
130 .as_deref()
131 .ok_or_else(|| DbError::Config("Missing password for Postgres".to_string()))?,
132 self.host
133 .as_deref()
134 .ok_or_else(|| DbError::Config("Missing host for Postgres".to_string()))?,
135 self.port
136 .ok_or_else(|| DbError::Config("Missing port for Postgres".to_string()))?,
137 self.database_name
138 .as_deref()
139 .ok_or_else(|| DbError::Config(
140 "Missing database_name for Postgres".to_string()
141 ))?
142 )),
143 DatabaseType::Mysql => Ok(format!(
144 "mysql://{}:{}@{}:{}/{}",
145 self.username
146 .as_deref()
147 .ok_or_else(|| DbError::Config("Missing username for MySQL".to_string()))?,
148 self.password
149 .as_deref()
150 .ok_or_else(|| DbError::Config("Missing password for MySQL".to_string()))?,
151 self.host
152 .as_deref()
153 .ok_or_else(|| DbError::Config("Missing host for MySQL".to_string()))?,
154 self.port
155 .ok_or_else(|| DbError::Config("Missing port for MySQL".to_string()))?,
156 self.database_name
157 .as_deref()
158 .ok_or_else(|| DbError::Config(
159 "Missing database_name for MySQL".to_string()
160 ))?
161 )),
162 DatabaseType::Sqlite => {
163 let path = self
164 .sqlite_path
165 .as_deref()
166 .ok_or_else(|| DbError::Config("Missing sqlite_path for SQLite".to_string()))?;
167 if path.is_empty() {
168 Ok("sqlite::memory:".to_string()) } else {
170 Ok(format!("sqlite:{}", path))
171 }
172 }
173 }
174 }
175
176 pub fn to_sqlx_any_options(&self) -> DbResult<sqlx::any::AnyConnectOptions> {
177 let cs = self.build_connection_string()?;
178 AnyConnectOptions::from_str(&cs).map_err(|e| {
179 DbError::Config(format!("Failed to parse connection string for sqlx: {}", e))
180 })
181 }
182}