use serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SqliteConfigError {
#[error("Pool size must be > 0")]
InvalidPoolSize,
#[error("Busy timeout should be >= 1000ms (was {0}ms)")]
BusyTimeoutTooLow(u64),
#[error("Retry attempts must be > 0")]
InvalidRetryConfig,
#[error("Invalid synchronous value: {0} (expected OFF, NORMAL, FULL, or EXTRA)")]
InvalidSynchronousValue(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SqliteConfig {
pub pool: PoolConfig,
pub pragmas: PragmaConfig,
pub retry: RetryConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolConfig {
pub max_size: u32,
pub min_idle: Option<u32>,
pub connection_timeout_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PragmaConfig {
pub busy_timeout_ms: u64,
pub wal_autocheckpoint: u32,
pub cache_size_kb: i32,
pub mmap_size_bytes: u64,
pub synchronous: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: u32,
pub base_delay_ms: u64,
pub max_delay_ms: u64,
pub exponential: bool,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_size: 10,
min_idle: None,
connection_timeout_ms: 30000,
}
}
}
impl Default for PragmaConfig {
fn default() -> Self {
Self {
busy_timeout_ms: 30000,
wal_autocheckpoint: 10000,
cache_size_kb: 65536, mmap_size_bytes: 268435456, synchronous: "NORMAL".to_string(),
}
}
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 5,
base_delay_ms: 50,
max_delay_ms: 5000,
exponential: true,
}
}
}
impl SqliteConfig {
pub fn from_env() -> Result<Self, SqliteConfigError> {
let config = Self {
pool: PoolConfig {
max_size: env_or("MAPROOM_SQLITE_POOL_SIZE", 10),
min_idle: env_opt("MAPROOM_SQLITE_MIN_IDLE"),
connection_timeout_ms: env_or("MAPROOM_SQLITE_TIMEOUT_MS", 30000),
},
pragmas: PragmaConfig {
busy_timeout_ms: env_or("MAPROOM_SQLITE_BUSY_TIMEOUT_MS", 30000),
wal_autocheckpoint: env_or("MAPROOM_SQLITE_WAL_CHECKPOINT", 10000),
cache_size_kb: env_or("MAPROOM_SQLITE_CACHE_SIZE_KB", 65536),
mmap_size_bytes: env_or("MAPROOM_SQLITE_MMAP_SIZE", 268435456),
synchronous: env_or("MAPROOM_SQLITE_SYNCHRONOUS", "NORMAL".to_string()),
},
retry: RetryConfig {
max_attempts: env_or("MAPROOM_SQLITE_RETRY_ATTEMPTS", 5),
base_delay_ms: env_or("MAPROOM_SQLITE_RETRY_BASE_MS", 50),
max_delay_ms: env_or("MAPROOM_SQLITE_RETRY_MAX_MS", 5000),
exponential: env_or("MAPROOM_SQLITE_RETRY_EXPONENTIAL", true),
},
};
config.validate()?;
Ok(config)
}
pub fn validate(&self) -> Result<(), SqliteConfigError> {
if self.pool.max_size == 0 {
return Err(SqliteConfigError::InvalidPoolSize);
}
if self.pragmas.busy_timeout_ms < 1000 {
return Err(SqliteConfigError::BusyTimeoutTooLow(
self.pragmas.busy_timeout_ms,
));
}
if !["OFF", "NORMAL", "FULL", "EXTRA"]
.contains(&self.pragmas.synchronous.to_uppercase().as_str())
{
return Err(SqliteConfigError::InvalidSynchronousValue(
self.pragmas.synchronous.clone(),
));
}
if self.retry.max_attempts == 0 {
return Err(SqliteConfigError::InvalidRetryConfig);
}
Ok(())
}
}
fn env_or<T: FromStr>(key: &str, default: T) -> T {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn env_opt<T: FromStr>(key: &str) -> Option<T> {
std::env::var(key).ok().and_then(|v| v.parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SqliteConfig::default();
assert_eq!(config.pool.max_size, 10);
assert_eq!(config.pool.min_idle, None);
assert_eq!(config.pool.connection_timeout_ms, 30000);
assert_eq!(config.pragmas.busy_timeout_ms, 30000);
assert_eq!(config.pragmas.wal_autocheckpoint, 10000);
assert_eq!(config.pragmas.cache_size_kb, 65536);
assert_eq!(config.pragmas.mmap_size_bytes, 268435456);
assert_eq!(config.pragmas.synchronous, "NORMAL");
assert_eq!(config.retry.max_attempts, 5);
assert_eq!(config.retry.base_delay_ms, 50);
assert_eq!(config.retry.max_delay_ms, 5000);
assert!(config.retry.exponential);
assert!(config.validate().is_ok());
}
#[test]
fn test_from_env() {
let original_pool_size = std::env::var("MAPROOM_SQLITE_POOL_SIZE").ok();
let original_busy_timeout = std::env::var("MAPROOM_SQLITE_BUSY_TIMEOUT_MS").ok();
let original_synchronous = std::env::var("MAPROOM_SQLITE_SYNCHRONOUS").ok();
std::env::set_var("MAPROOM_SQLITE_POOL_SIZE", "20");
std::env::set_var("MAPROOM_SQLITE_BUSY_TIMEOUT_MS", "60000");
std::env::set_var("MAPROOM_SQLITE_SYNCHRONOUS", "FULL");
let config = SqliteConfig::from_env().unwrap();
assert_eq!(config.pool.max_size, 20);
assert_eq!(config.pragmas.busy_timeout_ms, 60000);
assert_eq!(config.pragmas.synchronous, "FULL");
if let Some(val) = original_pool_size {
std::env::set_var("MAPROOM_SQLITE_POOL_SIZE", val);
} else {
std::env::remove_var("MAPROOM_SQLITE_POOL_SIZE");
}
if let Some(val) = original_busy_timeout {
std::env::set_var("MAPROOM_SQLITE_BUSY_TIMEOUT_MS", val);
} else {
std::env::remove_var("MAPROOM_SQLITE_BUSY_TIMEOUT_MS");
}
if let Some(val) = original_synchronous {
std::env::set_var("MAPROOM_SQLITE_SYNCHRONOUS", val);
} else {
std::env::remove_var("MAPROOM_SQLITE_SYNCHRONOUS");
}
}
#[test]
fn test_validation_rejects_invalid_pool_size() {
let mut config = SqliteConfig::default();
config.pool.max_size = 0;
assert!(matches!(
config.validate(),
Err(SqliteConfigError::InvalidPoolSize)
));
}
#[test]
fn test_validation_rejects_low_timeout() {
let mut config = SqliteConfig::default();
config.pragmas.busy_timeout_ms = 500;
let result = config.validate();
assert!(matches!(
result,
Err(SqliteConfigError::BusyTimeoutTooLow(500))
));
}
#[test]
fn test_validation_rejects_invalid_synchronous() {
let mut config = SqliteConfig::default();
config.pragmas.synchronous = "INVALID".to_string();
let result = config.validate();
assert!(matches!(
result,
Err(SqliteConfigError::InvalidSynchronousValue(_))
));
}
#[test]
fn test_validation_accepts_valid_synchronous_values() {
for value in &["OFF", "NORMAL", "FULL", "EXTRA", "normal", "full"] {
let mut config = SqliteConfig::default();
config.pragmas.synchronous = value.to_string();
assert!(
config.validate().is_ok(),
"Failed to validate synchronous value: {}",
value
);
}
}
#[test]
fn test_validation_rejects_zero_retry_attempts() {
let mut config = SqliteConfig::default();
config.retry.max_attempts = 0;
assert!(matches!(
config.validate(),
Err(SqliteConfigError::InvalidRetryConfig)
));
}
#[test]
fn test_env_or_parsing() {
std::env::remove_var("TEST_UNSET_VAR");
assert_eq!(env_or("TEST_UNSET_VAR", 42), 42);
std::env::set_var("TEST_SET_VAR", "100");
assert_eq!(env_or("TEST_SET_VAR", 42), 100);
std::env::remove_var("TEST_SET_VAR");
std::env::set_var("TEST_INVALID_VAR", "not_a_number");
assert_eq!(env_or::<i32>("TEST_INVALID_VAR", 42), 42);
std::env::remove_var("TEST_INVALID_VAR");
}
#[test]
fn test_env_opt_parsing() {
std::env::remove_var("TEST_OPT_UNSET");
assert_eq!(env_opt::<u32>("TEST_OPT_UNSET"), None);
std::env::set_var("TEST_OPT_SET", "100");
assert_eq!(env_opt::<u32>("TEST_OPT_SET"), Some(100));
std::env::remove_var("TEST_OPT_SET");
std::env::set_var("TEST_OPT_INVALID", "not_a_number");
assert_eq!(env_opt::<u32>("TEST_OPT_INVALID"), None);
std::env::remove_var("TEST_OPT_INVALID");
}
#[test]
fn test_min_idle_optional() {
let config = SqliteConfig::default();
assert_eq!(config.pool.min_idle, None);
let original = std::env::var("MAPROOM_SQLITE_MIN_IDLE").ok();
std::env::set_var("MAPROOM_SQLITE_MIN_IDLE", "5");
let config = SqliteConfig::from_env().unwrap();
assert_eq!(config.pool.min_idle, Some(5));
if let Some(val) = original {
std::env::set_var("MAPROOM_SQLITE_MIN_IDLE", val);
} else {
std::env::remove_var("MAPROOM_SQLITE_MIN_IDLE");
}
}
}