1use std::path::Path;
10
11use kmp_adapter_embedded::StorageEngine;
12use kmp_domain::PortError;
13
14pub const ENGINE_ENV: &str = "KMP_MCP_ENGINE";
16
17pub fn parse_engine(value: &str) -> Result<StorageEngine, PortError> {
20 match value.trim().to_ascii_lowercase().as_str() {
21 "sqlite" => Ok(StorageEngine::Sqlite),
22 other => Err(PortError::InvalidState(format!(
23 "unknown storage engine `{other}`; {ENGINE_ENV} only accepts `sqlite`"
24 ))),
25 }
26}
27
28pub fn resolve_engine_from_env() -> Result<Option<StorageEngine>, PortError> {
32 match std::env::var(ENGINE_ENV) {
33 Ok(value) if !value.trim().is_empty() => parse_engine(&value).map(Some),
34 _ => Ok(None),
35 }
36}
37
38pub fn resolve_engine_for_data_dir_from_env(
43 data_dir: &Path,
44) -> Result<Option<StorageEngine>, PortError> {
45 if let Some(engine) = resolve_engine_from_env()? {
46 return Ok(Some(engine));
47 }
48 Ok(default_engine_for_data_dir(data_dir))
49}
50
51pub fn default_engine_for_data_dir(data_dir: &Path) -> Option<StorageEngine> {
54 if data_dir.join("FORMAT_VERSION").exists() {
55 return None;
56 }
57 Some(StorageEngine::Sqlite)
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn sqlite_name_is_case_insensitive_and_trimmed() {
66 assert_eq!(
67 parse_engine(" SQLite ").expect("sqlite parses"),
68 StorageEngine::Sqlite
69 );
70 }
71
72 #[test]
73 fn a_retired_engine_name_is_just_an_unknown_selector() {
74 let error = parse_engine("redb").expect_err("the retired engine is unavailable");
75 assert!(error.to_string().contains("unknown storage engine"));
76 }
77
78 #[test]
79 fn unknown_engine_names_the_variable_and_the_choices() {
80 let error = parse_engine("postgres").expect_err("not an embedded engine");
81 let message = error.to_string();
82 assert!(message.contains("postgres"), "{message}");
83 assert!(message.contains(ENGINE_ENV), "{message}");
84 assert!(message.contains("only accepts `sqlite`"), "{message}");
85 }
86
87 #[test]
88 fn compiled_sqlite_is_only_the_implicit_choice_for_a_fresh_directory() {
89 let temp = tempfile::tempdir().expect("tempdir");
90 let fresh = temp.path().join("fresh");
91 assert_eq!(
92 default_engine_for_data_dir(&fresh),
93 Some(StorageEngine::Sqlite)
94 );
95
96 std::fs::create_dir_all(&fresh).expect("data dir");
97 std::fs::write(fresh.join("FORMAT_VERSION"), "1\n").expect("stamp");
98 assert_eq!(
99 default_engine_for_data_dir(&fresh),
100 None,
101 "an existing store must be opened from its stamp"
102 );
103 }
104}