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}