1use crate::DbPool;
5use crate::error::DbError;
6
7pub struct DbConfig {
9 pub url: String,
11 pub max_connections: u32,
13 pub pool_size: u32,
18}
19
20impl Default for DbConfig {
21 fn default() -> Self {
22 Self {
23 url: String::new(),
24 max_connections: 5,
25 pool_size: 5,
26 }
27 }
28}
29
30impl DbConfig {
31 pub async fn connect(&self) -> Result<DbPool, DbError> {
37 #[cfg(feature = "sqlite")]
38 {
39 Self::connect_sqlite(&self.url, self.max_connections, self.pool_size).await
40 }
41 #[cfg(feature = "postgres")]
42 {
43 Self::connect_postgres(&self.url, self.pool_size).await
44 }
45 }
46
47 #[cfg(feature = "sqlite")]
48 async fn connect_sqlite(
49 path: &str,
50 max_connections: u32,
51 pool_size: u32,
52 ) -> Result<DbPool, DbError> {
53 use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
54 use std::str::FromStr;
55
56 let url = if path == ":memory:" {
57 "sqlite::memory:".to_string()
58 } else {
59 if let Some(parent) = std::path::Path::new(path).parent()
60 && !parent.as_os_str().is_empty()
61 {
62 std::fs::create_dir_all(parent)?;
63 }
64 format!("sqlite:{path}?mode=rwc")
65 };
66
67 let opts = SqliteConnectOptions::from_str(&url)
68 .map_err(DbError::Sqlx)?
69 .create_if_missing(true)
70 .foreign_keys(true)
71 .busy_timeout(std::time::Duration::from_secs(5))
72 .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
73 .synchronous(sqlx::sqlite::SqliteSynchronous::Normal);
74
75 let effective_max = max_connections.max(pool_size);
78 let pool = SqlitePoolOptions::new()
79 .max_connections(effective_max)
80 .min_connections(1)
81 .acquire_timeout(std::time::Duration::from_secs(30))
82 .connect_with(opts)
83 .await
84 .map_err(DbError::Sqlx)?;
85
86 crate::migrate::run_migrations(&pool).await?;
87
88 #[cfg(unix)]
90 if path != ":memory:" {
91 use std::os::unix::fs::PermissionsExt;
92 if let Ok(metadata) = std::fs::metadata(path) {
93 let mut perms = metadata.permissions();
94 perms.set_mode(0o600);
95 let _ = std::fs::set_permissions(path, perms);
96 }
97 }
98
99 if path != ":memory:" {
102 sqlx::query("PRAGMA wal_checkpoint(PASSIVE)")
103 .execute(&pool)
104 .await
105 .map_err(DbError::Sqlx)?;
106 }
107
108 Ok(pool)
109 }
110
111 #[cfg(feature = "postgres")]
112 async fn connect_postgres(url: &str, pool_size: u32) -> Result<DbPool, DbError> {
113 use sqlx::postgres::PgPoolOptions;
114
115 if !url.contains("sslmode=") {
116 tracing::warn!(
117 "postgres connection string has no sslmode; plaintext connections are allowed"
118 );
119 }
120
121 let pool = PgPoolOptions::new()
122 .max_connections(pool_size)
123 .acquire_timeout(std::time::Duration::from_secs(30))
124 .connect(url)
125 .await
126 .map_err(|e| DbError::Connection {
127 url: redact_url(url).unwrap_or_else(|| "[redacted]".into()),
128 source: e,
129 })?;
130
131 crate::migrate::run_migrations(&pool).await?;
132
133 Ok(pool)
134 }
135}
136
137#[must_use]
144pub fn redact_url(url: &str) -> Option<String> {
145 use std::sync::LazyLock;
146 static RE: LazyLock<regex::Regex> =
147 LazyLock::new(|| regex::Regex::new(r"://[^:]+:[^@]+@").expect("static regex"));
148 if RE.is_match(url) {
149 Some(RE.replace(url, "://[redacted]@").into_owned())
150 } else {
151 None
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn redact_url_replaces_credentials() {
161 let url = "postgres://user:secret@localhost:5432/zeph";
162 let redacted = redact_url(url).unwrap();
163 assert_eq!(redacted, "postgres://[redacted]@localhost:5432/zeph");
164 assert!(!redacted.contains("secret"));
165 }
166
167 #[test]
168 fn redact_url_returns_none_for_no_credentials() {
169 let url = "postgres://localhost:5432/zeph";
171 assert!(redact_url(url).is_none());
172 }
173
174 #[test]
175 fn redact_url_handles_sqlite_path() {
176 let url = "sqlite:/path/to/db";
177 assert!(redact_url(url).is_none());
178 }
179}