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