aedb 0.3.1

Embedded Rust storage engine with transactional commits, WAL durability, and snapshot-consistent reads
Documentation
use crate::config::{AedbConfig, DurabilityMode, RecoveryMode, StorageMode};
use crate::error::AedbError;

pub(crate) fn validate_config(config: &AedbConfig) -> Result<(), AedbError> {
    if config.max_segment_bytes == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_segment_bytes must be > 0".into(),
        });
    }
    if config.max_segment_age_secs == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_segment_age_secs must be > 0".into(),
        });
    }
    if config.max_inflight_commits == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_inflight_commits must be > 0".into(),
        });
    }
    if config.max_commit_queue_bytes == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_commit_queue_bytes must be > 0".into(),
        });
    }
    if config.max_transaction_bytes == 0
        || config.max_transaction_bytes > config.max_commit_queue_bytes
    {
        return Err(AedbError::InvalidConfig {
            message: "max_transaction_bytes must be > 0 and <= max_commit_queue_bytes".into(),
        });
    }
    if config.max_transaction_bytes > crate::wal::frame::MAX_FRAME_BODY_BYTES {
        return Err(AedbError::InvalidConfig {
            message: format!(
                "max_transaction_bytes must be <= {} to fit WAL frame bounds",
                crate::wal::frame::MAX_FRAME_BODY_BYTES
            ),
        });
    }
    if config.commit_timeout_ms == 0 {
        return Err(AedbError::InvalidConfig {
            message: "commit_timeout_ms must be > 0".into(),
        });
    }
    if config.max_scan_rows == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_scan_rows must be > 0".into(),
        });
    }
    if config.max_batch_rows == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_batch_rows must be > 0".into(),
        });
    }
    if config.max_event_payload_bytes == 0 {
        return Err(AedbError::InvalidConfig {
            message: "max_event_payload_bytes must be > 0".into(),
        });
    }
    if config.max_kv_key_bytes == 0
        || config.max_kv_value_bytes == 0
        || config.max_table_value_bytes == 0
    {
        return Err(AedbError::InvalidConfig {
            message: "max_kv_key_bytes/max_kv_value_bytes/max_table_value_bytes must be > 0".into(),
        });
    }
    if config.prestage_shards == 0 {
        return Err(AedbError::InvalidConfig {
            message: "prestage_shards must be > 0".into(),
        });
    }
    if config.epoch_max_wait_us == 0
        || config.epoch_min_commits == 0
        || config.epoch_max_commits == 0
    {
        return Err(AedbError::InvalidConfig {
            message: "epoch_max_wait_us, epoch_min_commits, and epoch_max_commits must be > 0"
                .into(),
        });
    }
    if config.epoch_min_commits > config.epoch_max_commits {
        return Err(AedbError::InvalidConfig {
            message: "epoch_min_commits must be <= epoch_max_commits".into(),
        });
    }
    if config.adaptive_epoch_enabled {
        if config.adaptive_epoch_min_commits_floor == 0
            || config.adaptive_epoch_min_commits_ceiling == 0
            || config.adaptive_epoch_wait_us_floor == 0
            || config.adaptive_epoch_wait_us_ceiling == 0
            || config.adaptive_epoch_target_latency_us == 0
        {
            return Err(AedbError::InvalidConfig {
                message: "adaptive epoch tuning values must be > 0 when enabled".into(),
            });
        }
        if config.adaptive_epoch_min_commits_floor > config.adaptive_epoch_min_commits_ceiling {
            return Err(AedbError::InvalidConfig {
                message:
                    "adaptive_epoch_min_commits_floor must be <= adaptive_epoch_min_commits_ceiling"
                        .into(),
            });
        }
        if config.adaptive_epoch_wait_us_floor > config.adaptive_epoch_wait_us_ceiling {
            return Err(AedbError::InvalidConfig {
                message: "adaptive_epoch_wait_us_floor must be <= adaptive_epoch_wait_us_ceiling"
                    .into(),
            });
        }
    }
    if config.parallel_apply_enabled && config.parallel_worker_threads == 0 {
        return Err(AedbError::InvalidConfig {
            message: "parallel_worker_threads must be > 0 when parallel_apply_enabled".into(),
        });
    }
    if config.partition_lock_timeout_ms == 0 || config.epoch_apply_timeout_ms == 0 {
        return Err(AedbError::InvalidConfig {
            message: "partition_lock_timeout_ms and epoch_apply_timeout_ms must be > 0".into(),
        });
    }
    if config.max_versions == 0
        || config.version_store_full_snapshot_interval_deltas == 0
        || config.version_gc_interval_ms == 0
    {
        return Err(AedbError::InvalidConfig {
            message: "version store limits must be > 0".into(),
        });
    }
    if config.max_snapshot_age_ms == 0 || config.max_concurrent_snapshots == 0 {
        return Err(AedbError::InvalidConfig {
            message: "snapshot limits must be > 0".into(),
        });
    }
    if config.commit_broadcast_capacity == 0 {
        return Err(AedbError::InvalidConfig {
            message: "commit_broadcast_capacity must be > 0".into(),
        });
    }
    if matches!(config.durability_mode, DurabilityMode::Batch)
        && (config.batch_interval_ms == 0 || config.batch_max_bytes == 0)
    {
        return Err(AedbError::InvalidConfig {
            message: "batch mode requires batch_interval_ms and batch_max_bytes > 0".into(),
        });
    }
    if matches!(config.durability_mode, DurabilityMode::OsBuffered)
        && matches!(config.recovery_mode, RecoveryMode::Strict)
    {
        return Err(AedbError::InvalidConfig {
            message: "OsBuffered mode is not allowed with strict recovery".into(),
        });
    }
    if !config.hash_chain_required && matches!(config.recovery_mode, RecoveryMode::Strict) {
        return Err(AedbError::InvalidConfig {
            message: "strict recovery requires hash_chain_required=true".into(),
        });
    }
    if !config.coordinator_locking_enabled && matches!(config.recovery_mode, RecoveryMode::Strict) {
        return Err(AedbError::InvalidConfig {
            message: "strict recovery requires coordinator_locking_enabled=true".into(),
        });
    }
    if config.checkpoint_encryption_key.is_none() && config.checkpoint_key_id.is_some() {
        return Err(AedbError::InvalidConfig {
            message: "checkpoint_key_id requires checkpoint_encryption_key".into(),
        });
    }
    if !(-7..=22).contains(&config.checkpoint_compression_level) {
        return Err(AedbError::InvalidConfig {
            message: "checkpoint_compression_level must be between -7 and 22".into(),
        });
    }
    if let Some(key) = &config.manifest_hmac_key
        && key.is_empty()
    {
        return Err(AedbError::InvalidConfig {
            message: "manifest_hmac_key must not be empty".into(),
        });
    }
    Ok(())
}

pub(crate) fn validate_secure_config(config: &AedbConfig) -> Result<(), AedbError> {
    validate_config(config)?;
    if config.cursor_signing_key().is_none() {
        tracing::warn!(
            target: "aedb.config",
            "secure config has cursor_signing_key disabled; pagination cursors will not be HMAC-signed at the executor layer"
        );
    }
    let Some(hmac_key) = &config.manifest_hmac_key else {
        return Err(AedbError::InvalidConfig {
            message: "secure mode requires manifest_hmac_key".into(),
        });
    };
    validate_production_config_requirements(
        config,
        hmac_key,
        "secure mode",
        "secure mode requires manifest_hmac_key length >= 32 bytes",
    )
}

pub(crate) fn validate_arcana_config(config: &AedbConfig) -> Result<(), AedbError> {
    validate_config(config)?;
    if config.cursor_signing_key().is_none() {
        tracing::warn!(
            target: "aedb.config",
            "Arcana production profile has cursor_signing_key disabled; pagination cursors will not be HMAC-signed at the executor layer"
        );
    }
    let Some(hmac_key) = &config.manifest_hmac_key else {
        return Err(AedbError::InvalidConfig {
            message: "manifest_hmac_key is required for Arcana production profile".into(),
        });
    };
    validate_production_config_requirements(
        config,
        hmac_key,
        "Arcana production profile",
        "Arcana production profile requires manifest_hmac_key length >= 32 bytes",
    )
}

fn validate_production_config_requirements(
    config: &AedbConfig,
    hmac_key: &[u8],
    profile_name: &str,
    hmac_error: &str,
) -> Result<(), AedbError> {
    if hmac_key.len() < 32 {
        return Err(AedbError::InvalidConfig {
            message: hmac_error.into(),
        });
    }
    if !matches!(config.recovery_mode, RecoveryMode::Strict) {
        return Err(AedbError::InvalidConfig {
            message: format!("{profile_name} requires strict recovery"),
        });
    }
    if !matches!(config.durability_mode, DurabilityMode::Full) {
        return Err(AedbError::InvalidConfig {
            message: format!(
                "{profile_name} requires DurabilityMode::Full for crash-safe durability"
            ),
        });
    }
    if !config.hash_chain_required {
        return Err(AedbError::InvalidConfig {
            message: format!("{profile_name} requires hash_chain_required=true"),
        });
    }
    if !matches!(config.storage_mode, StorageMode::DiskBacked) {
        return Err(AedbError::InvalidConfig {
            message: format!("{profile_name} requires StorageMode::DiskBacked"),
        });
    }
    if config.persistent_value_inline_threshold_bytes != 0 {
        return Err(AedbError::InvalidConfig {
            message: format!(
                "{profile_name} requires persistent_value_inline_threshold_bytes=0 so KV payloads are disk-backed"
            ),
        });
    }
    Ok(())
}