Skip to main content

kmp_embedded/
engine.rs

1//! Which storage engine a fresh data directory gets.
2//!
3//! The choice is made once, when a directory is created, and recorded in its
4//! `FORMAT_VERSION`. `KMP_MCP_ENGINE` says what a *fresh* directory should
5//! be; an existing supported directory opens from its stamp. Retired layouts
6//! are rejected rather than reinterpreted.
7
8use std::path::Path;
9
10use kmp_adapter_embedded::StorageEngine;
11use kmp_domain::PortError;
12
13/// Compatibility environment variable. SQLite is the only accepted value.
14pub const ENGINE_ENV: &str = "KMP_MCP_ENGINE";
15
16/// Parses an engine name the way the environment variable and the CLI spell
17/// it. Pure, for testing; [`resolve_engine_from_env`] feeds it.
18pub fn parse_engine(value: &str) -> Result<StorageEngine, PortError> {
19    match value.trim().to_ascii_lowercase().as_str() {
20        "sqlite" => Ok(StorageEngine::Sqlite),
21        other => Err(PortError::InvalidState(format!(
22            "unknown storage engine `{other}`; {ENGINE_ENV} only accepts `sqlite`"
23        ))),
24    }
25}
26
27/// The engine `KMP_MCP_ENGINE` asks for, if it is set. Unset or empty means
28/// "no preference": a fresh directory gets the default, an existing one
29/// opens as it is.
30pub fn resolve_engine_from_env() -> Result<Option<StorageEngine>, PortError> {
31    match std::env::var(ENGINE_ENV) {
32        Ok(value) if !value.trim().is_empty() => parse_engine(&value).map(Some),
33        _ => Ok(None),
34    }
35}
36
37/// Resolves the engine for a particular data directory.
38///
39/// An explicit environment choice is always authoritative. With no choice,
40/// an existing store is opened as stamped; a fresh store uses SQLite.
41pub fn resolve_engine_for_data_dir_from_env(
42    data_dir: &Path,
43) -> Result<Option<StorageEngine>, PortError> {
44    if let Some(engine) = resolve_engine_from_env()? {
45        return Ok(Some(engine));
46    }
47    Ok(default_engine_for_data_dir(data_dir))
48}
49
50/// Implicit engine choice for a data directory when no operator override is
51/// present. Existing stores defer to their stamp; fresh stores use SQLite.
52pub fn default_engine_for_data_dir(data_dir: &Path) -> Option<StorageEngine> {
53    if data_dir.join("FORMAT_VERSION").exists() {
54        return None;
55    }
56    Some(StorageEngine::Sqlite)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn sqlite_name_is_case_insensitive_and_trimmed() {
65        assert_eq!(
66            parse_engine(" SQLite ").expect("sqlite parses"),
67            StorageEngine::Sqlite
68        );
69    }
70
71    #[test]
72    fn unknown_engine_names_the_variable_and_the_choices() {
73        let error = parse_engine("postgres").expect_err("not an embedded engine");
74        let message = error.to_string();
75        assert!(message.contains("postgres"), "{message}");
76        assert!(message.contains(ENGINE_ENV), "{message}");
77        assert!(message.contains("only accepts `sqlite`"), "{message}");
78    }
79
80    #[test]
81    fn compiled_sqlite_is_only_the_implicit_choice_for_a_fresh_directory() {
82        let temp = tempfile::tempdir().expect("tempdir");
83        let fresh = temp.path().join("fresh");
84        assert_eq!(
85            default_engine_for_data_dir(&fresh),
86            Some(StorageEngine::Sqlite)
87        );
88
89        std::fs::create_dir_all(&fresh).expect("data dir");
90        std::fs::write(fresh.join("FORMAT_VERSION"), "1\n").expect("stamp");
91        assert_eq!(
92            default_engine_for_data_dir(&fresh),
93            None,
94            "an existing store must be opened from its stamp"
95        );
96    }
97}