Done first draft of strongly typed migrations

Mikayla Maki created

Change summary

crates/auto_update/src/auto_update.rs      |   8 
crates/client/src/client.rs                |   4 
crates/client/src/telemetry.rs             |   4 
crates/db/examples/serialize-pane.rs       |  46 ---
crates/db/examples/serialize_workspace.rs  |  30 --
crates/db/src/db.rs                        |  46 +--
crates/db/src/kvp.rs                       |  13 
crates/db/src/workspace.rs                 | 294 +++++++++++++++++++++++
crates/db/src/workspace/items.rs           |  63 -----
crates/db/src/workspace/pane.rs            | 232 ------------------
crates/project/src/project.rs              |   2 
crates/sqlez/src/domain.rs                 |  39 +++
crates/sqlez/src/lib.rs                    |   1 
crates/sqlez/src/savepoint.rs              |   2 
crates/sqlez/src/thread_safe_connection.rs |  45 +-
crates/workspace/src/pane.rs               |   2 
crates/workspace/src/workspace.rs          |   5 
crates/zed/src/main.rs                     |   6 
18 files changed, 395 insertions(+), 447 deletions(-)

Detailed changes

crates/auto_update/src/auto_update.rs 🔗

@@ -2,7 +2,7 @@ mod update_notification;
 
 use anyhow::{anyhow, Context, Result};
 use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
-use db::Db;
+use db::{kvp::KeyValue, Db};
 use gpui::{
     actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MutableAppContext, Task, WeakViewHandle,
@@ -42,7 +42,7 @@ pub struct AutoUpdater {
     current_version: AppVersion,
     http_client: Arc<dyn HttpClient>,
     pending_poll: Option<Task<()>>,
-    db: project::Db,
+    db: project::Db<KeyValue>,
     server_url: String,
 }
 
@@ -57,7 +57,7 @@ impl Entity for AutoUpdater {
 }
 
 pub fn init(
-    db: Db,
+    db: Db<KeyValue>,
     http_client: Arc<dyn HttpClient>,
     server_url: String,
     cx: &mut MutableAppContext,
@@ -126,7 +126,7 @@ impl AutoUpdater {
 
     fn new(
         current_version: AppVersion,
-        db: project::Db,
+        db: project::Db<KeyValue>,
         http_client: Arc<dyn HttpClient>,
         server_url: String,
     ) -> Self {

crates/client/src/client.rs 🔗

@@ -11,7 +11,7 @@ use async_tungstenite::tungstenite::{
     error::Error as WebsocketError,
     http::{Request, StatusCode},
 };
-use db::Db;
+use db::{kvp::KeyValue, Db};
 use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
 use gpui::{
     actions,
@@ -1218,7 +1218,7 @@ impl Client {
         self.peer.respond_with_error(receipt, error)
     }
 
-    pub fn start_telemetry(&self, db: Db) {
+    pub fn start_telemetry(&self, db: Db<KeyValue>) {
         self.telemetry.start(db.clone());
     }
 

crates/client/src/telemetry.rs 🔗

@@ -1,5 +1,5 @@
 use crate::http::HttpClient;
-use db::Db;
+use db::{kvp::KeyValue, Db};
 use gpui::{
     executor::Background,
     serde_json::{self, value::Map, Value},
@@ -148,7 +148,7 @@ impl Telemetry {
         Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
     }
 
-    pub fn start(self: &Arc<Self>, db: Db) {
+    pub fn start(self: &Arc<Self>, db: Db<KeyValue>) {
         let this = self.clone();
         self.executor
             .spawn(

crates/db/examples/serialize-pane.rs 🔗

@@ -1,46 +0,0 @@
-use std::{fs::File, path::Path};
-
-const TEST_FILE: &'static str = "test-db.db";
-
-fn main() -> anyhow::Result<()> {
-    env_logger::init();
-
-    let db = db::Db::open_in_memory("db");
-
-    let file = Path::new(TEST_FILE);
-
-    let f = File::create(file)?;
-    drop(f);
-
-    // let workspace_1 = db.workspace_for_roots(&["/tmp"]);
-    // let workspace_2 = db.workspace_for_roots(&["/tmp", "/tmp2"]);
-    // let workspace_3 = db.workspace_for_roots(&["/tmp3", "/tmp2"]);
-
-    // db.save_dock_pane(
-    //     &workspace_1.workspace_id,
-    //     &SerializedDockPane {
-    //         anchor_position: DockAnchor::Expanded,
-    //         visible: true,
-    //     },
-    // );
-    // db.save_dock_pane(
-    //     &workspace_2.workspace_id,
-    //     &SerializedDockPane {
-    //         anchor_position: DockAnchor::Bottom,
-    //         visible: true,
-    //     },
-    // );
-    // db.save_dock_pane(
-    //     &workspace_3.workspace_id,
-    //     &SerializedDockPane {
-    //         anchor_position: DockAnchor::Right,
-    //         visible: false,
-    //     },
-    // );
-
-    db.write_to(file).ok();
-
-    println!("Wrote database!");
-
-    Ok(())
-}

crates/db/examples/serialize_workspace.rs 🔗

@@ -1,30 +0,0 @@
-use std::{fs::File, path::Path};
-
-const TEST_FILE: &'static str = "test-db.db";
-
-fn main() -> anyhow::Result<()> {
-    env_logger::init();
-    let db = db::Db::open_in_memory("db");
-
-    let file = Path::new(TEST_FILE);
-
-    let f = File::create(file)?;
-    drop(f);
-
-    db.write_kvp("test", "1")?;
-    db.write_kvp("test-2", "2")?;
-
-    db.workspace_for_roots(&["/tmp1"]);
-    db.workspace_for_roots(&["/tmp1", "/tmp2"]);
-    db.workspace_for_roots(&["/tmp1", "/tmp2", "/tmp3"]);
-    db.workspace_for_roots(&["/tmp2", "/tmp3"]);
-    db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]);
-    db.workspace_for_roots(&["/tmp2", "/tmp4"]);
-    db.workspace_for_roots(&["/tmp2"]);
-
-    db.write_to(file).ok();
-
-    println!("Wrote database!");
-
-    Ok(())
-}

crates/db/src/db.rs 🔗

@@ -7,18 +7,23 @@ use std::path::Path;
 
 use anyhow::Result;
 use indoc::indoc;
-use kvp::KVP_MIGRATION;
 use sqlez::connection::Connection;
+use sqlez::domain::Domain;
 use sqlez::thread_safe_connection::ThreadSafeConnection;
-use workspace::items::ITEM_MIGRATIONS;
-use workspace::pane::PANE_MIGRATIONS;
 
 pub use workspace::*;
 
+const INITIALIZE_QUERY: &'static str = indoc! {"
+    PRAGMA journal_mode=WAL;
+    PRAGMA synchronous=NORMAL;
+    PRAGMA foreign_keys=TRUE;
+    PRAGMA case_sensitive_like=TRUE;
+"};
+
 #[derive(Clone)]
-pub struct Db(ThreadSafeConnection);
+pub struct Db<D: Domain>(ThreadSafeConnection<D>);
 
-impl Deref for Db {
+impl<D: Domain> Deref for Db<D> {
     type Target = sqlez::connection::Connection;
 
     fn deref(&self) -> &Self::Target {
@@ -26,7 +31,7 @@ impl Deref for Db {
     }
 }
 
-impl Db {
+impl<D: Domain> Db<D> {
     /// Open or create a database at the given directory path.
     pub fn open(db_dir: &Path, channel: &'static str) -> Self {
         // Use 0 for now. Will implement incrementing and clearing of old db files soon TM
@@ -35,17 +40,15 @@ impl Db {
             .expect("Should be able to create the database directory");
         let db_path = current_db_dir.join(Path::new("db.sqlite"));
 
-        Db(initialize_connection(ThreadSafeConnection::new(
-            db_path.to_string_lossy().as_ref(),
-            true,
-        )))
+        Db(
+            ThreadSafeConnection::new(db_path.to_string_lossy().as_ref(), true)
+                .with_initialize_query(INITIALIZE_QUERY),
+        )
     }
 
     /// Open a in memory database for testing and as a fallback.
     pub fn open_in_memory(db_name: &str) -> Self {
-        Db(initialize_connection(ThreadSafeConnection::new(
-            db_name, false,
-        )))
+        Db(ThreadSafeConnection::new(db_name, false).with_initialize_query(INITIALIZE_QUERY))
     }
 
     pub fn persisting(&self) -> bool {
@@ -56,19 +59,8 @@ impl Db {
         let destination = Connection::open_file(dest.as_ref().to_string_lossy().as_ref());
         self.backup_main(&destination)
     }
-}
 
-fn initialize_connection(conn: ThreadSafeConnection) -> ThreadSafeConnection {
-    conn.with_initialize_query(indoc! {"
-        PRAGMA journal_mode=WAL;
-        PRAGMA synchronous=NORMAL;
-        PRAGMA foreign_keys=TRUE;
-        PRAGMA case_sensitive_like=TRUE;
-        "})
-        .with_migrations(&[
-            KVP_MIGRATION,
-            WORKSPACES_MIGRATION,
-            PANE_MIGRATIONS,
-            ITEM_MIGRATIONS,
-        ])
+    pub fn open_as<D2: Domain>(&self) -> Db<D2> {
+        Db(self.0.for_domain())
+    }
 }

crates/db/src/kvp.rs 🔗

@@ -1,7 +1,7 @@
 use super::Db;
 use anyhow::Result;
 use indoc::indoc;
-use sqlez::migrations::Migration;
+use sqlez::{connection::Connection, domain::Domain, migrations::Migration};
 
 pub(crate) const KVP_MIGRATION: Migration = Migration::new(
     "kvp",
@@ -13,7 +13,16 @@ pub(crate) const KVP_MIGRATION: Migration = Migration::new(
     "}],
 );
 
-impl Db {
+#[derive(Clone)]
+pub enum KeyValue {}
+
+impl Domain for KeyValue {
+    fn migrate(conn: &Connection) -> anyhow::Result<()> {
+        KVP_MIGRATION.run(conn)
+    }
+}
+
+impl Db<KeyValue> {
     pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
         self.select_row_bound("SELECT value FROM kv_store WHERE key = (?)")?(key)
     }

crates/db/src/workspace.rs 🔗

@@ -1,14 +1,24 @@
-pub(crate) mod items;
 pub mod model;
-pub(crate) mod pane;
 
-use anyhow::Context;
-use util::{iife, ResultExt};
+use anyhow::{bail, Context, Result};
+use util::{iife, unzip_option, ResultExt};
 
 use std::path::{Path, PathBuf};
 
 use indoc::indoc;
-use sqlez::migrations::Migration;
+use sqlez::{domain::Domain, migrations::Migration};
+
+use self::model::{
+    Axis, GroupId, PaneId, SerializedItem, SerializedItemKind, SerializedPane, SerializedPaneGroup,
+    SerializedWorkspace, WorkspaceId,
+};
+
+use super::Db;
+
+// 1) Move all of this into Workspace crate
+// 2) Deserialize items fully
+// 3) Typed prepares (including how you expect to pull data out)
+// 4) Investigate Tree column impls
 
 pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
     "workspace",
@@ -22,11 +32,58 @@ pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
     "}],
 );
 
-use self::model::{SerializedWorkspace, WorkspaceId};
+pub(crate) const PANE_MIGRATIONS: Migration = Migration::new(
+    "pane",
+    &[indoc! {"
+        CREATE TABLE pane_groups(
+            group_id INTEGER PRIMARY KEY,
+            workspace_id BLOB NOT NULL,
+            parent_group_id INTEGER, -- NULL indicates that this is a root node
+            position INTEGER, -- NULL indicates that this is a root node
+            axis TEXT NOT NULL, -- Enum:  'Vertical' / 'Horizontal'
+            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
+            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
+        ) STRICT;
+        
+        CREATE TABLE panes(
+            pane_id INTEGER PRIMARY KEY,
+            workspace_id BLOB NOT NULL,
+            parent_group_id INTEGER, -- NULL, this is a dock pane
+            position INTEGER, -- NULL, this is a dock pane
+            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
+            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
+        ) STRICT;
+    "}],
+);
+
+pub(crate) const ITEM_MIGRATIONS: Migration = Migration::new(
+    "item",
+    &[indoc! {"
+        CREATE TABLE items(
+            item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
+            workspace_id BLOB NOT NULL,
+            pane_id INTEGER NOT NULL,
+            kind TEXT NOT NULL,
+            position INTEGER NOT NULL,
+            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
+            FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE
+            PRIMARY KEY(item_id, workspace_id)
+        ) STRICT;
+    "}],
+);
 
-use super::Db;
+#[derive(Clone)]
+pub enum Workspace {}
+
+impl Domain for Workspace {
+    fn migrate(conn: &sqlez::connection::Connection) -> anyhow::Result<()> {
+        WORKSPACES_MIGRATION.run(&conn)?;
+        PANE_MIGRATIONS.run(&conn)?;
+        ITEM_MIGRATIONS.run(&conn)
+    }
+}
 
-impl Db {
+impl Db<Workspace> {
     /// Returns a serialized workspace for the given worktree_roots. If the passed array
     /// is empty, the most recent workspace is returned instead. If no workspace for the
     /// passed roots is stored, returns none.
@@ -129,6 +186,142 @@ impl Db {
         .log_err()
         .unwrap_or_default()
     }
+
+    pub(crate) fn get_center_pane_group(
+        &self,
+        workspace_id: &WorkspaceId,
+    ) -> Result<SerializedPaneGroup> {
+        self.get_pane_group_children(workspace_id, None)?
+            .into_iter()
+            .next()
+            .context("No center pane group")
+    }
+
+    fn get_pane_group_children<'a>(
+        &self,
+        workspace_id: &WorkspaceId,
+        group_id: Option<GroupId>,
+    ) -> Result<Vec<SerializedPaneGroup>> {
+        self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
+            SELECT group_id, axis, pane_id
+                FROM (SELECT group_id, axis, NULL as pane_id, position,  parent_group_id, workspace_id
+                FROM pane_groups
+                  UNION
+                SELECT NULL, NULL,  pane_id,  position,  parent_group_id, workspace_id
+                FROM panes
+                -- Remove the dock panes from the union
+                WHERE parent_group_id IS NOT NULL and position IS NOT NULL) 
+            WHERE parent_group_id IS ? AND workspace_id = ?
+            ORDER BY position
+            "})?((group_id, workspace_id))?
+            .into_iter()
+            .map(|(group_id, axis, pane_id)| {
+                if let Some((group_id, axis)) = group_id.zip(axis) {
+                    Ok(SerializedPaneGroup::Group {
+                        axis,
+                        children: self.get_pane_group_children(
+                            workspace_id,
+                            Some(group_id),
+                        )?,
+                    })
+                } else if let Some(pane_id) = pane_id {
+                    Ok(SerializedPaneGroup::Pane(SerializedPane {
+                        children: self.get_items(pane_id)?,
+                    }))
+                } else {
+                    bail!("Pane Group Child was neither a pane group or a pane");
+                }
+            })
+            .collect::<Result<_>>()
+    }
+
+    pub(crate) fn save_pane_group(
+        &self,
+        workspace_id: &WorkspaceId,
+        pane_group: &SerializedPaneGroup,
+        parent: Option<(GroupId, usize)>,
+    ) -> Result<()> {
+        if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) {
+            bail!("Pane groups must have a SerializedPaneGroup::Group at the root")
+        }
+
+        let (parent_id, position) = unzip_option(parent);
+
+        match pane_group {
+            SerializedPaneGroup::Group { axis, children } => {
+                let parent_id = self.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")?
+                    ((workspace_id, parent_id, position, *axis))?;
+
+                for (position, group) in children.iter().enumerate() {
+                    self.save_pane_group(workspace_id, group, Some((parent_id, position)))?
+                }
+                Ok(())
+            }
+            SerializedPaneGroup::Pane(pane) => self.save_pane(workspace_id, pane, parent),
+        }
+    }
+
+    pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
+        let pane_id = self.select_row_bound(indoc! {"
+                SELECT pane_id FROM panes 
+                WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
+            workspace_id,
+        )?
+        .context("No dock pane for workspace")?;
+
+        Ok(SerializedPane::new(
+            self.get_items(pane_id).context("Reading items")?,
+        ))
+    }
+
+    pub(crate) fn save_pane(
+        &self,
+        workspace_id: &WorkspaceId,
+        pane: &SerializedPane,
+        parent: Option<(GroupId, usize)>,
+    ) -> Result<()> {
+        let (parent_id, order) = unzip_option(parent);
+
+        let pane_id = self.insert_bound(
+            "INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)",
+        )?((workspace_id, parent_id, order))?;
+
+        self.save_items(workspace_id, pane_id, &pane.children)
+            .context("Saving items")
+    }
+
+    pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
+        Ok(self.select_bound(indoc! {"
+                SELECT item_id, kind FROM items
+                WHERE pane_id = ?
+                ORDER BY position"})?(pane_id)?
+        .into_iter()
+        .map(|(item_id, kind)| match kind {
+            SerializedItemKind::Terminal => SerializedItem::Terminal { item_id },
+            _ => unimplemented!(),
+        })
+        .collect())
+    }
+
+    pub(crate) fn save_items(
+        &self,
+        workspace_id: &WorkspaceId,
+        pane_id: PaneId,
+        items: &[SerializedItem],
+    ) -> Result<()> {
+        let mut delete_old = self
+            .exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?")
+            .context("Preparing deletion")?;
+        let mut insert_new = self.exec_bound(
+            "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)",
+        ).context("Preparing insertion")?;
+        for (position, item) in items.iter().enumerate() {
+            delete_old((workspace_id, pane_id, item.item_id()))?;
+            insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?;
+        }
+
+        Ok(())
+    }
 }
 
 #[cfg(test)]
@@ -214,4 +407,89 @@ mod tests {
             workspace_3
         );
     }
+
+    use crate::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
+
+    fn default_workspace(
+        dock_pane: SerializedPane,
+        center_group: &SerializedPaneGroup,
+    ) -> SerializedWorkspace {
+        SerializedWorkspace {
+            dock_anchor: crate::model::DockAnchor::Right,
+            dock_visible: false,
+            center_group: center_group.clone(),
+            dock_pane,
+        }
+    }
+
+    #[test]
+    fn test_basic_dock_pane() {
+        env_logger::try_init().ok();
+
+        let db = Db::open_in_memory("basic_dock_pane");
+
+        let dock_pane = crate::model::SerializedPane {
+            children: vec![
+                SerializedItem::Terminal { item_id: 1 },
+                SerializedItem::Terminal { item_id: 4 },
+                SerializedItem::Terminal { item_id: 2 },
+                SerializedItem::Terminal { item_id: 3 },
+            ],
+        };
+
+        let workspace = default_workspace(dock_pane, &Default::default());
+
+        db.save_workspace(&["/tmp"], None, &workspace);
+
+        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
+
+        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
+    }
+
+    #[test]
+    fn test_simple_split() {
+        env_logger::try_init().ok();
+
+        let db = Db::open_in_memory("simple_split");
+
+        //  -----------------
+        //  | 1,2   | 5,6   |
+        //  | - - - |       |
+        //  | 3,4   |       |
+        //  -----------------
+        let center_pane = SerializedPaneGroup::Group {
+            axis: crate::model::Axis::Horizontal,
+            children: vec![
+                SerializedPaneGroup::Group {
+                    axis: crate::model::Axis::Vertical,
+                    children: vec![
+                        SerializedPaneGroup::Pane(SerializedPane {
+                            children: vec![
+                                SerializedItem::Terminal { item_id: 1 },
+                                SerializedItem::Terminal { item_id: 2 },
+                            ],
+                        }),
+                        SerializedPaneGroup::Pane(SerializedPane {
+                            children: vec![
+                                SerializedItem::Terminal { item_id: 4 },
+                                SerializedItem::Terminal { item_id: 3 },
+                            ],
+                        }),
+                    ],
+                },
+                SerializedPaneGroup::Pane(SerializedPane {
+                    children: vec![
+                        SerializedItem::Terminal { item_id: 5 },
+                        SerializedItem::Terminal { item_id: 6 },
+                    ],
+                }),
+            ],
+        };
+
+        let workspace = default_workspace(Default::default(), &center_pane);
+
+        db.save_workspace(&["/tmp"], None, &workspace);
+
+        assert_eq!(workspace.center_group, center_pane);
+    }
 }

crates/db/src/workspace/items.rs 🔗

@@ -1,63 +0,0 @@
-use anyhow::{Context, Result};
-use indoc::indoc;
-use sqlez::migrations::Migration;
-
-use crate::{
-    model::{PaneId, SerializedItem, SerializedItemKind, WorkspaceId},
-    Db,
-};
-
-// 1) Move all of this into Workspace crate
-// 2) Deserialize items fully
-// 3) Typed prepares (including how you expect to pull data out)
-// 4) Investigate Tree column impls
-pub(crate) const ITEM_MIGRATIONS: Migration = Migration::new(
-    "item",
-    &[indoc! {"
-        CREATE TABLE items(
-            item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
-            workspace_id BLOB NOT NULL,
-            pane_id INTEGER NOT NULL,
-            kind TEXT NOT NULL,
-            position INTEGER NOT NULL,
-            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
-            FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE
-            PRIMARY KEY(item_id, workspace_id)
-        ) STRICT;
-    "}],
-);
-
-impl Db {
-    pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
-        Ok(self.select_bound(indoc! {"
-                SELECT item_id, kind FROM items
-                WHERE pane_id = ?
-                ORDER BY position"})?(pane_id)?
-        .into_iter()
-        .map(|(item_id, kind)| match kind {
-            SerializedItemKind::Terminal => SerializedItem::Terminal { item_id },
-            _ => unimplemented!(),
-        })
-        .collect())
-    }
-
-    pub(crate) fn save_items(
-        &self,
-        workspace_id: &WorkspaceId,
-        pane_id: PaneId,
-        items: &[SerializedItem],
-    ) -> Result<()> {
-        let mut delete_old = self
-            .exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?")
-            .context("Preparing deletion")?;
-        let mut insert_new = self.exec_bound(
-            "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)",
-        ).context("Preparing insertion")?;
-        for (position, item) in items.iter().enumerate() {
-            delete_old((workspace_id, pane_id, item.item_id()))?;
-            insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?;
-        }
-
-        Ok(())
-    }
-}

crates/db/src/workspace/pane.rs 🔗

@@ -1,232 +0,0 @@
-use anyhow::{bail, Context, Result};
-use indoc::indoc;
-use sqlez::migrations::Migration;
-use util::unzip_option;
-
-use crate::model::{Axis, GroupId, PaneId, SerializedPane};
-
-use super::{
-    model::{SerializedPaneGroup, WorkspaceId},
-    Db,
-};
-
-pub(crate) const PANE_MIGRATIONS: Migration = Migration::new(
-    "pane",
-    &[indoc! {"
-        CREATE TABLE pane_groups(
-            group_id INTEGER PRIMARY KEY,
-            workspace_id BLOB NOT NULL,
-            parent_group_id INTEGER, -- NULL indicates that this is a root node
-            position INTEGER, -- NULL indicates that this is a root node
-            axis TEXT NOT NULL, -- Enum:  'Vertical' / 'Horizontal'
-            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
-            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
-        ) STRICT;
-        
-        CREATE TABLE panes(
-            pane_id INTEGER PRIMARY KEY,
-            workspace_id BLOB NOT NULL,
-            parent_group_id INTEGER, -- NULL, this is a dock pane
-            position INTEGER, -- NULL, this is a dock pane
-            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
-            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
-        ) STRICT;
-    "}],
-);
-
-impl Db {
-    pub(crate) fn get_center_pane_group(
-        &self,
-        workspace_id: &WorkspaceId,
-    ) -> Result<SerializedPaneGroup> {
-        self.get_pane_group_children(workspace_id, None)?
-            .into_iter()
-            .next()
-            .context("No center pane group")
-    }
-
-    fn get_pane_group_children<'a>(
-        &self,
-        workspace_id: &WorkspaceId,
-        group_id: Option<GroupId>,
-    ) -> Result<Vec<SerializedPaneGroup>> {
-        self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
-            SELECT group_id, axis, pane_id
-                FROM (SELECT group_id, axis, NULL as pane_id, position,  parent_group_id, workspace_id
-                FROM pane_groups
-                  UNION
-                SELECT NULL, NULL,  pane_id,  position,  parent_group_id, workspace_id
-                FROM panes
-                -- Remove the dock panes from the union
-                WHERE parent_group_id IS NOT NULL and position IS NOT NULL) 
-            WHERE parent_group_id IS ? AND workspace_id = ?
-            ORDER BY position
-            "})?((group_id, workspace_id))?
-            .into_iter()
-            .map(|(group_id, axis, pane_id)| {
-                if let Some((group_id, axis)) = group_id.zip(axis) {
-                    Ok(SerializedPaneGroup::Group {
-                        axis,
-                        children: self.get_pane_group_children(
-                            workspace_id,
-                            Some(group_id),
-                        )?,
-                    })
-                } else if let Some(pane_id) = pane_id {
-                    Ok(SerializedPaneGroup::Pane(SerializedPane {
-                        children: self.get_items(pane_id)?,
-                    }))
-                } else {
-                    bail!("Pane Group Child was neither a pane group or a pane");
-                }
-            })
-            .collect::<Result<_>>()
-    }
-
-    pub(crate) fn save_pane_group(
-        &self,
-        workspace_id: &WorkspaceId,
-        pane_group: &SerializedPaneGroup,
-        parent: Option<(GroupId, usize)>,
-    ) -> Result<()> {
-        if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) {
-            bail!("Pane groups must have a SerializedPaneGroup::Group at the root")
-        }
-
-        let (parent_id, position) = unzip_option(parent);
-
-        match pane_group {
-            SerializedPaneGroup::Group { axis, children } => {
-                let parent_id = self.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")?
-                    ((workspace_id, parent_id, position, *axis))?;
-
-                for (position, group) in children.iter().enumerate() {
-                    self.save_pane_group(workspace_id, group, Some((parent_id, position)))?
-                }
-                Ok(())
-            }
-            SerializedPaneGroup::Pane(pane) => self.save_pane(workspace_id, pane, parent),
-        }
-    }
-
-    pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
-        let pane_id = self.select_row_bound(indoc! {"
-                SELECT pane_id FROM panes 
-                WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
-            workspace_id,
-        )?
-        .context("No dock pane for workspace")?;
-
-        Ok(SerializedPane::new(
-            self.get_items(pane_id).context("Reading items")?,
-        ))
-    }
-
-    pub(crate) fn save_pane(
-        &self,
-        workspace_id: &WorkspaceId,
-        pane: &SerializedPane,
-        parent: Option<(GroupId, usize)>,
-    ) -> Result<()> {
-        let (parent_id, order) = unzip_option(parent);
-
-        let pane_id = self.insert_bound(
-            "INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)",
-        )?((workspace_id, parent_id, order))?;
-
-        self.save_items(workspace_id, pane_id, &pane.children)
-            .context("Saving items")
-    }
-}
-
-#[cfg(test)]
-mod tests {
-
-    use crate::{
-        model::{SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace},
-        Db,
-    };
-
-    fn default_workspace(
-        dock_pane: SerializedPane,
-        center_group: &SerializedPaneGroup,
-    ) -> SerializedWorkspace {
-        SerializedWorkspace {
-            dock_anchor: crate::model::DockAnchor::Right,
-            dock_visible: false,
-            center_group: center_group.clone(),
-            dock_pane,
-        }
-    }
-
-    #[test]
-    fn test_basic_dock_pane() {
-        env_logger::try_init().ok();
-
-        let db = Db::open_in_memory("basic_dock_pane");
-
-        let dock_pane = crate::model::SerializedPane {
-            children: vec![
-                SerializedItem::Terminal { item_id: 1 },
-                SerializedItem::Terminal { item_id: 4 },
-                SerializedItem::Terminal { item_id: 2 },
-                SerializedItem::Terminal { item_id: 3 },
-            ],
-        };
-
-        let workspace = default_workspace(dock_pane, &Default::default());
-
-        db.save_workspace(&["/tmp"], None, &workspace);
-
-        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
-
-        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
-    }
-
-    #[test]
-    fn test_simple_split() {
-        env_logger::try_init().ok();
-
-        let db = Db::open_in_memory("simple_split");
-
-        //  -----------------
-        //  | 1,2   | 5,6   |
-        //  | - - - |       |
-        //  | 3,4   |       |
-        //  -----------------
-        let center_pane = SerializedPaneGroup::Group {
-            axis: crate::model::Axis::Horizontal,
-            children: vec![
-                SerializedPaneGroup::Group {
-                    axis: crate::model::Axis::Vertical,
-                    children: vec![
-                        SerializedPaneGroup::Pane(SerializedPane {
-                            children: vec![
-                                SerializedItem::Terminal { item_id: 1 },
-                                SerializedItem::Terminal { item_id: 2 },
-                            ],
-                        }),
-                        SerializedPaneGroup::Pane(SerializedPane {
-                            children: vec![
-                                SerializedItem::Terminal { item_id: 4 },
-                                SerializedItem::Terminal { item_id: 3 },
-                            ],
-                        }),
-                    ],
-                },
-                SerializedPaneGroup::Pane(SerializedPane {
-                    children: vec![
-                        SerializedItem::Terminal { item_id: 5 },
-                        SerializedItem::Terminal { item_id: 6 },
-                    ],
-                }),
-            ],
-        };
-
-        let workspace = default_workspace(Default::default(), &center_pane);
-
-        db.save_workspace(&["/tmp"], None, &workspace);
-
-        assert_eq!(workspace.center_group, center_pane);
-    }
-}

crates/project/src/project.rs 🔗

@@ -63,7 +63,7 @@ use std::{
 use thiserror::Error;
 use util::{defer, post_inc, ResultExt, TryFutureExt as _};
 
-pub use db::Db;
+pub use db::{kvp::KeyValue, Db};
 pub use fs::*;
 pub use worktree::*;
 

crates/sqlez/src/domain.rs 🔗

@@ -0,0 +1,39 @@
+use crate::connection::Connection;
+
+pub trait Domain: Send + Sync + Clone {
+    fn migrate(conn: &Connection) -> anyhow::Result<()>;
+}
+
+impl<D1: Domain, D2: Domain> Domain for (D1, D2) {
+    fn migrate(conn: &Connection) -> anyhow::Result<()> {
+        D1::migrate(conn)?;
+        D2::migrate(conn)
+    }
+}
+
+impl<D1: Domain, D2: Domain, D3: Domain> Domain for (D1, D2, D3) {
+    fn migrate(conn: &Connection) -> anyhow::Result<()> {
+        D1::migrate(conn)?;
+        D2::migrate(conn)?;
+        D3::migrate(conn)
+    }
+}
+
+impl<D1: Domain, D2: Domain, D3: Domain, D4: Domain> Domain for (D1, D2, D3, D4) {
+    fn migrate(conn: &Connection) -> anyhow::Result<()> {
+        D1::migrate(conn)?;
+        D2::migrate(conn)?;
+        D3::migrate(conn)?;
+        D4::migrate(conn)
+    }
+}
+
+impl<D1: Domain, D2: Domain, D3: Domain, D4: Domain, D5: Domain> Domain for (D1, D2, D3, D4, D5) {
+    fn migrate(conn: &Connection) -> anyhow::Result<()> {
+        D1::migrate(conn)?;
+        D2::migrate(conn)?;
+        D3::migrate(conn)?;
+        D4::migrate(conn)?;
+        D5::migrate(conn)
+    }
+}

crates/sqlez/src/lib.rs 🔗

@@ -1,5 +1,6 @@
 pub mod bindable;
 pub mod connection;
+pub mod domain;
 pub mod migrations;
 pub mod savepoint;
 pub mod statement;

crates/sqlez/src/thread_safe_connection.rs 🔗

@@ -1,26 +1,26 @@
-use std::{ops::Deref, sync::Arc};
+use std::{marker::PhantomData, ops::Deref, sync::Arc};
 
 use connection::Connection;
 use thread_local::ThreadLocal;
 
-use crate::{connection, migrations::Migration};
+use crate::{connection, domain::Domain};
 
-pub struct ThreadSafeConnection {
+pub struct ThreadSafeConnection<D: Domain> {
     uri: Arc<str>,
     persistent: bool,
     initialize_query: Option<&'static str>,
-    migrations: Option<&'static [Migration]>,
     connection: Arc<ThreadLocal<Connection>>,
+    _pd: PhantomData<D>,
 }
 
-impl ThreadSafeConnection {
+impl<D: Domain> ThreadSafeConnection<D> {
     pub fn new(uri: &str, persistent: bool) -> Self {
         Self {
             uri: Arc::from(uri),
             persistent,
             initialize_query: None,
-            migrations: None,
             connection: Default::default(),
+            _pd: PhantomData,
         }
     }
 
@@ -31,13 +31,6 @@ impl ThreadSafeConnection {
         self
     }
 
-    /// Migrations have to be run per connection because we fallback to memory
-    /// so this needs
-    pub fn with_migrations(mut self, migrations: &'static [Migration]) -> Self {
-        self.migrations = Some(migrations);
-        self
-    }
-
     /// Opens a new db connection with the initialized file path. This is internal and only
     /// called from the deref function.
     /// If opening fails, the connection falls back to a shared memory connection
@@ -50,21 +43,33 @@ impl ThreadSafeConnection {
     fn open_shared_memory(&self) -> Connection {
         Connection::open_memory(self.uri.as_ref())
     }
+
+    // Open a new connection for the given domain, leaving this
+    // connection intact.
+    pub fn for_domain<D2: Domain>(&self) -> ThreadSafeConnection<D2> {
+        ThreadSafeConnection {
+            uri: self.uri.clone(),
+            persistent: self.persistent,
+            initialize_query: self.initialize_query,
+            connection: Default::default(),
+            _pd: PhantomData,
+        }
+    }
 }
 
-impl Clone for ThreadSafeConnection {
+impl<D: Domain> Clone for ThreadSafeConnection<D> {
     fn clone(&self) -> Self {
         Self {
             uri: self.uri.clone(),
             persistent: self.persistent,
             initialize_query: self.initialize_query.clone(),
-            migrations: self.migrations.clone(),
             connection: self.connection.clone(),
+            _pd: PhantomData,
         }
     }
 }
 
-impl Deref for ThreadSafeConnection {
+impl<D: Domain> Deref for ThreadSafeConnection<D> {
     type Target = Connection;
 
     fn deref(&self) -> &Self::Target {
@@ -83,13 +88,7 @@ impl Deref for ThreadSafeConnection {
                 .unwrap();
             }
 
-            if let Some(migrations) = self.migrations {
-                for migration in migrations {
-                    migration
-                        .run(&connection)
-                        .expect(&format!("Migrations failed to execute: {:?}", migration));
-                }
-            }
+            D::migrate(&connection).expect("Migrations failed");
 
             connection
         })

crates/workspace/src/pane.rs 🔗

@@ -1925,7 +1925,7 @@ mod tests {
 
         let project = Project::test(fs, None, cx).await;
         let (_, workspace) =
-            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
+            cx.add_window(|cx| Workspace::new(None, project, |_, _| unimplemented!(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labled_item(&workspace, &pane, "A", cx);

crates/workspace/src/workspace.rs 🔗

@@ -15,7 +15,7 @@ use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
 use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
 use collections::{hash_map, HashMap, HashSet};
-use db::{model::SerializedWorkspace, Db};
+use db::{kvp::KeyValue, model::SerializedWorkspace, Db};
 use dock::{DefaultItemFactory, Dock, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
 use fs::{self, Fs};
@@ -1288,7 +1288,8 @@ impl Workspace {
 
             // Use the resolved worktree roots to get the serialized_db from the database
             let serialized_workspace = cx.read(|cx| {
-                cx.global::<Db>()
+                cx.global::<Db<KeyValue>>()
+                    .open_as::<db::Workspace>()
                     .workspace_for_roots(&Vec::from_iter(worktree_roots.into_iter())[..])
             });
 

crates/zed/src/main.rs 🔗

@@ -57,7 +57,7 @@ fn main() {
     init_panic_hook(app_version, http.clone(), app.background());
 
     let db = app.background().spawn(async move {
-        project::Db::open(&*zed::paths::DB_DIR, RELEASE_CHANNEL_NAME.as_str())
+        project::Db::<project::KeyValue>::open(&*zed::paths::DB_DIR, RELEASE_CHANNEL_NAME.as_str())
     });
 
     load_embedded_fonts(&app);
@@ -150,7 +150,7 @@ fn main() {
         let db = cx.background().block(db);
         cx.set_global(db);
 
-        client.start_telemetry(cx.global::<Db>().clone());
+        client.start_telemetry(cx.global::<Db<project::KeyValue>>().clone());
         client.report_event("start app", Default::default());
 
         let app_state = Arc::new(AppState {
@@ -165,7 +165,7 @@ fn main() {
             default_item_factory,
         });
         auto_update::init(
-            cx.global::<Db>().clone(),
+            cx.global::<Db<project::KeyValue>>().clone(),
             http,
             client::ZED_SERVER_URL.clone(),
             cx,