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