1use std::path::Path;
12
13use kmp_adapter_embedded::StorageEngine;
14use kmp_domain::PortError;
15
16pub const ENGINE_ENV: &str = "KMP_MCP_ENGINE";
20
21pub fn parse_engine(value: &str) -> Result<StorageEngine, PortError> {
24 match value.trim().to_ascii_lowercase().as_str() {
25 "redb" => Ok(StorageEngine::Redb),
26 "sqlite" => Ok(StorageEngine::Sqlite),
27 other => Err(PortError::InvalidState(format!(
28 "unknown storage engine `{other}`; {ENGINE_ENV} accepts `redb` or `sqlite`"
29 ))),
30 }
31}
32
33pub fn resolve_engine_from_env() -> Result<Option<StorageEngine>, PortError> {
37 match std::env::var(ENGINE_ENV) {
38 Ok(value) if !value.trim().is_empty() => parse_engine(&value).map(Some),
39 _ => Ok(None),
40 }
41}
42
43pub fn resolve_engine_for_data_dir_from_env(
51 data_dir: &Path,
52) -> Result<Option<StorageEngine>, PortError> {
53 if let Some(engine) = resolve_engine_from_env()? {
54 return Ok(Some(engine));
55 }
56 Ok(default_engine_for_data_dir(data_dir))
57}
58
59pub fn default_engine_for_data_dir(data_dir: &Path) -> Option<StorageEngine> {
63 if data_dir.join("FORMAT_VERSION").exists() {
64 return None;
65 }
66 StorageEngine::Sqlite
67 .is_compiled()
68 .then_some(StorageEngine::Sqlite)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn engine_names_are_case_insensitive_and_trimmed() {
77 assert_eq!(
78 parse_engine("redb").expect("redb parses"),
79 StorageEngine::Redb
80 );
81 assert_eq!(
82 parse_engine(" SQLite ").expect("sqlite parses"),
83 StorageEngine::Sqlite
84 );
85 }
86
87 #[test]
88 fn unknown_engine_names_the_variable_and_the_choices() {
89 let error = parse_engine("postgres").expect_err("not an embedded engine");
90 let message = error.to_string();
91 assert!(message.contains("postgres"), "{message}");
92 assert!(message.contains(ENGINE_ENV), "{message}");
93 assert!(message.contains("`redb` or `sqlite`"), "{message}");
94 }
95
96 #[test]
97 fn compiled_sqlite_is_only_the_implicit_choice_for_a_fresh_directory() {
98 let temp = tempfile::tempdir().expect("tempdir");
99 let fresh = temp.path().join("fresh");
100 let expected = StorageEngine::Sqlite
101 .is_compiled()
102 .then_some(StorageEngine::Sqlite);
103 assert_eq!(default_engine_for_data_dir(&fresh), expected);
104
105 std::fs::create_dir_all(&fresh).expect("data dir");
106 std::fs::write(fresh.join("FORMAT_VERSION"), "1\n").expect("stamp");
107 assert_eq!(
108 default_engine_for_data_dir(&fresh),
109 None,
110 "an existing store must be opened from its stamp"
111 );
112 }
113}