Skip to main content

reifydb_store_multi/hot/sqlite/
config.rs

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