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