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    /// How the SSTable read path accesses Data.db on disk.
106    ///
107    /// Defaults to [`DiskAccessMode::Auto`], which sizes each Data.db file
108    /// against system RAM and picks the backend automatically:
109    /// - files below [`Self::mmap_min_size_bytes`] use buffered I/O (mapping a
110    ///   tiny file is not worth the setup cost);
111    /// - files up to [`Self::direct_io_memory_fraction`] of system memory are
112    ///   **memory-mapped**, so repeated scans stay resident in the page cache;
113    /// - files larger than that fraction use **direct I/O** (`O_DIRECT` on
114    ///   Linux, `F_NOCACHE` on macOS), which bypasses the page cache so a
115    ///   single huge scan does not evict everything else the host has cached.
116    ///
117    /// Set an explicit [`DiskAccessMode::Buffered`], [`DiskAccessMode::Mmap`],
118    /// or [`DiskAccessMode::Direct`] to override the heuristic. The legacy
119    /// [`Self::use_mmap`] flag still forces mmap when `Auto` would otherwise
120    /// pick buffered, for backward compatibility.
121    ///
122    /// Can also be set at runtime via `CQLITE_DISK_ACCESS_MODE`
123    /// (`auto` / `buffered` / `mmap` / `direct`).
124    #[serde(default)]
125    pub disk_access_mode: DiskAccessMode,
126
127    /// Fraction of total system memory above which [`DiskAccessMode::Auto`]
128    /// switches a file from memory-mapped to direct I/O. Defaults to `0.5`
129    /// (half of RAM). Clamped to `(0.0, 1.0]`; values outside that range fall
130    /// back to the default. Ignored when system memory cannot be determined
131    /// (in which case `Auto` never escalates to direct I/O).
132    #[serde(default = "default_direct_io_memory_fraction")]
133    pub direct_io_memory_fraction: f64,
134
135    /// Read-ahead / prefetch strategy applied to the chosen backend.
136    ///
137    /// Defaults to [`PrefetchMode::Auto`], which advises the kernel for
138    /// sequential access on the mmap backend and uses a prefetch window of
139    /// [`Self::direct_io_prefetch_bytes`] on the direct-I/O backend. Set
140    /// [`PrefetchMode::Off`] to disable explicit hints (relying only on default
141    /// kernel read-ahead / single-block direct reads). Can also be set via
142    /// `CQLITE_PREFETCH` (`off` / `sequential` / `willneed` / `auto`).
143    #[serde(default)]
144    pub prefetch: PrefetchMode,
145
146    /// Size in bytes of the read-ahead window used by the direct-I/O backend
147    /// (and the buffered backend's reader capacity hint). Rounded up to the
148    /// I/O alignment. Defaults to 1 MiB. Only takes effect when
149    /// [`Self::prefetch`] is not [`PrefetchMode::Off`].
150    #[serde(default = "default_direct_io_prefetch_bytes")]
151    pub direct_io_prefetch_bytes: usize,
152}
153
154/// Selects which backend the SSTable read path uses for Data.db I/O.
155///
156/// See [`StorageConfig::disk_access_mode`] for the per-variant semantics and
157/// the [`DiskAccessMode::Auto`] sizing heuristic.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
159#[serde(rename_all = "lowercase")]
160pub enum DiskAccessMode {
161    /// Size each file against system RAM and pick buffered / mmap / direct.
162    #[default]
163    Auto,
164    /// Always use buffered file I/O through the OS page cache.
165    Buffered,
166    /// Always memory-map the file. Unlike the [`DiskAccessMode::Auto`] heuristic,
167    /// this honors the user's explicit request and is **not** gated by
168    /// [`StorageConfig::mmap_min_size_bytes`] (the size threshold only steers
169    /// `Auto`); a zero-length file still falls back to buffered I/O since an
170    /// empty map is invalid.
171    Mmap,
172    /// Always use direct I/O, bypassing the OS page cache.
173    Direct,
174}
175
176/// Selects the read-ahead hint applied to the active disk-access backend.
177///
178/// See [`StorageConfig::prefetch`]. `Sequential` / `WillNeed` map to the
179/// corresponding `madvise(2)` advice on the mmap backend; on the direct-I/O
180/// backend any non-`Off` value enables the [`StorageConfig::direct_io_prefetch_bytes`]
181/// read-ahead window.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
183#[serde(rename_all = "lowercase")]
184pub enum PrefetchMode {
185    /// No explicit prefetch hint; rely on default kernel behaviour.
186    Off,
187    /// Hint sequential access (aggressive read-ahead, drop-behind).
188    Sequential,
189    /// Hint that the mapped/region bytes will be needed soon (eager fault-in).
190    WillNeed,
191    /// Let the backend choose: sequential advice for mmap, windowed read-ahead
192    /// for direct I/O.
193    #[default]
194    Auto,
195}
196
197/// Default for [`StorageConfig::use_mmap`]: mmap is opt-in, so buffered I/O.
198fn default_use_mmap() -> bool {
199    false
200}
201
202/// Default for [`StorageConfig::mmap_min_size_bytes`]: one page.
203fn default_mmap_min_size_bytes() -> usize {
204    4096
205}
206
207/// Default for [`StorageConfig::direct_io_memory_fraction`]: half of RAM.
208fn default_direct_io_memory_fraction() -> f64 {
209    0.5
210}
211
212/// Default for [`StorageConfig::direct_io_prefetch_bytes`]: 1 MiB.
213fn default_direct_io_prefetch_bytes() -> usize {
214    1024 * 1024
215}
216
217impl Default for StorageConfig {
218    fn default() -> Self {
219        Self {
220            max_sstable_size: 64 * 1024 * 1024,        // 64MB
221            memtable_size_threshold: 16 * 1024 * 1024, // 16MB
222            compaction: CompactionConfig::default(),
223            block_size: 64 * 1024, // 64KB
224            compression: CompressionConfig::default(),
225            enable_bloom_filters: true,
226            bloom_filter_fp_rate: 0.01,
227            io_threads: num_cpus::get().min(4),
228            sync_mode: SyncMode::Normal,
229            // Opt-in; buffered I/O is the portable, safe default. Shared with
230            // the serde defaults so the two can never drift.
231            use_mmap: default_use_mmap(),
232            mmap_min_size_bytes: default_mmap_min_size_bytes(),
233            disk_access_mode: DiskAccessMode::default(),
234            direct_io_memory_fraction: default_direct_io_memory_fraction(),
235            prefetch: PrefetchMode::default(),
236            direct_io_prefetch_bytes: default_direct_io_prefetch_bytes(),
237        }
238    }
239}
240
241/// Compaction strategy configuration
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct CompactionConfig {
244    /// Compaction strategy to use
245    pub strategy: CompactionStrategy,
246
247    /// Maximum number of SSTables before triggering compaction
248    pub max_sstables: usize,
249
250    /// Size ratio for triggering compaction
251    pub size_ratio: f64,
252
253    /// Maximum compaction threads
254    pub max_threads: usize,
255
256    /// Compaction interval for background compaction
257    pub background_interval: Duration,
258
259    /// Enable automatic background compaction
260    pub auto_compaction: bool,
261}
262
263impl Default for CompactionConfig {
264    fn default() -> Self {
265        Self {
266            strategy: CompactionStrategy::Leveled,
267            max_sstables: 10,
268            size_ratio: 2.0,
269            max_threads: 2,
270            background_interval: Duration::from_secs(300), // 5 minutes
271            auto_compaction: true,
272        }
273    }
274}
275
276/// Memory management configuration
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct MemoryConfig {
279    /// Maximum total memory usage (default: 1GB)
280    pub max_memory: u64,
281
282    /// Block cache configuration
283    pub block_cache: CacheConfig,
284
285    /// Row cache configuration  
286    pub row_cache: CacheConfig,
287
288    /// Query result cache configuration
289    pub query_cache: CacheConfig,
290
291    /// Memory allocator settings
292    pub allocator: AllocatorConfig,
293}
294
295impl Default for MemoryConfig {
296    fn default() -> Self {
297        let max_memory = 1024 * 1024 * 1024; // 1GB
298
299        Self {
300            max_memory,
301            block_cache: CacheConfig {
302                enabled: true,
303                max_size: max_memory / 4, // 256MB
304                policy: CachePolicy::Lru,
305            },
306            row_cache: CacheConfig {
307                enabled: true,
308                max_size: max_memory / 8, // 128MB
309                policy: CachePolicy::Lru,
310            },
311            query_cache: CacheConfig {
312                enabled: true,
313                max_size: max_memory / 16, // 64MB
314                policy: CachePolicy::Lru,
315            },
316            allocator: AllocatorConfig::default(),
317        }
318    }
319}
320
321/// Cache configuration
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct CacheConfig {
324    /// Enable this cache
325    pub enabled: bool,
326
327    /// Maximum cache size in bytes
328    pub max_size: u64,
329
330    /// Cache eviction policy
331    pub policy: CachePolicy,
332}
333
334/// Cache eviction policies
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub enum CachePolicy {
337    /// Least Recently Used
338    Lru,
339    /// Least Frequently Used
340    Lfu,
341    /// Adaptive Replacement Cache
342    Arc,
343}
344
345/// Memory allocator configuration
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct AllocatorConfig {
348    /// Use custom allocator for better performance
349    pub use_custom: bool,
350
351    /// Pool size for small allocations
352    pub small_pool_size: u64,
353
354    /// Pool size for large allocations
355    pub large_pool_size: u64,
356}
357
358impl Default for AllocatorConfig {
359    fn default() -> Self {
360        Self {
361            use_custom: false,                  // Conservative default
362            small_pool_size: 64 * 1024 * 1024,  // 64MB
363            large_pool_size: 256 * 1024 * 1024, // 256MB
364        }
365    }
366}
367
368/// Query engine configuration
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct QueryConfig {
371    /// Maximum query execution time
372    pub max_execution_time: Duration,
373
374    /// Maximum number of rows to return in a result set
375    pub max_result_rows: u64,
376
377    /// Query plan cache size
378    pub plan_cache_size: usize,
379
380    /// Enable query optimization
381    pub enable_optimization: bool,
382
383    /// Parallel query execution configuration
384    pub parallel: ParallelQueryConfig,
385
386    /// Query cache size (for plan caching)
387    pub query_cache_size: Option<usize>,
388
389    /// Query parallelism thread count
390    pub query_parallelism: Option<usize>,
391
392    /// Number of iterations for query analysis
393    pub analyze_iterations: Option<usize>,
394}
395
396impl Default for QueryConfig {
397    fn default() -> Self {
398        Self {
399            max_execution_time: Duration::from_secs(300), // 5 minutes
400            max_result_rows: 1_000_000,
401            plan_cache_size: 1000,
402            enable_optimization: true,
403            parallel: ParallelQueryConfig::default(),
404            query_cache_size: Some(100),
405            query_parallelism: Some(num_cpus::get()),
406            analyze_iterations: Some(5),
407        }
408    }
409}
410
411/// Parallel query execution configuration
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct ParallelQueryConfig {
414    /// Enable parallel query execution
415    pub enabled: bool,
416
417    /// Maximum number of parallel threads
418    pub max_threads: usize,
419
420    /// Minimum result set size to trigger parallel execution
421    pub min_parallel_rows: u64,
422}
423
424impl Default for ParallelQueryConfig {
425    fn default() -> Self {
426        Self {
427            enabled: true,
428            max_threads: num_cpus::get(),
429            min_parallel_rows: 10_000,
430        }
431    }
432}
433
434/// Performance and optimization configuration
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct PerformanceConfig {
437    /// Enable performance metrics collection
438    pub enable_metrics: bool,
439
440    /// Metrics collection interval
441    pub metrics_interval: Duration,
442
443    /// Enable detailed profiling
444    pub enable_profiling: bool,
445
446    /// Background task configuration
447    pub background_tasks: BackgroundTaskConfig,
448}
449
450impl Default for PerformanceConfig {
451    fn default() -> Self {
452        Self {
453            enable_metrics: true,
454            metrics_interval: Duration::from_secs(60),
455            enable_profiling: false,
456            background_tasks: BackgroundTaskConfig::default(),
457        }
458    }
459}
460
461/// Background task configuration
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct BackgroundTaskConfig {
464    /// Enable background statistics collection
465    pub enable_stats: bool,
466
467    /// Statistics collection interval
468    pub stats_interval: Duration,
469
470    /// Enable background cleanup tasks
471    pub enable_cleanup: bool,
472
473    /// Cleanup task interval
474    pub cleanup_interval: Duration,
475}
476
477impl Default for BackgroundTaskConfig {
478    fn default() -> Self {
479        Self {
480            enable_stats: true,
481            stats_interval: Duration::from_secs(300), // 5 minutes
482            enable_cleanup: true,
483            cleanup_interval: Duration::from_secs(3600), // 1 hour
484        }
485    }
486}
487
488/// WASM-specific configuration
489#[cfg(target_arch = "wasm32")]
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct WasmConfig {
492    /// Use IndexedDB for persistent storage
493    pub use_indexeddb: bool,
494
495    /// Maximum memory usage in WASM (default: 256MB)
496    pub max_memory: u64,
497
498    /// Enable WASM SIMD optimizations
499    pub enable_simd: bool,
500
501    /// Enable Web Workers for background tasks
502    pub enable_workers: bool,
503
504    /// Maximum number of Web Workers
505    pub max_workers: usize,
506}
507
508#[cfg(target_arch = "wasm32")]
509impl Default for WasmConfig {
510    fn default() -> Self {
511        Self {
512            use_indexeddb: true,
513            max_memory: 256 * 1024 * 1024, // 256MB
514            enable_simd: true,
515            enable_workers: true,
516            max_workers: 4,
517        }
518    }
519}
520
521/// Compression algorithms
522#[derive(Debug, Clone, Serialize, Deserialize)]
523pub enum CompressionAlgorithm {
524    /// No compression
525    None,
526    /// LZ4 compression (fast)
527    Lz4,
528    /// Snappy compression (balanced)
529    Snappy,
530    /// Deflate compression (good compression ratio)
531    Deflate,
532    /// ZSTD compression (high compression ratio)
533    Zstd,
534}
535
536/// Compression configuration
537#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct CompressionConfig {
539    /// Enable compression
540    pub enabled: bool,
541
542    /// Compression algorithm to use
543    pub algorithm: CompressionAlgorithm,
544
545    /// Compression level (algorithm-specific)
546    pub level: i32,
547
548    /// Minimum block size to compress (smaller blocks are stored uncompressed)
549    pub min_block_size: u32,
550}
551
552impl Default for CompressionConfig {
553    fn default() -> Self {
554        Self {
555            enabled: true,
556            algorithm: CompressionAlgorithm::Lz4,
557            level: 1,             // Fast compression
558            min_block_size: 1024, // 1KB minimum
559        }
560    }
561}
562
563/// Compaction strategies
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub enum CompactionStrategy {
566    /// Simple size-based compaction
567    Size,
568    /// Leveled compaction (like LevelDB)
569    Leveled,
570    /// Universal compaction
571    Universal,
572}
573
574/// Durability sync modes
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub enum SyncMode {
577    /// No explicit syncing (fastest, least durable)
578    None,
579    /// Normal syncing (balanced)
580    Normal,
581    /// Full sync for every write (slowest, most durable)
582    Full,
583}
584
585impl Config {
586    /// Create a configuration optimized for memory usage
587    pub fn memory_optimized() -> Self {
588        let mut config = Self::default();
589
590        // Reduce memory usage
591        config.storage.memtable_size_threshold = 4 * 1024 * 1024; // 4MB
592        config.storage.max_sstable_size = 16 * 1024 * 1024; // 16MB
593        config.memory.max_memory = 256 * 1024 * 1024; // 256MB
594        config.memory.block_cache.max_size = 64 * 1024 * 1024; // 64MB
595        config.memory.row_cache.max_size = 32 * 1024 * 1024; // 32MB
596        config.memory.query_cache.max_size = 16 * 1024 * 1024; // 16MB
597
598        // Enable aggressive compression
599        config.storage.compression.algorithm = CompressionAlgorithm::Zstd;
600        config.storage.compression.enabled = true;
601
602        config
603    }
604
605    /// Create a configuration optimized for performance
606    pub fn performance_optimized() -> Self {
607        let mut config = Self::default();
608
609        // Increase memory usage for better performance
610        config.storage.memtable_size_threshold = 64 * 1024 * 1024; // 64MB
611        config.storage.max_sstable_size = 256 * 1024 * 1024; // 256MB
612        config.memory.max_memory = 4 * 1024 * 1024 * 1024; // 4GB
613
614        // Use faster compression
615        config.storage.compression.algorithm = CompressionAlgorithm::Lz4;
616        config.storage.compression.enabled = true;
617
618        // More aggressive caching
619        config.memory.block_cache.max_size = 1024 * 1024 * 1024; // 1GB
620        config.memory.row_cache.max_size = 512 * 1024 * 1024; // 512MB
621        config.memory.query_cache.max_size = 256 * 1024 * 1024; // 256MB
622
623        // More I/O threads
624        config.storage.io_threads = num_cpus::get();
625
626        config
627    }
628
629    /// Create a configuration optimized for WASM deployment
630    #[cfg(target_arch = "wasm32")]
631    pub fn wasm_optimized() -> Self {
632        let mut config = Self::memory_optimized();
633
634        // WASM-specific optimizations
635        config.wasm.max_memory = 128 * 1024 * 1024; // 128MB
636        config.wasm.enable_simd = true;
637        config.wasm.enable_workers = false; // Conservative default
638
639        // Reduce overall memory usage for WASM
640        config.memory.max_memory = 128 * 1024 * 1024; // 128MB
641        config.storage.memtable_size_threshold = 2 * 1024 * 1024; // 2MB
642        config.storage.max_sstable_size = 8 * 1024 * 1024; // 8MB
643
644        // Disable background tasks that may not work well in WASM
645        config.storage.compaction.auto_compaction = false;
646        config.performance.background_tasks.enable_stats = false;
647        config.performance.background_tasks.enable_cleanup = false;
648
649        config
650    }
651
652    /// Create a test-optimized configuration
653    #[cfg(test)]
654    pub fn test_config() -> Self {
655        let mut config = Config::default();
656
657        // Disable background tasks that can cause test hangs
658        config.storage.compaction.auto_compaction = false;
659        config.performance.background_tasks.enable_stats = false;
660        config.performance.background_tasks.enable_cleanup = false;
661
662        // Reduce timeouts for faster test execution
663        config.query.max_execution_time = std::time::Duration::from_secs(1);
664        config.storage.compaction.background_interval = std::time::Duration::from_secs(10);
665
666        // Smaller memory usage for tests
667        config.memory.max_memory = 64 * 1024 * 1024; // 64MB
668        config.storage.memtable_size_threshold = 1024 * 1024; // 1MB
669        config.storage.max_sstable_size = 4 * 1024 * 1024; // 4MB
670
671        config
672    }
673
674    /// Validate the configuration
675    pub fn validate(&self) -> crate::Result<()> {
676        // Validate memory limits
677        if self.memory.max_memory == 0 {
678            return Err(crate::Error::configuration(
679                "max_memory must be greater than 0",
680            ));
681        }
682
683        // Validate cache sizes don't exceed total memory
684        let total_cache = self.memory.block_cache.max_size
685            + self.memory.row_cache.max_size
686            + self.memory.query_cache.max_size;
687
688        if total_cache > self.memory.max_memory {
689            return Err(crate::Error::configuration(
690                "total cache size exceeds max_memory",
691            ));
692        }
693
694        // Validate storage settings
695        if self.storage.block_size == 0 {
696            return Err(crate::Error::configuration(
697                "block_size must be greater than 0",
698            ));
699        }
700
701        if self.storage.memtable_size_threshold == 0 {
702            return Err(crate::Error::configuration(
703                "memtable_size_threshold must be greater than 0",
704            ));
705        }
706
707        // Validate bloom filter settings
708        if self.storage.enable_bloom_filters
709            && (self.storage.bloom_filter_fp_rate <= 0.0
710                || self.storage.bloom_filter_fp_rate >= 1.0)
711        {
712            return Err(crate::Error::configuration(
713                "bloom_filter_fp_rate must be between 0 and 1",
714            ));
715        }
716
717        Ok(())
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn test_default_config() {
727        let config = Config::default();
728        assert!(config.storage.compression.enabled);
729        assert!(config.storage.enable_bloom_filters);
730        assert!(config.memory.block_cache.enabled);
731    }
732
733    #[test]
734    fn test_memory_optimized_config() {
735        let config = Config::memory_optimized();
736        assert!(
737            config.storage.memtable_size_threshold
738                < Config::default().storage.memtable_size_threshold
739        );
740        assert!(config.memory.max_memory < Config::default().memory.max_memory);
741    }
742
743    #[test]
744    fn test_performance_optimized_config() {
745        let config = Config::performance_optimized();
746        assert!(
747            config.storage.memtable_size_threshold
748                > Config::default().storage.memtable_size_threshold
749        );
750        assert!(config.memory.max_memory > Config::default().memory.max_memory);
751    }
752
753    #[test]
754    fn test_config_validation() {
755        let mut config = Config::default();
756        assert!(config.validate().is_ok());
757
758        // Test invalid max_memory
759        config.memory.max_memory = 0;
760        assert!(config.validate().is_err());
761
762        // Reset and test invalid cache sizes
763        config = Config::default();
764        config.memory.block_cache.max_size = config.memory.max_memory + 1;
765        assert!(config.validate().is_err());
766    }
767
768    #[test]
769    fn test_storage_validation_errors() {
770        let mut config = Config::default();
771
772        // Test invalid block_size (should trigger line 573-574)
773        config.storage.block_size = 0;
774        let result = config.validate();
775        assert!(result.is_err());
776        assert!(result
777            .unwrap_err()
778            .to_string()
779            .contains("block_size must be greater than 0"));
780
781        // Reset and test invalid memtable_size_threshold (should trigger line 579-580)
782        config = Config::default();
783        config.storage.memtable_size_threshold = 0;
784        let result = config.validate();
785        assert!(result.is_err());
786        assert!(result
787            .unwrap_err()
788            .to_string()
789            .contains("memtable_size_threshold must be greater than 0"));
790
791        // Reset and test invalid bloom filter false positive rate (should trigger line 589-590)
792        config = Config::default();
793        config.storage.enable_bloom_filters = true;
794        config.storage.bloom_filter_fp_rate = 0.0; // Invalid: exactly 0
795        let result = config.validate();
796        assert!(result.is_err());
797        assert!(result
798            .unwrap_err()
799            .to_string()
800            .contains("bloom_filter_fp_rate must be between 0 and 1"));
801
802        // Test another invalid bloom filter false positive rate
803        config.storage.bloom_filter_fp_rate = 1.0; // Invalid: exactly 1
804        let result = config.validate();
805        assert!(result.is_err());
806
807        // Test bloom filter rate above 1
808        config.storage.bloom_filter_fp_rate = 1.5; // Invalid: greater than 1
809        let result = config.validate();
810        assert!(result.is_err());
811
812        // Test bloom filter rate below 0
813        config.storage.bloom_filter_fp_rate = -0.1; // Invalid: less than 0
814        let result = config.validate();
815        assert!(result.is_err());
816    }
817
818    #[test]
819    fn test_valid_bloom_filter_config() {
820        let mut config = Config::default();
821        config.storage.enable_bloom_filters = true;
822        config.storage.bloom_filter_fp_rate = 0.01; // Valid rate
823        assert!(config.validate().is_ok());
824
825        config.storage.bloom_filter_fp_rate = 0.5; // Valid rate
826        assert!(config.validate().is_ok());
827
828        config.storage.bloom_filter_fp_rate = 0.99; // Valid rate
829        assert!(config.validate().is_ok());
830    }
831
832    #[test]
833    fn test_storage_config_deserializes_without_mmap_fields() {
834        // Backward compatibility: a config payload serialized before the mmap
835        // fields existed omits `use_mmap` / `mmap_min_size_bytes`. It must still
836        // deserialize, defaulting to the safe buffered backend.
837        let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
838        let obj = value.as_object_mut().unwrap();
839        obj.remove("use_mmap");
840        obj.remove("mmap_min_size_bytes");
841        assert!(!obj.contains_key("use_mmap"));
842
843        let restored: StorageConfig =
844            serde_json::from_value(value).expect("old payload must still deserialize");
845        assert!(!restored.use_mmap, "missing use_mmap must default to false");
846        assert_eq!(
847            restored.mmap_min_size_bytes, 4096,
848            "missing mmap_min_size_bytes must default to one page"
849        );
850    }
851
852    #[test]
853    fn test_full_config_deserializes_without_mmap_fields() {
854        // Same guarantee through the top-level Config, mirroring how the Python
855        // bindings parse a JSON/dict payload into `cqlite_core::Config`.
856        let mut value = serde_json::to_value(Config::default()).unwrap();
857        let storage = value
858            .get_mut("storage")
859            .and_then(|s| s.as_object_mut())
860            .unwrap();
861        storage.remove("use_mmap");
862        storage.remove("mmap_min_size_bytes");
863
864        let restored: Config =
865            serde_json::from_value(value).expect("old Config payload must still deserialize");
866        assert!(!restored.storage.use_mmap);
867        assert_eq!(restored.storage.mmap_min_size_bytes, 4096);
868        restored.validate().expect("restored config must validate");
869    }
870
871    #[test]
872    fn test_mmap_fields_roundtrip_when_present() {
873        // When the fields ARE present (e.g. a user opting in), they round-trip.
874        let mut config = StorageConfig::default();
875        config.use_mmap = true;
876        config.mmap_min_size_bytes = 8192;
877        let json = serde_json::to_string(&config).unwrap();
878        let restored: StorageConfig = serde_json::from_str(&json).unwrap();
879        assert!(restored.use_mmap);
880        assert_eq!(restored.mmap_min_size_bytes, 8192);
881    }
882
883    #[test]
884    fn test_disk_access_defaults() {
885        // The new fields default to the size-aware Auto backend with Auto
886        // prefetch, a half-RAM direct-I/O threshold, and a 1 MiB window.
887        let config = StorageConfig::default();
888        assert_eq!(config.disk_access_mode, DiskAccessMode::Auto);
889        assert_eq!(config.prefetch, PrefetchMode::Auto);
890        assert_eq!(config.direct_io_memory_fraction, 0.5);
891        assert_eq!(config.direct_io_prefetch_bytes, 1024 * 1024);
892    }
893
894    #[test]
895    fn test_storage_config_deserializes_without_disk_access_fields() {
896        // Backward compatibility: a payload predating the disk-access fields must
897        // still deserialize, defaulting to Auto / Auto / 0.5 / 1 MiB.
898        let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
899        let obj = value.as_object_mut().unwrap();
900        for key in [
901            "disk_access_mode",
902            "direct_io_memory_fraction",
903            "prefetch",
904            "direct_io_prefetch_bytes",
905        ] {
906            obj.remove(key);
907        }
908
909        let restored: StorageConfig =
910            serde_json::from_value(value).expect("old payload must still deserialize");
911        assert_eq!(restored.disk_access_mode, DiskAccessMode::Auto);
912        assert_eq!(restored.prefetch, PrefetchMode::Auto);
913        assert_eq!(restored.direct_io_memory_fraction, 0.5);
914        assert_eq!(restored.direct_io_prefetch_bytes, 1024 * 1024);
915    }
916
917    #[test]
918    fn test_disk_access_fields_roundtrip() {
919        // Explicit selections round-trip, including the lowercase enum encoding.
920        let mut config = StorageConfig::default();
921        config.disk_access_mode = DiskAccessMode::Direct;
922        config.prefetch = PrefetchMode::WillNeed;
923        config.direct_io_memory_fraction = 0.25;
924        config.direct_io_prefetch_bytes = 2 * 1024 * 1024;
925        let json = serde_json::to_string(&config).unwrap();
926        assert!(json.contains("\"direct\""), "enum must serialize lowercase");
927        assert!(json.contains("\"willneed\""));
928        let restored: StorageConfig = serde_json::from_str(&json).unwrap();
929        assert_eq!(restored.disk_access_mode, DiskAccessMode::Direct);
930        assert_eq!(restored.prefetch, PrefetchMode::WillNeed);
931        assert_eq!(restored.direct_io_memory_fraction, 0.25);
932        assert_eq!(restored.direct_io_prefetch_bytes, 2 * 1024 * 1024);
933    }
934
935    #[test]
936    fn test_bloom_filter_disabled() {
937        let mut config = Config::default();
938        config.storage.enable_bloom_filters = false;
939        config.storage.bloom_filter_fp_rate = 0.0; // Should be ignored when bloom filters disabled
940        assert!(config.validate().is_ok());
941
942        config.storage.bloom_filter_fp_rate = 1.0; // Should be ignored when bloom filters disabled
943        assert!(config.validate().is_ok());
944
945        config.storage.bloom_filter_fp_rate = -1.0; // Should be ignored when bloom filters disabled
946        assert!(config.validate().is_ok());
947    }
948}