diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 07670e309a..97dfce0e19 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -1,5 +1,4 @@ pub mod kvp; -mod migrations; pub mod workspace; use std::fs; @@ -11,8 +10,9 @@ use indoc::indoc; use kvp::KVP_MIGRATION; use sqlez::connection::Connection; use sqlez::thread_safe_connection::ThreadSafeConnection; - +use workspace::items::ITEM_MIGRATIONS; use workspace::pane::PANE_MIGRATIONS; + pub use workspace::*; #[derive(Clone)] @@ -35,32 +35,21 @@ impl Db { .expect("Should be able to create the database directory"); let db_path = current_db_dir.join(Path::new("db.sqlite")); - Db( - ThreadSafeConnection::new(db_path.to_string_lossy().as_ref(), true) - .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]), - ) - } - - pub fn persisting(&self) -> bool { - self.persistent() + Db(initialize_connection(ThreadSafeConnection::new( + db_path.to_string_lossy().as_ref(), + true, + ))) } /// Open a in memory database for testing and as a fallback. pub fn open_in_memory(db_name: &str) -> Self { - Db(ThreadSafeConnection::new(db_name, false) - .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])) + Db(initialize_connection(ThreadSafeConnection::new( + db_name, false, + ))) + } + + pub fn persisting(&self) -> bool { + self.persistent() } pub fn write_to>(&self, dest: P) -> Result<()> { @@ -68,3 +57,18 @@ impl Db { 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, + ]) +} diff --git a/crates/db/src/migrations.rs b/crates/db/src/migrations.rs deleted file mode 100644 index a95654f420..0000000000 --- a/crates/db/src/migrations.rs +++ /dev/null @@ -1,14 +0,0 @@ -// // use crate::items::ITEMS_M_1; -// use crate::{kvp::KVP_M_1, pane::PANE_M_1, WORKSPACES_MIGRATION}; - -// // 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(KVP_M_1), -// M::up(WORKSPACE_M_1), -// M::up(PANE_M_1) -// ]); -// } diff --git a/crates/db/src/workspace.rs b/crates/db/src/workspace.rs index cf09bdd06e..b1d139066f 100644 --- a/crates/db/src/workspace.rs +++ b/crates/db/src/workspace.rs @@ -1,4 +1,4 @@ -mod items; +pub(crate) mod items; pub mod model; pub(crate) mod pane; @@ -58,8 +58,14 @@ impl Db { .flatten()?; Some(SerializedWorkspace { - dock_pane: self.get_dock_pane(&workspace_id)?, - center_group: self.get_center_group(&workspace_id), + dock_pane: self + .get_dock_pane(&workspace_id) + .context("Getting dock pane") + .log_err()?, + center_group: self + .get_center_group(&workspace_id) + .context("Getting center group") + .log_err()?, dock_anchor, dock_visible, }) @@ -70,231 +76,152 @@ impl Db { pub fn save_workspace>( &self, worktree_roots: &[P], - workspace: SerializedWorkspace, + old_roots: Option<&[P]>, + workspace: &SerializedWorkspace, ) { let workspace_id: WorkspaceId = worktree_roots.into(); - self.with_savepoint("update_worktrees", |conn| { + self.with_savepoint("update_worktrees", || { + if let Some(old_roots) = old_roots { + let old_id: WorkspaceId = old_roots.into(); + + self.prepare("DELETE FROM WORKSPACES WHERE workspace_id = ?")? + .with_bindings(&old_id)? + .exec()?; + } + // Delete any previous workspaces with the same roots. This cascades to all // other tables that are based on the same roots set. // Insert new workspace into workspaces table if none were found - self.prepare(indoc!{" - DELETE FROM workspaces WHERE workspace_id = ?1; - INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?1, ?, ?)"})? + self.prepare("DELETE FROM workspaces WHERE workspace_id = ?;")? + .with_bindings(&workspace_id)? + .exec()?; + + self.prepare( + "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)", + )? .with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))? .exec()?; - + // Save center pane group and dock pane - Self::save_center_group(&workspace_id, &workspace.center_group, conn)?; - Self::save_dock_pane(&workspace_id, &workspace.dock_pane, conn)?; + self.save_center_group(&workspace_id, &workspace.center_group)?; + self.save_dock_pane(&workspace_id, &workspace.dock_pane)?; Ok(()) }) - .with_context(|| format!("Update workspace with roots {:?}", worktree_roots.iter().map(|p| p.as_ref()).collect::>())) + .with_context(|| { + format!( + "Update workspace with roots {:?}", + worktree_roots + .iter() + .map(|p| p.as_ref()) + .collect::>() + ) + }) .log_err(); } /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots pub fn recent_workspaces(&self, limit: usize) -> Vec> { iife!({ - Ok::<_, anyhow::Error>(self.prepare("SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?")? + // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html + Ok::<_, anyhow::Error>( + self.prepare( + "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?", + )? .with_bindings(limit)? .rows::()? - .into_iter().map(|id| id.0) - .collect::>>()) - - }).log_err().unwrap_or_default() + .into_iter() + .map(|id| id.paths()) + .collect::>>(), + ) + }) + .log_err() + .unwrap_or_default() } } #[cfg(test)] mod tests { + use crate::{ + model::{ + DockAnchor::{Bottom, Expanded, Right}, + SerializedWorkspace, + }, + Db, + }; - // use std::{path::PathBuf, thread::sleep, time::Duration}; + #[test] + fn test_basic_functionality() { + env_logger::init(); - // use crate::Db; + let db = Db::open_in_memory("test_basic_functionality"); - // use super::WorkspaceId; + let workspace_1 = SerializedWorkspace { + dock_anchor: Bottom, + dock_visible: true, + center_group: Default::default(), + dock_pane: Default::default(), + }; - // #[test] - // fn test_workspace_saving() { - // env_logger::init(); - // let db = Db::open_in_memory("test_new_worktrees_for_roots"); + let workspace_2 = SerializedWorkspace { + dock_anchor: Expanded, + dock_visible: false, + center_group: Default::default(), + dock_pane: Default::default(), + }; - // // Test nothing returned with no roots at first - // assert_eq!(db.workspace_for_roots::(&[]), None); + let workspace_3 = SerializedWorkspace { + dock_anchor: Right, + dock_visible: true, + center_group: Default::default(), + dock_pane: Default::default(), + }; - // // Test creation - // let workspace_1 = db.workspace_for_roots::(&[]); - // assert_eq!(workspace_1.workspace_id, WorkspaceId(1)); + db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1); + db.save_workspace(&["/tmp"], None, &workspace_2); - // // Ensure the timestamps are different - // sleep(Duration::from_secs(1)); - // db.make_new_workspace::(&[]); + db.write_to("test.db").unwrap(); - // // Test pulling another value from recent workspaces - // let workspace_2 = db.workspace_for_roots::(&[]); - // assert_eq!(workspace_2.workspace_id, WorkspaceId(2)); + // Test that paths are treated as a set + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_1 + ); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), + workspace_1 + ); - // // Ensure the timestamps are different - // sleep(Duration::from_secs(1)); + // Make sure that other keys work + assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); + assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); - // // Test creating a new workspace that doesn't exist already - // let workspace_3 = db.workspace_for_roots(&["/tmp", "/tmp2"]); - // assert_eq!(workspace_3.workspace_id, WorkspaceId(3)); + // Test 'mutate' case of updating a pre-existing id + db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2); + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_2 + ); - // // Make sure it's in the recent workspaces.... - // let workspace_3 = db.workspace_for_roots::(&[]); - // assert_eq!(workspace_3.workspace_id, WorkspaceId(3)); + // Test other mechanism for mutating + db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3); + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_3 + ); - // // And that it can be pulled out again - // let workspace_3 = db.workspace_for_roots(&["/tmp", "/tmp2"]); - // assert_eq!(workspace_3.workspace_id, WorkspaceId(3)); - // } - - // #[test] - // fn test_empty_worktrees() { - // let db = Db::open_in_memory("test_empty_worktrees"); - - // assert_eq!(None, db.workspace::(&[])); - - // db.make_new_workspace::(&[]); //ID 1 - // db.make_new_workspace::(&[]); //ID 2 - // db.update_worktrees(&WorkspaceId(1), &["/tmp", "/tmp2"]); - - // // Sanity check - // assert_eq!(db.workspace(&["/tmp", "/tmp2"]).unwrap().0, WorkspaceId(1)); - - // db.update_worktrees::(&WorkspaceId(1), &[]); - - // // Make sure 'no worktrees' fails correctly. returning [1, 2] from this - // // call would be semantically correct (as those are the workspaces that - // // don't have roots) but I'd prefer that this API to either return exactly one - // // workspace, and None otherwise - // assert_eq!(db.workspace::(&[]), None,); - - // assert_eq!(db.last_workspace().unwrap().0, WorkspaceId(1)); - - // assert_eq!( - // db.recent_workspaces(2), - // vec![Vec::::new(), Vec::::new()], - // ) - // } - - // #[test] - // fn test_more_workspace_ids() { - // let data = &[ - // (WorkspaceId(1), vec!["/tmp1"]), - // (WorkspaceId(2), vec!["/tmp1", "/tmp2"]), - // (WorkspaceId(3), vec!["/tmp1", "/tmp2", "/tmp3"]), - // (WorkspaceId(4), vec!["/tmp2", "/tmp3"]), - // (WorkspaceId(5), vec!["/tmp2", "/tmp3", "/tmp4"]), - // (WorkspaceId(6), vec!["/tmp2", "/tmp4"]), - // (WorkspaceId(7), vec!["/tmp2"]), - // ]; - - // let db = Db::open_in_memory("test_more_workspace_ids"); - - // for (workspace_id, entries) in data { - // db.make_new_workspace::(&[]); - // db.update_worktrees(workspace_id, entries); - // } - - // assert_eq!(WorkspaceId(1), db.workspace(&["/tmp1"]).unwrap().0); - // assert_eq!(db.workspace(&["/tmp1", "/tmp2"]).unwrap().0, WorkspaceId(2)); - // assert_eq!( - // db.workspace(&["/tmp1", "/tmp2", "/tmp3"]).unwrap().0, - // WorkspaceId(3) - // ); - // assert_eq!(db.workspace(&["/tmp2", "/tmp3"]).unwrap().0, WorkspaceId(4)); - // assert_eq!( - // db.workspace(&["/tmp2", "/tmp3", "/tmp4"]).unwrap().0, - // WorkspaceId(5) - // ); - // assert_eq!(db.workspace(&["/tmp2", "/tmp4"]).unwrap().0, WorkspaceId(6)); - // assert_eq!(db.workspace(&["/tmp2"]).unwrap().0, WorkspaceId(7)); - - // assert_eq!(db.workspace(&["/tmp1", "/tmp5"]), None); - // assert_eq!(db.workspace(&["/tmp5"]), None); - // assert_eq!(db.workspace(&["/tmp2", "/tmp3", "/tmp4", "/tmp5"]), None); - // } - - // #[test] - // fn test_detect_workspace_id() { - // let data = &[ - // (WorkspaceId(1), vec!["/tmp"]), - // (WorkspaceId(2), vec!["/tmp", "/tmp2"]), - // (WorkspaceId(3), vec!["/tmp", "/tmp2", "/tmp3"]), - // ]; - - // let db = Db::open_in_memory("test_detect_workspace_id"); - - // for (workspace_id, entries) in data { - // db.make_new_workspace::(&[]); - // db.update_worktrees(workspace_id, entries); - // } - - // assert_eq!(db.workspace(&["/tmp2"]), None); - // assert_eq!(db.workspace(&["/tmp2", "/tmp3"]), None); - // assert_eq!(db.workspace(&["/tmp"]).unwrap().0, WorkspaceId(1)); - // assert_eq!(db.workspace(&["/tmp", "/tmp2"]).unwrap().0, WorkspaceId(2)); - // assert_eq!( - // db.workspace(&["/tmp", "/tmp2", "/tmp3"]).unwrap().0, - // WorkspaceId(3) - // ); - // } - - // #[test] - // fn test_tricky_overlapping_updates() { - // // DB state: - // // (/tree) -> ID: 1 - // // (/tree, /tree2) -> ID: 2 - // // (/tree2, /tree3) -> ID: 3 - - // // -> User updates 2 to: (/tree2, /tree3) - - // // DB state: - // // (/tree) -> ID: 1 - // // (/tree2, /tree3) -> ID: 2 - // // Get rid of 3 for garbage collection - - // let data = &[ - // (WorkspaceId(1), vec!["/tmp"]), - // (WorkspaceId(2), vec!["/tmp", "/tmp2"]), - // (WorkspaceId(3), vec!["/tmp2", "/tmp3"]), - // ]; - - // let db = Db::open_in_memory("test_tricky_overlapping_update"); - - // // Load in the test data - // for (workspace_id, entries) in data { - // db.make_new_workspace::(&[]); - // db.update_worktrees(workspace_id, entries); - // } - - // sleep(Duration::from_secs(1)); - // // Execute the update - // db.update_worktrees(&WorkspaceId(2), &["/tmp2", "/tmp3"]); - - // // Make sure that workspace 3 doesn't exist - // assert_eq!(db.workspace(&["/tmp2", "/tmp3"]).unwrap().0, WorkspaceId(2)); - - // // And that workspace 1 was untouched - // assert_eq!(db.workspace(&["/tmp"]).unwrap().0, WorkspaceId(1)); - - // // And that workspace 2 is no longer registered under these roots - // assert_eq!(db.workspace(&["/tmp", "/tmp2"]), None); - - // assert_eq!(db.last_workspace().unwrap().0, WorkspaceId(2)); - - // let recent_workspaces = db.recent_workspaces(10); - // assert_eq!( - // recent_workspaces.get(0).unwrap(), - // &vec![PathBuf::from("/tmp2"), PathBuf::from("/tmp3")] - // ); - // assert_eq!( - // recent_workspaces.get(1).unwrap(), - // &vec![PathBuf::from("/tmp")] - // ); - // } + // Make sure that updating paths differently also works + db.save_workspace( + &["/tmp3", "/tmp4", "/tmp2"], + Some(&["/tmp", "/tmp2"]), + &workspace_3, + ); + assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]) + .unwrap(), + workspace_3 + ); + } } diff --git a/crates/db/src/workspace/items.rs b/crates/db/src/workspace/items.rs index c3405974d5..87437ccf73 100644 --- a/crates/db/src/workspace/items.rs +++ b/crates/db/src/workspace/items.rs @@ -1,13 +1,11 @@ -// use std::{ -// ffi::OsStr, -// fmt::Display, -// hash::Hash, -// os::unix::prelude::OsStrExt, -// path::{Path, PathBuf}, -// sync::Arc, -// }; +use anyhow::{Context, Result}; +use indoc::indoc; +use sqlez::migrations::Migration; -// use anyhow::Result; +use crate::{ + model::{ItemId, PaneId, SerializedItem, SerializedItemKind, WorkspaceId}, + Db, +}; // use collections::HashSet; // use rusqlite::{named_params, params, types::FromSql}; @@ -65,45 +63,61 @@ // ) STRICT; // "; -// enum SerializedItemKind { -// Editor, -// Diagnostics, -// ProjectSearch, -// Terminal, -// } +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; + "}], +); -// struct SerializedItemRow { -// kind: SerializedItemKind, -// item_id: usize, -// path: Option>, -// query: Option, -// } +impl Db { + pub(crate) fn get_items(&self, pane_id: PaneId) -> Result> { + Ok(self + .prepare(indoc! {" + SELECT item_id, kind FROM items + WHERE pane_id = ? + ORDER BY position"})? + .with_bindings(pane_id)? + .rows::<(ItemId, SerializedItemKind)>()? + .into_iter() + .map(|(item_id, kind)| match kind { + SerializedItemKind::Terminal => SerializedItem::Terminal { item_id }, + _ => unimplemented!(), + }) + .collect()) + } -// #[derive(Debug, PartialEq, Eq)] -// pub enum SerializedItem { -// Editor { item_id: usize, path: Arc }, -// Diagnostics { item_id: usize }, -// ProjectSearch { item_id: usize, query: String }, -// Terminal { item_id: usize }, -// } + pub(crate) fn save_items( + &self, + workspace_id: &WorkspaceId, + pane_id: PaneId, + items: &[SerializedItem], + ) -> Result<()> { + let mut delete_old = self + .prepare("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?") + .context("Preparing deletion")?; + let mut insert_new = self.prepare( + "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)", + ).context("Preparing insertion")?; + for (position, item) in items.iter().enumerate() { + delete_old + .with_bindings((workspace_id, pane_id, item.item_id()))? + .exec()?; -// impl SerializedItem { -// pub fn item_id(&self) -> usize { -// match self { -// SerializedItem::Editor { item_id, .. } => *item_id, -// SerializedItem::Diagnostics { item_id } => *item_id, -// SerializedItem::ProjectSearch { item_id, .. } => *item_id, -// SerializedItem::Terminal { item_id } => *item_id, -// } -// } -// } + insert_new + .with_bindings((item.item_id(), workspace_id, pane_id, item.kind(), position))? + .exec()?; + } -// impl Db { -// pub fn get_item(&self, item_id: ItemId) -> SerializedItem { -// unimplemented!() -// } - -// pub fn save_item(&self, workspace_id: WorkspaceId, item: &SerializedItem) {} - -// pub fn close_item(&self, item_id: ItemId) {} -// } + Ok(()) + } +} diff --git a/crates/db/src/workspace/model.rs b/crates/db/src/workspace/model.rs index 1a6b4ee41f..a2bb0c1cd2 100644 --- a/crates/db/src/workspace/model.rs +++ b/crates/db/src/workspace/model.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use anyhow::{bail, Result}; @@ -8,8 +11,14 @@ use sqlez::{ statement::Statement, }; -#[derive(Debug, PartialEq, Eq, Clone)] -pub(crate) struct WorkspaceId(pub(crate) Vec); +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct WorkspaceId(Vec); + +impl WorkspaceId { + pub fn paths(self) -> Vec { + self.0 + } +} impl, T: IntoIterator> From for WorkspaceId { fn from(iterator: T) -> Self { @@ -74,7 +83,7 @@ impl Column for DockAnchor { pub(crate) type WorkspaceRow = (WorkspaceId, DockAnchor, bool); -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct SerializedWorkspace { pub dock_anchor: DockAnchor, pub dock_visible: bool, @@ -82,19 +91,134 @@ pub struct SerializedWorkspace { pub dock_pane: SerializedPane, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Default)] pub struct SerializedPaneGroup { axis: Axis, children: Vec, } -#[derive(Debug)] -pub struct SerializedPane { - _children: Vec, +impl SerializedPaneGroup { + pub fn new() -> Self { + SerializedPaneGroup { + axis: Axis::Horizontal, + children: Vec::new(), + } + } } -#[derive(Debug)] -pub enum SerializedItemKind {} +#[derive(Debug, PartialEq, Eq, Default)] +pub struct SerializedPane { + pub(crate) children: Vec, +} -#[derive(Debug)] -pub enum SerializedItem {} +impl SerializedPane { + pub fn new(children: Vec) -> Self { + SerializedPane { children } + } +} + +pub type GroupId = i64; +pub type PaneId = i64; +pub type ItemId = usize; + +pub(crate) enum SerializedItemKind { + Editor, + Diagnostics, + ProjectSearch, + Terminal, +} + +impl Bind for SerializedItemKind { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + SerializedItemKind::Editor => "Editor", + SerializedItemKind::Diagnostics => "Diagnostics", + SerializedItemKind::ProjectSearch => "ProjectSearch", + SerializedItemKind::Terminal => "Terminal", + } + .bind(statement, start_index) + } +} + +impl Column for SerializedItemKind { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(anchor_text, next_index)| { + Ok(( + match anchor_text.as_ref() { + "Editor" => SerializedItemKind::Editor, + "Diagnostics" => SerializedItemKind::Diagnostics, + "ProjectSearch" => SerializedItemKind::ProjectSearch, + "Terminal" => SerializedItemKind::Terminal, + _ => bail!("Stored serialized item kind is incorrect"), + }, + next_index, + )) + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SerializedItem { + Editor { item_id: usize, path: Arc }, + Diagnostics { item_id: usize }, + ProjectSearch { item_id: usize, query: String }, + Terminal { item_id: usize }, +} + +impl SerializedItem { + pub fn item_id(&self) -> usize { + match self { + SerializedItem::Editor { item_id, .. } => *item_id, + SerializedItem::Diagnostics { item_id } => *item_id, + SerializedItem::ProjectSearch { item_id, .. } => *item_id, + SerializedItem::Terminal { item_id } => *item_id, + } + } + + pub(crate) fn kind(&self) -> SerializedItemKind { + match self { + SerializedItem::Editor { .. } => SerializedItemKind::Editor, + SerializedItem::Diagnostics { .. } => SerializedItemKind::Diagnostics, + SerializedItem::ProjectSearch { .. } => SerializedItemKind::ProjectSearch, + SerializedItem::Terminal { .. } => SerializedItemKind::Terminal, + } + } +} + +#[cfg(test)] +mod tests { + use sqlez::connection::Connection; + + use crate::model::DockAnchor; + + use super::WorkspaceId; + + #[test] + fn test_workspace_round_trips() { + let db = Connection::open_memory("workspace_id_round_trips"); + + db.exec(indoc::indoc! {" + CREATE TABLE workspace_id_test( + workspace_id BLOB, + dock_anchor TEXT + );"}) + .unwrap(); + + let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]); + + db.prepare("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)") + .unwrap() + .with_bindings((&workspace_id, DockAnchor::Bottom)) + .unwrap() + .exec() + .unwrap(); + + assert_eq!( + db.prepare("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1") + .unwrap() + .row::<(WorkspaceId, DockAnchor)>() + .unwrap(), + (WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom) + ); + } +} diff --git a/crates/db/src/workspace/pane.rs b/crates/db/src/workspace/pane.rs index 73306707cf..f2b7fc8ef0 100644 --- a/crates/db/src/workspace/pane.rs +++ b/crates/db/src/workspace/pane.rs @@ -1,8 +1,9 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use indoc::indoc; -use sqlez::{connection::Connection, migrations::Migration}; +use sqlez::migrations::Migration; +use util::unzip_option; -use crate::model::SerializedPane; +use crate::model::{GroupId, PaneId, SerializedPane}; use super::{ model::{SerializedPaneGroup, WorkspaceId}, @@ -19,79 +20,31 @@ pub(crate) const PANE_MIGRATIONS: Migration = Migration::new( axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal' FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY(parent_group) REFERENCES pane_groups(group_id) ON DELETE CASCADE - PRIMARY KEY(group_id, workspace_id) ) STRICT; CREATE TABLE panes( pane_id INTEGER PRIMARY KEY, workspace_id BLOB NOT NULL, group_id INTEGER, -- If null, this is a dock pane - idx INTEGER NOT NULL, + position INTEGER, -- If null, this is a dock pane FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY(group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - PRIMARY KEY(pane_id, workspace_id) - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique - pane_id INTEGER NOT NULL, - workspace_id BLOB NOT NULL, - kind TEXT 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_center_group(&self, _workspace: &WorkspaceId) -> SerializedPaneGroup { - unimplemented!() + pub(crate) fn get_center_group( + &self, + _workspace_id: &WorkspaceId, + ) -> Result { + Ok(SerializedPaneGroup::new()) } - pub(crate) fn _get_pane_group(&self, _workspace: &WorkspaceId) -> SerializedPaneGroup { - unimplemented!() - // let axis = self.get_pane_group_axis(pane_group_id); - // let mut children: Vec<(usize, PaneGroupChild)> = Vec::new(); - // for child_row in self.get_pane_group_children(pane_group_id) { - // if let Some(child_pane_id) = child_row.child_pane_id { - // children.push(( - // child_row.index, - // PaneGroupChild::Pane(self.get_pane(PaneId { - // workspace_id: pane_group_id.workspace_id, - // pane_id: child_pane_id, - // })), - // )); - // } else if let Some(child_group_id) = child_row.child_group_id { - // children.push(( - // child_row.index, - // PaneGroupChild::Group(self.get_pane_group(PaneGroupId { - // workspace_id: pane_group_id.workspace_id, - // group_id: child_group_id, - // })), - // )); - // } - // } - // children.sort_by_key(|(index, _)| *index); - - // SerializedPaneGroup { - // group_id: pane_group_id, - // axis, - // children: children.into_iter().map(|(_, child)| child).collect(), - // } - } - - // fn _get_pane_group_children( - // &self, - // _pane_group_id: PaneGroupId, - // ) -> impl Iterator { - // Vec::new().into_iter() - // } - pub(crate) fn save_center_group( - _workspace: &WorkspaceId, + &self, + _workspace_id: &WorkspaceId, _center_pane_group: &SerializedPaneGroup, - _connection: &Connection, ) -> Result<()> { // Delete the center pane group for this workspace and any of its children // Generate new pane group IDs as we go through @@ -99,51 +52,86 @@ impl Db { Ok(()) } - pub(crate) fn get_dock_pane(&self, _workspace: &WorkspaceId) -> Option { - unimplemented!() + pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result { + let pane_id = self + .prepare(indoc! {" + SELECT pane_id FROM panes + WHERE workspace_id = ? AND group_id IS NULL AND position IS NULL"})? + .with_bindings(workspace_id)? + .row::()?; + + Ok(SerializedPane::new( + self.get_items(pane_id).context("Reading items")?, + )) } pub(crate) fn save_dock_pane( - _workspace: &WorkspaceId, - _dock_pane: &SerializedPane, - _connection: &Connection, + &self, + workspace: &WorkspaceId, + dock_pane: &SerializedPane, ) -> Result<()> { - // iife!({ - // self.prepare( - // "INSERT INTO dock_panes (workspace_id, anchor_position, visible) VALUES (?, ?, ?);", - // )? - // .with_bindings(dock_pane.to_row(workspace))? - // .insert() - // }) - // .log_err(); - Ok(()) + self.save_pane(workspace, &dock_pane, None) + } + + 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 + .prepare("INSERT INTO panes(workspace_id, group_id, position) VALUES (?, ?, ?)")? + .with_bindings((workspace_id, parent_id, order))? + .insert()? as PaneId; + + self.save_items(workspace_id, pane_id, &pane.children) + .context("Saving items") } } #[cfg(test)] mod tests { - // use crate::{items::ItemId, pane::SerializedPane, Db, DockAnchor}; + use crate::{ + model::{SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace}, + Db, + }; - // use super::{PaneGroupChild, SerializedDockPane, SerializedPaneGroup}; + fn default_workspace( + dock_pane: SerializedPane, + center_group: SerializedPaneGroup, + ) -> SerializedWorkspace { + SerializedWorkspace { + dock_anchor: crate::model::DockAnchor::Right, + dock_visible: false, + center_group, + dock_pane, + } + } - // #[test] - // fn test_basic_dock_pane() { - // let db = Db::open_in_memory("basic_dock_pane"); + #[test] + fn test_basic_dock_pane() { + let db = Db::open_in_memory("basic_dock_pane"); - // let workspace = db.workspace_for_roots(&["/tmp"]); + 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 dock_pane = SerializedDockPane { - // anchor_position: DockAnchor::Expanded, - // visible: true, - // }; + let workspace = default_workspace(dock_pane, SerializedPaneGroup::new()); - // db.save_dock_pane(&workspace.workspace_id, &dock_pane); + db.save_workspace(&["/tmp"], None, &workspace); - // let new_workspace = db.workspace_for_roots(&["/tmp"]); + let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); - // assert_eq!(new_workspace.dock_pane.unwrap(), dock_pane); - // } + assert_eq!(workspace.dock_pane, new_workspace.dock_pane); + } // #[test] // fn test_dock_simple_split() { diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 1ce350a550..7a3483bcea 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -178,8 +178,29 @@ impl Column for (T1, T2, T3, T4) let (first, next_index) = T1::column(statement, start_index)?; let (second, next_index) = T2::column(statement, next_index)?; let (third, next_index) = T3::column(statement, next_index)?; - let (forth, next_index) = T4::column(statement, next_index)?; - Ok(((first, second, third, forth), next_index)) + let (fourth, next_index) = T4::column(statement, next_index)?; + Ok(((first, second, third, fourth), next_index)) + } +} + +impl Bind for (T1, T2, T3, T4, T5) { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = self.0.bind(statement, start_index)?; + let next_index = self.1.bind(statement, next_index)?; + let next_index = self.2.bind(statement, next_index)?; + let next_index = self.3.bind(statement, next_index)?; + self.4.bind(statement, next_index) + } +} + +impl Column for (T1, T2, T3, T4, T5) { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (first, next_index) = T1::column(statement, start_index)?; + let (second, next_index) = T2::column(statement, next_index)?; + let (third, next_index) = T3::column(statement, next_index)?; + let (fourth, next_index) = T4::column(statement, next_index)?; + let (fifth, next_index) = T5::column(statement, next_index)?; + Ok(((first, second, third, fourth, fifth), next_index)) } } diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index fcc180a48d..04a12cfc97 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -99,7 +99,29 @@ impl Connection { } pub(crate) fn last_error(&self) -> Result<()> { - unsafe { error_to_result(sqlite3_errcode(self.sqlite3)) } + unsafe { + let code = sqlite3_errcode(self.sqlite3); + const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW]; + if NON_ERROR_CODES.contains(&code) { + return Ok(()); + } + + let message = sqlite3_errmsg(self.sqlite3); + let message = if message.is_null() { + None + } else { + Some( + String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) + .into_owned(), + ) + }; + + Err(anyhow!( + "Sqlite call failed with code {} and message: {:?}", + code as isize, + message + )) + } } } @@ -109,31 +131,6 @@ impl Drop for Connection { } } -pub(crate) fn error_to_result(code: std::os::raw::c_int) -> Result<()> { - const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW]; - unsafe { - if NON_ERROR_CODES.contains(&code) { - return Ok(()); - } - - let message = sqlite3_errstr(code); - let message = if message.is_null() { - None - } else { - Some( - String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) - .into_owned(), - ) - }; - - Err(anyhow!( - "Sqlite call failed with code {} and message: {:?}", - code as isize, - message - )) - } -} - #[cfg(test)] mod test { use anyhow::Result; @@ -213,6 +210,35 @@ mod test { ); } + #[test] + fn bool_round_trips() { + let connection = Connection::open_memory("bool_round_trips"); + connection + .exec(indoc! {" + CREATE TABLE bools ( + t INTEGER, + f INTEGER + );"}) + .unwrap(); + + connection + .prepare("INSERT INTO bools(t, f) VALUES (?, ?);") + .unwrap() + .with_bindings((true, false)) + .unwrap() + .exec() + .unwrap(); + + assert_eq!( + &connection + .prepare("SELECT * FROM bools;") + .unwrap() + .row::<(bool, bool)>() + .unwrap(), + &(true, false) + ); + } + #[test] fn backup_works() { let connection1 = Connection::open_memory("backup_works"); diff --git a/crates/sqlez/src/savepoint.rs b/crates/sqlez/src/savepoint.rs index 3d7830dd91..ba4b1e774b 100644 --- a/crates/sqlez/src/savepoint.rs +++ b/crates/sqlez/src/savepoint.rs @@ -8,11 +8,11 @@ impl Connection { // point is released. pub fn with_savepoint(&self, name: impl AsRef, f: F) -> Result where - F: FnOnce(&Connection) -> Result, + F: FnOnce() -> Result, { let name = name.as_ref().to_owned(); self.exec(format!("SAVEPOINT {}", &name))?; - let result = f(self); + let result = f(); match result { Ok(_) => { self.exec(format!("RELEASE {}", name))?; @@ -30,11 +30,11 @@ impl Connection { // point is released. pub fn with_savepoint_rollback(&self, name: impl AsRef, f: F) -> Result> where - F: FnOnce(&Connection) -> Result>, + F: FnOnce() -> Result>, { let name = name.as_ref().to_owned(); self.exec(format!("SAVEPOINT {}", &name))?; - let result = f(self); + let result = f(); match result { Ok(Some(_)) => { self.exec(format!("RELEASE {}", name))?; @@ -69,21 +69,21 @@ mod tests { let save1_text = "test save1"; let save2_text = "test save2"; - connection.with_savepoint("first", |save1| { - save1 + connection.with_savepoint("first", || { + connection .prepare("INSERT INTO text(text, idx) VALUES (?, ?)")? .with_bindings((save1_text, 1))? .exec()?; - assert!(save1 - .with_savepoint("second", |save2| -> Result, anyhow::Error> { - save2 + assert!(connection + .with_savepoint("second", || -> Result, anyhow::Error> { + connection .prepare("INSERT INTO text(text, idx) VALUES (?, ?)")? .with_bindings((save2_text, 2))? .exec()?; assert_eq!( - save2 + connection .prepare("SELECT text FROM text ORDER BY text.idx ASC")? .rows::()?, vec![save1_text, save2_text], @@ -95,20 +95,20 @@ mod tests { .is_some()); assert_eq!( - save1 + connection .prepare("SELECT text FROM text ORDER BY text.idx ASC")? .rows::()?, vec![save1_text], ); - save1.with_savepoint_rollback::<(), _>("second", |save2| { - save2 + connection.with_savepoint_rollback::<(), _>("second", || { + connection .prepare("INSERT INTO text(text, idx) VALUES (?, ?)")? .with_bindings((save2_text, 2))? .exec()?; assert_eq!( - save2 + connection .prepare("SELECT text FROM text ORDER BY text.idx ASC")? .rows::()?, vec![save1_text, save2_text], @@ -118,20 +118,20 @@ mod tests { })?; assert_eq!( - save1 + connection .prepare("SELECT text FROM text ORDER BY text.idx ASC")? .rows::()?, vec![save1_text], ); - save1.with_savepoint_rollback("second", |save2| { - save2 + connection.with_savepoint_rollback("second", || { + connection .prepare("INSERT INTO text(text, idx) VALUES (?, ?)")? .with_bindings((save2_text, 2))? .exec()?; assert_eq!( - save2 + connection .prepare("SELECT text FROM text ORDER BY text.idx ASC")? .rows::()?, vec![save1_text, save2_text], @@ -141,7 +141,7 @@ mod tests { })?; assert_eq!( - save1 + connection .prepare("SELECT text FROM text ORDER BY text.idx ASC")? .rows::()?, vec![save1_text, save2_text], diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index e2b59d86f1..f0de8703ab 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result}; use libsqlite3_sys::*; use crate::bindable::{Bind, Column}; -use crate::connection::{error_to_result, Connection}; +use crate::connection::Connection; pub struct Statement<'a> { raw_statement: *mut sqlite3_stmt, @@ -48,7 +48,9 @@ impl<'a> Statement<'a> { 0 as *mut _, ); - connection.last_error().context("Prepare call failed.")?; + connection + .last_error() + .with_context(|| format!("Prepare call failed for query:\n{}", query.as_ref()))?; } Ok(statement) @@ -309,10 +311,7 @@ impl<'a> Statement<'a> { impl<'a> Drop for Statement<'a> { fn drop(&mut self) { - unsafe { - let error = sqlite3_finalize(self.raw_statement); - error_to_result(error).expect("failed error"); - }; + unsafe { sqlite3_finalize(self.raw_statement) }; } } @@ -327,9 +326,9 @@ mod test { let connection1 = Connection::open_memory("blob_round_trips"); connection1 .exec(indoc! {" - CREATE TABLE blobs ( - data BLOB - );"}) + CREATE TABLE blobs ( + data BLOB + );"}) .unwrap(); let blob = &[0, 1, 2, 4, 8, 16, 32, 64]; @@ -352,4 +351,41 @@ mod test { let mut read = connection1.prepare("SELECT * FROM blobs;").unwrap(); assert_eq!(read.step().unwrap(), StepResult::Done); } + + #[test] + pub fn maybe_returns_options() { + let connection = Connection::open_memory("maybe_returns_options"); + connection + .exec(indoc! {" + CREATE TABLE texts ( + text TEXT + );"}) + .unwrap(); + + assert!(connection + .prepare("SELECT text FROM texts") + .unwrap() + .maybe_row::() + .unwrap() + .is_none()); + + let text_to_insert = "This is a test"; + + connection + .prepare("INSERT INTO texts VALUES (?)") + .unwrap() + .with_bindings(text_to_insert) + .unwrap() + .exec() + .unwrap(); + + assert_eq!( + connection + .prepare("SELECT text FROM texts") + .unwrap() + .maybe_row::() + .unwrap(), + Some(text_to_insert.to_string()) + ); + } } diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 3757da5854..19d17c1190 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -204,6 +204,16 @@ impl Iterator for RandomCharIter { } } +// copy unstable standard feature option unzip +// https://github.com/rust-lang/rust/issues/87800 +// Remove when this ship in Rust 1.66 or 1.67 +pub fn unzip_option(option: Option<(T, U)>) -> (Option, Option) { + match option { + Some((a, b)) => (Some(a), Some(b)), + None => (None, None), + } +} + #[macro_export] macro_rules! iife { ($block:block) => {