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