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