Files
zed/crates/db/src/kvp.rs
T
ee3f40fe25 Re-add MultiWorkspace (#48800)
Release Notes:

- Added agent panel restoration. Now restarting your editor won't cause
your thread to be forgotten.

---------

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Cameron Mcloughlin <cameron.studdstreet@gmail.com>
2026-02-12 01:06:23 +00:00

257 lines
7.9 KiB
Rust

use anyhow::Context as _;
use gpui::App;
use sqlez_macros::sql;
use util::ResultExt as _;
use crate::{
query,
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
write_and_log,
};
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
impl Domain for KeyValueStore {
const NAME: &str = stringify!(KeyValueStore);
const MIGRATIONS: &[&str] = &[
sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
),
sql!(
CREATE TABLE IF NOT EXISTS scoped_kv_store(
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY(namespace, key)
) STRICT;
),
];
}
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
pub trait Dismissable {
const KEY: &'static str;
fn dismissed() -> bool {
KEY_VALUE_STORE
.read_kvp(Self::KEY)
.log_err()
.is_some_and(|s| s.is_some())
}
fn set_dismissed(is_dismissed: bool, cx: &mut App) {
write_and_log(cx, move || async move {
if is_dismissed {
KEY_VALUE_STORE
.write_kvp(Self::KEY.into(), "1".into())
.await
} else {
KEY_VALUE_STORE.delete_kvp(Self::KEY.into()).await
}
})
}
}
impl KeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {
SELECT value FROM kv_store WHERE key = (?)
}
}
pub async fn write_kvp(&self, key: String, value: String) -> anyhow::Result<()> {
log::debug!("Writing key-value pair for key {key}");
self.write_kvp_inner(key, value).await
}
query! {
async fn write_kvp_inner(key: String, value: String) -> Result<()> {
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
}
}
query! {
pub async fn delete_kvp(key: String) -> Result<()> {
DELETE FROM kv_store WHERE key = (?)
}
}
pub fn scoped<'a>(&'a self, namespace: &'a str) -> ScopedKeyValueStore<'a> {
ScopedKeyValueStore {
store: self,
namespace,
}
}
}
pub struct ScopedKeyValueStore<'a> {
store: &'a KeyValueStore,
namespace: &'a str,
}
impl ScopedKeyValueStore<'_> {
pub fn read(&self, key: &str) -> anyhow::Result<Option<String>> {
self.store.select_row_bound::<(&str, &str), String>(
"SELECT value FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
)?((self.namespace, key))
.context("Failed to read from scoped_kv_store")
}
pub async fn write(&self, key: String, value: String) -> anyhow::Result<()> {
let namespace = self.namespace.to_owned();
self.store
.write(move |connection| {
connection.exec_bound::<(&str, &str, &str)>(
"INSERT OR REPLACE INTO scoped_kv_store(namespace, key, value) VALUES ((?), (?), (?))",
)?((&namespace, &key, &value))
.context("Failed to write to scoped_kv_store")
})
.await
}
pub async fn delete(&self, key: String) -> anyhow::Result<()> {
let namespace = self.namespace.to_owned();
self.store
.write(move |connection| {
connection.exec_bound::<(&str, &str)>(
"DELETE FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
)?((&namespace, &key))
.context("Failed to delete from scoped_kv_store")
})
.await
}
pub async fn delete_all(&self) -> anyhow::Result<()> {
let namespace = self.namespace.to_owned();
self.store
.write(move |connection| {
connection
.exec_bound::<&str>("DELETE FROM scoped_kv_store WHERE namespace = (?)")?(
&namespace,
)
.context("Failed to delete_all from scoped_kv_store")
})
.await
}
}
#[cfg(test)]
mod tests {
use crate::kvp::KeyValueStore;
#[gpui::test]
async fn test_kvp() {
let db = KeyValueStore::open_test_db("test_kvp").await;
assert_eq!(db.read_kvp("key-1").unwrap(), None);
db.write_kvp("key-1".to_string(), "one".to_string())
.await
.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
db.write_kvp("key-1".to_string(), "one-2".to_string())
.await
.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
db.write_kvp("key-2".to_string(), "two".to_string())
.await
.unwrap();
assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
db.delete_kvp("key-1".to_string()).await.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), None);
}
#[gpui::test]
async fn test_scoped_kvp() {
let db = KeyValueStore::open_test_db("test_scoped_kvp").await;
let scope_a = db.scoped("namespace-a");
let scope_b = db.scoped("namespace-b");
// Reading a missing key returns None
assert_eq!(scope_a.read("key-1").unwrap(), None);
// Writing and reading back a key works
scope_a
.write("key-1".to_string(), "value-a1".to_string())
.await
.unwrap();
assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
// Two namespaces with the same key don't collide
scope_b
.write("key-1".to_string(), "value-b1".to_string())
.await
.unwrap();
assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
// delete removes a single key without affecting others in the namespace
scope_a
.write("key-2".to_string(), "value-a2".to_string())
.await
.unwrap();
scope_a.delete("key-1".to_string()).await.unwrap();
assert_eq!(scope_a.read("key-1").unwrap(), None);
assert_eq!(scope_a.read("key-2").unwrap(), Some("value-a2".to_string()));
assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
// delete_all removes all keys in a namespace without affecting other namespaces
scope_a
.write("key-3".to_string(), "value-a3".to_string())
.await
.unwrap();
scope_a.delete_all().await.unwrap();
assert_eq!(scope_a.read("key-2").unwrap(), None);
assert_eq!(scope_a.read("key-3").unwrap(), None);
assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
}
}
pub struct GlobalKeyValueStore(ThreadSafeConnection);
impl Domain for GlobalKeyValueStore {
const NAME: &str = stringify!(GlobalKeyValueStore);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
}
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
impl GlobalKeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {
SELECT value FROM kv_store WHERE key = (?)
}
}
pub async fn write_kvp(&self, key: String, value: String) -> anyhow::Result<()> {
log::debug!("Writing global key-value pair for key {key}");
self.write_kvp_inner(key, value).await
}
query! {
async fn write_kvp_inner(key: String, value: String) -> Result<()> {
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
}
}
query! {
pub async fn delete_kvp(key: String) -> Result<()> {
DELETE FROM kv_store WHERE key = (?)
}
}
}