Skip to main content

reifydb_sqlite/
lib.rs

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