use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub storage: StorageConfig,
pub memory: MemoryConfig,
pub query: QueryConfig,
pub performance: PerformanceConfig,
#[cfg(target_arch = "wasm32")]
pub wasm: WasmConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub max_sstable_size: u64,
pub memtable_size_threshold: u64,
pub compaction: CompactionConfig,
pub block_size: u32,
pub compression: CompressionConfig,
pub enable_bloom_filters: bool,
pub bloom_filter_fp_rate: f64,
pub io_threads: usize,
pub sync_mode: SyncMode,
#[serde(default = "default_use_mmap")]
pub use_mmap: bool,
#[serde(default = "default_mmap_min_size_bytes")]
pub mmap_min_size_bytes: usize,
#[serde(default)]
pub disk_access_mode: DiskAccessMode,
#[serde(default = "default_direct_io_memory_fraction")]
pub direct_io_memory_fraction: f64,
#[serde(default)]
pub prefetch: PrefetchMode,
#[serde(default = "default_direct_io_prefetch_bytes")]
pub direct_io_prefetch_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum DiskAccessMode {
#[default]
Auto,
Buffered,
Mmap,
Direct,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PrefetchMode {
Off,
Sequential,
WillNeed,
#[default]
Auto,
}
fn default_use_mmap() -> bool {
false
}
fn default_mmap_min_size_bytes() -> usize {
4096
}
fn default_direct_io_memory_fraction() -> f64 {
0.5
}
fn default_direct_io_prefetch_bytes() -> usize {
1024 * 1024
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
max_sstable_size: 64 * 1024 * 1024, memtable_size_threshold: 16 * 1024 * 1024, compaction: CompactionConfig::default(),
block_size: 64 * 1024, compression: CompressionConfig::default(),
enable_bloom_filters: true,
bloom_filter_fp_rate: 0.01,
io_threads: num_cpus::get().min(4),
sync_mode: SyncMode::Normal,
use_mmap: default_use_mmap(),
mmap_min_size_bytes: default_mmap_min_size_bytes(),
disk_access_mode: DiskAccessMode::default(),
direct_io_memory_fraction: default_direct_io_memory_fraction(),
prefetch: PrefetchMode::default(),
direct_io_prefetch_bytes: default_direct_io_prefetch_bytes(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionConfig {
pub auto_compaction: bool,
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
auto_compaction: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MemoryConfig {
pub max_memory: u64,
pub block_cache: CacheConfig,
}
impl Default for MemoryConfig {
fn default() -> Self {
let max_memory = 1024 * 1024 * 1024;
Self {
max_memory,
block_cache: CacheConfig {
enabled: true,
max_size: max_memory / 4, policy: CachePolicy::Lru,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub enabled: bool,
pub max_size: u64,
pub policy: CachePolicy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CachePolicy {
Lru,
}
pub const DEFAULT_MAX_RESULT_BYTES: u64 = 64 * 1024 * 1024;
fn default_max_result_bytes() -> u64 {
DEFAULT_MAX_RESULT_BYTES
}
fn default_max_result_rows() -> u64 {
1_000_000
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ReadPathMode {
#[default]
Auto,
Point,
Full,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryConfig {
pub max_execution_time: Duration,
#[serde(default)]
pub forced_read_path: Option<ReadPathMode>,
#[serde(default = "default_max_result_rows")]
pub max_result_rows: u64,
#[serde(default = "default_max_result_bytes")]
pub max_result_bytes: u64,
pub plan_cache_size: usize,
pub enable_optimization: bool,
pub parallel: ParallelQueryConfig,
pub query_cache_size: Option<usize>,
pub query_parallelism: Option<usize>,
pub analyze_iterations: Option<usize>,
}
impl Default for QueryConfig {
fn default() -> Self {
Self {
max_execution_time: Duration::from_secs(300), forced_read_path: None,
max_result_rows: 1_000_000,
max_result_bytes: DEFAULT_MAX_RESULT_BYTES,
plan_cache_size: 1000,
enable_optimization: true,
parallel: ParallelQueryConfig::default(),
query_cache_size: Some(100),
query_parallelism: Some(num_cpus::get()),
analyze_iterations: Some(5),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParallelQueryConfig {
pub enabled: bool,
pub max_threads: usize,
pub min_parallel_rows: u64,
}
impl Default for ParallelQueryConfig {
fn default() -> Self {
Self {
enabled: true,
max_threads: num_cpus::get(),
min_parallel_rows: 10_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub enable_metrics: bool,
pub metrics_interval: Duration,
pub enable_profiling: bool,
pub background_tasks: BackgroundTaskConfig,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
enable_metrics: true,
metrics_interval: Duration::from_secs(60),
enable_profiling: false,
background_tasks: BackgroundTaskConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackgroundTaskConfig {
pub enable_stats: bool,
pub stats_interval: Duration,
pub enable_cleanup: bool,
pub cleanup_interval: Duration,
}
impl Default for BackgroundTaskConfig {
fn default() -> Self {
Self {
enable_stats: true,
stats_interval: Duration::from_secs(300), enable_cleanup: true,
cleanup_interval: Duration::from_secs(3600), }
}
}
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmConfig {
pub use_indexeddb: bool,
pub max_memory: u64,
pub enable_simd: bool,
pub enable_workers: bool,
pub max_workers: usize,
}
#[cfg(target_arch = "wasm32")]
impl Default for WasmConfig {
fn default() -> Self {
Self {
use_indexeddb: true,
max_memory: 256 * 1024 * 1024, enable_simd: true,
enable_workers: true,
max_workers: 4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompressionAlgorithm {
None,
Lz4,
Snappy,
Deflate,
Zstd,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionConfig {
pub enabled: bool,
pub algorithm: CompressionAlgorithm,
pub level: i32,
pub min_block_size: u32,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
enabled: true,
algorithm: CompressionAlgorithm::Lz4,
level: 1, min_block_size: 1024, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncMode {
None,
Normal,
Full,
}
impl Config {
pub fn memory_optimized() -> Self {
let mut config = Self::default();
config.storage.memtable_size_threshold = 4 * 1024 * 1024; config.storage.max_sstable_size = 16 * 1024 * 1024; config.memory.max_memory = 256 * 1024 * 1024; config.memory.block_cache.max_size = 64 * 1024 * 1024;
config.storage.compression.algorithm = CompressionAlgorithm::Zstd;
config.storage.compression.enabled = true;
config
}
pub fn performance_optimized() -> Self {
let mut config = Self::default();
config.storage.memtable_size_threshold = 64 * 1024 * 1024; config.storage.max_sstable_size = 256 * 1024 * 1024; config.memory.max_memory = 4 * 1024 * 1024 * 1024;
config.storage.compression.algorithm = CompressionAlgorithm::Lz4;
config.storage.compression.enabled = true;
config.memory.block_cache.max_size = 1024 * 1024 * 1024;
config.storage.io_threads = num_cpus::get();
config
}
#[cfg(target_arch = "wasm32")]
pub fn wasm_optimized() -> Self {
let mut config = Self::memory_optimized();
config.wasm.max_memory = 128 * 1024 * 1024; config.wasm.enable_simd = true;
config.wasm.enable_workers = false;
config.memory.max_memory = 128 * 1024 * 1024; config.storage.memtable_size_threshold = 2 * 1024 * 1024; config.storage.max_sstable_size = 8 * 1024 * 1024;
config.storage.compaction.auto_compaction = false;
config.performance.background_tasks.enable_stats = false;
config.performance.background_tasks.enable_cleanup = false;
config
}
#[cfg(test)]
pub fn test_config() -> Self {
let mut config = Config::default();
config.storage.compaction.auto_compaction = false;
config.performance.background_tasks.enable_stats = false;
config.performance.background_tasks.enable_cleanup = false;
config.query.max_execution_time = std::time::Duration::from_secs(1);
config.memory.max_memory = 64 * 1024 * 1024; config.storage.memtable_size_threshold = 1024 * 1024; config.storage.max_sstable_size = 4 * 1024 * 1024;
config
}
pub fn validate(&self) -> crate::Result<()> {
if self.memory.max_memory == 0 {
return Err(crate::Error::configuration(
"max_memory must be greater than 0",
));
}
if self.memory.block_cache.max_size > self.memory.max_memory {
return Err(crate::Error::configuration(
"block_cache.max_size exceeds max_memory",
));
}
if self.storage.block_size == 0 {
return Err(crate::Error::configuration(
"block_size must be greater than 0",
));
}
if self.storage.memtable_size_threshold == 0 {
return Err(crate::Error::configuration(
"memtable_size_threshold must be greater than 0",
));
}
if self.storage.enable_bloom_filters
&& (self.storage.bloom_filter_fp_rate <= 0.0
|| self.storage.bloom_filter_fp_rate >= 1.0)
{
return Err(crate::Error::configuration(
"bloom_filter_fp_rate must be between 0 and 1",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.storage.compression.enabled);
assert!(config.storage.enable_bloom_filters);
assert!(config.memory.block_cache.enabled);
}
#[test]
fn test_memory_optimized_config() {
let config = Config::memory_optimized();
assert!(
config.storage.memtable_size_threshold
< Config::default().storage.memtable_size_threshold
);
assert!(config.memory.max_memory < Config::default().memory.max_memory);
}
#[test]
fn test_performance_optimized_config() {
let config = Config::performance_optimized();
assert!(
config.storage.memtable_size_threshold
> Config::default().storage.memtable_size_threshold
);
assert!(config.memory.max_memory > Config::default().memory.max_memory);
}
#[test]
fn budget_fields_deserialize_with_serde_default_when_absent() {
let mut value =
serde_json::to_value(QueryConfig::default()).expect("serialize QueryConfig");
let obj = value
.as_object_mut()
.expect("QueryConfig serializes as object");
obj.remove("max_result_bytes");
obj.remove("max_result_rows");
assert!(
!obj.contains_key("max_result_bytes") && !obj.contains_key("max_result_rows"),
"both fields must be absent for this regression to be meaningful"
);
let restored: QueryConfig =
serde_json::from_value(value).expect("old config (no budget fields) must deserialize");
assert_eq!(
restored.max_result_bytes, DEFAULT_MAX_RESULT_BYTES,
"absent max_result_bytes must take the serde default"
);
assert_eq!(
restored.max_result_rows,
default_max_result_rows(),
"absent max_result_rows must take the serde default"
);
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
assert!(config.validate().is_ok());
config.memory.max_memory = 0;
assert!(config.validate().is_err());
config = Config::default();
config.memory.block_cache.max_size = config.memory.max_memory + 1;
assert!(config.validate().is_err());
}
#[test]
fn forced_read_path_defaults_absent_and_roundtrips() {
assert_eq!(QueryConfig::default().forced_read_path, None);
let mut value = serde_json::to_value(QueryConfig::default()).unwrap();
value
.as_object_mut()
.unwrap()
.remove("forced_read_path")
.expect("field present when serialized");
let restored: QueryConfig = serde_json::from_value(value).unwrap();
assert_eq!(restored.forced_read_path, None);
for (mode, tag) in [
(ReadPathMode::Point, "point"),
(ReadPathMode::Full, "full"),
(ReadPathMode::Auto, "auto"),
] {
let mut cfg = QueryConfig::default();
cfg.forced_read_path = Some(mode);
let json = serde_json::to_string(&cfg).unwrap();
assert!(
json.contains(tag),
"mode {mode:?} must serialize as {tag:?}: {json}"
);
let restored: QueryConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.forced_read_path, Some(mode));
}
}
#[test]
fn test_storage_validation_errors() {
let mut config = Config::default();
config.storage.block_size = 0;
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("block_size must be greater than 0"));
config = Config::default();
config.storage.memtable_size_threshold = 0;
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("memtable_size_threshold must be greater than 0"));
config = Config::default();
config.storage.enable_bloom_filters = true;
config.storage.bloom_filter_fp_rate = 0.0; let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("bloom_filter_fp_rate must be between 0 and 1"));
config.storage.bloom_filter_fp_rate = 1.0; let result = config.validate();
assert!(result.is_err());
config.storage.bloom_filter_fp_rate = 1.5; let result = config.validate();
assert!(result.is_err());
config.storage.bloom_filter_fp_rate = -0.1; let result = config.validate();
assert!(result.is_err());
}
#[test]
fn test_valid_bloom_filter_config() {
let mut config = Config::default();
config.storage.enable_bloom_filters = true;
config.storage.bloom_filter_fp_rate = 0.01; assert!(config.validate().is_ok());
config.storage.bloom_filter_fp_rate = 0.5; assert!(config.validate().is_ok());
config.storage.bloom_filter_fp_rate = 0.99; assert!(config.validate().is_ok());
}
#[test]
fn test_storage_config_deserializes_without_mmap_fields() {
let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
let obj = value.as_object_mut().unwrap();
obj.remove("use_mmap");
obj.remove("mmap_min_size_bytes");
assert!(!obj.contains_key("use_mmap"));
let restored: StorageConfig =
serde_json::from_value(value).expect("old payload must still deserialize");
assert!(!restored.use_mmap, "missing use_mmap must default to false");
assert_eq!(
restored.mmap_min_size_bytes, 4096,
"missing mmap_min_size_bytes must default to one page"
);
}
#[test]
fn test_full_config_deserializes_without_mmap_fields() {
let mut value = serde_json::to_value(Config::default()).unwrap();
let storage = value
.get_mut("storage")
.and_then(|s| s.as_object_mut())
.unwrap();
storage.remove("use_mmap");
storage.remove("mmap_min_size_bytes");
let restored: Config =
serde_json::from_value(value).expect("old Config payload must still deserialize");
assert!(!restored.storage.use_mmap);
assert_eq!(restored.storage.mmap_min_size_bytes, 4096);
restored.validate().expect("restored config must validate");
}
#[test]
fn test_mmap_fields_roundtrip_when_present() {
let mut config = StorageConfig::default();
config.use_mmap = true;
config.mmap_min_size_bytes = 8192;
let json = serde_json::to_string(&config).unwrap();
let restored: StorageConfig = serde_json::from_str(&json).unwrap();
assert!(restored.use_mmap);
assert_eq!(restored.mmap_min_size_bytes, 8192);
}
#[test]
fn test_disk_access_defaults() {
let config = StorageConfig::default();
assert_eq!(config.disk_access_mode, DiskAccessMode::Auto);
assert_eq!(config.prefetch, PrefetchMode::Auto);
assert_eq!(config.direct_io_memory_fraction, 0.5);
assert_eq!(config.direct_io_prefetch_bytes, 1024 * 1024);
}
#[test]
fn test_storage_config_deserializes_without_disk_access_fields() {
let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
let obj = value.as_object_mut().unwrap();
for key in [
"disk_access_mode",
"direct_io_memory_fraction",
"prefetch",
"direct_io_prefetch_bytes",
] {
obj.remove(key);
}
let restored: StorageConfig =
serde_json::from_value(value).expect("old payload must still deserialize");
assert_eq!(restored.disk_access_mode, DiskAccessMode::Auto);
assert_eq!(restored.prefetch, PrefetchMode::Auto);
assert_eq!(restored.direct_io_memory_fraction, 0.5);
assert_eq!(restored.direct_io_prefetch_bytes, 1024 * 1024);
}
#[test]
fn test_disk_access_fields_roundtrip() {
let mut config = StorageConfig::default();
config.disk_access_mode = DiskAccessMode::Direct;
config.prefetch = PrefetchMode::WillNeed;
config.direct_io_memory_fraction = 0.25;
config.direct_io_prefetch_bytes = 2 * 1024 * 1024;
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"direct\""), "enum must serialize lowercase");
assert!(json.contains("\"willneed\""));
let restored: StorageConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.disk_access_mode, DiskAccessMode::Direct);
assert_eq!(restored.prefetch, PrefetchMode::WillNeed);
assert_eq!(restored.direct_io_memory_fraction, 0.25);
assert_eq!(restored.direct_io_prefetch_bytes, 2 * 1024 * 1024);
}
#[test]
fn test_bloom_filter_disabled() {
let mut config = Config::default();
config.storage.enable_bloom_filters = false;
config.storage.bloom_filter_fp_rate = 0.0; assert!(config.validate().is_ok());
config.storage.bloom_filter_fp_rate = 1.0; assert!(config.validate().is_ok());
config.storage.bloom_filter_fp_rate = -1.0; assert!(config.validate().is_ok());
}
#[test]
fn removed_memory_knobs_fail_closed() {
let old_shape = serde_json::json!({
"max_memory": 1_073_741_824u64,
"block_cache": { "enabled": true, "max_size": 268_435_456u64, "policy": "Lru" },
"row_cache": { "enabled": true, "max_size": 134_217_728u64, "policy": "Lru" },
"query_cache": { "enabled": true, "max_size": 67_108_864u64, "policy": "Lru" },
"allocator": { "use_custom": false, "small_pool_size": 1u64, "large_pool_size": 2u64 }
});
assert!(
serde_json::from_value::<MemoryConfig>(old_shape).is_err(),
"a MemoryConfig naming removed knobs (row_cache/query_cache/allocator) must fail closed"
);
for removed in ["row_cache", "query_cache", "allocator"] {
let mut v = serde_json::json!({
"max_memory": 1_073_741_824u64,
"block_cache": { "enabled": true, "max_size": 268_435_456u64, "policy": "Lru" },
});
v.as_object_mut()
.unwrap()
.insert(removed.to_string(), serde_json::json!({ "enabled": true }));
assert!(
serde_json::from_value::<MemoryConfig>(v).is_err(),
"a MemoryConfig naming the removed `{removed}` knob must fail closed"
);
}
}
#[test]
fn removed_cache_policy_variants_fail_closed() {
for variant in ["Lfu", "Arc"] {
let v = serde_json::json!({ "enabled": true, "max_size": 1u64, "policy": variant });
assert!(
serde_json::from_value::<CacheConfig>(v).is_err(),
"a CacheConfig naming the removed CachePolicy::{variant} variant must fail closed"
);
}
let ok = serde_json::json!({ "enabled": true, "max_size": 1u64, "policy": "Lru" });
assert!(serde_json::from_value::<CacheConfig>(ok).is_ok());
}
#[test]
fn retained_budget_knob_deserializes_and_validates() {
let mem: MemoryConfig = serde_json::from_value(serde_json::json!({
"max_memory": 1_073_741_824u64,
"block_cache": { "enabled": true, "max_size": 268_435_456u64, "policy": "Lru" },
}))
.expect("retained-only MemoryConfig must deserialize");
assert_eq!(mem.block_cache.max_size, 268_435_456);
let config = Config::default();
assert!(config.validate().is_ok());
assert!(config.memory.block_cache.max_size > 0);
}
}