Skip to main content

sbol_db_sqlite/repo/
config.rs

1//! Durable instance configuration over SQLite.
2//!
3//! Entries live in `sbh_app_config`, mapping a key to a TEXT column holding the
4//! serialized JSON value. [`set`](ConfigStore::set) upserts on the primary key,
5//! so a later write to a key overwrites its value and refreshes `updated_at`.
6
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use sbol_db_core::{ConfigEntry, DomainError};
10use sbol_db_storage::ConfigStore;
11use serde_json::Value;
12use sqlx::Row;
13
14use crate::pool::db_err;
15use crate::SqlitePool;
16
17#[derive(Clone)]
18pub struct SqliteConfigStore {
19    pool: SqlitePool,
20}
21
22impl SqliteConfigStore {
23    pub fn new(pool: SqlitePool) -> Self {
24        Self { pool }
25    }
26}
27
28/// Parse a stored JSON string into a [`Value`], mapping a decode failure to a
29/// database error since the column is written only by [`SqliteConfigStore::set`].
30fn parse_value(text: &str) -> Result<Value, DomainError> {
31    serde_json::from_str(text)
32        .map_err(|e| DomainError::Database(format!("config value is not valid JSON: {e}")))
33}
34
35#[async_trait]
36impl ConfigStore for SqliteConfigStore {
37    async fn get(&self, key: &str) -> Result<Option<Value>, DomainError> {
38        let row = sqlx::query("SELECT value FROM sbh_app_config WHERE key = ?")
39            .bind(key)
40            .fetch_optional(&self.pool)
41            .await
42            .map_err(db_err)?;
43        match row {
44            Some(r) => Ok(Some(parse_value(
45                &r.try_get::<String, _>("value").map_err(db_err)?,
46            )?)),
47            None => Ok(None),
48        }
49    }
50
51    async fn set(&self, key: &str, value: &Value) -> Result<(), DomainError> {
52        let text = serde_json::to_string(value)?;
53        sqlx::query(
54            "INSERT INTO sbh_app_config (key, value, updated_at) VALUES (?, ?, ?) \
55             ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
56        )
57        .bind(key)
58        .bind(text)
59        .bind(Utc::now())
60        .execute(&self.pool)
61        .await
62        .map_err(db_err)?;
63        Ok(())
64    }
65
66    async fn get_all(&self) -> Result<Vec<ConfigEntry>, DomainError> {
67        let rows = sqlx::query("SELECT key, value, updated_at FROM sbh_app_config")
68            .fetch_all(&self.pool)
69            .await
70            .map_err(db_err)?;
71        let mut out = Vec::with_capacity(rows.len());
72        for r in rows {
73            out.push(ConfigEntry {
74                key: r.try_get("key").map_err(db_err)?,
75                value: parse_value(&r.try_get::<String, _>("value").map_err(db_err)?)?,
76                updated_at: r
77                    .try_get::<DateTime<Utc>, _>("updated_at")
78                    .map_err(db_err)?,
79            });
80        }
81        Ok(out)
82    }
83
84    async fn delete(&self, key: &str) -> Result<(), DomainError> {
85        sqlx::query("DELETE FROM sbh_app_config WHERE key = ?")
86            .bind(key)
87            .execute(&self.pool)
88            .await
89            .map_err(db_err)?;
90        Ok(())
91    }
92}