use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.storage.compression.enabled);
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.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"));
}
#[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 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);
}
#[test]
fn hard_limit_not_above_flush_threshold_is_rejected_and_names_both_values() {
let mut config = Config::default();
config.storage.memtable_size_threshold = 64 * 1024 * 1024;
config.storage.memtable_hard_limit = 32 * 1024 * 1024;
let err = config
.validate()
.expect_err("a ceiling below the flush threshold wedges the engine")
.to_string();
assert!(err.contains(&(64 * 1024 * 1024).to_string()), "{err}");
assert!(err.contains(&(32 * 1024 * 1024).to_string()), "{err}");
config.storage.memtable_hard_limit = config.storage.memtable_size_threshold;
let err = config
.validate()
.expect_err("hard_limit == memtable_size_threshold leaves no headroom")
.to_string();
assert!(
err.contains("strictly greater"),
"the rejection must say the bound is STRICT, not merely name the rule: {err}"
);
config.storage.memtable_hard_limit = config.storage.memtable_size_threshold + 1;
config
.validate()
.expect("one byte of headroom is a coherent, if tight, configuration");
}
#[test]
fn zero_compaction_min_threshold_is_rejected() {
let mut config = Config::default();
config.storage.compaction.min_threshold = 0;
let err = config
.validate()
.expect_err("min_threshold 0 must be rejected")
.to_string();
assert!(err.contains("compaction.min_threshold"), "{err}");
config.storage.compaction.min_threshold = 1;
config.validate().expect("min_threshold 1 must be accepted");
}
#[test]
fn compaction_thresholds_are_only_judged_when_auto_compaction_is_on() {
let mut config = Config::default();
config.storage.compaction.auto_compaction = false;
config.storage.compaction.min_threshold = 0;
config.storage.compaction.max_threshold = 0;
config
.validate()
.expect("thresholds documented as ignored must not be judged");
config.storage.compaction.auto_compaction = true;
let err = config
.validate()
.expect_err("with compaction on, a zero eligibility bar is still refused")
.to_string();
assert!(err.contains("compaction.min_threshold"), "{err}");
let mut config = Config::default();
config.storage.compaction.auto_compaction = false;
config.storage.compaction.min_threshold = 8;
config.storage.compaction.max_threshold = 4;
config
.validate()
.expect("an ignored merge-width cap below the bar must not be judged");
config.storage.compaction.auto_compaction = true;
let err = config
.validate()
.expect_err("with compaction on, max below min is still refused")
.to_string();
assert!(err.contains("compaction.max_threshold"), "{err}");
}
#[test]
fn compaction_max_below_min_is_rejected_and_names_both_values() {
let mut config = Config::default();
config.storage.compaction.min_threshold = 8;
config.storage.compaction.max_threshold = 4;
let err = config
.validate()
.expect_err("max_threshold below min_threshold can never admit a merge")
.to_string();
assert!(err.contains('8'), "{err}");
assert!(err.contains('4'), "{err}");
config.storage.compaction.max_threshold = 8;
config
.validate()
.expect("max_threshold == min_threshold must be accepted");
}
#[test]
fn a_byte_count_above_the_targets_usize_max_is_rejected_not_clamped() {
let usize_max_bytes = u64::try_from(usize::MAX).unwrap_or(u64::MAX);
let Some(unaddressable) = usize_max_bytes.checked_add(1) else {
assert_eq!(usize_max_bytes, u64::MAX);
return;
};
for knob in ["memtable_size_threshold", "memtable_hard_limit"] {
let mut config = Config::default();
match knob {
"memtable_size_threshold" => {
config.storage.memtable_size_threshold = unaddressable;
config.storage.memtable_hard_limit = unaddressable;
}
_ => config.storage.memtable_hard_limit = unaddressable,
}
let err = config
.validate()
.expect_err("an unaddressable byte count must be rejected")
.to_string();
assert!(err.contains("addressable"), "{knob}: {err}");
}
}
#[test]
fn storage_config_deserializes_without_the_issue_1697_fields() {
let mut value = serde_json::to_value(StorageConfig::default()).unwrap();
let obj = value.as_object_mut().unwrap();
obj.remove("memtable_hard_limit");
let compaction = obj
.get_mut("compaction")
.and_then(|c| c.as_object_mut())
.unwrap();
compaction.remove("min_threshold");
compaction.remove("max_threshold");
assert!(!compaction.contains_key("min_threshold"));
let restored: StorageConfig =
serde_json::from_value(value).expect("a pre-#1697 payload must still deserialize");
let defaults = StorageConfig::default();
assert_eq!(
restored.memtable_hard_limit, defaults.memtable_hard_limit,
"absent memtable_hard_limit must default to the 256MB admission ceiling"
);
assert_eq!(
restored.compaction.min_threshold, defaults.compaction.min_threshold,
"absent compaction.min_threshold must default to STCS's 4"
);
assert_eq!(
restored.compaction.max_threshold, defaults.compaction.max_threshold,
"absent compaction.max_threshold must default to STCS's 32"
);
}
#[test]
fn full_config_deserializes_and_validates_without_the_issue_1697_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("memtable_hard_limit");
let compaction = storage
.get_mut("compaction")
.and_then(|c| c.as_object_mut())
.unwrap();
compaction.remove("min_threshold");
compaction.remove("max_threshold");
let restored: Config =
serde_json::from_value(value).expect("a pre-#1697 Config payload must still deserialize");
restored
.validate()
.expect("the defaulted fields must satisfy validate's new rules");
assert_eq!(restored.storage.memtable_hard_limit, 256 * 1024 * 1024);
assert_eq!(restored.storage.compaction.min_threshold, 4);
assert_eq!(restored.storage.compaction.max_threshold, 32);
}
#[test]
fn the_issue_1697_fields_roundtrip_when_present() {
let mut config = StorageConfig::default();
config.memtable_hard_limit = 512 * 1024 * 1024;
config.compaction.min_threshold = 6;
config.compaction.max_threshold = 12;
let json = serde_json::to_string(&config).unwrap();
let restored: StorageConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.memtable_hard_limit, 512 * 1024 * 1024);
assert_eq!(restored.compaction.min_threshold, 6);
assert_eq!(restored.compaction.max_threshold, 12);
}