Skip to main content

reifydb_sqlite/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
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 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/// RAII guard returned by `SqliteConfig::test()` / `SqliteConfig::in_memory()` that removes
39/// the on-disk artifacts created under a temp / shared-memory directory when dropped.
40///
41/// Two artifact families are cleaned up:
42/// - the file at the base path itself (used by the CDC backend), and
43/// - the directory at `base.with_extension("")` (where the embedded API factory drops `multi.db` / `single.db` and
44///   their `-wal` / `-shm` companions, see `pkg/rust/reifydb/src/api/mod.rs`).
45///
46/// `DbPath::File` paths are left alone; the caller owns those.
47#[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	/// Disarm the guard so Drop becomes a no-op. Use when the underlying database
64	/// has been intentionally moved elsewhere and you do not want the files removed.
65	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}