working items schema

K Simmons created

Change summary

crates/db/src/db.rs               |  40 -----
crates/db/src/items.rs            | 236 +++++++++++++++++++++++++++++++++
crates/db/src/kvp.rs              |  37 +++++
crates/db/src/migrations.rs       |  17 +
crates/db/src/serialized_item.rs  |  22 ---
crates/workspace/src/workspace.rs |   1 
6 files changed, 286 insertions(+), 67 deletions(-)

Detailed changes

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<Connection>,
@@ -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()));
-    }
-}

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<Self> {
+        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<Vec<SerializedItem>> {
+        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<u8> = 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<str> = row.get(3)?;
+                            Ok(SerializedItem::ProjectSearch(id, query.to_string()))
+                        }
+                        SerializedItemKind::Diagnostics => Ok(SerializedItem::Diagnostics(id)),
+                    }
+                })?
+                .collect::<Result<Vec<SerializedItem>, 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::<SerializedItem>::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(())
+    }
+}

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<Option<String>> {
         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(())
+    }
+}

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),
+    ]);
 }

crates/db/src/serialized_item.rs 🔗

@@ -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,
-}