Skip to main content

reifydb_sqlite/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2026 ReifyDB
3
4//! Shared SQLite configuration and connection plumbing used by ReifyDB storage subsystems. Owns the typed
5//! representation of paths (file, tmpfs, in-memory), open flags, journal/sync/temp-store modes, and pragma settings,
6//! and exposes the connection wrapper that the buffer and persistent tiers wrap their storage on top of.
7//!
8//! The crate is configuration-only: it does not implement any `core::interface::store` trait. Storage backends
9//! (`store-multi`, `store-single`) consume `SqliteConfig` to spin up their persistent tier; nothing here knows about
10//! deltas, versions, or the encoded-key layout.
11
12#[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/// RAII guard returned by `SqliteConfig::test()` / `SqliteConfig::in_memory()` that removes
38/// the on-disk artifacts created under a temp / shared-memory directory when dropped.
39///
40/// Two artifact families are cleaned up:
41/// - the file at the base path itself (used by the CDC backend), and
42/// - the directory at `base.with_extension("")` (where the embedded API factory drops `multi.db` / `single.db` and
43///   their `-wal` / `-shm` companions, see `pkg/rust/reifydb/src/api/mod.rs`).
44///
45/// `DbPath::File` paths are left alone; the caller owns those.
46#[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	/// Disarm the guard so Drop becomes a no-op. Use when the underlying database
63	/// has been intentionally moved elsewhere and you do not want the files removed.
64	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}