Skip to main content

reifydb_store_multi/hot/sqlite/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4#[cfg(not(target_os = "linux"))]
5use std::env;
6use std::path::{Path, PathBuf};
7
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Eq, PartialEq)]
11pub enum DbPath {
12	File(PathBuf),
13	Tmpfs(PathBuf),  // tmpfs-backed file for WAL support + cleanup
14	Memory(PathBuf), // RAM-backed file for storage with WAL support + cleanup
15}
16
17fn memory_dir() -> PathBuf {
18	#[cfg(target_os = "linux")]
19	{
20		PathBuf::from("/dev/shm")
21	}
22	#[cfg(not(target_os = "linux"))]
23	{
24		env::temp_dir()
25	}
26}
27
28/// Configuration for SQLite storage backend
29#[derive(Debug, Clone)]
30pub struct SqliteConfig {
31	pub path: DbPath,
32	pub flags: OpenFlags,
33	pub journal_mode: JournalMode,
34	pub synchronous_mode: SynchronousMode,
35	pub temp_store: TempStore,
36	pub cache_size: u32,
37	pub wal_autocheckpoint: u32,
38	pub page_size: u32, // Page size in bytes (must be power of 2, 512-65536)
39	pub mmap_size: u64, // Memory-mapped I/O size in bytes
40}
41
42impl SqliteConfig {
43	/// Create a new SqliteConfig with the specified database path
44	pub fn new<P: AsRef<Path>>(path: P) -> Self {
45		Self {
46			path: DbPath::File(path.as_ref().to_path_buf()),
47			flags: OpenFlags::default(),
48			journal_mode: JournalMode::Wal,
49			synchronous_mode: SynchronousMode::Normal,
50			temp_store: TempStore::Memory,
51			cache_size: 20000,
52			wal_autocheckpoint: 1000,
53			page_size: 4096, // SQLite default
54			mmap_size: 0,    // Disabled by default
55		}
56	}
57
58	/// Create a safety-first configuration optimized for data integrity
59	/// - WAL journal mode for crash recovery
60	/// - FULL synchronous mode for maximum durability
61	/// - FILE temp store for persistence
62	/// - Conservative pool size
63	pub fn safe<P: AsRef<Path>>(path: P) -> Self {
64		Self {
65			path: DbPath::File(path.as_ref().to_path_buf()),
66			flags: OpenFlags::default(),
67			journal_mode: JournalMode::Wal,
68			synchronous_mode: SynchronousMode::Full,
69			temp_store: TempStore::File,
70			cache_size: 20000,
71			wal_autocheckpoint: 1000,
72			page_size: 4096, // SQLite default
73			mmap_size: 0,    // Disabled by default
74		}
75	}
76
77	/// Create a high-performance configuration optimized for speed
78	/// - MEMORY journal mode for fastest writes
79	/// - OFF synchronous mode for minimal disk I/O
80	/// - MEMORY temp store for fastest temp operations
81	/// - Larger pool size for concurrency
82	pub fn fast<P: AsRef<Path>>(path: P) -> Self {
83		Self {
84			path: DbPath::File(path.as_ref().to_path_buf()),
85			flags: OpenFlags::default(),
86			journal_mode: JournalMode::Memory,
87			synchronous_mode: SynchronousMode::Off,
88			temp_store: TempStore::Memory,
89			cache_size: 10000,
90			wal_autocheckpoint: 10000,
91			page_size: 16384,     // Larger page size for bulk operations
92			mmap_size: 268435456, // 256MB mmap for performance
93		}
94	}
95
96	/// Create a tmpfs configuration for temporary database storage
97	/// - Tmpfs-backed database with WAL mode for concurrent access
98	/// - Uses /tmp which may or may not be tmpfs (system-dependent)
99	/// - WAL journal mode for concurrent readers + single writer
100	/// - OFF synchronous mode for maximum speed
101	/// - MEMORY temp store
102	/// - Automatic cleanup on drop
103	pub fn tmpfs() -> Self {
104		Self {
105			path: DbPath::Tmpfs(PathBuf::from(format!("/tmp/reifydb_tmpfs_{}.db", Uuid::new_v4()))),
106			flags: OpenFlags::default(),
107			journal_mode: JournalMode::Wal,
108			synchronous_mode: SynchronousMode::Off,
109			temp_store: TempStore::Memory,
110			cache_size: 20000,
111			wal_autocheckpoint: 10000,
112			page_size: 16384,     // Larger page size for bulk operations
113			mmap_size: 268435456, // 256MB mmap for RAM-backed storage
114		}
115	}
116
117	/// Create an in-memory configuration for production use
118	/// - RAM-only database with WAL mode for concurrent access
119	/// - Uses /dev/shm on Linux, temp dir on other platforms
120	/// - WAL journal mode for concurrent readers + single writer
121	/// - NORMAL synchronous mode (safe for RAM storage)
122	/// - MEMORY temp store
123	/// - Automatic cleanup on drop
124	pub fn in_memory() -> Self {
125		Self {
126			path: DbPath::Memory(memory_dir().join(format!("reifydb_mem_{}.db", Uuid::new_v4()))),
127			flags: OpenFlags::default(),
128			journal_mode: JournalMode::Wal,
129			synchronous_mode: SynchronousMode::Off,
130			temp_store: TempStore::Memory,
131			cache_size: 20000,
132			wal_autocheckpoint: 10000,
133			page_size: 16384,     // Larger page size for bulk operations
134			mmap_size: 268435456, // 256MB mmap for RAM-backed storage
135		}
136	}
137
138	/// Create a test configuration optimized for testing with in-memory database
139	/// - RAM-only database with WAL mode for concurrent access
140	/// - Uses /dev/shm on Linux, temp dir on other platforms
141	/// - WAL journal mode for concurrent readers + single writer
142	/// - FULL synchronous mode for test safety
143	/// - MEMORY temp store for fastest temp operations
144	/// - Automatic cleanup on drop
145	pub fn test() -> Self {
146		Self {
147			path: DbPath::Memory(memory_dir().join(format!("reifydb_test_{}.db", Uuid::new_v4()))),
148			flags: OpenFlags::default(),
149			journal_mode: JournalMode::Wal,
150			synchronous_mode: SynchronousMode::Off,
151			temp_store: TempStore::Memory,
152			cache_size: 10000,
153			wal_autocheckpoint: 10000,
154			page_size: 4096, // Default for tests
155			mmap_size: 0,    // Disabled for tests
156		}
157	}
158
159	/// Set the database file path
160	pub fn path<P: AsRef<Path>>(mut self, path: P) -> Self {
161		self.path = DbPath::File(path.as_ref().to_path_buf());
162		self
163	}
164
165	/// Set the SQLite open flags
166	pub fn flags(mut self, flags: OpenFlags) -> Self {
167		self.flags = flags;
168		self
169	}
170
171	/// Set the journal mode
172	pub fn journal_mode(mut self, mode: JournalMode) -> Self {
173		self.journal_mode = mode;
174		self
175	}
176
177	/// Set the synchronous mode
178	pub fn synchronous_mode(mut self, mode: SynchronousMode) -> Self {
179		self.synchronous_mode = mode;
180		self
181	}
182
183	/// Set the temp store location
184	pub fn temp_store(mut self, store: TempStore) -> Self {
185		self.temp_store = store;
186		self
187	}
188
189	/// Set the cache size in KB (will be negated when passed to SQLite)
190	pub fn cache_size(mut self, size_kb: u32) -> Self {
191		self.cache_size = size_kb;
192		self
193	}
194
195	/// Set WAL auto-checkpoint threshold in pages (0 = disable)
196	pub fn wal_autocheckpoint(mut self, pages: u32) -> Self {
197		self.wal_autocheckpoint = pages;
198		self
199	}
200
201	/// Set the page size in bytes (must be a power of 2 between 512 and 65536)
202	/// Note: This must be set before the database is created. Changing page size
203	/// on an existing database requires a VACUUM operation.
204	pub fn page_size(mut self, size: u32) -> Self {
205		self.page_size = size;
206		self
207	}
208
209	/// Set the memory-mapped I/O size in bytes (0 = disabled)
210	/// Larger values can improve read performance for in-memory databases
211	pub fn mmap_size(mut self, size: u64) -> Self {
212		self.mmap_size = size;
213		self
214	}
215}
216
217impl Default for SqliteConfig {
218	fn default() -> Self {
219		Self::new("reify.reifydb")
220	}
221}
222
223/// SQLite database open flags
224#[derive(Debug, Clone)]
225pub struct OpenFlags {
226	pub read_write: bool,
227	pub create: bool,
228	pub full_mutex: bool,
229	pub no_mutex: bool,
230	pub shared_cache: bool,
231	pub private_cache: bool,
232	pub uri: bool,
233}
234
235impl OpenFlags {
236	/// Create a new OpenFlags configuration
237	pub fn new() -> Self {
238		Self::default()
239	}
240
241	/// Enable read-write access (default: true)
242	pub fn read_write(mut self, enabled: bool) -> Self {
243		self.read_write = enabled;
244		self
245	}
246
247	/// Enable creation of database if it doesn't exist (default: true)
248	pub fn create(mut self, enabled: bool) -> Self {
249		self.create = enabled;
250		self
251	}
252
253	/// Use full mutex locking (default: true)
254	pub fn full_mutex(mut self, enabled: bool) -> Self {
255		self.full_mutex = enabled;
256		self.no_mutex = !enabled;
257		self
258	}
259
260	/// Disable mutex locking (default: false)
261	pub fn no_mutex(mut self, enabled: bool) -> Self {
262		self.no_mutex = enabled;
263		self.full_mutex = !enabled;
264		self
265	}
266
267	/// Enable shared cache (default: false)
268	pub fn shared_cache(mut self, enabled: bool) -> Self {
269		self.shared_cache = enabled;
270		self.private_cache = !enabled;
271		self
272	}
273
274	/// Enable private cache (default: false)
275	pub fn private_cache(mut self, enabled: bool) -> Self {
276		self.private_cache = enabled;
277		self.shared_cache = !enabled;
278		self
279	}
280
281	/// Enable URI filename interpretation (default: false)
282	pub fn uri(mut self, enabled: bool) -> Self {
283		self.uri = enabled;
284		self
285	}
286}
287
288impl Default for OpenFlags {
289	fn default() -> Self {
290		Self {
291			read_write: true,
292			create: true,
293			full_mutex: true,
294			no_mutex: false,
295			shared_cache: false,
296			private_cache: false,
297			uri: false,
298		}
299	}
300}
301
302/// SQLite journal mode options
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum JournalMode {
305	/// Delete journal files after each transaction
306	Delete,
307	/// Truncate journal files to zero length instead of deleting
308	Truncate,
309	/// Persist journal files
310	Persist,
311	/// Use memory for journaling
312	Memory,
313	/// Write-Ahead Logging mode (recommended for concurrent access)
314	Wal,
315	/// No journaling (unsafe)
316	Off,
317}
318
319impl JournalMode {
320	pub(crate) fn as_str(&self) -> &'static str {
321		match self {
322			JournalMode::Delete => "DELETE",
323			JournalMode::Truncate => "TRUNCATE",
324			JournalMode::Persist => "PERSIST",
325			JournalMode::Memory => "MEMORY",
326			JournalMode::Wal => "WAL",
327			JournalMode::Off => "OFF",
328		}
329	}
330}
331
332/// SQLite synchronous mode options
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum SynchronousMode {
335	/// No sync calls (fastest, but may corrupt on power loss)
336	Off,
337	/// Sync only at critical moments (good balance of safety and speed)
338	Normal,
339	/// Sync more frequently (safer but slower)
340	Full,
341	/// Sync even more frequently (safest but slowest)
342	Extra,
343}
344
345impl SynchronousMode {
346	pub(crate) fn as_str(&self) -> &'static str {
347		match self {
348			SynchronousMode::Off => "OFF",
349			SynchronousMode::Normal => "NORMAL",
350			SynchronousMode::Full => "FULL",
351			SynchronousMode::Extra => "EXTRA",
352		}
353	}
354}
355
356/// SQLite temporary storage location
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum TempStore {
359	/// Use default storage (usually disk)
360	Default,
361	/// Store temporary data in files
362	File,
363	/// Store temporary data in memory (faster)
364	Memory,
365}
366
367impl TempStore {
368	pub(crate) fn as_str(&self) -> &'static str {
369		match self {
370			TempStore::Default => "DEFAULT",
371			TempStore::File => "FILE",
372			TempStore::Memory => "MEMORY",
373		}
374	}
375}
376
377#[cfg(test)]
378pub mod tests {
379	use reifydb_testing::tempdir::temp_dir;
380
381	use super::*;
382
383	#[test]
384	fn test_config_fluent_api() {
385		let config = SqliteConfig::new("/tmp/test.reifydb")
386			.journal_mode(JournalMode::Wal)
387			.synchronous_mode(SynchronousMode::Normal)
388			.temp_store(TempStore::Memory)
389			.cache_size(30000)
390			.flags(OpenFlags::new().read_write(true).create(true).full_mutex(true));
391
392		assert_eq!(config.path, DbPath::File(PathBuf::from("/tmp/test.reifydb")));
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		assert_eq!(config.cache_size, 30000);
397		assert!(config.flags.read_write);
398		assert!(config.flags.create);
399		assert!(config.flags.full_mutex);
400	}
401
402	#[test]
403	fn test_enum_string_conversion() {
404		assert_eq!(JournalMode::Wal.as_str(), "WAL");
405		assert_eq!(SynchronousMode::Normal.as_str(), "NORMAL");
406		assert_eq!(TempStore::Memory.as_str(), "MEMORY");
407	}
408
409	#[test]
410	fn test_all_journal_modes() {
411		assert_eq!(JournalMode::Delete.as_str(), "DELETE");
412		assert_eq!(JournalMode::Truncate.as_str(), "TRUNCATE");
413		assert_eq!(JournalMode::Persist.as_str(), "PERSIST");
414		assert_eq!(JournalMode::Memory.as_str(), "MEMORY");
415		assert_eq!(JournalMode::Wal.as_str(), "WAL");
416		assert_eq!(JournalMode::Off.as_str(), "OFF");
417	}
418
419	#[test]
420	fn test_all_synchronous_modes() {
421		assert_eq!(SynchronousMode::Off.as_str(), "OFF");
422		assert_eq!(SynchronousMode::Normal.as_str(), "NORMAL");
423		assert_eq!(SynchronousMode::Full.as_str(), "FULL");
424		assert_eq!(SynchronousMode::Extra.as_str(), "EXTRA");
425	}
426
427	#[test]
428	fn test_all_temp_store_modes() {
429		assert_eq!(TempStore::Default.as_str(), "DEFAULT");
430		assert_eq!(TempStore::File.as_str(), "FILE");
431		assert_eq!(TempStore::Memory.as_str(), "MEMORY");
432	}
433
434	#[test]
435	fn test_default_config() {
436		let config = SqliteConfig::default();
437		assert_eq!(config.path, DbPath::File(PathBuf::from("reify.reifydb")));
438		assert_eq!(config.journal_mode, JournalMode::Wal);
439		assert_eq!(config.synchronous_mode, SynchronousMode::Normal);
440		assert_eq!(config.temp_store, TempStore::Memory);
441	}
442
443	#[test]
444	fn test_safe_config() {
445		temp_dir(|db_path| {
446			let db_file = db_path.join("safe.reifydb");
447			let config = SqliteConfig::safe(&db_file);
448
449			assert_eq!(config.path, DbPath::File(db_file));
450			assert_eq!(config.journal_mode, JournalMode::Wal);
451			assert_eq!(config.synchronous_mode, SynchronousMode::Full);
452			assert_eq!(config.temp_store, TempStore::File);
453			Ok(())
454		})
455		.expect("test failed");
456	}
457
458	#[test]
459	fn test_fast_config() {
460		temp_dir(|db_path| {
461			let db_file = db_path.join("fast.reifydb");
462			let config = SqliteConfig::fast(&db_file);
463
464			assert_eq!(config.path, DbPath::File(db_file));
465			assert_eq!(config.journal_mode, JournalMode::Memory);
466			assert_eq!(config.synchronous_mode, SynchronousMode::Off);
467			assert_eq!(config.temp_store, TempStore::Memory);
468			Ok(())
469		})
470		.expect("test failed");
471	}
472
473	#[test]
474	fn test_tmpfs_config() {
475		let config = SqliteConfig::tmpfs();
476
477		// Path should be Tmpfs variant with /tmp prefix
478		match config.path {
479			DbPath::Tmpfs(path) => {
480				assert!(path.to_string_lossy().starts_with("/tmp/reifydb_tmpfs_"));
481				assert!(path.to_string_lossy().ends_with(".db"));
482			}
483			_ => panic!("Expected DbPath::Tmpfs variant"),
484		}
485
486		assert_eq!(config.journal_mode, JournalMode::Wal);
487		assert_eq!(config.synchronous_mode, SynchronousMode::Off);
488		assert_eq!(config.temp_store, TempStore::Memory);
489		assert_eq!(config.cache_size, 20000);
490		assert_eq!(config.wal_autocheckpoint, 10000);
491	}
492
493	#[test]
494	fn test_config_chaining() {
495		temp_dir(|db_path| {
496			let db_file = db_path.join("chain.reifydb");
497
498			let config = SqliteConfig::new(&db_file)
499				.journal_mode(JournalMode::Delete)
500				.synchronous_mode(SynchronousMode::Extra)
501				.temp_store(TempStore::File)
502				.flags(OpenFlags::new().read_write(false).create(false).shared_cache(true));
503
504			assert_eq!(config.journal_mode, JournalMode::Delete);
505			assert_eq!(config.synchronous_mode, SynchronousMode::Extra);
506			assert_eq!(config.temp_store, TempStore::File);
507			assert!(!config.flags.read_write);
508			assert!(!config.flags.create);
509			assert!(config.flags.shared_cache);
510			Ok(())
511		})
512		.expect("test failed");
513	}
514
515	#[test]
516	fn test_open_flags_mutex_exclusivity() {
517		let flags = OpenFlags::new().full_mutex(true);
518		assert!(flags.full_mutex);
519		assert!(!flags.no_mutex);
520
521		let flags = OpenFlags::new().no_mutex(true);
522		assert!(!flags.full_mutex);
523		assert!(flags.no_mutex);
524	}
525
526	#[test]
527	fn test_open_flags_cache_exclusivity() {
528		let flags = OpenFlags::new().shared_cache(true);
529		assert!(flags.shared_cache);
530		assert!(!flags.private_cache);
531
532		let flags = OpenFlags::new().private_cache(true);
533		assert!(!flags.shared_cache);
534		assert!(flags.private_cache);
535	}
536
537	#[test]
538	fn test_open_flags_all_combinations() {
539		let flags =
540			OpenFlags::new().read_write(true).create(true).full_mutex(true).shared_cache(true).uri(true);
541
542		assert!(flags.read_write);
543		assert!(flags.create);
544		assert!(flags.full_mutex);
545		assert!(!flags.no_mutex);
546		assert!(flags.shared_cache);
547		assert!(!flags.private_cache);
548		assert!(flags.uri);
549	}
550
551	#[test]
552	fn test_path_handling() {
553		temp_dir(|db_path| {
554			// Test with file path
555			let file_path = db_path.join("test.reifydb");
556			let config = SqliteConfig::new(&file_path);
557			assert_eq!(config.path, DbPath::File(file_path));
558
559			// Test with directory path
560			let config = SqliteConfig::new(db_path);
561			assert_eq!(config.path, DbPath::File(db_path.to_path_buf()));
562			Ok(())
563		})
564		.expect("test failed");
565	}
566}