axion_db/
config.rs

1// axion-db/src/config.rs
2use 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 // Default to Postgres
17    }
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>, // Default/current schema
63    pub connection_string: Option<String>,
64    pub pool_options: Option<PoolOptionsConfig>,
65    // For SQLite, this would be the file path
66    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    /// Builds the connection string or returns an error if essential parts are missing.
118    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()) // In-memory if path is empty
169                } 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}