use crate::error::{DbError, DbResult};
#[derive(Debug, Clone)]
#[cfg_attr(
any(feature = "armour", feature = "postcard-codec"),
derive(serde::Deserialize)
)]
pub struct Config {
pub shard_count: usize,
pub max_file_size: u64,
pub compaction_threshold: f64,
pub enable_fsync: bool,
pub write_buffer_size: usize,
#[cfg(feature = "var-collections")]
pub cache: CacheConfig,
pub shard_prefix_bits: usize,
pub reversed: bool,
pub hints: bool,
#[cfg(feature = "encryption")]
pub encryption_key: Option<[u8; 32]>,
}
#[cfg(feature = "var-collections")]
#[derive(Debug, Clone)]
#[cfg_attr(
any(feature = "armour", feature = "postcard-codec"),
derive(serde::Deserialize)
)]
pub struct CacheConfig {
pub max_size: u64,
pub estimated_items: usize,
}
impl Config {
pub fn test() -> Self {
Self {
shard_count: 2,
hints: true,
..Self::default()
}
}
pub fn validate(&self) -> DbResult<()> {
if self.shard_count == 0 || self.shard_count > 255 {
return Err(DbError::Config("shard_count must be between 1 and 255"));
}
if self.max_file_size < 4096 {
return Err(DbError::Config("max_file_size must be at least 4096"));
}
if self.write_buffer_size < 4096 {
return Err(DbError::Config("write_buffer_size must be at least 4096"));
}
if self.max_file_size > u32::MAX as u64 {
return Err(DbError::Config(
"max_file_size must not exceed u32::MAX (4 GiB)",
));
}
if (self.write_buffer_size as u64) > (u32::MAX as u64) - 4096 {
return Err(DbError::Config(
"write_buffer_size must not exceed u32::MAX - 4096",
));
}
if (self.write_buffer_size as u64) > self.max_file_size {
return Err(DbError::Config(
"max_file_size must be >= write_buffer_size",
));
}
if self.shard_prefix_bits > u8::MAX as usize {
return Err(DbError::Config("shard_prefix_bits must be <= 255"));
}
if !self.compaction_threshold.is_finite()
|| !(0.0..=1.0).contains(&self.compaction_threshold)
{
return Err(DbError::Config(
"compaction_threshold must be a finite value in [0.0, 1.0]",
));
}
#[cfg(feature = "encryption")]
if self.encryption_key.is_some() {
if self.write_buffer_size < 8192 {
return Err(DbError::Config(
"write_buffer_size must be at least 8192 when encryption is enabled",
));
}
if !self.write_buffer_size.is_multiple_of(4096) {
return Err(DbError::Config(
"write_buffer_size must be a multiple of 4096 when encryption is enabled",
));
}
}
Ok(())
}
}
impl Default for Config {
fn default() -> Self {
#[cfg(debug_assertions)]
let shard_count = std::thread::available_parallelism()
.map(|p| p.get() / 2)
.unwrap_or(2)
.clamp(1, 4);
#[cfg(not(debug_assertions))]
let shard_count = std::thread::available_parallelism()
.map(|p| p.get() / 2)
.unwrap_or(4)
.clamp(1, 8);
Self {
shard_count,
max_file_size: 256 * 1024 * 1024,
compaction_threshold: 0.3,
enable_fsync: false,
write_buffer_size: 1024 * 1024, #[cfg(feature = "var-collections")]
cache: CacheConfig::default(),
shard_prefix_bits: 0,
reversed: true,
hints: false,
#[cfg(feature = "encryption")]
encryption_key: None,
}
}
}
#[cfg(feature = "var-collections")]
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_size: 0,
estimated_items: 100_000,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_for_test() -> Config {
Config {
shard_count: 2,
max_file_size: 256 * 1024 * 1024,
compaction_threshold: 0.3,
enable_fsync: false,
write_buffer_size: 1024 * 1024,
#[cfg(feature = "var-collections")]
cache: CacheConfig::default(),
shard_prefix_bits: 0,
reversed: true,
hints: false,
#[cfg(feature = "encryption")]
encryption_key: None,
}
}
#[test]
fn test_default_config_is_valid() {
let cfg = Config::default();
assert!(cfg.shard_count >= 1);
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_rejects_max_file_size_smaller_than_write_buffer() {
let mut cfg = default_for_test();
cfg.max_file_size = 4096;
cfg.write_buffer_size = 8192;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_rejects_write_buffer_at_u32_limit() {
let mut cfg = default_for_test();
cfg.write_buffer_size = (u32::MAX as usize) - 4095;
cfg.max_file_size = u32::MAX as u64;
assert!(cfg.validate().is_err());
}
#[cfg(feature = "encryption")]
#[test]
fn validate_rejects_small_write_buffer_under_encryption() {
let mut cfg = default_for_test();
cfg.encryption_key = Some([0u8; 32]);
cfg.write_buffer_size = 4096;
assert!(cfg.validate().is_err());
}
#[cfg(feature = "encryption")]
#[test]
fn validate_rejects_non_page_multiple_write_buffer_under_encryption() {
let mut cfg = default_for_test();
cfg.encryption_key = Some([0u8; 32]);
cfg.write_buffer_size = 8192 + 100;
cfg.max_file_size = 256 * 1024 * 1024;
assert!(cfg.validate().is_err());
}
#[cfg(feature = "encryption")]
#[test]
fn validate_accepts_page_multiple_8k_under_encryption() {
let mut cfg = default_for_test();
cfg.encryption_key = Some([0u8; 32]);
cfg.write_buffer_size = 8192;
cfg.max_file_size = 256 * 1024 * 1024;
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_accepts_plain_4096_write_buffer() {
let mut cfg = default_for_test();
cfg.write_buffer_size = 4096;
cfg.max_file_size = 8192;
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_rejects_shard_prefix_bits_over_255() {
let mut cfg = default_for_test();
cfg.shard_prefix_bits = 256;
let err = cfg.validate().unwrap_err();
assert!(
err.to_string().contains("shard_prefix_bits"),
"expected shard_prefix_bits error, got: {err}"
);
}
#[test]
fn validate_rejects_compaction_threshold_nan() {
let mut cfg = default_for_test();
cfg.compaction_threshold = f64::NAN;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_rejects_compaction_threshold_negative() {
let mut cfg = default_for_test();
cfg.compaction_threshold = -0.1;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_rejects_compaction_threshold_over_one() {
let mut cfg = default_for_test();
cfg.compaction_threshold = 1.1;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_accepts_compaction_threshold_half() {
let mut cfg = default_for_test();
cfg.compaction_threshold = 0.5;
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_accepts_compaction_threshold_boundaries() {
let mut cfg = default_for_test();
cfg.compaction_threshold = 0.0;
assert!(cfg.validate().is_ok());
cfg.compaction_threshold = 1.0;
assert!(cfg.validate().is_ok());
}
}