1pub mod schema;
13
14use crate::config::DbConfig;
15use crate::error::KipukaError;
16
17pub type Db = sqlx::AnyPool;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum DbKind {
27 Sqlite,
28 Postgres,
29 MariaDb,
30}
31
32impl DbKind {
33 pub fn from_url(url: &str) -> Self {
35 if url.starts_with("postgres") {
36 DbKind::Postgres
37 } else if url.starts_with("mariadb") || url.starts_with("mysql") {
38 DbKind::MariaDb
39 } else {
40 DbKind::Sqlite
41 }
42 }
43}
44
45static IS_POSTGRES: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
51
52#[allow(dead_code)]
58pub fn pg_sql(s: &'static str) -> &'static str {
59 if !IS_POSTGRES.get().copied().unwrap_or(false) {
60 return s;
61 }
62 static CACHE: std::sync::OnceLock<
63 std::sync::Mutex<std::collections::HashMap<usize, &'static str>>,
64 > = std::sync::OnceLock::new();
65 let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
66 let key = s.as_ptr() as usize;
67 {
68 let guard = cache.lock().unwrap();
69 if let Some(&cached) = guard.get(&key) {
70 return cached;
71 }
72 }
73 let mut result = String::with_capacity(s.len() + 16);
75 let mut param_num = 0u32;
76 for ch in s.chars() {
77 if ch == '?' {
78 param_num += 1;
79 result.push('$');
80 result.push_str(¶m_num.to_string());
81 } else {
82 result.push(ch);
83 }
84 }
85 let leaked: &'static str = Box::leak(result.into_boxed_str());
86 cache.lock().unwrap().insert(key, leaked);
87 leaked
88}
89
90#[allow(dead_code)]
96pub(crate) fn pg_sql_dynamic(s: String) -> String {
97 if !IS_POSTGRES.get().copied().unwrap_or(false) {
98 return s;
99 }
100 let mut result = String::with_capacity(s.len() + 16);
101 let mut param_num = 0u32;
102 for ch in s.chars() {
103 if ch == '?' {
104 param_num += 1;
105 result.push('$');
106 result.push_str(¶m_num.to_string());
107 } else {
108 result.push(ch);
109 }
110 }
111 result
112}
113
114pub async fn init_pool(config: &DbConfig, resolved_url: &str) -> Result<(Db, DbKind), KipukaError> {
118 sqlx::any::install_default_drivers();
124
125 let url = resolved_url.to_string();
126
127 let kind = DbKind::from_url(&url);
128 let _ = IS_POSTGRES.set(kind == DbKind::Postgres);
129
130 let pool_opts = sqlx::any::AnyPoolOptions::new()
131 .acquire_timeout(std::time::Duration::from_secs(config.connect_timeout_secs))
132 .max_lifetime(std::time::Duration::from_secs(config.max_lifetime_secs));
133
134 let in_memory =
135 kind == DbKind::Sqlite && (url.contains(":memory:") || url.contains("mode=memory"));
136 let pool_opts = if in_memory {
137 pool_opts
139 .max_connections(1)
140 .min_connections(1)
141 .max_lifetime(None)
142 .idle_timeout(None)
143 } else if let Some(max) = config.max_connections {
144 pool_opts.max_connections(max)
145 } else {
146 match kind {
147 DbKind::Sqlite => pool_opts.max_connections(1),
148 _ => pool_opts.max_connections(10),
149 }
150 };
151
152 let pool_opts = if let Some(min) = config.min_connections.filter(|_| !in_memory) {
153 pool_opts.min_connections(min)
154 } else {
155 pool_opts
156 };
157
158 let pool = pool_opts
159 .connect(&url)
160 .await
161 .map_err(|e| KipukaError::Db(format!("failed to connect to database: {e}")))?;
162
163 if kind == DbKind::Sqlite && config.sqlite_wal {
165 sqlx::query("PRAGMA journal_mode=WAL")
166 .execute(&pool)
167 .await
168 .map_err(|e| KipukaError::Db(format!("failed to enable WAL mode: {e}")))?;
169 }
170
171 Ok((pool, kind))
172}
173
174pub async fn init_ro_pool(
181 primary: &Db,
182 _config: &DbConfig,
183 kind: DbKind,
184 resolved_url: &str,
185) -> Result<Db, KipukaError> {
186 let url = resolved_url.to_string();
187
188 if kind != DbKind::Sqlite || (url.contains(":memory:") || url.contains("mode=memory")) {
190 return Ok(primary.clone());
191 }
192
193 let ro_url = if url.contains('?') {
195 format!("{url}&mode=ro")
196 } else {
197 format!("{url}?mode=ro")
198 };
199
200 let pool = sqlx::any::AnyPoolOptions::new()
201 .max_connections(4)
202 .connect(&ro_url)
203 .await
204 .map_err(|e| KipukaError::Db(format!("failed to connect RO pool: {e}")))?;
205
206 Ok(pool)
207}
208
209pub async fn begin_write(
214 pool: &Db,
215 kind: DbKind,
216) -> Result<sqlx::Transaction<'_, sqlx::Any>, KipukaError> {
217 let tx = if kind == DbKind::Sqlite {
218 pool.begin_with("BEGIN IMMEDIATE").await
219 } else {
220 pool.begin().await
221 }
222 .map_err(|e| KipukaError::Db(format!("begin transaction failed: {e}")))?;
223 Ok(tx)
224}
225
226pub async fn run_migrations(pool: &Db, kind: DbKind) -> Result<(), KipukaError> {
228 crate::db::schema::run_migrations(pool, kind).await
229}