Skip to main content

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, Default)]
28pub enum DatabasePath {
29    /// In-memory database.
30    #[default]
31    Memory,
32    /// File-based database.
33    File(PathBuf),
34}
35
36impl DatabasePath {
37    /// Get the path string for SQLite.
38    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    /// Check if this is an in-memory database.
46    pub fn is_memory(&self) -> bool {
47        matches!(self, Self::Memory)
48    }
49}
50
51/// SQLite synchronous mode.
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub enum SynchronousMode {
54    /// Synchronous OFF - Fastest but unsafe.
55    Off,
56    /// Synchronous NORMAL - Good balance.
57    #[default]
58    Normal,
59    /// Synchronous FULL - Safe but slower.
60    Full,
61    /// Synchronous EXTRA - Maximum safety.
62    Extra,
63}
64
65impl SynchronousMode {
66    /// Get the SQLite pragma value.
67    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/// SQLite journal mode.
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub enum JournalMode {
80    /// DELETE - Default mode, deletes journal after transaction.
81    Delete,
82    /// TRUNCATE - Truncates journal instead of deleting.
83    Truncate,
84    /// PERSIST - Keep journal file, zero out on commit.
85    Persist,
86    /// MEMORY - Keep journal in memory.
87    Memory,
88    /// WAL - Write-Ahead Logging (best for concurrent access).
89    #[default]
90    Wal,
91    /// OFF - No journal (dangerous).
92    Off,
93}
94
95impl JournalMode {
96    /// Get the SQLite pragma value.
97    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), // 2MB cache
117            synchronous: SynchronousMode::Normal,
118            journal_mode: JournalMode::Wal,
119        }
120    }
121}
122
123impl SqliteConfig {
124    /// Create a new configuration for an in-memory database.
125    pub fn memory() -> Self {
126        Self {
127            path: DatabasePath::Memory,
128            ..Default::default()
129        }
130    }
131
132    /// Create a new configuration for a file-based database.
133    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    /// Parse a SQLite URL into configuration.
141    ///
142    /// Supported formats:
143    /// - `sqlite::memory:` - In-memory database
144    /// - `sqlite://path/to/db.sqlite` - File-based database
145    /// - `sqlite:///absolute/path/db.sqlite` - Absolute path
146    /// - `file:path/to/db.sqlite` - Alternative format
147    pub fn from_url(url: impl AsRef<str>) -> SqliteResult<Self> {
148        let url_str = url.as_ref();
149
150        // Handle special memory URL
151        if url_str == "sqlite::memory:" || url_str == ":memory:" {
152            return Ok(Self::memory());
153        }
154
155        // Parse the URL
156        let path = if let Some(path_part) = url_str.strip_prefix("sqlite://") {
157            // Handle query parameters
158            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            // Assume it's a direct file path
174            url_str.to_string()
175        };
176
177        let mut config = Self::file(&path);
178
179        // Parse query parameters if present
180        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    /// Get the path string for SQLite.
234    pub fn path_str(&self) -> &str {
235        self.path.as_str()
236    }
237
238    /// Generate the initialization SQL for this configuration.
239    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    /// Set the database path.
268    pub fn path(mut self, path: DatabasePath) -> Self {
269        self.path = path;
270        self
271    }
272
273    /// Enable or disable foreign keys.
274    pub fn foreign_keys(mut self, enabled: bool) -> Self {
275        self.foreign_keys = enabled;
276        self
277    }
278
279    /// Enable or disable WAL mode.
280    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    /// Set the busy timeout in milliseconds.
289    pub fn busy_timeout(mut self, ms: u32) -> Self {
290        self.busy_timeout_ms = Some(ms);
291        self
292    }
293
294    /// Set the cache size.
295    pub fn cache_size(mut self, size: i32) -> Self {
296        self.cache_size = Some(size);
297        self
298    }
299
300    /// Set the synchronous mode.
301    pub fn synchronous(mut self, mode: SynchronousMode) -> Self {
302        self.synchronous = mode;
303        self
304    }
305
306    /// Set the journal mode.
307    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}