Skip to main content

kmp_embedded/
engine.rs

1//! Which storage engine a fresh data directory gets
2//! ([ADR-018](../../../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 directory opens with the engine it was created with, and
7//! asking for a different one is refused rather than quietly ignored — a
8//! store that opens as the wrong engine behind a user's back is exactly the
9//! silent divergence that ends in "why can't my second host open it".
10
11use kmp_adapter_embedded::StorageEngine;
12use kmp_domain::PortError;
13
14/// Which engine to create a fresh data directory with: `redb` (default) or
15/// `sqlite`. Ignored for an existing directory only in the sense that it
16/// must agree with it.
17pub const ENGINE_ENV: &str = "KMP_MCP_ENGINE";
18
19/// Parses an engine name the way the environment variable and the CLI spell
20/// it. Pure, for testing; [`resolve_engine_from_env`] feeds it.
21pub fn parse_engine(value: &str) -> Result<StorageEngine, PortError> {
22    match value.trim().to_ascii_lowercase().as_str() {
23        "redb" => Ok(StorageEngine::Redb),
24        "sqlite" => Ok(StorageEngine::Sqlite),
25        other => Err(PortError::InvalidState(format!(
26            "unknown storage engine `{other}`; {ENGINE_ENV} accepts `redb` or `sqlite`"
27        ))),
28    }
29}
30
31/// The engine `KMP_MCP_ENGINE` asks for, if it is set. Unset or empty means
32/// "no preference": a fresh directory gets the default, an existing one
33/// opens as it is.
34pub fn resolve_engine_from_env() -> Result<Option<StorageEngine>, PortError> {
35    match std::env::var(ENGINE_ENV) {
36        Ok(value) if !value.trim().is_empty() => parse_engine(&value).map(Some),
37        _ => Ok(None),
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn engine_names_are_case_insensitive_and_trimmed() {
47        assert_eq!(
48            parse_engine("redb").expect("redb parses"),
49            StorageEngine::Redb
50        );
51        assert_eq!(
52            parse_engine(" SQLite ").expect("sqlite parses"),
53            StorageEngine::Sqlite
54        );
55    }
56
57    #[test]
58    fn unknown_engine_names_the_variable_and_the_choices() {
59        let error = parse_engine("postgres").expect_err("not an embedded engine");
60        let message = error.to_string();
61        assert!(message.contains("postgres"), "{message}");
62        assert!(message.contains(ENGINE_ENV), "{message}");
63        assert!(message.contains("`redb` or `sqlite`"), "{message}");
64    }
65}