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