diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index fb7197548998d956c8c7853530189fab0f8f57eb..73aa2378a4d6cef52d21b773bb82780adc5b0ffe 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -1,6 +1,6 @@ +mod items; mod kvp; mod migrations; -mod serialized_item; use anyhow::Result; use migrations::MIGRATIONS; @@ -9,8 +9,8 @@ use rusqlite::Connection; use std::path::Path; use std::sync::Arc; +pub use items::*; pub use kvp::*; -pub use serialized_item::*; pub struct Db { connection: Mutex, @@ -46,39 +46,3 @@ impl Db { })) } } - -#[cfg(test)] -mod tests { - use super::*; - use tempdir::TempDir; - - #[gpui::test] - fn test_db() { - let dir = TempDir::new("db-test").unwrap(); - let fake_db = Db::open_in_memory().unwrap(); - let real_db = Db::open(&dir.path().join("test.db")).unwrap(); - - for db in [&real_db, &fake_db] { - assert_eq!(db.read_kvp("key-1").unwrap(), None); - - db.write_kvp("key-1", "one").unwrap(); - assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string())); - - db.write_kvp("key-2", "two").unwrap(); - assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string())); - - db.delete_kvp("key-1").unwrap(); - assert_eq!(db.read_kvp("key-1").unwrap(), None); - } - - drop(real_db); - - let real_db = Db::open(&dir.path().join("test.db")).unwrap(); - - real_db.write_kvp("key-1", "one").unwrap(); - assert_eq!(real_db.read_kvp("key-1").unwrap(), None); - - real_db.write_kvp("key-2", "two").unwrap(); - assert_eq!(real_db.read_kvp("key-2").unwrap(), Some("two".to_string())); - } -} diff --git a/crates/db/src/items.rs b/crates/db/src/items.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a49cd5569768fb2614342f9e910ec10abe7f357 --- /dev/null +++ b/crates/db/src/items.rs @@ -0,0 +1,236 @@ +use std::{ffi::OsStr, os::unix::prelude::OsStrExt, path::PathBuf, sync::Arc}; + +use anyhow::Result; +use rusqlite::{ + named_params, params, + types::{FromSql, FromSqlError, FromSqlResult, ValueRef}, +}; + +use super::Db; + +pub(crate) const ITEMS_M_1: &str = " +CREATE TABLE items( + id INTEGER PRIMARY KEY, + kind TEXT +) STRICT; +CREATE TABLE item_path( + item_id INTEGER PRIMARY KEY, + path BLOB +) STRICT; +CREATE TABLE item_query( + item_id INTEGER PRIMARY KEY, + query TEXT +) STRICT; +"; + +#[derive(PartialEq, Eq, Hash, Debug)] +pub enum SerializedItemKind { + Editor, + Terminal, + ProjectSearch, + Diagnostics, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SerializedItem { + Editor(usize, PathBuf), + Terminal(usize), + ProjectSearch(usize, String), + Diagnostics(usize), +} + +impl FromSql for SerializedItemKind { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Null => Err(FromSqlError::InvalidType), + ValueRef::Integer(_) => Err(FromSqlError::InvalidType), + ValueRef::Real(_) => Err(FromSqlError::InvalidType), + ValueRef::Text(bytes) => { + let str = std::str::from_utf8(bytes).map_err(|_| FromSqlError::InvalidType)?; + match str { + "Editor" => Ok(SerializedItemKind::Editor), + "Terminal" => Ok(SerializedItemKind::Terminal), + "ProjectSearch" => Ok(SerializedItemKind::ProjectSearch), + "Diagnostics" => Ok(SerializedItemKind::Diagnostics), + _ => Err(FromSqlError::InvalidType), + } + } + ValueRef::Blob(_) => Err(FromSqlError::InvalidType), + } + } +} + +impl SerializedItem { + fn kind(&self) -> SerializedItemKind { + match self { + SerializedItem::Editor(_, _) => SerializedItemKind::Editor, + SerializedItem::Terminal(_) => SerializedItemKind::Terminal, + SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch, + SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics, + } + } + + fn id(&self) -> usize { + match self { + SerializedItem::Editor(id, _) + | SerializedItem::Terminal(id) + | SerializedItem::ProjectSearch(id, _) + | SerializedItem::Diagnostics(id) => *id, + } + } +} + +impl Db { + fn write_item(&self, serialized_item: SerializedItem) -> Result<()> { + let mut lock = self.connection.lock(); + let tx = lock.transaction()?; + + // Serialize the item + let id = serialized_item.id(); + { + let kind = format!("{:?}", serialized_item.kind()); + + let mut stmt = + tx.prepare_cached("INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))")?; + + stmt.execute(params![id, kind])?; + } + + // Serialize item data + match &serialized_item { + SerializedItem::Editor(_, path) => { + let mut stmt = tx.prepare_cached( + "INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))", + )?; + + let path_bytes = path.as_os_str().as_bytes(); + stmt.execute(params![id, path_bytes])?; + } + SerializedItem::ProjectSearch(_, query) => { + let mut stmt = tx.prepare_cached( + "INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))", + )?; + + stmt.execute(params![id, query])?; + } + _ => {} + } + + tx.commit()?; + + Ok(()) + } + + fn delete_item(&self, item_id: usize) -> Result<()> { + let lock = self.connection.lock(); + + let mut stmt = lock.prepare_cached( + " + DELETE FROM items WHERE id = (:id); + DELETE FROM item_path WHERE id = (:id); + DELETE FROM item_query WHERE id = (:id); + ", + )?; + + stmt.execute(named_params! {":id": item_id})?; + + Ok(()) + } + + fn take_items(&self) -> Result> { + let mut lock = self.connection.lock(); + + let tx = lock.transaction()?; + + // When working with transactions in rusqlite, need to make this kind of scope + // To make the borrow stuff work correctly. Don't know why, rust is wild. + let result = { + let mut read_stmt = tx.prepare_cached( + " + SELECT items.id, items.kind, item_path.path, item_query.query + FROM items + LEFT JOIN item_path + ON items.id = item_path.item_id + LEFT JOIN item_query + ON items.id = item_query.item_id + ORDER BY items.id + ", + )?; + + let result = read_stmt + .query_map([], |row| { + let id: usize = row.get(0)?; + let kind: SerializedItemKind = row.get(1)?; + + match kind { + SerializedItemKind::Editor => { + let buf: Vec = row.get(2)?; + let path: PathBuf = OsStr::from_bytes(&buf).into(); + + Ok(SerializedItem::Editor(id, path)) + } + SerializedItemKind::Terminal => Ok(SerializedItem::Terminal(id)), + SerializedItemKind::ProjectSearch => { + let query: Arc = row.get(3)?; + Ok(SerializedItem::ProjectSearch(id, query.to_string())) + } + SerializedItemKind::Diagnostics => Ok(SerializedItem::Diagnostics(id)), + } + })? + .collect::, rusqlite::Error>>()?; + + let mut delete_stmt = tx.prepare_cached( + "DELETE FROM items; + DELETE FROM item_path; + DELETE FROM item_query;", + )?; + + delete_stmt.execute([])?; + + result + }; + + tx.commit()?; + + Ok(result) + } +} + +#[cfg(test)] +mod test { + use anyhow::Result; + + use super::*; + + #[test] + fn test_items_round_trip() -> Result<()> { + let db = Db::open_in_memory()?; + + let mut items = vec![ + SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")), + SerializedItem::Terminal(1), + SerializedItem::ProjectSearch(2, "Test query!".to_string()), + SerializedItem::Diagnostics(3), + ]; + + for item in items.iter() { + db.write_item(item.clone())?; + } + + assert_eq!(items, db.take_items()?); + + // Check that it's empty, as expected + assert_eq!(Vec::::new(), db.take_items()?); + + for item in items.iter() { + db.write_item(item.clone())?; + } + + items.remove(2); + db.delete_item(2)?; + + assert_eq!(items, db.take_items()?); + + Ok(()) + } +} diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index d23e6ae5b05900c7067f8c73b6cc2fc12f555877..ca1b15e45a1cdfb0e16573fc6d603b9be369f262 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -3,6 +3,13 @@ use rusqlite::OptionalExtension; use super::Db; +pub(crate) const KVP_M_1: &str = " +CREATE TABLE kv_store( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +) STRICT; +"; + impl Db { pub fn read_kvp(&self, key: &str) -> Result> { let lock = self.connection.lock(); @@ -14,7 +21,7 @@ impl Db { pub fn delete_kvp(&self, key: &str) -> Result<()> { let lock = self.connection.lock(); - let mut stmt = lock.prepare_cached("SELECT value FROM kv_store WHERE key = (?)")?; + let mut stmt = lock.prepare_cached("DELETE FROM kv_store WHERE key = (?)")?; stmt.execute([key])?; @@ -32,3 +39,31 @@ impl Db { Ok(()) } } + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use super::*; + + #[test] + fn test_kvp() -> Result<()> { + let db = Db::open_in_memory()?; + + assert_eq!(db.read_kvp("key-1")?, None); + + db.write_kvp("key-1", "one")?; + assert_eq!(db.read_kvp("key-1")?, Some("one".to_string())); + + db.write_kvp("key-1", "one-2")?; + assert_eq!(db.read_kvp("key-1")?, Some("one-2".to_string())); + + db.write_kvp("key-2", "two")?; + assert_eq!(db.read_kvp("key-2")?, Some("two".to_string())); + + db.delete_kvp("key-1")?; + assert_eq!(db.read_kvp("key-1")?, None); + + Ok(()) + } +} diff --git a/crates/db/src/migrations.rs b/crates/db/src/migrations.rs index 21db94e973a9adac5cbec0fca95f9cb0e23cab83..35943825369e515e9e548df92dfb2c0d39887fb9 100644 --- a/crates/db/src/migrations.rs +++ b/crates/db/src/migrations.rs @@ -1,10 +1,15 @@ use rusqlite_migration::{Migrations, M}; +use crate::items::ITEMS_M_1; +use crate::kvp::KVP_M_1; + +// This must be ordered by development time! Only ever add new migrations to the end!! +// Bad things will probably happen if you don't monotonically edit this vec!!!! +// And no re-ordering ever!!!!!!!!!! The results of these migrations are on the user's +// file system and so everything we do here is locked in _f_o_r_e_v_e_r_. lazy_static::lazy_static! { - pub static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![M::up( - "CREATE TABLE kv_store( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) STRICT;", - )]); + pub static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![ + M::up(KVP_M_1), + M::up(ITEMS_M_1), + ]); } diff --git a/crates/db/src/serialized_item.rs b/crates/db/src/serialized_item.rs deleted file mode 100644 index 9f6284fa07bdfdcc10e9e697c768bf1643b29c79..0000000000000000000000000000000000000000 --- a/crates/db/src/serialized_item.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; - -use super::Db; - -impl Db {} - -#[derive(PartialEq, Eq, Hash)] -pub enum SerializedItemKind { - Editor, - Terminal, - ProjectSearch, - Diagnostics, -} - -pub enum SerializedItem { - Editor(PathBuf, String), - Terminal, - ProjectSearch(String), - Diagnostics, -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 95ab8c496437061f03768e1591594bf27bff6beb..5f6a89a32581a43fb3a7e30512d4212ff61f06bd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -260,6 +260,7 @@ pub enum ItemEvent { UpdateTab, UpdateBreadcrumbs, Edit, + Serialize(SerializedItem), } pub trait Item: View {