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(
107                    "DELETE FROM enhance_settings WHERE id = ?1",
108                    params![id_str],
109                )
110            })
111            .await
112            .map_err(map_isle_err)?;
113        if n == 0 {
114            Err(EnhanceSettingStoreError::NotFound(id_for_notfound))
115        } else {
116            Ok(())
117        }
118    }
119
120    async fn list(&self) -> Result<Vec<EnhanceSettingId>, EnhanceSettingStoreError> {
121        let rows = self
122            .isle
123            .call(|conn| {
124                let mut stmt = conn.prepare("SELECT id FROM enhance_settings ORDER BY id ASC")?;
125                let iter = stmt.query_map([], |row| row.get::<_, String>(0))?;
126                let mut out = Vec::new();
127                for r in iter {
128                    out.push(r?);
129                }
130                Ok(out)
131            })
132            .await
133            .map_err(map_isle_err)?;
134        Ok(rows.into_iter().map(EnhanceSettingId::new).collect())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::application::VersionSelector;
142    use crate::blueprint::store::BlueprintId;
143    use crate::enhance::setting::EnhanceSettingMeta;
144
145    fn dummy_setting(id: &str, bp: &str) -> EnhanceSetting {
146        EnhanceSetting {
147            id: id.into(),
148            blueprint_id: BlueprintId::new(bp.to_string()),
149            ttl_secs: 10,
150            version: VersionSelector::default(),
151            verifier_axes: vec!["des".into()],
152            meta: EnhanceSettingMeta::default(),
153        }
154    }
155
156    #[tokio::test]
157    async fn put_then_get_returns_same_setting() {
158        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
159        let id = EnhanceSettingId::new("s1");
160        s.put(&id, dummy_setting("s1", "bp-1")).await.unwrap();
161        let got = s.get(&id).await.unwrap();
162        assert_eq!(got.id, "s1");
163        assert_eq!(got.blueprint_id.as_str(), "bp-1");
164        assert_eq!(got.ttl_secs, 10);
165        drop(s);
166        driver.shutdown().await.unwrap();
167    }
168
169    #[tokio::test]
170    async fn put_overwrites_existing() {
171        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
172        let id = EnhanceSettingId::new("s1");
173        s.put(&id, dummy_setting("s1", "bp-1")).await.unwrap();
174        let mut updated = dummy_setting("s1", "bp-2");
175        updated.ttl_secs = 99;
176        s.put(&id, updated).await.unwrap();
177        let got = s.get(&id).await.unwrap();
178        assert_eq!(got.blueprint_id.as_str(), "bp-2");
179        assert_eq!(got.ttl_secs, 99);
180        drop(s);
181        driver.shutdown().await.unwrap();
182    }
183
184    #[tokio::test]
185    async fn get_missing_returns_not_found() {
186        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
187        let err = s.get(&EnhanceSettingId::new("nope")).await.unwrap_err();
188        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
189        drop(s);
190        driver.shutdown().await.unwrap();
191    }
192
193    #[tokio::test]
194    async fn delete_missing_returns_not_found() {
195        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
196        let err = s.delete(&EnhanceSettingId::new("nope")).await.unwrap_err();
197        assert!(matches!(err, EnhanceSettingStoreError::NotFound(_)));
198        drop(s);
199        driver.shutdown().await.unwrap();
200    }
201
202    #[tokio::test]
203    async fn list_returns_sorted_ids() {
204        let (s, driver) = SqliteEnhanceSettingStore::open_in_memory().await.unwrap();
205        s.put(&EnhanceSettingId::new("b"), dummy_setting("b", "bp"))
206            .await
207            .unwrap();
208        s.put(&EnhanceSettingId::new("a"), dummy_setting("a", "bp"))
209            .await
210            .unwrap();
211        s.put(&EnhanceSettingId::new("c"), dummy_setting("c", "bp"))
212            .await
213            .unwrap();
214        let ids: Vec<_> = s.list().await.unwrap().into_iter().map(|i| i.0).collect();
215        assert_eq!(ids, vec!["a", "b", "c"]);
216        drop(s);
217        driver.shutdown().await.unwrap();
218    }
219
220    #[tokio::test]
221    async fn persists_across_reopen() {
222        let dir = tempfile::tempdir().unwrap();
223        let path = dir.path().join("settings.db");
224
225        {
226            let (s, driver) = SqliteEnhanceSettingStore::open(&path).await.unwrap();
227            s.put(
228                &EnhanceSettingId::new("keep"),
229                dummy_setting("keep", "bp-x"),
230            )
231            .await
232            .unwrap();
233            drop(s);
234            driver.shutdown().await.unwrap();
235        }
236
237        let (s, driver) = SqliteEnhanceSettingStore::open(&path).await.unwrap();
238        let got = s.get(&EnhanceSettingId::new("keep")).await.unwrap();
239        assert_eq!(got.blueprint_id.as_str(), "bp-x");
240        drop(s);
241        driver.shutdown().await.unwrap();
242    }
243}