prax_sqlite/
config.rs

1//! SQLite configuration.
2
3use std::path::{Path, PathBuf};
4
5use crate::error::{SqliteError, SqliteResult};
6
7/// SQLite database configuration.
8#[derive(Debug, Clone)]
9pub struct SqliteConfig {
10    /// Database path (or ":memory:" for in-memory).
11    pub path: DatabasePath,
12    /// Enable foreign keys.
13    pub foreign_keys: bool,
14    /// Enable WAL mode.
15    pub wal_mode: bool,
16    /// Busy timeout in milliseconds.
17    pub busy_timeout_ms: Option<u32>,
18    /// Cache size (in pages, negative for KB).
19    pub cache_size: Option<i32>,
20    /// Synchronous mode.
21    pub synchronous: SynchronousMode,
22    /// Journal mode.
23    pub journal_mode: JournalMode,
24}
25
26/// Database path configuration.
27#[derive(Debug, Clone)]
28pub enum DatabasePath {
29    /// In-memory database.
30    Memory,
31    /// File-based database.
32    File(PathBuf),
33}
34
35impl DatabasePath {
36    /// Get the path string for SQLite.
37    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    /// Check if this is an in-memory database.
45    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/// SQLite synchronous mode.
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58pub enum SynchronousMode {
59    /// Synchronous OFF - Fastest but unsafe.
60    Off,
61    /// Synchronous NORMAL - Good balance.
62    #[default]
63    Normal,
64    /// Synchronous FULL - Safe but slower.
65    Full,
66    /// Synchronous EXTRA - Maximum safety.
67    Extra,
68}
69
70impl SynchronousMode {
71    /// Get the SQLite pragma value.
72    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/// SQLite journal mode.
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
84pub enum JournalMode {
85    /// DELETE - Default mode, deletes journal after transaction.
86    Delete,
87    /// TRUNCATE - Truncates journal instead of deleting.
88    Truncate,
89    /// PERSIST - Keep journal file, zero out on commit.
90    Persist,
91    /// MEMORY - Keep journal in memory.
92    Memory,
93    /// WAL - Write-Ahead Logging (best for concurrent access).
94    #[default]
95    Wal,
96    /// OFF - No journal (dangerous).
97    Off,
98}
99
100impl JournalMode {
101    /// Get the SQLite pragma value.
102    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), // 2MB cache
122            synchronous: SynchronousMode::Normal,
123            journal_mode: JournalMode::Wal,
124        }
125    }
126}
127
128impl SqliteConfig {
129    /// Create a new configuration for an in-memory database.
130    pub fn memory() -> Self {
131        Self {
132            path: DatabasePath::Memory,
133            ..Default::default()
134        }
135    }
136
137    /// Create a new configuration for a file-based database.
138    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    /// Parse a SQLite URL into configuration.
146    ///
147    /// Supported formats:
148    /// - `sqlite::memory:` - In-memory database
149    /// - `sqlite://path/to/db.sqlite` - File-based database
150    /// - `sqlite:///absolute/path/db.sqlite` - Absolute path
151    /// - `file:path/to/db.sqlite` - Alternative format
152    pub fn from_url(url: impl AsRef<str>) -> SqliteResult<Self> {
153        let url_str = url.as_ref();
154
155        // Handle special memory URL
156        if url_str == "sqlite::memory:" || url_str == ":memory:" {
157            return Ok(Self::memory());
158        }
159
160        // Parse the URL
161        let path = if url_str.starts_with("sqlite://") {
162            let path_part = &url_str["sqlite://".len()..];
163            // Handle query parameters
164            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            // Assume it's a direct file path
182            url_str.to_string()
183        };
184
185        let mut config = Self::file(&path);
186
187        // Parse query parameters if present
188        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    /// Get the path string for SQLite.
242    pub fn path_str(&self) -> &str {
243        self.path.as_str()
244    }
245
246    /// Generate the initialization SQL for this configuration.
247    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    /// Set the database path.
276    pub fn path(mut self, path: DatabasePath) -> Self {
277        self.path = path;
278        self
279    }
280
281    /// Enable or disable foreign keys.
282    pub fn foreign_keys(mut self, enabled: bool) -> Self {
283        self.foreign_keys = enabled;
284        self
285    }
286
287    /// Enable or disable WAL mode.
288    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    /// Set the busy timeout in milliseconds.
297    pub fn busy_timeout(mut self, ms: u32) -> Self {
298        self.busy_timeout_ms = Some(ms);
299        self
300    }
301
302    /// Set the cache size.
303    pub fn cache_size(mut self, size: i32) -> Self {
304        self.cache_size = Some(size);
305        self
306    }
307
308    /// Set the synchronous mode.
309    pub fn synchronous(mut self, mode: SynchronousMode) -> Self {
310        self.synchronous = mode;
311        self
312    }
313
314    /// Set the journal mode.
315    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}