Skip to main content

cqlite_core/
config.rs

1//! Configuration management for CQLite
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6/// Main configuration structure for CQLite database
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct Config {
9    /// Storage engine configuration
10    pub storage: StorageConfig,
11
12    /// Memory management configuration
13    pub memory: MemoryConfig,
14
15    /// Query engine configuration
16    pub query: QueryConfig,
17
18    /// Performance and optimization settings
19    pub performance: PerformanceConfig,
20
21    /// WASM-specific configuration
22    #[cfg(target_arch = "wasm32")]
23    pub wasm: WasmConfig,
24}
25
26/// Storage engine configuration
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct StorageConfig {
29    /// Maximum SSTable file size in bytes (default: 64MB)
30    pub max_sstable_size: u64,
31
32    /// MemTable size threshold for flushing (default: 16MB)
33    pub memtable_size_threshold: u64,
34
35    /// Compaction configuration
36    pub compaction: CompactionConfig,
37
38    /// Block size for SSTable data blocks (default: 64KB)
39    pub block_size: u32,
40
41    /// Compression configuration
42    pub compression: CompressionConfig,
43
44    /// Enable bloom filters for SSTables
45    pub enable_bloom_filters: bool,
46
47    /// Bloom filter false positive rate (default: 0.01)
48    pub bloom_filter_fp_rate: f64,
49
50    /// Number of background threads for I/O operations
51    pub io_threads: usize,
52
53    /// Sync mode for durability
54    pub sync_mode: SyncMode,
55
56    /// Memory-map SSTable Data.db files instead of using buffered file I/O.
57    ///
58    /// **Opt-in.** Defaults to `false` (buffered I/O), which is portable and
59    /// safe on every filesystem. When enabled, the reader maps Data.db files at
60    /// or above [`Self::mmap_min_size_bytes`] into the process address space and
61    /// serves reads from the OS page cache with no per-block `read` syscall,
62    /// mirroring Cassandra's `disk_access_mode: mmap`. This speeds up repeated
63    /// local scans of the same files.
64    ///
65    /// # Safety / platform constraints
66    ///
67    /// A memory map aliases the file's bytes for the reader's lifetime. Only
68    /// enable this when the SSTables are **immutable local files**:
69    /// - Mutating, truncating, or deleting a mapped file out from under a live
70    ///   reader is undefined behaviour and can raise `SIGBUS`, terminating the
71    ///   process. CQLite never rewrites its own mapped inputs, but external
72    ///   tools must not either.
73    /// - Network and overlay filesystems (NFS, SMB, FUSE, some container
74    ///   overlays) can fault mid-read after a successful map; prefer buffered
75    ///   I/O there.
76    ///
77    /// # Interaction with the write engine (Issue #591)
78    ///
79    /// This setting only affects the read path. Compaction always reads its
80    /// input SSTables through buffered I/O regardless of `use_mmap`, and deletes
81    /// each input by removing its `TOC.txt` first (unpublishing it) before the
82    /// data components, best-effort. So enabling mmap for queries is safe
83    /// alongside background compaction: a compaction never holds a mapping over a
84    /// file it then deletes, and on Windows a data file still pinned by a mapped
85    /// reader becomes an invisible orphan (reclaimed on the next startup) rather
86    /// than a failed delete or a source of duplicate rows.
87    ///
88    /// Can also be enabled at runtime by setting `CQLITE_USE_MMAP=1`.
89    ///
90    /// `#[serde(default)]` keeps configs serialized before this field existed
91    /// (which omit it) deserializing successfully, defaulting to buffered I/O.
92    #[serde(default = "default_use_mmap")]
93    pub use_mmap: bool,
94
95    /// Minimum Data.db file size (bytes) before [`Self::use_mmap`] takes effect.
96    ///
97    /// Files smaller than this use buffered I/O even when `use_mmap` is set,
98    /// since the per-file mapping overhead is not worthwhile for tiny files and
99    /// mapping a zero-length file is invalid. Defaults to one page (4096).
100    ///
101    /// `#[serde(default)]` for backward compatibility with older payloads.
102    #[serde(default = "default_mmap_min_size_bytes")]
103    pub mmap_min_size_bytes: usize,
104}
105
106/// Default for [`StorageConfig::use_mmap`]: mmap is opt-in, so buffered I/O.
107fn default_use_mmap() -> bool {
108    false
109}
110
111/// Default for [`StorageConfig::mmap_min_size_bytes`]: one page.
112fn default_mmap_min_size_bytes() -> usize {
113    4096
114}
115
116impl Default for StorageConfig {
117    fn default() -> Self {
118        Self {
119            max_sstable_size: 64 * 1024 * 1024,        // 64MB
120            memtable_size_threshold: 16 * 1024 * 1024, // 16MB
121            compaction: CompactionConfig::default(),
122            block_size: 64 * 1024, // 64KB
123            compression: CompressionConfig::default(),
124            enable_bloom_filters: true,
125            bloom_filter_fp_rate: 0.01,
126            io_threads: num_cpus::get().min(4),
127            sync_mode: SyncMode::Normal,
128            // Opt-in; buffered I/O is the portable, safe default. Shared with
129            // the serde defaults so the two can never drift.
130            use_mmap: default_use_mmap(),
131            mmap_min_size_bytes: default_mmap_min_size_bytes(),
132        }
133    }
134}
135
136/// Compaction strategy configuration
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct CompactionConfig {
139    /// Compaction strategy to use
140    pub strategy: CompactionStrategy,
141
142    /// Maximum number of SSTables before triggering compaction
143    pub max_sstables: usize,
144
145    /// Size ratio for triggering compaction
146    pub size_ratio: f64,
147
148    /// Maximum compaction threads
149    pub max_threads: usize,
150
151    /// Compaction interval for background compaction
152    pub background_interval: Duration,
153
154    /// Enable automatic background compaction
155    pub auto_compaction: bool,
156}
157
158impl Default for CompactionConfig {
159    fn default() -> Self {
160        Self {
161            strategy: CompactionStrategy::Leveled,
162            max_sstables: 10,
163            size_ratio: 2.0,
164            max_threads: 2,
165            background_interval: Duration::from_secs(300), // 5 minutes
166            auto_compaction: true,
167        }
168    }
169}
170
171/// Memory management configuration
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct MemoryConfig {
174    /// Maximum total memory usage (default: 1GB)
175    pub max_memory: u64,
176
177    /// Block cache configuration
178    pub block_cache: CacheConfig,
179
180    /// Row cache configuration  
181    pub row_cache: CacheConfig,
182
183    /// Query result cache configuration
184    pub query_cache: CacheConfig,
185
186    /// Memory allocator settings
187    pub allocator: AllocatorConfig,
188}
189
190impl Default for MemoryConfig {
191    fn default() -> Self {
192        let max_memory = 1024 * 1024 * 1024; // 1GB
193
194        Self {
195            max_memory,
196            block_cache: CacheConfig {
197                enabled: true,
198                max_size: max_memory / 4, // 256MB
199                policy: CachePolicy::Lru,
200            },
201            row_cache: CacheConfig {
202                enabled: true,
203                max_size: max_memory / 8, // 128MB
204                policy: CachePolicy::Lru,
205            },
206            query_cache: CacheConfig {
207                enabled: true,
208                max_size: max_memory / 16, // 64MB
209                policy: CachePolicy::Lru,
210            },
211            allocator: AllocatorConfig::default(),
212        }
213    }
214}
215
216/// Cache configuration
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct CacheConfig {
219    /// Enable this cache
220    pub enabled: bool,
221
222    /// Maximum cache size in bytes
223    pub max_size: u64,
224
225    /// Cache eviction policy
226    pub policy: CachePolicy,
227}
228
229/// Cache eviction policies
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub enum CachePolicy {
232    /// Least Recently Used
233    Lru,
234    /// Least Frequently Used
235    Lfu,
236    /// Adaptive Replacement Cache
237    Arc,
238}
239
240/// Memory allocator configuration
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct AllocatorConfig {
243    /// Use custom allocator for better performance
244    pub use_custom: bool,
245
246    /// Pool size for small allocations
247    pub small_pool_size: u64,
248
249    /// Pool size for large allocations
250    pub large_pool_size: u64,
251}
252
253impl Default for AllocatorConfig {
254    fn default() -> Self {
255        Self {
256            use_custom: false,                  // Conservative default
257            small_pool_size: 64 * 1024 * 1024,  // 64MB
258            large_pool_size: 256 * 1024 * 1024, // 256MB
259        }
260    }
261}
262
263/// Query engine configuration
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct QueryConfig {
266    /// Maximum query execution time
267    pub max_execution_time: Duration,
268
269    /// Maximum number of rows to return in a result set
270    pub max_result_rows: u64,
271
272    /// Query plan cache size
273    pub plan_cache_size: usize,
274
275    /// Enable query optimization
276    pub enable_optimization: bool,
277
278    /// Parallel query execution configuration
279    pub parallel: ParallelQueryConfig,
280
281    /// Query cache size (for plan caching)
282    pub query_cache_size: Option<usize>,
283
284    /// Query parallelism thread count
285    pub query_parallelism: Option<usize>,
286
287    /// Number of iterations for query analysis
288    pub analyze_iterations: Option<usize>,
289}
290
291impl Default for QueryConfig {
292    fn default() -> Self {
293        Self {
294            max_execution_time: Duration::from_secs(300), // 5 minutes
295            max_result_rows: 1_000_000,
296            plan_cache_size: 1000,
297            enable_optimization: true,
298            parallel: ParallelQueryConfig::default(),
299            query_cache_size: Some(100),
300            query_parallelism: Some(num_cpus::get()),
301            analyze_iterations: Some(5),
302        }
303    }
304}
305
306/// Parallel query execution configuration
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct ParallelQueryConfig {
309    /// Enable parallel query execution
310    pub enabled: bool,
311
312    /// Maximum number of parallel threads
313    pub max_threads: usize,
314
315    /// Minimum result set size to trigger parallel execution
316    pub min_parallel_rows: u64,
317}
318
319impl Default for ParallelQueryConfig {
320    fn default() -> Self {
321        Self {
322            enabled: true,
323            max_threads: num_cpus::get(),
324            min_parallel_rows: 10_000,
325        }
326    }
327}
328
329/// Performance and optimization configuration
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct PerformanceConfig {
332    /// Enable performance metrics collection
333    pub enable_metrics: bool,
334
335    /// Metrics collection interval
336    pub metrics_interval: Duration,
337
338    /// Enable detailed profiling
339    pub enable_profiling: bool,
340
341    /// Background task configuration
342    pub background_tasks: BackgroundTaskConfig,
343}
344
345impl Default for PerformanceConfig {
346    fn default() -> Self {
347        Self {
348            enable_metrics: true,
349            metrics_interval: Duration::from_secs(60),
350            enable_profiling: false,
351            background_tasks: BackgroundTaskConfig::default(),
352        }
353    }
354}
355
356/// Background task configuration
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct BackgroundTaskConfig {
359    /// Enable background statistics collection
360    pub enable_stats: bool,
361
362    /// Statistics collection interval
363    pub stats_interval: Duration,
364
365    /// Enable background cleanup tasks
366    pub enable_cleanup: bool,
367
368    /// Cleanup task interval
369    pub cleanup_interval: Duration,
370}
371
372impl Default for BackgroundTaskConfig {
373    fn default() -> Self {
374        Self {
375            enable_stats: true,
376            stats_interval: Duration::from_secs(300), // 5 minutes
377            enable_cleanup: true,
378            cleanup_interval: Duration::from_secs(3600), // 1 hour
379        }
380    }
381}
382
383/// WASM-specific configuration
384#[cfg(target_arch = "wasm32")]
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct WasmConfig {
387    /// Use IndexedDB for persistent storage
388    pub use_indexeddb: bool,
389
390    /// Maximum memory usage in WASM (default: 256MB)
391    pub max_memory: u64,
392
393    /// Enable WASM SIMD optimizations
394    pub enable_simd: bool,
395
396    /// Enable Web Workers for background tasks
397    pub enable_workers: bool,
398
399    /// Maximum number of Web Workers
400    pub max_workers: usize,
401}
402
403#[cfg(target_arch = "wasm32")]
404impl Default for WasmConfig {
405    fn default() -> Self {
406        Self {
407            use_indexeddb: true,
408            max_memory: 256 * 1024 * 1024, // 256MB
409            enable_simd: true,
410            enable_workers: true,
411            max_workers: 4,
412        }
413    }
414}
415
416/// Compression algorithms
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub enum CompressionAlgorithm {
419    /// No compression
420    None,
421    /// LZ4 compression (fast)
422    Lz4,
423    /// Snappy compression (balanced)
424    Snappy,
425    /// Deflate compression (good compression ratio)
426    Deflate,
427    /// ZSTD compression (high compression ratio)
428    Zstd,
429}
430
431/// Compression configuration
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct CompressionConfig {
434    /// Enable compression
435    pub enabled: bool,
436
437    /// Compression algorithm to use
438    pub algorithm: CompressionAlgorithm,
439
440    /// Compression level (algorithm-specific)
441    pub level: i32,
442
443    /// Minimum block size to compress (smaller blocks are stored uncompressed)
444    pub min_block_size: u32,
445}
446
447impl Default for CompressionConfig {
448    fn default() -> Self {
449        Self {
450            enabled: true,
451            algorithm: CompressionAlgorithm::Lz4,
452            level: 1,             // Fast compression
453            min_block_size: 1024, // 1KB minimum
454        }
455    }
456}
457
458/// Compaction strategies
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub enum CompactionStrategy {
461    /// Simple size-based compaction
462    Size,
463    /// Leveled compaction (like LevelDB)
464    Leveled,
465    /// Universal compaction
466    Universal,
467}
468
469/// Durability sync modes
470#[derive(Debug, Clone, Serialize, Deserialize)]
471pub enum SyncMode {
472    /// No explicit syncing (fastest, least durable)
473    None,
474    /// Normal syncing (balanced)
475    Normal,
476    /// Full sync for every write (slowest, most durable)
477    Full,
478}
479
480impl Config {
481    /// Create a configuration optimized for memory usage
482    pub fn memory_optimized() -> Self {
483        let mut config = Self::default();
484
485        // Reduce memory usage
486        config.storage.memtable_size_threshold = 4 * 1024 * 1024; // 4MB
487        config.storage.max_sstable_size = 16 * 1024 * 1024; // 16MB
488        config.memory.max_memory = 256 * 1024 * 1024; // 256MB
489        config.memory.block_cache.max_size = 64 * 1024 * 1024; // 64MB
490        config.memory.row_cache.max_size = 32 * 1024 * 1024; // 32MB
491        config.memory.query_cache.max_size = 16 * 1024 * 1024; // 16MB
492
493        // Enable aggressive compression
494        config.storage.compression.algorithm = CompressionAlgorithm::Zstd;
495        config.storage.compression.enabled = true;
496
497        config
498    }
499
500    /// Create a configuration optimized for performance
501    pub fn performance_optimized() -> Self {
502        let mut config = Self::default();
503
504        // Increase memory usage for better performance
505        config.storage.memtable_size_threshold = 64 * 1024 * 1024; // 64MB
506        config.storage.max_sstable_size = 256 * 1024 * 1024; // 256MB
507        config.memory.max_memory = 4 * 1024 * 1024 * 1024; // 4GB
508
509        // Use faster compression
510        config.storage.compression.algorithm = CompressionAlgorithm::Lz4;
511        config.storage.compression.enabled = true;
512
513        // More aggressive caching
514        config.memory.block_cache.max_size = 1024 * 1024 * 1024; // 1GB
515        config.memory.row_cache.max_size = 512 * 1024 * 1024; // 512MB
516        config.memory.query_cache.max_size = 256 * 1024 * 1024; // 256MB
517
518        // More I/O threads
519        config.storage.io_threads = num_cpus::get();
520
521        config
522    }
523
524    /// Create a configuration optimized for WASM deployment
525    #[cfg(target_arch = "wasm32")]
526    pub fn wasm_optimized() -> Self {
527        let mut config = Self::memory_optimized();
528
529        // WASM-specific optimizations
530        config.wasm.max_memory = 128 * 1024 * 1024; // 128MB
531        config.wasm.enable_simd = true;
532        config.wasm.enable_workers = false; // Conservative default
533
534        // Reduce overall memory usage for WASM
535        config.memory.max_memory = 128 * 1024 * 1024; // 128MB
536        config.storage.memtable_size_threshold = 2 * 1024 * 1024; // 2MB
537        config.storage.max_sstable_size = 8 * 1024 * 1024; // 8MB
538
539        // Disable background tasks that may not work well in WASM
540        config.storage.compaction.auto_compaction = false;
541        config.performance.background_tasks.enable_stats = false;
542        config.performance.background_tasks.enable_cleanup = false;
543
544        config
545    }
546
547    /// Create a test-optimized configuration
548    #[cfg(test)]
549    pub fn test_config() -> Self {
550        let mut config = Config::default();
551
552        // Disable background tasks that can cause test hangs
553        config.storage.compaction.auto_compaction = false;
554        config.performance.background_tasks.enable_stats = false;
555        config.performance.background_tasks.enable_cleanup = false;
556
557        // Reduce timeouts for faster test execution
558        config.query.max_execution_time = std::time::Duration::from_secs(1);
559        config.storage.compaction.background_interval = std::time::Duration::from_secs(10);
560
561        // Smaller memory usage for tests
562        config.memory.max_memory = 64 * 1024 * 1024; // 64MB
563        config.storage.memtable_size_threshold = 1024 * 1024; // 1MB
564        config.storage.max_sstable_size = 4 * 1024 * 1024; // 4MB
565
566        config
567    }
568
569    /// Validate the configuration
570    pub fn validate(&self) -> crate::Result<()> {
571        // Validate memory limits
572        if self.memory.max_memory == 0 {
573            return Err(crate::Error::configuration(
574                "max_memory must be greater than 0",
575            ));
576        }
577
578        // Validate cache sizes don't exceed total memory
579        let total_cache = self.memory.block_cache.max_size
580            + self.memory.row_cache.max_size
581            + self.memory.query_cache.max_size;
582
583        if total_cache > self.memory.max_memory {
584            return Err(crate::Error::configuration(
585                "total cache size exceeds max_memory",
586            ));
587        }
588
589        // Validate storage settings
590        if self.storage.block_size == 0 {
591            return Err(crate::Error::configuration(
592                "block_size must be greater than 0",
593            ));
594        }
595
596        if self.storage.memtable_size_threshold == 0 {
597            return Err(crate::Error::configuration(
598                "memtable_size_threshold must be greater than 0",
599            ));
600        }
601
602        // Validate bloom filter settings
603        if self.storage.enable_bloom_filters
604            && (self.storage.bloom_filter_fp_rate <= 0.0
605                || self.storage.bloom_filter_fp_rate >= 1.0)
606        {
607            return Err(crate::Error::configuration(
608                "bloom_filter_fp_rate must be between 0 and 1",
609            ));
610        }
611
612        Ok(())
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    #[test]
621    fn test_default_config() {
622        let config = Config::default();
623        assert!(config.storage.compression.enabled);
624        assert!(config.storage.enable_bloom_filters);
625        assert!(config.memory.block_cache.enabled);
626    }
627
628    #[test]
629    fn test_memory_optimized_config() {
630        let config = Config::memory_optimized();
631        assert!(
632            config.storage.memtable_size_threshold
633                < Config::default().storage.memtable_size_threshold
634        );
635        assert!(config.memory.max_memory < Config::default().memory.max_memory);
636    }
637
638    #[test]
639    fn test_performance_optimized_config() {
640        let config = Config::performance_optimized();
641        assert!(
642            config.storage.memtable_size_threshold
643                > Config::default().storage.memtable_size_threshold
644        );
645        assert!(config.memory.max_memory > Config::default().memory.max_memory);
646    }
647
648    #[test]
649    fn test_config_validation() {
650        let mut config = Config::default();
651        assert!(config.validate().is_ok());
652
653        // Test invalid max_memory
654        config.memory.max_memory = 0;
655        assert!(config.validate().is_err());
656
657        // Reset and test invalid cache sizes
658        config = Config::default();
659        config.memory.block_cache.max_size = config.memory.max_memory + 1;
660        assert!(config.validate().is_err());
661    }
662
663    #[test]
664    fn test_storage_validation_errors() {
665        let mut config = Config::default();
666
667        // Test invalid block_size (should trigger line 573-574)
668        config.storage.block_size = 0;
669        let result = config.validate();
670        assert!(result.is_err());
671        assert!(result
672            .unwrap_err()
673            .to_string()
674            .contains("block_size must be greater than 0"));
675
676        // Reset and test invalid memtable_size_threshold (should trigger line 579-580)
677        config = Config::default();
678        config.storage.memtable_size_threshold = 0;
679        let result = config.validate();
680        assert!(result.is_err());
681        assert!(result
682            .unwrap_err()
683            .to_string()
684            .contains("memtable_size_threshold must be greater than 0"));
685
686        // Reset and test invalid bloom filter false positive rate (should trigger line 589-590)
687        config = Config::default();
688        config.storage.enable_bloom_filters = true;
689        config.storage.bloom_filter_fp_rate = 0.0; // Invalid: exactly 0
690        let result = config.validate();
691        assert!(result.is_err());
692        assert!(result
693            .unwrap_err()
694            .to_string()
695            .contains("bloom_filter_fp_rate must be between 0 and 1"));
696
697        // Test another invalid bloom filter false positive rate
698        config.storage.bloom_filter_fp_rate = 1.0; // Invalid: exactly 1
699        let result = config.validate();
700        assert!(result.is_err());
701
702        // Test bloom filter rate above 1
703        config.storage.bloom_filter_fp_rate = 1.5; // Invalid: greater than 1
704        let result = config.validate();
705        assert!(result.is_err());
706
707        // Test bloom filter rate below 0
708        config.storage.bloom_filter_fp_rate = -0.1; // Invalid: less than 0
709        let result = config.validate();
710        assert!(result.is_err());
711    }
712
713    #[test]
714    fn test_valid_bloom_filter_config() {
715        let mut config = Config::default();
716        config.storage.enable_bloom_filters = true;
717        config.storage.bloom_filter_fp_rate = 0.01; // Valid rate
718        assert!(config.validate().is_ok());
719
720        config.storage.bloom_filter_fp_rate = 0.5; // Valid rate
721        assert!(config.validate().is_ok());
722
723        config.storage.bloom_filter_fp_rate = 0.99; // Valid rate
724        assert!(config.validate().is_ok());
725    }
726
727    #[test]
728    fn test_storage_config_deserializes_without_mmap_fields() {
729        // Backward compatibility: a config payload serialized before the mmap
730        // fields existed omits `use_mmap` / `mmap_min_size_bytes`. It must still
731        // deserialize, defaulting to the safe buffered backend.
732        let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
733        let obj = value.as_object_mut().unwrap();
734        obj.remove("use_mmap");
735        obj.remove("mmap_min_size_bytes");
736        assert!(!obj.contains_key("use_mmap"));
737
738        let restored: StorageConfig =
739            serde_json::from_value(value).expect("old payload must still deserialize");
740        assert!(!restored.use_mmap, "missing use_mmap must default to false");
741        assert_eq!(
742            restored.mmap_min_size_bytes, 4096,
743            "missing mmap_min_size_bytes must default to one page"
744        );
745    }
746
747    #[test]
748    fn test_full_config_deserializes_without_mmap_fields() {
749        // Same guarantee through the top-level Config, mirroring how the Python
750        // bindings parse a JSON/dict payload into `cqlite_core::Config`.
751        let mut value = serde_json::to_value(Config::default()).unwrap();
752        let storage = value
753            .get_mut("storage")
754            .and_then(|s| s.as_object_mut())
755            .unwrap();
756        storage.remove("use_mmap");
757        storage.remove("mmap_min_size_bytes");
758
759        let restored: Config =
760            serde_json::from_value(value).expect("old Config payload must still deserialize");
761        assert!(!restored.storage.use_mmap);
762        assert_eq!(restored.storage.mmap_min_size_bytes, 4096);
763        restored.validate().expect("restored config must validate");
764    }
765
766    #[test]
767    fn test_mmap_fields_roundtrip_when_present() {
768        // When the fields ARE present (e.g. a user opting in), they round-trip.
769        let mut config = StorageConfig::default();
770        config.use_mmap = true;
771        config.mmap_min_size_bytes = 8192;
772        let json = serde_json::to_string(&config).unwrap();
773        let restored: StorageConfig = serde_json::from_str(&json).unwrap();
774        assert!(restored.use_mmap);
775        assert_eq!(restored.mmap_min_size_bytes, 8192);
776    }
777
778    #[test]
779    fn test_bloom_filter_disabled() {
780        let mut config = Config::default();
781        config.storage.enable_bloom_filters = false;
782        config.storage.bloom_filter_fp_rate = 0.0; // Should be ignored when bloom filters disabled
783        assert!(config.validate().is_ok());
784
785        config.storage.bloom_filter_fp_rate = 1.0; // Should be ignored when bloom filters disabled
786        assert!(config.validate().is_ok());
787
788        config.storage.bloom_filter_fp_rate = -1.0; // Should be ignored when bloom filters disabled
789        assert!(config.validate().is_ok());
790    }
791}