1use std::path::{Path, PathBuf};
4
5use crate::error::{SqliteError, SqliteResult};
6
7#[derive(Debug, Clone)]
9pub struct SqliteConfig {
10 pub path: DatabasePath,
12 pub foreign_keys: bool,
14 pub wal_mode: bool,
16 pub busy_timeout_ms: Option<u32>,
18 pub cache_size: Option<i32>,
20 pub synchronous: SynchronousMode,
22 pub journal_mode: JournalMode,
24}
25
26#[derive(Debug, Clone, Default)]
28pub enum DatabasePath {
29 #[default]
31 Memory,
32 File(PathBuf),
34}
35
36impl DatabasePath {
37 pub fn as_str(&self) -> &str {
39 match self {
40 Self::Memory => ":memory:",
41 Self::File(path) => path.to_str().unwrap_or(":memory:"),
42 }
43 }
44
45 pub fn is_memory(&self) -> bool {
47 matches!(self, Self::Memory)
48 }
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub enum SynchronousMode {
54 Off,
56 #[default]
58 Normal,
59 Full,
61 Extra,
63}
64
65impl SynchronousMode {
66 pub fn as_pragma(&self) -> &'static str {
68 match self {
69 Self::Off => "OFF",
70 Self::Normal => "NORMAL",
71 Self::Full => "FULL",
72 Self::Extra => "EXTRA",
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub enum JournalMode {
80 Delete,
82 Truncate,
84 Persist,
86 Memory,
88 #[default]
90 Wal,
91 Off,
93}
94
95impl JournalMode {
96 pub fn as_pragma(&self) -> &'static str {
98 match self {
99 Self::Delete => "DELETE",
100 Self::Truncate => "TRUNCATE",
101 Self::Persist => "PERSIST",
102 Self::Memory => "MEMORY",
103 Self::Wal => "WAL",
104 Self::Off => "OFF",
105 }
106 }
107}
108
109impl Default for SqliteConfig {
110 fn default() -> Self {
111 Self {
112 path: DatabasePath::Memory,
113 foreign_keys: true,
114 wal_mode: true,
115 busy_timeout_ms: Some(5000),
116 cache_size: Some(-2000), synchronous: SynchronousMode::Normal,
118 journal_mode: JournalMode::Wal,
119 }
120 }
121}
122
123impl SqliteConfig {
124 pub fn memory() -> Self {
126 Self {
127 path: DatabasePath::Memory,
128 ..Default::default()
129 }
130 }
131
132 pub fn file(path: impl AsRef<Path>) -> Self {
134 Self {
135 path: DatabasePath::File(path.as_ref().to_path_buf()),
136 ..Default::default()
137 }
138 }
139
140 pub fn from_url(url: impl AsRef<str>) -> SqliteResult<Self> {
148 let url_str = url.as_ref();
149
150 if url_str == "sqlite::memory:" || url_str == ":memory:" {
152 return Ok(Self::memory());
153 }
154
155 let path = if let Some(path_part) = url_str.strip_prefix("sqlite://") {
157 let path_only = path_part.split('?').next().unwrap_or(path_part);
159 if path_only.is_empty() {
160 return Err(SqliteError::config("database path is required"));
161 }
162 path_only.to_string()
163 } else if let Some(path_part) = url_str.strip_prefix("sqlite:") {
164 let path_only = path_part.split('?').next().unwrap_or(path_part);
165 if path_only == ":memory:" {
166 return Ok(Self::memory());
167 }
168 path_only.to_string()
169 } else if let Some(path_part) = url_str.strip_prefix("file:") {
170 let path_only = path_part.split('?').next().unwrap_or(path_part);
171 path_only.to_string()
172 } else {
173 url_str.to_string()
175 };
176
177 let mut config = Self::file(&path);
178
179 if let Some(query_start) = url_str.find('?') {
181 let query = &url_str[query_start + 1..];
182 for pair in query.split('&') {
183 if let Some((key, value)) = pair.split_once('=') {
184 match key {
185 "mode" if value == "memory" => {
186 config.path = DatabasePath::Memory;
187 }
188 "foreign_keys" => {
189 config.foreign_keys = value == "true" || value == "1";
190 }
191 "wal_mode" => {
192 config.wal_mode = value == "true" || value == "1";
193 }
194 "busy_timeout" => {
195 if let Ok(ms) = value.parse() {
196 config.busy_timeout_ms = Some(ms);
197 }
198 }
199 "cache_size" => {
200 if let Ok(size) = value.parse() {
201 config.cache_size = Some(size);
202 }
203 }
204 "synchronous" => {
205 config.synchronous = match value.to_lowercase().as_str() {
206 "off" => SynchronousMode::Off,
207 "normal" => SynchronousMode::Normal,
208 "full" => SynchronousMode::Full,
209 "extra" => SynchronousMode::Extra,
210 _ => SynchronousMode::Normal,
211 };
212 }
213 "journal_mode" => {
214 config.journal_mode = match value.to_lowercase().as_str() {
215 "delete" => JournalMode::Delete,
216 "truncate" => JournalMode::Truncate,
217 "persist" => JournalMode::Persist,
218 "memory" => JournalMode::Memory,
219 "wal" => JournalMode::Wal,
220 "off" => JournalMode::Off,
221 _ => JournalMode::Wal,
222 };
223 }
224 _ => {}
225 }
226 }
227 }
228 }
229
230 Ok(config)
231 }
232
233 pub fn path_str(&self) -> &str {
235 self.path.as_str()
236 }
237
238 pub fn init_sql(&self) -> String {
240 let mut sql = String::new();
241
242 if self.foreign_keys {
243 sql.push_str("PRAGMA foreign_keys = ON;\n");
244 }
245
246 sql.push_str(&format!(
247 "PRAGMA journal_mode = {};\n",
248 self.journal_mode.as_pragma()
249 ));
250
251 sql.push_str(&format!(
252 "PRAGMA synchronous = {};\n",
253 self.synchronous.as_pragma()
254 ));
255
256 if let Some(timeout) = self.busy_timeout_ms {
257 sql.push_str(&format!("PRAGMA busy_timeout = {};\n", timeout));
258 }
259
260 if let Some(cache) = self.cache_size {
261 sql.push_str(&format!("PRAGMA cache_size = {};\n", cache));
262 }
263
264 sql
265 }
266
267 pub fn path(mut self, path: DatabasePath) -> Self {
269 self.path = path;
270 self
271 }
272
273 pub fn foreign_keys(mut self, enabled: bool) -> Self {
275 self.foreign_keys = enabled;
276 self
277 }
278
279 pub fn wal_mode(mut self, enabled: bool) -> Self {
281 self.wal_mode = enabled;
282 if enabled {
283 self.journal_mode = JournalMode::Wal;
284 }
285 self
286 }
287
288 pub fn busy_timeout(mut self, ms: u32) -> Self {
290 self.busy_timeout_ms = Some(ms);
291 self
292 }
293
294 pub fn cache_size(mut self, size: i32) -> Self {
296 self.cache_size = Some(size);
297 self
298 }
299
300 pub fn synchronous(mut self, mode: SynchronousMode) -> Self {
302 self.synchronous = mode;
303 self
304 }
305
306 pub fn journal_mode(mut self, mode: JournalMode) -> Self {
308 self.journal_mode = mode;
309 self
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_config_memory() {
319 let config = SqliteConfig::memory();
320 assert!(config.path.is_memory());
321 assert_eq!(config.path.as_str(), ":memory:");
322 }
323
324 #[test]
325 fn test_config_file() {
326 let config = SqliteConfig::file("test.db");
327 assert!(!config.path.is_memory());
328 assert_eq!(config.path.as_str(), "test.db");
329 }
330
331 #[test]
332 fn test_config_from_url_memory() {
333 let config = SqliteConfig::from_url("sqlite::memory:").unwrap();
334 assert!(config.path.is_memory());
335
336 let config = SqliteConfig::from_url(":memory:").unwrap();
337 assert!(config.path.is_memory());
338 }
339
340 #[test]
341 fn test_config_from_url_file() {
342 let config = SqliteConfig::from_url("sqlite://./test.db").unwrap();
343 assert!(!config.path.is_memory());
344 assert_eq!(config.path.as_str(), "./test.db");
345 }
346
347 #[test]
348 fn test_config_from_url_with_options() {
349 let config = SqliteConfig::from_url(
350 "sqlite://./test.db?foreign_keys=true&busy_timeout=10000&synchronous=full",
351 )
352 .unwrap();
353
354 assert!(config.foreign_keys);
355 assert_eq!(config.busy_timeout_ms, Some(10000));
356 assert_eq!(config.synchronous, SynchronousMode::Full);
357 }
358
359 #[test]
360 fn test_init_sql() {
361 let config = SqliteConfig::default();
362 let sql = config.init_sql();
363
364 assert!(sql.contains("foreign_keys = ON"));
365 assert!(sql.contains("journal_mode = WAL"));
366 assert!(sql.contains("synchronous = NORMAL"));
367 }
368
369 #[test]
370 fn test_builder_pattern() {
371 let config = SqliteConfig::memory()
372 .foreign_keys(false)
373 .busy_timeout(3000)
374 .synchronous(SynchronousMode::Full)
375 .journal_mode(JournalMode::Memory);
376
377 assert!(!config.foreign_keys);
378 assert_eq!(config.busy_timeout_ms, Some(3000));
379 assert_eq!(config.synchronous, SynchronousMode::Full);
380 assert_eq!(config.journal_mode, JournalMode::Memory);
381 }
382
383 #[test]
384 fn test_synchronous_mode_pragma() {
385 assert_eq!(SynchronousMode::Off.as_pragma(), "OFF");
386 assert_eq!(SynchronousMode::Normal.as_pragma(), "NORMAL");
387 assert_eq!(SynchronousMode::Full.as_pragma(), "FULL");
388 assert_eq!(SynchronousMode::Extra.as_pragma(), "EXTRA");
389 }
390
391 #[test]
392 fn test_journal_mode_pragma() {
393 assert_eq!(JournalMode::Delete.as_pragma(), "DELETE");
394 assert_eq!(JournalMode::Wal.as_pragma(), "WAL");
395 assert_eq!(JournalMode::Memory.as_pragma(), "MEMORY");
396 }
397}