Skip to main content

reifydb_sqlite/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4#[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		// Optional fields exist so a caller can opt out deliberately. A constructor that quietly
434		// shipped None would hand everyone SQLite's own defaults - a rollback journal on a fresh
435		// database, temp files on disk, 2 MB of page cache per connection.
436		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}