Skip to main content

mlua_swarm/store/enhance_setting/
sqlite.rs

1//! `SqliteEnhanceSettingStore` — SQLite-backed [`EnhanceSettingStore`].
2//!
3//! Body shape is captured as a single JSON blob per row (the setting is
4//! already `Serialize + Deserialize`), so schema evolution of
5//! `EnhanceSetting` does not require a migration on this table.
6
7use super::{EnhanceSetting, EnhanceSettingId, EnhanceSettingStore, EnhanceSettingStoreError};
8use async_trait::async_trait;
9use rusqlite::{params, OptionalExtension};
10use rusqlite_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
11use std::path::Path;
12
13const SCHEMA_SQL: &str = "\
14CREATE TABLE IF NOT EXISTS enhance_settings (\
15  id       TEXT PRIMARY KEY, \
16  body_json TEXT NOT NULL\
17);\
18";
19
20/// SQLite-backed [`EnhanceSettingStore`].
21pub struct SqliteEnhanceSettingStore {
22    isle: AsyncIsle,
23}
24
25impl SqliteEnhanceSettingStore {
26    /// Open (or create) a SQLite file and apply the schema.
27    pub async fn open(
28        path: impl AsRef<Path>,
29    ) -> Result<(Self, AsyncIsleDriver), EnhanceSettingStoreError> {
30        let (isle, driver) = AsyncIsle::spawn(path.as_ref().to_path_buf(), |conn| {
31            conn.execute_batch(SCHEMA_SQL)
32        })
33        .await
34        .map_err(map_isle_err)?;
35        Ok((Self { isle }, driver))
36    }
37
38    /// Open an ephemeral in-memory database (tests).
39    pub async fn open_in_memory() -> Result<(Self, AsyncIsleDriver), EnhanceSettingStoreError> {
40        let (isle, driver) = AsyncIsle::open_in_memory(|conn| conn.execute_batch(SCHEMA_SQL))
41            .await
42            .map_err(map_isle_err)?;
43        Ok((Self { isle }, driver))
44    }
45}
46
47fn map_isle_err(e: IsleError) -> EnhanceSettingStoreError {
48    EnhanceSettingStoreError::Other(format!("sqlite: {e}"))
49}
50
51#[async_trait]
52impl EnhanceSettingStore for SqliteEnhanceSettingStore {
53    fn name(&self) -> &str {
54        "sqlite"
55    }
56
57    async fn get(&self, id: &EnhanceSettingId) -> Result<EnhanceSetting, EnhanceSettingStoreError> {
58        let id_str = id.0.clone();
59        let id_for_notfound = id.clone();
60        let row = self
61            .isle
62            .call(move |conn| {
63                conn.query_row(
64                    "SELECT body_json FROM enhance_settings WHERE id = ?1",
65                    params![id_str],
66                    |row| row.get::<_, String>(0),
67                )
68                .optional()
69            })
70            .await
71            .map_err(map_isle_err)?;
72        match row {
73            Some(json_text) => serde_json::from_str::<EnhanceSetting>(&json_text)
74                .map_err(|e| EnhanceSettingStoreError::Other(format!("decode: {e}"))),
75            None => Err(EnhanceSettingStoreError::NotFound(id_for_notfound)),
76        }
77    }
78
79    async fn put(
80        &self,
81        id: &EnhanceSettingId,
82        setting: EnhanceSetting,
83    ) -> Result<(), EnhanceSettingStoreError> {
84        let id_str = id.0.clone();
85        let json_text = serde_json::to_string(&setting)
86            .map_err(|e| EnhanceSettingStoreError::Other(format!("encode: {e}")))?;
87        self.isle
88            .call(move |conn| {
89                conn.execute(
90                    "INSERT INTO enhance_settings (id, body_json) VALUES (?1, ?2) \
91                     ON CONFLICT(id) DO UPDATE SET body_json = excluded.body_json",
92                    params![id_str, json_text],
93                )
94                .map(|_| ())
95            })
96            .await
97            .map_err(map_isle_err)
98    }
99
100    async fn delete(&self, id: &EnhanceSettingId) -> Result<(), EnhanceSettingStoreError> {
101        let id_str = id.0.clone();
102        let id_for_notfound = id.clone();
103        let n = self
104            .isle
105            .call(move |conn| {
106                conn.execute("DELETE FROM enhance_settings WHERE id = ?1", params![id_str])
107            })
108            .await
109            .map_err(map_isle_err)?;
110        if n == 0 {
111            Err(EnhanceSettingStoreError::NotFound(id_for_notfound))
112        } else {
113            Ok(())
114        }
115    }
116
117    async fn list(&self) -> Result<Vec<EnhanceSettingId>, EnhanceSettingStoreError> {
118        let rows = self
119            .isle
120            .call(|conn| {
121                let mut stmt = conn.prepare("SELECT id FROM enhance_settings ORDER BY id ASC")?;
122                let iter = stmt.query_map([], |row| row.get::<_, String>(0))?;
123                let mut out = Vec::new();
124                for r in iter {
125                    out.push(r?);
126                }
127                Ok(out)
128            })
129            .await
130            .map_err(map_isle_err)?;
131        Ok(rows.into_iter().map(EnhanceSettingId::new).collect())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::application::VersionSelector;
139    use crate::blueprint::store::BlueprintId;
140    use crate::enhance::setting::EnhanceSettingMeta;
141
142    fn dummy_setting(id: &str, bp: &str) -> EnhanceSetting {
143        EnhanceSetting {
144            id: id.into(),
145            blueprint_id: BlueprintId::new(bp.to_string()),
146            ttl_secs: 10,
147            version: VersionSelector::default(),
148            verifier_axes: vec!["des".into()],
149            meta: EnhanceSettingMeta::default(),
150        }
151    }
152
153    #[tokio::test]
154    async fn put_then_get_returns_same_setting() {
155        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
156        let id = EnhanceSettingId::new("s1");
157        s.put(&id, dummy_setting("s1", "bp-1")).await.unwrap();
158        let got = s.get(&id).await.unwrap();
159        assert_eq!(got.id, "s1");
160        assert_eq!(got.blueprint_id.as_str(), "bp-1");
161        assert_eq!(got.ttl_secs, 10);
162        drop(s);
163        driver.shutdown().await.unwrap();
164    }
165
166    #[tokio::test]
167    async fn put_overwrites_existing() {
168        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
169        let id = EnhanceSettingId::new("s1");
170        s.put(&id, dummy_setting("s1", "bp-1")).await.unwrap();
171        let mut updated = dummy_setting("s1", "bp-2");
172        updated.ttl_secs = 99;
173        s.put(&id, updated).await.unwrap();
174        let got = s.get(&id).await.unwrap();
175        assert_eq!(got.blueprint_id.as_str(), "bp-2");
176        assert_eq!(got.ttl_secs, 99);
177        drop(s);
178        driver.shutdown().await.unwrap();
179    }
180
181    #[tokio::test]
182    async fn get_missing_returns_not_found() {
183        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
184        let err = s.get(&EnhanceSettingId::new("nope")).await.unwrap_err();
185        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
186        drop(s);
187        driver.shutdown().await.unwrap();
188    }
189
190    #[tokio::test]
191    async fn delete_missing_returns_not_found() {
192        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
193        let err = s.delete(&EnhanceSettingId::new("nope")).await.unwrap_err();
194        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
195        drop(s);
196        driver.shutdown().await.unwrap();
197    }
198
199    #[tokio::test]
200    async fn list_returns_sorted_ids() {
201        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
202        s.put(&EnhanceSettingId::new("b"), dummy_setting("b", "bp"))
203            .await
204            .unwrap();
205        s.put(&EnhanceSettingId::new("a"), dummy_setting("a", "bp"))
206            .await
207            .unwrap();
208        s.put(&EnhanceSettingId::new("c"), dummy_setting("c", "bp"))
209            .await
210            .unwrap();
211        let ids: Vec<_> = s.list().await.unwrap().into_iter().map(|i| i.0).collect();
212        assert_eq!(ids, vec!["a", "b", "c"]);
213        drop(s);
214        driver.shutdown().await.unwrap();
215    }
216
217    #[tokio::test]
218    async fn persists_across_reopen() {
219        let dir = tempfile::tempdir().unwrap();
220        let path = dir.path().join("settings.db");
221
222        {
223            let (s, driver) = SqliteEnhanceSettingStore::open(&path).await.unwrap();
224            s.put(&EnhanceSettingId::new("keep"), dummy_setting("keep", "bp-x"))
225                .await
226                .unwrap();
227            drop(s);
228            driver.shutdown().await.unwrap();
229        }
230
231        let (s, driver) = SqliteEnhanceSettingStore::open(&path).await.unwrap();
232        let got = s.get(&EnhanceSettingId::new("keep")).await.unwrap();
233        assert_eq!(got.blueprint_id.as_str(), "bp-x");
234        drop(s);
235        driver.shutdown().await.unwrap();
236    }
237}