Skip to main content

kmp_embedded/
engine.rs

1//! Which storage engine a fresh data directory gets
2//! ([historical ADR-018](https://github.com/underpass-ai/kmp/blob/v0.5.0/archive/docs/adr/ADR-018-multi-process-embedded-store.md)).
3//!
4//! The choice is made once, when a directory is created, and recorded in its
5//! `FORMAT_VERSION`. `KMP_MCP_ENGINE` says what a *fresh* directory should
6//! be; an existing supported directory opens from its stamp. Retired layouts
7//! are rejected rather than reinterpreted.
8
9use std::path::Path;
10
11use kmp_adapter_embedded::StorageEngine;
12use kmp_domain::PortError;
13
14/// Compatibility environment variable. SQLite is the only accepted value.
15pub const ENGINE_ENV: &str = "KMP_MCP_ENGINE";
16
17/// Parses an engine name the way the environment variable and the CLI spell
18/// it. Pure, for testing; [`resolve_engine_from_env`] feeds it.
19pub 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
28/// The engine `KMP_MCP_ENGINE` asks for, if it is set. Unset or empty means
29/// "no preference": a fresh directory gets the default, an existing one
30/// opens as it is.
31pub 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
38/// Resolves the engine for a particular data directory.
39///
40/// An explicit environment choice is always authoritative. With no choice,
41/// an existing store is opened as stamped; a fresh store uses SQLite.
42pub 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
51/// Implicit engine choice for a data directory when no operator override is
52/// present. Existing stores defer to their stamp; fresh stores use SQLite.
53pub 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}