kvp.rs

  1use anyhow::Context as _;
  2use gpui::App;
  3use sqlez_macros::sql;
  4use util::ResultExt as _;
  5
  6use crate::{
  7    query,
  8    sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
  9    write_and_log,
 10};
 11
 12pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
 13
 14impl KeyValueStore {
 15    pub fn from_app_db(db: &crate::AppDatabase) -> Self {
 16        Self(db.0.clone())
 17    }
 18}
 19
 20impl Domain for KeyValueStore {
 21    const NAME: &str = stringify!(KeyValueStore);
 22
 23    const MIGRATIONS: &[&str] = &[
 24        sql!(
 25            CREATE TABLE IF NOT EXISTS kv_store(
 26                key TEXT PRIMARY KEY,
 27                value TEXT NOT NULL
 28            ) STRICT;
 29        ),
 30        sql!(
 31            CREATE TABLE IF NOT EXISTS scoped_kv_store(
 32                namespace TEXT NOT NULL,
 33                key TEXT NOT NULL,
 34                value TEXT NOT NULL,
 35                PRIMARY KEY(namespace, key)
 36            ) STRICT;
 37        ),
 38    ];
 39}
 40
 41crate::static_connection!(KeyValueStore, []);
 42
 43pub trait Dismissable {
 44    const KEY: &'static str;
 45
 46    fn dismissed(cx: &App) -> bool {
 47        KeyValueStore::global(cx)
 48            .read_kvp(Self::KEY)
 49            .log_err()
 50            .is_some_and(|s| s.is_some())
 51    }
 52
 53    fn set_dismissed(is_dismissed: bool, cx: &mut App) {
 54        let db = KeyValueStore::global(cx);
 55        write_and_log(cx, move || async move {
 56            if is_dismissed {
 57                db.write_kvp(Self::KEY.into(), "1".into()).await
 58            } else {
 59                db.delete_kvp(Self::KEY.into()).await
 60            }
 61        })
 62    }
 63}
 64
 65impl KeyValueStore {
 66    query! {
 67        pub fn read_kvp(key: &str) -> Result<Option<String>> {
 68            SELECT value FROM kv_store WHERE key = (?)
 69        }
 70    }
 71
 72    pub async fn write_kvp(&self, key: String, value: String) -> anyhow::Result<()> {
 73        log::debug!("Writing key-value pair for key {key}");
 74        self.write_kvp_inner(key, value).await
 75    }
 76
 77    query! {
 78        async fn write_kvp_inner(key: String, value: String) -> Result<()> {
 79            INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
 80        }
 81    }
 82
 83    query! {
 84        pub async fn delete_kvp(key: String) -> Result<()> {
 85            DELETE FROM kv_store WHERE key = (?)
 86        }
 87    }
 88
 89    pub fn scoped<'a>(&'a self, namespace: &'a str) -> ScopedKeyValueStore<'a> {
 90        ScopedKeyValueStore {
 91            store: self,
 92            namespace,
 93        }
 94    }
 95}
 96
 97pub struct ScopedKeyValueStore<'a> {
 98    store: &'a KeyValueStore,
 99    namespace: &'a str,
100}
101
102impl ScopedKeyValueStore<'_> {
103    pub fn read(&self, key: &str) -> anyhow::Result<Option<String>> {
104        self.store.select_row_bound::<(&str, &str), String>(
105            "SELECT value FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
106        )?((self.namespace, key))
107        .context("Failed to read from scoped_kv_store")
108    }
109
110    pub async fn write(&self, key: String, value: String) -> anyhow::Result<()> {
111        let namespace = self.namespace.to_owned();
112        self.store
113            .write(move |connection| {
114                connection.exec_bound::<(&str, &str, &str)>(
115                    "INSERT OR REPLACE INTO scoped_kv_store(namespace, key, value) VALUES ((?), (?), (?))",
116                )?((&namespace, &key, &value))
117                .context("Failed to write to scoped_kv_store")
118            })
119            .await
120    }
121
122    pub async fn delete(&self, key: String) -> anyhow::Result<()> {
123        let namespace = self.namespace.to_owned();
124        self.store
125            .write(move |connection| {
126                connection.exec_bound::<(&str, &str)>(
127                    "DELETE FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
128                )?((&namespace, &key))
129                .context("Failed to delete from scoped_kv_store")
130            })
131            .await
132    }
133
134    pub async fn delete_all(&self) -> anyhow::Result<()> {
135        let namespace = self.namespace.to_owned();
136        self.store
137            .write(move |connection| {
138                connection
139                    .exec_bound::<&str>("DELETE FROM scoped_kv_store WHERE namespace = (?)")?(
140                    &namespace,
141                )
142                .context("Failed to delete_all from scoped_kv_store")
143            })
144            .await
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::kvp::KeyValueStore;
151
152    #[gpui::test]
153    async fn test_kvp() {
154        let db = KeyValueStore::open_test_db("test_kvp").await;
155
156        assert_eq!(db.read_kvp("key-1").unwrap(), None);
157
158        db.write_kvp("key-1".to_string(), "one".to_string())
159            .await
160            .unwrap();
161        assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
162
163        db.write_kvp("key-1".to_string(), "one-2".to_string())
164            .await
165            .unwrap();
166        assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
167
168        db.write_kvp("key-2".to_string(), "two".to_string())
169            .await
170            .unwrap();
171        assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
172
173        db.delete_kvp("key-1".to_string()).await.unwrap();
174        assert_eq!(db.read_kvp("key-1").unwrap(), None);
175    }
176
177    #[gpui::test]
178    async fn test_scoped_kvp() {
179        let db = KeyValueStore::open_test_db("test_scoped_kvp").await;
180
181        let scope_a = db.scoped("namespace-a");
182        let scope_b = db.scoped("namespace-b");
183
184        // Reading a missing key returns None
185        assert_eq!(scope_a.read("key-1").unwrap(), None);
186
187        // Writing and reading back a key works
188        scope_a
189            .write("key-1".to_string(), "value-a1".to_string())
190            .await
191            .unwrap();
192        assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
193
194        // Two namespaces with the same key don't collide
195        scope_b
196            .write("key-1".to_string(), "value-b1".to_string())
197            .await
198            .unwrap();
199        assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
200        assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
201
202        // delete removes a single key without affecting others in the namespace
203        scope_a
204            .write("key-2".to_string(), "value-a2".to_string())
205            .await
206            .unwrap();
207        scope_a.delete("key-1".to_string()).await.unwrap();
208        assert_eq!(scope_a.read("key-1").unwrap(), None);
209        assert_eq!(scope_a.read("key-2").unwrap(), Some("value-a2".to_string()));
210        assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
211
212        // delete_all removes all keys in a namespace without affecting other namespaces
213        scope_a
214            .write("key-3".to_string(), "value-a3".to_string())
215            .await
216            .unwrap();
217        scope_a.delete_all().await.unwrap();
218        assert_eq!(scope_a.read("key-2").unwrap(), None);
219        assert_eq!(scope_a.read("key-3").unwrap(), None);
220        assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
221    }
222}
223
224pub struct GlobalKeyValueStore(ThreadSafeConnection);
225
226impl Domain for GlobalKeyValueStore {
227    const NAME: &str = stringify!(GlobalKeyValueStore);
228    const MIGRATIONS: &[&str] = &[sql!(
229        CREATE TABLE IF NOT EXISTS kv_store(
230            key TEXT PRIMARY KEY,
231            value TEXT NOT NULL
232        ) STRICT;
233    )];
234}
235
236impl std::ops::Deref for GlobalKeyValueStore {
237    type Target = ThreadSafeConnection;
238    fn deref(&self) -> &Self::Target {
239        &self.0
240    }
241}
242
243static GLOBAL_KEY_VALUE_STORE: std::sync::LazyLock<GlobalKeyValueStore> =
244    std::sync::LazyLock::new(|| {
245        let db_dir = crate::database_dir();
246        GlobalKeyValueStore(smol::block_on(crate::open_db::<GlobalKeyValueStore>(
247            db_dir, "global",
248        )))
249    });
250
251impl GlobalKeyValueStore {
252    pub fn global() -> &'static Self {
253        &GLOBAL_KEY_VALUE_STORE
254    }
255
256    query! {
257        pub fn read_kvp(key: &str) -> Result<Option<String>> {
258            SELECT value FROM kv_store WHERE key = (?)
259        }
260    }
261
262    pub async fn write_kvp(&self, key: String, value: String) -> anyhow::Result<()> {
263        log::debug!("Writing global key-value pair for key {key}");
264        self.write_kvp_inner(key, value).await
265    }
266
267    query! {
268        async fn write_kvp_inner(key: String, value: String) -> Result<()> {
269            INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
270        }
271    }
272
273    query! {
274        pub async fn delete_kvp(key: String) -> Result<()> {
275            DELETE FROM kv_store WHERE key = (?)
276        }
277    }
278}