1#[cfg(not(target_os = "linux"))]
13use std::env;
14use std::{
15 fs::{remove_dir_all, remove_file},
16 path::{Path, PathBuf},
17};
18
19use uuid::Uuid;
20
21#[cfg(not(target_arch = "wasm32"))]
22pub mod connection;
23#[cfg(not(target_arch = "wasm32"))]
24pub mod error;
25#[cfg(not(target_arch = "wasm32"))]
26pub mod pragma;
27
28#[derive(Debug, Clone, Eq, PartialEq)]
29pub enum DbPath {
30 File(PathBuf),
31
32 Tmpfs(PathBuf),
33
34 Memory(PathBuf),
35}
36
37#[derive(Debug)]
47pub struct SqliteTempPathGuard {
48 base_path: Option<PathBuf>,
49}
50
51impl SqliteTempPathGuard {
52 pub fn new(path: &DbPath) -> Self {
53 let base_path = match path {
54 DbPath::Memory(p) | DbPath::Tmpfs(p) => Some(p.clone()),
55 DbPath::File(_) => None,
56 };
57 Self {
58 base_path,
59 }
60 }
61
62 pub fn disarm(&mut self) {
65 self.base_path = None;
66 }
67}
68
69impl Drop for SqliteTempPathGuard {
70 fn drop(&mut self) {
71 let Some(base) = self.base_path.take() else {
72 return;
73 };
74 let _ = remove_file(&base);
75 for suffix in ["-shm", "-wal", "-journal"] {
76 let mut companion = base.clone().into_os_string();
77 companion.push(suffix);
78 let _ = remove_file(PathBuf::from(companion));
79 }
80 let derived_dir = base.with_extension("");
81 if derived_dir != base {
82 let _ = remove_dir_all(&derived_dir);
83 }
84 }
85}
86
87fn memory_dir() -> PathBuf {
88 #[cfg(target_os = "linux")]
89 {
90 PathBuf::from("/dev/shm")
91 }
92 #[cfg(not(target_os = "linux"))]
93 {
94 env::temp_dir()
95 }
96}
97
98#[derive(Debug, Clone)]
99pub struct SqliteConfig {
100 pub path: DbPath,
101 pub flags: OpenFlags,
102 pub journal_mode: JournalMode,
103 pub synchronous_mode: SynchronousMode,
104 pub temp_store: TempStore,
105 pub cache_size: u32,
106 pub wal_autocheckpoint: u32,
107 pub page_size: u32,
108 pub mmap_size: u64,
109 pub prepared_statement_cache_capacity: u32,
110 pub read_pool_size: u32,
111}
112
113impl SqliteConfig {
114 pub fn new<P: AsRef<Path>>(path: P) -> Self {
115 Self {
116 path: DbPath::File(path.as_ref().to_path_buf()),
117 flags: OpenFlags::default(),
118 journal_mode: JournalMode::Wal,
119 synchronous_mode: SynchronousMode::Normal,
120 temp_store: TempStore::Memory,
121 cache_size: 2000,
122 wal_autocheckpoint: 1000,
123 page_size: 4096,
124 mmap_size: 64 * 1024 * 1024,
125 prepared_statement_cache_capacity: 1024,
126 read_pool_size: 4,
127 }
128 }
129
130 pub fn safe<P: AsRef<Path>>(path: P) -> Self {
131 Self {
132 path: DbPath::File(path.as_ref().to_path_buf()),
133 flags: OpenFlags::default(),
134 journal_mode: JournalMode::Wal,
135 synchronous_mode: SynchronousMode::Full,
136 temp_store: TempStore::File,
137 cache_size: 2000,
138 wal_autocheckpoint: 1000,
139 page_size: 4096,
140 mmap_size: 0,
141 prepared_statement_cache_capacity: 128,
142 read_pool_size: 4,
143 }
144 }
145
146 pub fn fast<P: AsRef<Path>>(path: P) -> Self {
147 Self {
148 path: DbPath::File(path.as_ref().to_path_buf()),
149 flags: OpenFlags::default(),
150 journal_mode: JournalMode::Wal,
151 synchronous_mode: SynchronousMode::Off,
152 temp_store: TempStore::Memory,
153 cache_size: 10000,
154 wal_autocheckpoint: 10000,
155 page_size: 16384,
156 mmap_size: 256 * 1024 * 1024,
157 prepared_statement_cache_capacity: 256,
158 read_pool_size: 8,
159 }
160 }
161
162 pub fn tmpfs() -> Self {
163 Self {
164 path: DbPath::Tmpfs(PathBuf::from(format!("/tmp/reifydb_{}.db", Uuid::new_v4()))),
165 flags: OpenFlags::default(),
166 journal_mode: JournalMode::Wal,
167 synchronous_mode: SynchronousMode::Off,
168 temp_store: TempStore::Memory,
169 cache_size: 2000,
170 wal_autocheckpoint: 10000,
171 page_size: 16384,
172 mmap_size: 0,
173 prepared_statement_cache_capacity: 128,
174 read_pool_size: 4,
175 }
176 }
177
178 pub fn in_memory() -> (Self, SqliteTempPathGuard) {
179 let path = DbPath::Memory(memory_dir().join(format!("reifydb_{}.db", Uuid::new_v4())));
180 let guard = SqliteTempPathGuard::new(&path);
181 (
182 Self {
183 path,
184 flags: OpenFlags::default(),
185 journal_mode: JournalMode::Wal,
186 synchronous_mode: SynchronousMode::Off,
187 temp_store: TempStore::Memory,
188 cache_size: 2000,
189 wal_autocheckpoint: 10000,
190 page_size: 16384,
191 mmap_size: 0,
192 prepared_statement_cache_capacity: 128,
193 read_pool_size: 2,
194 },
195 guard,
196 )
197 }
198
199 pub fn test() -> (Self, SqliteTempPathGuard) {
200 let path = DbPath::Memory(memory_dir().join(format!("reifydb_{}.db", Uuid::new_v4())));
201 let guard = SqliteTempPathGuard::new(&path);
202 (
203 Self {
204 path,
205 flags: OpenFlags::default(),
206 journal_mode: JournalMode::Wal,
207 synchronous_mode: SynchronousMode::Off,
208 temp_store: TempStore::Memory,
209 cache_size: 1000,
210 wal_autocheckpoint: 10000,
211 page_size: 4096,
212 mmap_size: 0,
213 prepared_statement_cache_capacity: 32,
214 read_pool_size: 2,
215 },
216 guard,
217 )
218 }
219
220 pub fn path<P: AsRef<Path>>(mut self, path: P) -> Self {
221 self.path = DbPath::File(path.as_ref().to_path_buf());
222 self
223 }
224
225 pub fn flags(mut self, flags: OpenFlags) -> Self {
226 self.flags = flags;
227 self
228 }
229
230 pub fn journal_mode(mut self, mode: JournalMode) -> Self {
231 self.journal_mode = mode;
232 self
233 }
234
235 pub fn synchronous_mode(mut self, mode: SynchronousMode) -> Self {
236 self.synchronous_mode = mode;
237 self
238 }
239
240 pub fn temp_store(mut self, store: TempStore) -> Self {
241 self.temp_store = store;
242 self
243 }
244
245 pub fn read_pool_size(mut self, size: u32) -> Self {
246 self.read_pool_size = size.max(1);
247 self
248 }
249
250 pub fn cache_size(mut self, size_kb: u32) -> Self {
251 self.cache_size = size_kb;
252 self
253 }
254
255 pub fn wal_autocheckpoint(mut self, pages: u32) -> Self {
256 self.wal_autocheckpoint = pages;
257 self
258 }
259
260 pub fn page_size(mut self, size: u32) -> Self {
261 self.page_size = size;
262 self
263 }
264
265 pub fn mmap_size(mut self, size: u64) -> Self {
266 self.mmap_size = size;
267 self
268 }
269}
270
271impl Default for SqliteConfig {
272 fn default() -> Self {
273 Self::new("reifydb.db")
274 }
275}
276
277#[derive(Debug, Clone)]
278pub struct OpenFlags {
279 pub read_write: bool,
280 pub create: bool,
281 pub full_mutex: bool,
282 pub no_mutex: bool,
283 pub shared_cache: bool,
284 pub private_cache: bool,
285 pub uri: bool,
286}
287
288impl OpenFlags {
289 pub fn new() -> Self {
290 Self::default()
291 }
292
293 pub fn read_write(mut self, enabled: bool) -> Self {
294 self.read_write = enabled;
295 self
296 }
297
298 pub fn create(mut self, enabled: bool) -> Self {
299 self.create = enabled;
300 self
301 }
302
303 pub fn full_mutex(mut self, enabled: bool) -> Self {
304 self.full_mutex = enabled;
305 self.no_mutex = !enabled;
306 self
307 }
308
309 pub fn no_mutex(mut self, enabled: bool) -> Self {
310 self.no_mutex = enabled;
311 self.full_mutex = !enabled;
312 self
313 }
314
315 pub fn shared_cache(mut self, enabled: bool) -> Self {
316 self.shared_cache = enabled;
317 self.private_cache = !enabled;
318 self
319 }
320
321 pub fn private_cache(mut self, enabled: bool) -> Self {
322 self.private_cache = enabled;
323 self.shared_cache = !enabled;
324 self
325 }
326
327 pub fn uri(mut self, enabled: bool) -> Self {
328 self.uri = enabled;
329 self
330 }
331}
332
333impl Default for OpenFlags {
334 fn default() -> Self {
335 Self {
336 read_write: true,
337 create: true,
338 full_mutex: true,
339 no_mutex: false,
340 shared_cache: false,
341 private_cache: false,
342 uri: false,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum JournalMode {
349 Delete,
350 Truncate,
351 Persist,
352 Memory,
353 Wal,
354 Off,
355}
356
357impl JournalMode {
358 pub fn as_str(&self) -> &'static str {
359 match self {
360 JournalMode::Delete => "DELETE",
361 JournalMode::Truncate => "TRUNCATE",
362 JournalMode::Persist => "PERSIST",
363 JournalMode::Memory => "MEMORY",
364 JournalMode::Wal => "WAL",
365 JournalMode::Off => "OFF",
366 }
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub enum SynchronousMode {
372 Off,
373 Normal,
374 Full,
375 Extra,
376}
377
378impl SynchronousMode {
379 pub fn as_str(&self) -> &'static str {
380 match self {
381 SynchronousMode::Off => "OFF",
382 SynchronousMode::Normal => "NORMAL",
383 SynchronousMode::Full => "FULL",
384 SynchronousMode::Extra => "EXTRA",
385 }
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub enum TempStore {
391 Default,
392 File,
393 Memory,
394}
395
396impl TempStore {
397 pub fn as_str(&self) -> &'static str {
398 match self {
399 TempStore::Default => "DEFAULT",
400 TempStore::File => "FILE",
401 TempStore::Memory => "MEMORY",
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use reifydb_testing::tempdir::temp_dir;
409
410 use super::*;
411
412 #[test]
413 fn test_config_fluent_api() {
414 let config = SqliteConfig::new("/tmp/test.reifydb")
415 .journal_mode(JournalMode::Wal)
416 .synchronous_mode(SynchronousMode::Normal)
417 .temp_store(TempStore::Memory)
418 .cache_size(30000)
419 .flags(OpenFlags::new().read_write(true).create(true).full_mutex(true));
420
421 assert_eq!(config.path, DbPath::File(PathBuf::from("/tmp/test.reifydb")));
422 assert_eq!(config.journal_mode, JournalMode::Wal);
423 assert_eq!(config.synchronous_mode, SynchronousMode::Normal);
424 assert_eq!(config.temp_store, TempStore::Memory);
425 assert_eq!(config.cache_size, 30000);
426 assert!(config.flags.read_write);
427 assert!(config.flags.create);
428 assert!(config.flags.full_mutex);
429 }
430
431 #[test]
432 fn test_enum_string_conversion() {
433 assert_eq!(JournalMode::Wal.as_str(), "WAL");
434 assert_eq!(SynchronousMode::Normal.as_str(), "NORMAL");
435 assert_eq!(TempStore::Memory.as_str(), "MEMORY");
436 }
437
438 #[test]
439 fn test_all_journal_modes() {
440 assert_eq!(JournalMode::Delete.as_str(), "DELETE");
441 assert_eq!(JournalMode::Truncate.as_str(), "TRUNCATE");
442 assert_eq!(JournalMode::Persist.as_str(), "PERSIST");
443 assert_eq!(JournalMode::Memory.as_str(), "MEMORY");
444 assert_eq!(JournalMode::Wal.as_str(), "WAL");
445 assert_eq!(JournalMode::Off.as_str(), "OFF");
446 }
447
448 #[test]
449 fn test_all_synchronous_modes() {
450 assert_eq!(SynchronousMode::Off.as_str(), "OFF");
451 assert_eq!(SynchronousMode::Normal.as_str(), "NORMAL");
452 assert_eq!(SynchronousMode::Full.as_str(), "FULL");
453 assert_eq!(SynchronousMode::Extra.as_str(), "EXTRA");
454 }
455
456 #[test]
457 fn test_all_temp_store_modes() {
458 assert_eq!(TempStore::Default.as_str(), "DEFAULT");
459 assert_eq!(TempStore::File.as_str(), "FILE");
460 assert_eq!(TempStore::Memory.as_str(), "MEMORY");
461 }
462
463 #[test]
464 fn test_default_config() {
465 let config = SqliteConfig::default();
466 assert_eq!(config.path, DbPath::File(PathBuf::from("reifydb.db")));
467 assert_eq!(config.journal_mode, JournalMode::Wal);
468 assert_eq!(config.synchronous_mode, SynchronousMode::Normal);
469 assert_eq!(config.temp_store, TempStore::Memory);
470 }
471
472 #[test]
473 fn test_safe_config() {
474 temp_dir(|db_path| {
475 let db_file = db_path.join("safe.reifydb");
476 let config = SqliteConfig::safe(&db_file);
477
478 assert_eq!(config.path, DbPath::File(db_file));
479 assert_eq!(config.journal_mode, JournalMode::Wal);
480 assert_eq!(config.synchronous_mode, SynchronousMode::Full);
481 assert_eq!(config.temp_store, TempStore::File);
482 Ok(())
483 })
484 .expect("test failed");
485 }
486
487 #[test]
488 fn test_fast_config() {
489 temp_dir(|db_path| {
490 let db_file = db_path.join("fast.reifydb");
491 let config = SqliteConfig::fast(&db_file);
492
493 assert_eq!(config.path, DbPath::File(db_file));
494 assert_eq!(config.journal_mode, JournalMode::Wal);
495 assert_eq!(config.synchronous_mode, SynchronousMode::Off);
496 assert_eq!(config.temp_store, TempStore::Memory);
497 Ok(())
498 })
499 .expect("test failed");
500 }
501
502 #[test]
503 fn test_tmpfs_config() {
504 let config = SqliteConfig::tmpfs();
505
506 match config.path {
507 DbPath::Tmpfs(path) => {
508 assert!(path.to_string_lossy().starts_with("/tmp/reifydb_"));
509 assert!(path.to_string_lossy().ends_with(".db"));
510 }
511 _ => panic!("Expected DbPath::Tmpfs variant"),
512 }
513
514 assert_eq!(config.journal_mode, JournalMode::Wal);
515 assert_eq!(config.synchronous_mode, SynchronousMode::Off);
516 assert_eq!(config.temp_store, TempStore::Memory);
517 assert_eq!(config.cache_size, 2000);
518 assert_eq!(config.wal_autocheckpoint, 10000);
519 }
520
521 #[test]
522 fn test_config_chaining() {
523 temp_dir(|db_path| {
524 let db_file = db_path.join("chain.reifydb");
525
526 let config = SqliteConfig::new(&db_file)
527 .journal_mode(JournalMode::Delete)
528 .synchronous_mode(SynchronousMode::Extra)
529 .temp_store(TempStore::File)
530 .flags(OpenFlags::new().read_write(false).create(false).shared_cache(true));
531
532 assert_eq!(config.journal_mode, JournalMode::Delete);
533 assert_eq!(config.synchronous_mode, SynchronousMode::Extra);
534 assert_eq!(config.temp_store, TempStore::File);
535 assert!(!config.flags.read_write);
536 assert!(!config.flags.create);
537 assert!(config.flags.shared_cache);
538 Ok(())
539 })
540 .expect("test failed");
541 }
542
543 #[test]
544 fn test_open_flags_mutex_exclusivity() {
545 let flags = OpenFlags::new().full_mutex(true);
546 assert!(flags.full_mutex);
547 assert!(!flags.no_mutex);
548
549 let flags = OpenFlags::new().no_mutex(true);
550 assert!(!flags.full_mutex);
551 assert!(flags.no_mutex);
552 }
553
554 #[test]
555 fn test_open_flags_cache_exclusivity() {
556 let flags = OpenFlags::new().shared_cache(true);
557 assert!(flags.shared_cache);
558 assert!(!flags.private_cache);
559
560 let flags = OpenFlags::new().private_cache(true);
561 assert!(!flags.shared_cache);
562 assert!(flags.private_cache);
563 }
564
565 #[test]
566 fn test_open_flags_all_combinations() {
567 let flags =
568 OpenFlags::new().read_write(true).create(true).full_mutex(true).shared_cache(true).uri(true);
569
570 assert!(flags.read_write);
571 assert!(flags.create);
572 assert!(flags.full_mutex);
573 assert!(!flags.no_mutex);
574 assert!(flags.shared_cache);
575 assert!(!flags.private_cache);
576 assert!(flags.uri);
577 }
578
579 #[test]
580 fn test_path_handling() {
581 temp_dir(|db_path| {
582 let file_path = db_path.join("test.reifydb");
583 let config = SqliteConfig::new(&file_path);
584 assert_eq!(config.path, DbPath::File(file_path));
585
586 let config = SqliteConfig::new(db_path);
587 assert_eq!(config.path, DbPath::File(db_path.to_path_buf()));
588 Ok(())
589 })
590 .expect("test failed");
591 }
592}