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