Skip to main content

reifydb_sqlite/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4//! Shared SQLite paths, flags, pragma settings and connection plumbing for the storage subsystems.
5//!
6//! Configuration only: no `core::interface::store` trait is implemented here, and nothing knows about deltas,
7//! versions or the encoded-key layout.
8
9#[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/// Removes both the base-path file and the sibling directory at `base.with_extension("")`, where the embedded
39/// API factory drops `multi.db` / `single.db` and their `-wal` / `-shm` companions.
40/// `DbPath::File` is left alone; the caller owns those.
41#[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	/// Makes Drop a no-op, for when the database has been moved elsewhere on purpose.
58	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		// Optional fields exist so a caller can opt out deliberately. A constructor that quietly
443		// shipped None would hand everyone SQLite's own defaults - a rollback journal on a fresh
444		// database, temp files on disk, 2 MB of page cache per connection.
445		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}