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 issues **no** mmap `madvise`
138    /// (relying on the kernel's default read-ahead) and only enables the
139    /// direct-I/O prefetch window of [`Self::direct_io_prefetch_bytes`]. 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. For mmap this issues **no** madvise and relies on
192    /// the kernel's default read-ahead: `MADV_SEQUENTIAL`'s drop-behind evicts
193    /// hot pages under concurrent write load and inflates the read-side p99 tail
194    /// (issue #1143), so `Auto` avoids it while keeping the isolated mmap win.
195    /// For direct I/O it enables the windowed read-ahead
196    /// ([`StorageConfig::direct_io_prefetch_bytes`]). Request
197    /// [`PrefetchMode::Sequential`] explicitly for `MADV_SEQUENTIAL` behaviour.
198    #[default]
199    Auto,
200}
201
202/// Default for [`StorageConfig::use_mmap`]: mmap is opt-in, so buffered I/O.
203fn default_use_mmap() -> bool {
204    false
205}
206
207/// Default for [`StorageConfig::mmap_min_size_bytes`]: one page.
208fn default_mmap_min_size_bytes() -> usize {
209    4096
210}
211
212/// Default for [`StorageConfig::direct_io_memory_fraction`]: half of RAM.
213fn default_direct_io_memory_fraction() -> f64 {
214    0.5
215}
216
217/// Default for [`StorageConfig::direct_io_prefetch_bytes`]: 1 MiB.
218fn default_direct_io_prefetch_bytes() -> usize {
219    1024 * 1024
220}
221
222impl Default for StorageConfig {
223    fn default() -> Self {
224        Self {
225            max_sstable_size: 64 * 1024 * 1024,        // 64MB
226            memtable_size_threshold: 16 * 1024 * 1024, // 16MB
227            compaction: CompactionConfig::default(),
228            block_size: 64 * 1024, // 64KB
229            compression: CompressionConfig::default(),
230            enable_bloom_filters: true,
231            bloom_filter_fp_rate: 0.01,
232            io_threads: num_cpus::get().min(4),
233            sync_mode: SyncMode::Normal,
234            // Opt-in; buffered I/O is the portable, safe default. Shared with
235            // the serde defaults so the two can never drift.
236            use_mmap: default_use_mmap(),
237            mmap_min_size_bytes: default_mmap_min_size_bytes(),
238            disk_access_mode: DiskAccessMode::default(),
239            direct_io_memory_fraction: default_direct_io_memory_fraction(),
240            prefetch: PrefetchMode::default(),
241            direct_io_prefetch_bytes: default_direct_io_prefetch_bytes(),
242        }
243    }
244}
245
246/// Compaction strategy configuration.
247///
248/// The write engine implements Size-Tiered Compaction Strategy (STCS) and
249/// installs it by default (issue #1619). `auto_compaction` is the authoritative
250/// on/off switch consumed by the write path via
251/// `WriteEngineConfig::with_compaction_config`. Previously this struct also
252/// carried `strategy`/`max_sstables`/`size_ratio`/`max_threads`/
253/// `background_interval` fields that were never read by any behavior; they were
254/// removed (issue #1619) rather than left decorative.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct CompactionConfig {
257    /// Enable automatic (STCS) compaction. When `false`, the write engine
258    /// installs no merge policy and `maintenance_step` is a no-op.
259    pub auto_compaction: bool,
260}
261
262impl Default for CompactionConfig {
263    fn default() -> Self {
264        Self {
265            auto_compaction: true,
266        }
267    }
268}
269
270/// Memory management configuration
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct MemoryConfig {
273    /// Maximum total memory usage (default: 1GB)
274    pub max_memory: u64,
275
276    /// Block cache configuration
277    pub block_cache: CacheConfig,
278
279    /// Row cache configuration  
280    pub row_cache: CacheConfig,
281
282    /// Query result cache configuration
283    pub query_cache: CacheConfig,
284
285    /// Memory allocator settings
286    pub allocator: AllocatorConfig,
287}
288
289impl Default for MemoryConfig {
290    fn default() -> Self {
291        let max_memory = 1024 * 1024 * 1024; // 1GB
292
293        Self {
294            max_memory,
295            block_cache: CacheConfig {
296                enabled: true,
297                max_size: max_memory / 4, // 256MB
298                policy: CachePolicy::Lru,
299            },
300            row_cache: CacheConfig {
301                enabled: true,
302                max_size: max_memory / 8, // 128MB
303                policy: CachePolicy::Lru,
304            },
305            query_cache: CacheConfig {
306                enabled: true,
307                max_size: max_memory / 16, // 64MB
308                policy: CachePolicy::Lru,
309            },
310            allocator: AllocatorConfig::default(),
311        }
312    }
313}
314
315/// Cache configuration
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct CacheConfig {
318    /// Enable this cache
319    pub enabled: bool,
320
321    /// Maximum cache size in bytes
322    pub max_size: u64,
323
324    /// Cache eviction policy
325    pub policy: CachePolicy,
326}
327
328/// Cache eviction policies
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub enum CachePolicy {
331    /// Least Recently Used
332    Lru,
333    /// Least Frequently Used
334    Lfu,
335    /// Adaptive Replacement Cache
336    Arc,
337}
338
339/// Memory allocator configuration
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct AllocatorConfig {
342    /// Use custom allocator for better performance
343    pub use_custom: bool,
344
345    /// Pool size for small allocations
346    pub small_pool_size: u64,
347
348    /// Pool size for large allocations
349    pub large_pool_size: u64,
350}
351
352impl Default for AllocatorConfig {
353    fn default() -> Self {
354        Self {
355            use_custom: false,                  // Conservative default
356            small_pool_size: 64 * 1024 * 1024,  // 64MB
357            large_pool_size: 256 * 1024 * 1024, // 256MB
358        }
359    }
360}
361
362/// Default byte ceiling for a materialized SELECT result set (issue #1582).
363///
364/// 64 MiB. See [`QueryConfig::max_result_bytes`] for the derivation from the
365/// project's <128MB process memory target.
366pub const DEFAULT_MAX_RESULT_BYTES: u64 = 64 * 1024 * 1024;
367
368/// Serde default for [`QueryConfig::max_result_bytes`] (issue #1582).
369///
370/// Backward-compat: a `QueryConfig` serialized before this field existed (e.g.
371/// a Python JSON/dict config) has no `max_result_bytes` key. Without a serde
372/// default, deserialization fails with a missing-field error; with it, such a
373/// config takes the shipped [`DEFAULT_MAX_RESULT_BYTES`] budget.
374fn default_max_result_bytes() -> u64 {
375    DEFAULT_MAX_RESULT_BYTES
376}
377
378/// Serde default for [`QueryConfig::max_result_rows`] (issue #1582).
379///
380/// Backward-compat + robustness: a `QueryConfig` serialized without this key
381/// (or a partial JSON/dict config) still deserializes, taking the shipped
382/// 1,000,000-row secondary safety valve rather than failing with a missing
383/// field. Keeps the knob real (not decorative) and consistent with
384/// [`default_max_result_bytes`].
385fn default_max_result_rows() -> u64 {
386    1_000_000
387}
388
389/// Query engine configuration
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct QueryConfig {
392    /// Maximum query execution time
393    pub max_execution_time: Duration,
394
395    /// Maximum number of rows to return in a result set.
396    ///
397    /// A *secondary* safety valve, retained for defense-in-depth (issue #1582).
398    /// The primary guard on a materialized result is now `max_result_bytes`: a
399    /// row count is the wrong unit because 1M skinny rows can fit comfortably
400    /// while 100k wide rows blow the <128MB memory target. Still load-bearing:
401    /// the materializing SELECT path enforces this row-count ceiling alongside
402    /// the byte budget (lowering it makes a wide-row-count result trip even
403    /// under the byte budget), so it is a real knob, not decoration.
404    #[serde(default = "default_max_result_rows")]
405    pub max_result_rows: u64,
406
407    /// Byte ceiling on a MATERIALIZED result set (issue #1582 / D6).
408    ///
409    /// While the SELECT executor collects a materialized `Vec<QueryRow>`, it
410    /// tracks a running estimate of the result's logical size (via the shared
411    /// [`crate::memory::estimate_row_size`] estimator) and fails with
412    /// [`crate::Error::ResultTooLarge`] once this ceiling is crossed — telling
413    /// the caller to add a `LIMIT` or use the streaming API. This is the
414    /// correct-unit primary guard; `max_result_rows` remains as a secondary
415    /// valve. Streaming queries are bounded by their channel buffer, so this
416    /// budget does not apply to them.
417    ///
418    /// Default: [`DEFAULT_MAX_RESULT_BYTES`] (64 MiB). Chosen well below the
419    /// project's <128MB process memory target: the estimator measures *logical*
420    /// content bytes and does not count per-row container overhead
421    /// (`HashMap<Arc<str>, Value>` slots, `String`/`Vec` capacity slack, row
422    /// metadata), which in practice roughly doubles real heap use — so a 64 MiB
423    /// logical ceiling keeps a fully-materialized result comfortably inside the
424    /// process budget while leaving headroom for readers, caches, and decode
425    /// buffers.
426    #[serde(default = "default_max_result_bytes")]
427    pub max_result_bytes: u64,
428
429    /// Query plan cache size
430    pub plan_cache_size: usize,
431
432    /// Enable query optimization
433    pub enable_optimization: bool,
434
435    /// Parallel query execution configuration
436    pub parallel: ParallelQueryConfig,
437
438    /// Query cache size (for plan caching)
439    pub query_cache_size: Option<usize>,
440
441    /// Query parallelism thread count
442    pub query_parallelism: Option<usize>,
443
444    /// Number of iterations for query analysis
445    pub analyze_iterations: Option<usize>,
446}
447
448impl Default for QueryConfig {
449    fn default() -> Self {
450        Self {
451            max_execution_time: Duration::from_secs(300), // 5 minutes
452            max_result_rows: 1_000_000,
453            max_result_bytes: DEFAULT_MAX_RESULT_BYTES,
454            plan_cache_size: 1000,
455            enable_optimization: true,
456            parallel: ParallelQueryConfig::default(),
457            query_cache_size: Some(100),
458            query_parallelism: Some(num_cpus::get()),
459            analyze_iterations: Some(5),
460        }
461    }
462}
463
464/// Parallel query execution configuration
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ParallelQueryConfig {
467    /// Enable parallel query execution
468    pub enabled: bool,
469
470    /// Maximum number of parallel threads
471    pub max_threads: usize,
472
473    /// Minimum result set size to trigger parallel execution
474    pub min_parallel_rows: u64,
475}
476
477impl Default for ParallelQueryConfig {
478    fn default() -> Self {
479        Self {
480            enabled: true,
481            max_threads: num_cpus::get(),
482            min_parallel_rows: 10_000,
483        }
484    }
485}
486
487/// Performance and optimization configuration
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct PerformanceConfig {
490    /// Enable performance metrics collection
491    pub enable_metrics: bool,
492
493    /// Metrics collection interval
494    pub metrics_interval: Duration,
495
496    /// Enable detailed profiling
497    pub enable_profiling: bool,
498
499    /// Background task configuration
500    pub background_tasks: BackgroundTaskConfig,
501}
502
503impl Default for PerformanceConfig {
504    fn default() -> Self {
505        Self {
506            enable_metrics: true,
507            metrics_interval: Duration::from_secs(60),
508            enable_profiling: false,
509            background_tasks: BackgroundTaskConfig::default(),
510        }
511    }
512}
513
514/// Background task configuration
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct BackgroundTaskConfig {
517    /// Enable background statistics collection
518    pub enable_stats: bool,
519
520    /// Statistics collection interval
521    pub stats_interval: Duration,
522
523    /// Enable background cleanup tasks
524    pub enable_cleanup: bool,
525
526    /// Cleanup task interval
527    pub cleanup_interval: Duration,
528}
529
530impl Default for BackgroundTaskConfig {
531    fn default() -> Self {
532        Self {
533            enable_stats: true,
534            stats_interval: Duration::from_secs(300), // 5 minutes
535            enable_cleanup: true,
536            cleanup_interval: Duration::from_secs(3600), // 1 hour
537        }
538    }
539}
540
541/// WASM-specific configuration
542#[cfg(target_arch = "wasm32")]
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct WasmConfig {
545    /// Use IndexedDB for persistent storage
546    pub use_indexeddb: bool,
547
548    /// Maximum memory usage in WASM (default: 256MB)
549    pub max_memory: u64,
550
551    /// Enable WASM SIMD optimizations
552    pub enable_simd: bool,
553
554    /// Enable Web Workers for background tasks
555    pub enable_workers: bool,
556
557    /// Maximum number of Web Workers
558    pub max_workers: usize,
559}
560
561#[cfg(target_arch = "wasm32")]
562impl Default for WasmConfig {
563    fn default() -> Self {
564        Self {
565            use_indexeddb: true,
566            max_memory: 256 * 1024 * 1024, // 256MB
567            enable_simd: true,
568            enable_workers: true,
569            max_workers: 4,
570        }
571    }
572}
573
574/// Compression algorithms
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub enum CompressionAlgorithm {
577    /// No compression
578    None,
579    /// LZ4 compression (fast)
580    Lz4,
581    /// Snappy compression (balanced)
582    Snappy,
583    /// Deflate compression (good compression ratio)
584    Deflate,
585    /// ZSTD compression (high compression ratio)
586    Zstd,
587}
588
589/// Compression configuration
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct CompressionConfig {
592    /// Enable compression
593    pub enabled: bool,
594
595    /// Compression algorithm to use
596    pub algorithm: CompressionAlgorithm,
597
598    /// Compression level (algorithm-specific)
599    pub level: i32,
600
601    /// Minimum block size to compress (smaller blocks are stored uncompressed)
602    pub min_block_size: u32,
603}
604
605impl Default for CompressionConfig {
606    fn default() -> Self {
607        Self {
608            enabled: true,
609            algorithm: CompressionAlgorithm::Lz4,
610            level: 1,             // Fast compression
611            min_block_size: 1024, // 1KB minimum
612        }
613    }
614}
615
616/// Durability sync modes
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub enum SyncMode {
619    /// No explicit syncing (fastest, least durable)
620    None,
621    /// Normal syncing (balanced)
622    Normal,
623    /// Full sync for every write (slowest, most durable)
624    Full,
625}
626
627impl Config {
628    /// Create a configuration optimized for memory usage
629    pub fn memory_optimized() -> Self {
630        let mut config = Self::default();
631
632        // Reduce memory usage
633        config.storage.memtable_size_threshold = 4 * 1024 * 1024; // 4MB
634        config.storage.max_sstable_size = 16 * 1024 * 1024; // 16MB
635        config.memory.max_memory = 256 * 1024 * 1024; // 256MB
636        config.memory.block_cache.max_size = 64 * 1024 * 1024; // 64MB
637        config.memory.row_cache.max_size = 32 * 1024 * 1024; // 32MB
638        config.memory.query_cache.max_size = 16 * 1024 * 1024; // 16MB
639
640        // Enable aggressive compression
641        config.storage.compression.algorithm = CompressionAlgorithm::Zstd;
642        config.storage.compression.enabled = true;
643
644        config
645    }
646
647    /// Create a configuration optimized for performance
648    pub fn performance_optimized() -> Self {
649        let mut config = Self::default();
650
651        // Increase memory usage for better performance
652        config.storage.memtable_size_threshold = 64 * 1024 * 1024; // 64MB
653        config.storage.max_sstable_size = 256 * 1024 * 1024; // 256MB
654        config.memory.max_memory = 4 * 1024 * 1024 * 1024; // 4GB
655
656        // Use faster compression
657        config.storage.compression.algorithm = CompressionAlgorithm::Lz4;
658        config.storage.compression.enabled = true;
659
660        // More aggressive caching
661        config.memory.block_cache.max_size = 1024 * 1024 * 1024; // 1GB
662        config.memory.row_cache.max_size = 512 * 1024 * 1024; // 512MB
663        config.memory.query_cache.max_size = 256 * 1024 * 1024; // 256MB
664
665        // More I/O threads
666        config.storage.io_threads = num_cpus::get();
667
668        config
669    }
670
671    /// Create a configuration optimized for WASM deployment
672    #[cfg(target_arch = "wasm32")]
673    pub fn wasm_optimized() -> Self {
674        let mut config = Self::memory_optimized();
675
676        // WASM-specific optimizations
677        config.wasm.max_memory = 128 * 1024 * 1024; // 128MB
678        config.wasm.enable_simd = true;
679        config.wasm.enable_workers = false; // Conservative default
680
681        // Reduce overall memory usage for WASM
682        config.memory.max_memory = 128 * 1024 * 1024; // 128MB
683        config.storage.memtable_size_threshold = 2 * 1024 * 1024; // 2MB
684        config.storage.max_sstable_size = 8 * 1024 * 1024; // 8MB
685
686        // Disable background tasks that may not work well in WASM
687        config.storage.compaction.auto_compaction = false;
688        config.performance.background_tasks.enable_stats = false;
689        config.performance.background_tasks.enable_cleanup = false;
690
691        config
692    }
693
694    /// Create a test-optimized configuration
695    #[cfg(test)]
696    pub fn test_config() -> Self {
697        let mut config = Config::default();
698
699        // Disable background tasks that can cause test hangs
700        config.storage.compaction.auto_compaction = false;
701        config.performance.background_tasks.enable_stats = false;
702        config.performance.background_tasks.enable_cleanup = false;
703
704        // Reduce timeouts for faster test execution
705        config.query.max_execution_time = std::time::Duration::from_secs(1);
706
707        // Smaller memory usage for tests
708        config.memory.max_memory = 64 * 1024 * 1024; // 64MB
709        config.storage.memtable_size_threshold = 1024 * 1024; // 1MB
710        config.storage.max_sstable_size = 4 * 1024 * 1024; // 4MB
711
712        config
713    }
714
715    /// Validate the configuration
716    pub fn validate(&self) -> crate::Result<()> {
717        // Validate memory limits
718        if self.memory.max_memory == 0 {
719            return Err(crate::Error::configuration(
720                "max_memory must be greater than 0",
721            ));
722        }
723
724        // Validate cache sizes don't exceed total memory
725        let total_cache = self.memory.block_cache.max_size
726            + self.memory.row_cache.max_size
727            + self.memory.query_cache.max_size;
728
729        if total_cache > self.memory.max_memory {
730            return Err(crate::Error::configuration(
731                "total cache size exceeds max_memory",
732            ));
733        }
734
735        // Validate storage settings
736        if self.storage.block_size == 0 {
737            return Err(crate::Error::configuration(
738                "block_size must be greater than 0",
739            ));
740        }
741
742        if self.storage.memtable_size_threshold == 0 {
743            return Err(crate::Error::configuration(
744                "memtable_size_threshold must be greater than 0",
745            ));
746        }
747
748        // Validate bloom filter settings
749        if self.storage.enable_bloom_filters
750            && (self.storage.bloom_filter_fp_rate <= 0.0
751                || self.storage.bloom_filter_fp_rate >= 1.0)
752        {
753            return Err(crate::Error::configuration(
754                "bloom_filter_fp_rate must be between 0 and 1",
755            ));
756        }
757
758        Ok(())
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn test_default_config() {
768        let config = Config::default();
769        assert!(config.storage.compression.enabled);
770        assert!(config.storage.enable_bloom_filters);
771        assert!(config.memory.block_cache.enabled);
772    }
773
774    #[test]
775    fn test_memory_optimized_config() {
776        let config = Config::memory_optimized();
777        assert!(
778            config.storage.memtable_size_threshold
779                < Config::default().storage.memtable_size_threshold
780        );
781        assert!(config.memory.max_memory < Config::default().memory.max_memory);
782    }
783
784    #[test]
785    fn test_performance_optimized_config() {
786        let config = Config::performance_optimized();
787        assert!(
788            config.storage.memtable_size_threshold
789                > Config::default().storage.memtable_size_threshold
790        );
791        assert!(config.memory.max_memory > Config::default().memory.max_memory);
792    }
793
794    /// Issue #1582: a `QueryConfig` serialized BEFORE the byte-budget fields
795    /// existed (e.g. a pre-upgrade Python JSON/dict config) has no
796    /// `max_result_bytes`/`max_result_rows` keys. The `#[serde(default = ...)]`
797    /// on both fields must let it deserialize, taking the shipped defaults rather
798    /// than failing with a missing-field error.
799    #[test]
800    fn budget_fields_deserialize_with_serde_default_when_absent() {
801        // Serialize a default QueryConfig, then STRIP both budget fields to
802        // emulate an old serialized config that predates them.
803        let mut value =
804            serde_json::to_value(QueryConfig::default()).expect("serialize QueryConfig");
805        let obj = value
806            .as_object_mut()
807            .expect("QueryConfig serializes as object");
808        obj.remove("max_result_bytes");
809        obj.remove("max_result_rows");
810        assert!(
811            !obj.contains_key("max_result_bytes") && !obj.contains_key("max_result_rows"),
812            "both fields must be absent for this regression to be meaningful"
813        );
814
815        let restored: QueryConfig =
816            serde_json::from_value(value).expect("old config (no budget fields) must deserialize");
817        assert_eq!(
818            restored.max_result_bytes, DEFAULT_MAX_RESULT_BYTES,
819            "absent max_result_bytes must take the serde default"
820        );
821        assert_eq!(
822            restored.max_result_rows,
823            default_max_result_rows(),
824            "absent max_result_rows must take the serde default"
825        );
826    }
827
828    #[test]
829    fn test_config_validation() {
830        let mut config = Config::default();
831        assert!(config.validate().is_ok());
832
833        // Test invalid max_memory
834        config.memory.max_memory = 0;
835        assert!(config.validate().is_err());
836
837        // Reset and test invalid cache sizes
838        config = Config::default();
839        config.memory.block_cache.max_size = config.memory.max_memory + 1;
840        assert!(config.validate().is_err());
841    }
842
843    #[test]
844    fn test_storage_validation_errors() {
845        let mut config = Config::default();
846
847        // Test invalid block_size (should trigger line 573-574)
848        config.storage.block_size = 0;
849        let result = config.validate();
850        assert!(result.is_err());
851        assert!(result
852            .unwrap_err()
853            .to_string()
854            .contains("block_size must be greater than 0"));
855
856        // Reset and test invalid memtable_size_threshold (should trigger line 579-580)
857        config = Config::default();
858        config.storage.memtable_size_threshold = 0;
859        let result = config.validate();
860        assert!(result.is_err());
861        assert!(result
862            .unwrap_err()
863            .to_string()
864            .contains("memtable_size_threshold must be greater than 0"));
865
866        // Reset and test invalid bloom filter false positive rate (should trigger line 589-590)
867        config = Config::default();
868        config.storage.enable_bloom_filters = true;
869        config.storage.bloom_filter_fp_rate = 0.0; // Invalid: exactly 0
870        let result = config.validate();
871        assert!(result.is_err());
872        assert!(result
873            .unwrap_err()
874            .to_string()
875            .contains("bloom_filter_fp_rate must be between 0 and 1"));
876
877        // Test another invalid bloom filter false positive rate
878        config.storage.bloom_filter_fp_rate = 1.0; // Invalid: exactly 1
879        let result = config.validate();
880        assert!(result.is_err());
881
882        // Test bloom filter rate above 1
883        config.storage.bloom_filter_fp_rate = 1.5; // Invalid: greater than 1
884        let result = config.validate();
885        assert!(result.is_err());
886
887        // Test bloom filter rate below 0
888        config.storage.bloom_filter_fp_rate = -0.1; // Invalid: less than 0
889        let result = config.validate();
890        assert!(result.is_err());
891    }
892
893    #[test]
894    fn test_valid_bloom_filter_config() {
895        let mut config = Config::default();
896        config.storage.enable_bloom_filters = true;
897        config.storage.bloom_filter_fp_rate = 0.01; // Valid rate
898        assert!(config.validate().is_ok());
899
900        config.storage.bloom_filter_fp_rate = 0.5; // Valid rate
901        assert!(config.validate().is_ok());
902
903        config.storage.bloom_filter_fp_rate = 0.99; // Valid rate
904        assert!(config.validate().is_ok());
905    }
906
907    #[test]
908    fn test_storage_config_deserializes_without_mmap_fields() {
909        // Backward compatibility: a config payload serialized before the mmap
910        // fields existed omits `use_mmap` / `mmap_min_size_bytes`. It must still
911        // deserialize, defaulting to the safe buffered backend.
912        let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
913        let obj = value.as_object_mut().unwrap();
914        obj.remove("use_mmap");
915        obj.remove("mmap_min_size_bytes");
916        assert!(!obj.contains_key("use_mmap"));
917
918        let restored: StorageConfig =
919            serde_json::from_value(value).expect("old payload must still deserialize");
920        assert!(!restored.use_mmap, "missing use_mmap must default to false");
921        assert_eq!(
922            restored.mmap_min_size_bytes, 4096,
923            "missing mmap_min_size_bytes must default to one page"
924        );
925    }
926
927    #[test]
928    fn test_full_config_deserializes_without_mmap_fields() {
929        // Same guarantee through the top-level Config, mirroring how the Python
930        // bindings parse a JSON/dict payload into `cqlite_core::Config`.
931        let mut value = serde_json::to_value(Config::default()).unwrap();
932        let storage = value
933            .get_mut("storage")
934            .and_then(|s| s.as_object_mut())
935            .unwrap();
936        storage.remove("use_mmap");
937        storage.remove("mmap_min_size_bytes");
938
939        let restored: Config =
940            serde_json::from_value(value).expect("old Config payload must still deserialize");
941        assert!(!restored.storage.use_mmap);
942        assert_eq!(restored.storage.mmap_min_size_bytes, 4096);
943        restored.validate().expect("restored config must validate");
944    }
945
946    #[test]
947    fn test_mmap_fields_roundtrip_when_present() {
948        // When the fields ARE present (e.g. a user opting in), they round-trip.
949        let mut config = StorageConfig::default();
950        config.use_mmap = true;
951        config.mmap_min_size_bytes = 8192;
952        let json = serde_json::to_string(&config).unwrap();
953        let restored: StorageConfig = serde_json::from_str(&json).unwrap();
954        assert!(restored.use_mmap);
955        assert_eq!(restored.mmap_min_size_bytes, 8192);
956    }
957
958    #[test]
959    fn test_disk_access_defaults() {
960        // The new fields default to the size-aware Auto backend with Auto
961        // prefetch, a half-RAM direct-I/O threshold, and a 1 MiB window.
962        let config = StorageConfig::default();
963        assert_eq!(config.disk_access_mode, DiskAccessMode::Auto);
964        assert_eq!(config.prefetch, PrefetchMode::Auto);
965        assert_eq!(config.direct_io_memory_fraction, 0.5);
966        assert_eq!(config.direct_io_prefetch_bytes, 1024 * 1024);
967    }
968
969    #[test]
970    fn test_storage_config_deserializes_without_disk_access_fields() {
971        // Backward compatibility: a payload predating the disk-access fields must
972        // still deserialize, defaulting to Auto / Auto / 0.5 / 1 MiB.
973        let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
974        let obj = value.as_object_mut().unwrap();
975        for key in [
976            "disk_access_mode",
977            "direct_io_memory_fraction",
978            "prefetch",
979            "direct_io_prefetch_bytes",
980        ] {
981            obj.remove(key);
982        }
983
984        let restored: StorageConfig =
985            serde_json::from_value(value).expect("old payload must still deserialize");
986        assert_eq!(restored.disk_access_mode, DiskAccessMode::Auto);
987        assert_eq!(restored.prefetch, PrefetchMode::Auto);
988        assert_eq!(restored.direct_io_memory_fraction, 0.5);
989        assert_eq!(restored.direct_io_prefetch_bytes, 1024 * 1024);
990    }
991
992    #[test]
993    fn test_disk_access_fields_roundtrip() {
994        // Explicit selections round-trip, including the lowercase enum encoding.
995        let mut config = StorageConfig::default();
996        config.disk_access_mode = DiskAccessMode::Direct;
997        config.prefetch = PrefetchMode::WillNeed;
998        config.direct_io_memory_fraction = 0.25;
999        config.direct_io_prefetch_bytes = 2 * 1024 * 1024;
1000        let json = serde_json::to_string(&config).unwrap();
1001        assert!(json.contains("\"direct\""), "enum must serialize lowercase");
1002        assert!(json.contains("\"willneed\""));
1003        let restored: StorageConfig = serde_json::from_str(&json).unwrap();
1004        assert_eq!(restored.disk_access_mode, DiskAccessMode::Direct);
1005        assert_eq!(restored.prefetch, PrefetchMode::WillNeed);
1006        assert_eq!(restored.direct_io_memory_fraction, 0.25);
1007        assert_eq!(restored.direct_io_prefetch_bytes, 2 * 1024 * 1024);
1008    }
1009
1010    #[test]
1011    fn test_bloom_filter_disabled() {
1012        let mut config = Config::default();
1013        config.storage.enable_bloom_filters = false;
1014        config.storage.bloom_filter_fp_rate = 0.0; // Should be ignored when bloom filters disabled
1015        assert!(config.validate().is_ok());
1016
1017        config.storage.bloom_filter_fp_rate = 1.0; // Should be ignored when bloom filters disabled
1018        assert!(config.validate().is_ok());
1019
1020        config.storage.bloom_filter_fp_rate = -1.0; // Should be ignored when bloom filters disabled
1021        assert!(config.validate().is_ok());
1022    }
1023}