Refined sqlez, implemented 60% of workspace serialization sql
This commit is contained in:
parent
6b214acbc4
commit
0186289420
11 changed files with 569 additions and 433 deletions
|
@ -1,5 +1,4 @@
|
||||||
pub mod kvp;
|
pub mod kvp;
|
||||||
mod migrations;
|
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
@ -11,8 +10,9 @@ use indoc::indoc;
|
||||||
use kvp::KVP_MIGRATION;
|
use kvp::KVP_MIGRATION;
|
||||||
use sqlez::connection::Connection;
|
use sqlez::connection::Connection;
|
||||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||||
|
use workspace::items::ITEM_MIGRATIONS;
|
||||||
use workspace::pane::PANE_MIGRATIONS;
|
use workspace::pane::PANE_MIGRATIONS;
|
||||||
|
|
||||||
pub use workspace::*;
|
pub use workspace::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -35,32 +35,21 @@ impl Db {
|
||||||
.expect("Should be able to create the database directory");
|
.expect("Should be able to create the database directory");
|
||||||
let db_path = current_db_dir.join(Path::new("db.sqlite"));
|
let db_path = current_db_dir.join(Path::new("db.sqlite"));
|
||||||
|
|
||||||
Db(
|
Db(initialize_connection(ThreadSafeConnection::new(
|
||||||
ThreadSafeConnection::new(db_path.to_string_lossy().as_ref(), true)
|
db_path.to_string_lossy().as_ref(),
|
||||||
.with_initialize_query(indoc! {"
|
true,
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a in memory database for testing and as a fallback.
|
/// Open a in memory database for testing and as a fallback.
|
||||||
pub fn open_in_memory(db_name: &str) -> Self {
|
pub fn open_in_memory(db_name: &str) -> Self {
|
||||||
Db(ThreadSafeConnection::new(db_name, false)
|
Db(initialize_connection(ThreadSafeConnection::new(
|
||||||
.with_initialize_query(indoc! {"
|
db_name, false,
|
||||||
PRAGMA journal_mode=WAL;
|
)))
|
||||||
PRAGMA synchronous=NORMAL;
|
}
|
||||||
PRAGMA foreign_keys=TRUE;
|
|
||||||
PRAGMA case_sensitive_like=TRUE;
|
pub fn persisting(&self) -> bool {
|
||||||
"})
|
self.persistent()
|
||||||
.with_migrations(&[KVP_MIGRATION, WORKSPACES_MIGRATION, PANE_MIGRATIONS]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_to<P: AsRef<Path>>(&self, dest: P) -> Result<()> {
|
pub fn write_to<P: AsRef<Path>>(&self, dest: P) -> Result<()> {
|
||||||
|
@ -68,3 +57,18 @@ impl Db {
|
||||||
self.backup_main(&destination)
|
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,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
// ]);
|
|
||||||
// }
|
|
|
@ -1,4 +1,4 @@
|
||||||
mod items;
|
pub(crate) mod items;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub(crate) mod pane;
|
pub(crate) mod pane;
|
||||||
|
|
||||||
|
@ -58,8 +58,14 @@ impl Db {
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
|
|
||||||
Some(SerializedWorkspace {
|
Some(SerializedWorkspace {
|
||||||
dock_pane: self.get_dock_pane(&workspace_id)?,
|
dock_pane: self
|
||||||
center_group: self.get_center_group(&workspace_id),
|
.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_anchor,
|
||||||
dock_visible,
|
dock_visible,
|
||||||
})
|
})
|
||||||
|
@ -70,231 +76,152 @@ impl Db {
|
||||||
pub fn save_workspace<P: AsRef<Path>>(
|
pub fn save_workspace<P: AsRef<Path>>(
|
||||||
&self,
|
&self,
|
||||||
worktree_roots: &[P],
|
worktree_roots: &[P],
|
||||||
workspace: SerializedWorkspace,
|
old_roots: Option<&[P]>,
|
||||||
|
workspace: &SerializedWorkspace,
|
||||||
) {
|
) {
|
||||||
let workspace_id: WorkspaceId = worktree_roots.into();
|
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
|
// Delete any previous workspaces with the same roots. This cascades to all
|
||||||
// other tables that are based on the same roots set.
|
// other tables that are based on the same roots set.
|
||||||
// Insert new workspace into workspaces table if none were found
|
// Insert new workspace into workspaces table if none were found
|
||||||
self.prepare(indoc!{"
|
self.prepare("DELETE FROM workspaces WHERE workspace_id = ?;")?
|
||||||
DELETE FROM workspaces WHERE workspace_id = ?1;
|
.with_bindings(&workspace_id)?
|
||||||
INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?1, ?, ?)"})?
|
.exec()?;
|
||||||
|
|
||||||
|
self.prepare(
|
||||||
|
"INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
|
||||||
|
)?
|
||||||
.with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?
|
.with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?
|
||||||
.exec()?;
|
.exec()?;
|
||||||
|
|
||||||
// Save center pane group and dock pane
|
// Save center pane group and dock pane
|
||||||
Self::save_center_group(&workspace_id, &workspace.center_group, conn)?;
|
self.save_center_group(&workspace_id, &workspace.center_group)?;
|
||||||
Self::save_dock_pane(&workspace_id, &workspace.dock_pane, conn)?;
|
self.save_dock_pane(&workspace_id, &workspace.dock_pane)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.with_context(|| format!("Update workspace with roots {:?}", worktree_roots.iter().map(|p| p.as_ref()).collect::<Vec<_>>()))
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Update workspace with roots {:?}",
|
||||||
|
worktree_roots
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.as_ref())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
|
/// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
|
||||||
pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
|
pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
|
||||||
iife!({
|
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)?
|
.with_bindings(limit)?
|
||||||
.rows::<WorkspaceId>()?
|
.rows::<WorkspaceId>()?
|
||||||
.into_iter().map(|id| id.0)
|
.into_iter()
|
||||||
.collect::<Vec<Vec<PathBuf>>>())
|
.map(|id| id.paths())
|
||||||
|
.collect::<Vec<Vec<PathBuf>>>(),
|
||||||
}).log_err().unwrap_or_default()
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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]
|
let workspace_2 = SerializedWorkspace {
|
||||||
// fn test_workspace_saving() {
|
dock_anchor: Expanded,
|
||||||
// env_logger::init();
|
dock_visible: false,
|
||||||
// let db = Db::open_in_memory("test_new_worktrees_for_roots");
|
center_group: Default::default(),
|
||||||
|
dock_pane: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
// // Test nothing returned with no roots at first
|
let workspace_3 = SerializedWorkspace {
|
||||||
// assert_eq!(db.workspace_for_roots::<String>(&[]), None);
|
dock_anchor: Right,
|
||||||
|
dock_visible: true,
|
||||||
|
center_group: Default::default(),
|
||||||
|
dock_pane: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
// // Test creation
|
db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1);
|
||||||
// let workspace_1 = db.workspace_for_roots::<String>(&[]);
|
db.save_workspace(&["/tmp"], None, &workspace_2);
|
||||||
// assert_eq!(workspace_1.workspace_id, WorkspaceId(1));
|
|
||||||
|
|
||||||
// // Ensure the timestamps are different
|
db.write_to("test.db").unwrap();
|
||||||
// sleep(Duration::from_secs(1));
|
|
||||||
// db.make_new_workspace::<String>(&[]);
|
|
||||||
|
|
||||||
// // Test pulling another value from recent workspaces
|
// Test that paths are treated as a set
|
||||||
// let workspace_2 = db.workspace_for_roots::<String>(&[]);
|
assert_eq!(
|
||||||
// assert_eq!(workspace_2.workspace_id, WorkspaceId(2));
|
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
|
// Make sure that other keys work
|
||||||
// sleep(Duration::from_secs(1));
|
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
|
// Test 'mutate' case of updating a pre-existing id
|
||||||
// let workspace_3 = db.workspace_for_roots(&["/tmp", "/tmp2"]);
|
db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2);
|
||||||
// assert_eq!(workspace_3.workspace_id, WorkspaceId(3));
|
assert_eq!(
|
||||||
|
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
|
||||||
|
workspace_2
|
||||||
|
);
|
||||||
|
|
||||||
// // Make sure it's in the recent workspaces....
|
// Test other mechanism for mutating
|
||||||
// let workspace_3 = db.workspace_for_roots::<String>(&[]);
|
db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3);
|
||||||
// assert_eq!(workspace_3.workspace_id, WorkspaceId(3));
|
assert_eq!(
|
||||||
|
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
|
||||||
|
workspace_3
|
||||||
|
);
|
||||||
|
|
||||||
// // And that it can be pulled out again
|
// Make sure that updating paths differently also works
|
||||||
// let workspace_3 = db.workspace_for_roots(&["/tmp", "/tmp2"]);
|
db.save_workspace(
|
||||||
// assert_eq!(workspace_3.workspace_id, WorkspaceId(3));
|
&["/tmp3", "/tmp4", "/tmp2"],
|
||||||
// }
|
Some(&["/tmp", "/tmp2"]),
|
||||||
|
&workspace_3,
|
||||||
// #[test]
|
);
|
||||||
// fn test_empty_worktrees() {
|
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
|
||||||
// let db = Db::open_in_memory("test_empty_worktrees");
|
assert_eq!(
|
||||||
|
db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
|
||||||
// assert_eq!(None, db.workspace::<String>(&[]));
|
.unwrap(),
|
||||||
|
workspace_3
|
||||||
// db.make_new_workspace::<String>(&[]); //ID 1
|
);
|
||||||
// db.make_new_workspace::<String>(&[]); //ID 2
|
}
|
||||||
// db.update_worktrees(&WorkspaceId(1), &["/tmp", "/tmp2"]);
|
|
||||||
|
|
||||||
// // Sanity check
|
|
||||||
// assert_eq!(db.workspace(&["/tmp", "/tmp2"]).unwrap().0, WorkspaceId(1));
|
|
||||||
|
|
||||||
// db.update_worktrees::<String>(&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::<String>(&[]), None,);
|
|
||||||
|
|
||||||
// assert_eq!(db.last_workspace().unwrap().0, WorkspaceId(1));
|
|
||||||
|
|
||||||
// assert_eq!(
|
|
||||||
// db.recent_workspaces(2),
|
|
||||||
// vec![Vec::<PathBuf>::new(), Vec::<PathBuf>::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::<String>(&[]);
|
|
||||||
// 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::<String>(&[]);
|
|
||||||
// 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::<String>(&[]);
|
|
||||||
// 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")]
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
// use std::{
|
use anyhow::{Context, Result};
|
||||||
// ffi::OsStr,
|
use indoc::indoc;
|
||||||
// fmt::Display,
|
use sqlez::migrations::Migration;
|
||||||
// hash::Hash,
|
|
||||||
// os::unix::prelude::OsStrExt,
|
|
||||||
// path::{Path, PathBuf},
|
|
||||||
// sync::Arc,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// use anyhow::Result;
|
use crate::{
|
||||||
|
model::{ItemId, PaneId, SerializedItem, SerializedItemKind, WorkspaceId},
|
||||||
|
Db,
|
||||||
|
};
|
||||||
// use collections::HashSet;
|
// use collections::HashSet;
|
||||||
// use rusqlite::{named_params, params, types::FromSql};
|
// use rusqlite::{named_params, params, types::FromSql};
|
||||||
|
|
||||||
|
@ -65,45 +63,61 @@
|
||||||
// ) STRICT;
|
// ) STRICT;
|
||||||
// ";
|
// ";
|
||||||
|
|
||||||
// enum SerializedItemKind {
|
pub(crate) const ITEM_MIGRATIONS: Migration = Migration::new(
|
||||||
// Editor,
|
"item",
|
||||||
// Diagnostics,
|
&[indoc! {"
|
||||||
// ProjectSearch,
|
CREATE TABLE items(
|
||||||
// Terminal,
|
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 {
|
impl Db {
|
||||||
// kind: SerializedItemKind,
|
pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
|
||||||
// item_id: usize,
|
Ok(self
|
||||||
// path: Option<Arc<Path>>,
|
.prepare(indoc! {"
|
||||||
// query: Option<String>,
|
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(crate) fn save_items(
|
||||||
// pub enum SerializedItem {
|
&self,
|
||||||
// Editor { item_id: usize, path: Arc<Path> },
|
workspace_id: &WorkspaceId,
|
||||||
// Diagnostics { item_id: usize },
|
pane_id: PaneId,
|
||||||
// ProjectSearch { item_id: usize, query: String },
|
items: &[SerializedItem],
|
||||||
// Terminal { item_id: usize },
|
) -> 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 {
|
insert_new
|
||||||
// pub fn item_id(&self) -> usize {
|
.with_bindings((item.item_id(), workspace_id, pane_id, item.kind(), position))?
|
||||||
// match self {
|
.exec()?;
|
||||||
// SerializedItem::Editor { item_id, .. } => *item_id,
|
}
|
||||||
// SerializedItem::Diagnostics { item_id } => *item_id,
|
|
||||||
// SerializedItem::ProjectSearch { item_id, .. } => *item_id,
|
|
||||||
// SerializedItem::Terminal { item_id } => *item_id,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Db {
|
Ok(())
|
||||||
// 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) {}
|
|
||||||
// }
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
|
@ -8,8 +11,14 @@ use sqlez::{
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct WorkspaceId(pub(crate) Vec<PathBuf>);
|
pub(crate) struct WorkspaceId(Vec<PathBuf>);
|
||||||
|
|
||||||
|
impl WorkspaceId {
|
||||||
|
pub fn paths(self) -> Vec<PathBuf> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceId {
|
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceId {
|
||||||
fn from(iterator: T) -> Self {
|
fn from(iterator: T) -> Self {
|
||||||
|
@ -74,7 +83,7 @@ impl Column for DockAnchor {
|
||||||
|
|
||||||
pub(crate) type WorkspaceRow = (WorkspaceId, DockAnchor, bool);
|
pub(crate) type WorkspaceRow = (WorkspaceId, DockAnchor, bool);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct SerializedWorkspace {
|
pub struct SerializedWorkspace {
|
||||||
pub dock_anchor: DockAnchor,
|
pub dock_anchor: DockAnchor,
|
||||||
pub dock_visible: bool,
|
pub dock_visible: bool,
|
||||||
|
@ -82,19 +91,134 @@ pub struct SerializedWorkspace {
|
||||||
pub dock_pane: SerializedPane,
|
pub dock_pane: SerializedPane,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Default)]
|
||||||
pub struct SerializedPaneGroup {
|
pub struct SerializedPaneGroup {
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
children: Vec<SerializedPaneGroup>,
|
children: Vec<SerializedPaneGroup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl SerializedPaneGroup {
|
||||||
pub struct SerializedPane {
|
pub fn new() -> Self {
|
||||||
_children: Vec<SerializedItem>,
|
SerializedPaneGroup {
|
||||||
|
axis: Axis::Horizontal,
|
||||||
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq, Default)]
|
||||||
pub enum SerializedItemKind {}
|
pub struct SerializedPane {
|
||||||
|
pub(crate) children: Vec<SerializedItem>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl SerializedPane {
|
||||||
pub enum SerializedItem {}
|
pub fn new(children: Vec<SerializedItem>) -> 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<i32> {
|
||||||
|
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<Path> },
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use indoc::indoc;
|
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::{
|
use super::{
|
||||||
model::{SerializedPaneGroup, WorkspaceId},
|
model::{SerializedPaneGroup, WorkspaceId},
|
||||||
|
@ -19,79 +20,31 @@ pub(crate) const PANE_MIGRATIONS: Migration = Migration::new(
|
||||||
axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal'
|
axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal'
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY(parent_group) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
FOREIGN KEY(parent_group) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||||
PRIMARY KEY(group_id, workspace_id)
|
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
CREATE TABLE panes(
|
CREATE TABLE panes(
|
||||||
pane_id INTEGER PRIMARY KEY,
|
pane_id INTEGER PRIMARY KEY,
|
||||||
workspace_id BLOB NOT NULL,
|
workspace_id BLOB NOT NULL,
|
||||||
group_id INTEGER, -- If null, this is a dock pane
|
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(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY(group_id) REFERENCES pane_groups(group_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;
|
) STRICT;
|
||||||
"}],
|
"}],
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
pub(crate) fn get_center_group(&self, _workspace: &WorkspaceId) -> SerializedPaneGroup {
|
pub(crate) fn get_center_group(
|
||||||
unimplemented!()
|
&self,
|
||||||
|
_workspace_id: &WorkspaceId,
|
||||||
|
) -> Result<SerializedPaneGroup> {
|
||||||
|
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<Item = PaneGroupChildRow> {
|
|
||||||
// Vec::new().into_iter()
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub(crate) fn save_center_group(
|
pub(crate) fn save_center_group(
|
||||||
_workspace: &WorkspaceId,
|
&self,
|
||||||
|
_workspace_id: &WorkspaceId,
|
||||||
_center_pane_group: &SerializedPaneGroup,
|
_center_pane_group: &SerializedPaneGroup,
|
||||||
_connection: &Connection,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Delete the center pane group for this workspace and any of its children
|
// Delete the center pane group for this workspace and any of its children
|
||||||
// Generate new pane group IDs as we go through
|
// Generate new pane group IDs as we go through
|
||||||
|
@ -99,51 +52,86 @@ impl Db {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_dock_pane(&self, _workspace: &WorkspaceId) -> Option<SerializedPane> {
|
pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
|
||||||
unimplemented!()
|
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::<PaneId>()?;
|
||||||
|
|
||||||
|
Ok(SerializedPane::new(
|
||||||
|
self.get_items(pane_id).context("Reading items")?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn save_dock_pane(
|
pub(crate) fn save_dock_pane(
|
||||||
_workspace: &WorkspaceId,
|
&self,
|
||||||
_dock_pane: &SerializedPane,
|
workspace: &WorkspaceId,
|
||||||
_connection: &Connection,
|
dock_pane: &SerializedPane,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// iife!({
|
self.save_pane(workspace, &dock_pane, None)
|
||||||
// self.prepare(
|
}
|
||||||
// "INSERT INTO dock_panes (workspace_id, anchor_position, visible) VALUES (?, ?, ?);",
|
|
||||||
// )?
|
pub(crate) fn save_pane(
|
||||||
// .with_bindings(dock_pane.to_row(workspace))?
|
&self,
|
||||||
// .insert()
|
workspace_id: &WorkspaceId,
|
||||||
// })
|
pane: &SerializedPane,
|
||||||
// .log_err();
|
parent: Option<(GroupId, usize)>,
|
||||||
Ok(())
|
) -> 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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]
|
#[test]
|
||||||
// fn test_basic_dock_pane() {
|
fn test_basic_dock_pane() {
|
||||||
// let db = Db::open_in_memory("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 {
|
let workspace = default_workspace(dock_pane, SerializedPaneGroup::new());
|
||||||
// anchor_position: DockAnchor::Expanded,
|
|
||||||
// visible: true,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// 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]
|
// #[test]
|
||||||
// fn test_dock_simple_split() {
|
// fn test_dock_simple_split() {
|
||||||
|
|
|
@ -178,8 +178,29 @@ impl<T1: Column, T2: Column, T3: Column, T4: Column> Column for (T1, T2, T3, T4)
|
||||||
let (first, next_index) = T1::column(statement, start_index)?;
|
let (first, next_index) = T1::column(statement, start_index)?;
|
||||||
let (second, next_index) = T2::column(statement, next_index)?;
|
let (second, next_index) = T2::column(statement, next_index)?;
|
||||||
let (third, next_index) = T3::column(statement, next_index)?;
|
let (third, next_index) = T3::column(statement, next_index)?;
|
||||||
let (forth, next_index) = T4::column(statement, next_index)?;
|
let (fourth, next_index) = T4::column(statement, next_index)?;
|
||||||
Ok(((first, second, third, forth), next_index))
|
Ok(((first, second, third, fourth), next_index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind, T5: Bind> Bind for (T1, T2, T3, T4, T5) {
|
||||||
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
|
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<T1: Column, T2: Column, T3: Column, T4: Column, T5: Column> 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,24 +99,14 @@ impl Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn last_error(&self) -> Result<()> {
|
pub(crate) fn last_error(&self) -> Result<()> {
|
||||||
unsafe { error_to_result(sqlite3_errcode(self.sqlite3)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Connection {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe { sqlite3_close(self.sqlite3) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn error_to_result(code: std::os::raw::c_int) -> Result<()> {
|
|
||||||
const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW];
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let code = sqlite3_errcode(self.sqlite3);
|
||||||
|
const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW];
|
||||||
if NON_ERROR_CODES.contains(&code) {
|
if NON_ERROR_CODES.contains(&code) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = sqlite3_errstr(code);
|
let message = sqlite3_errmsg(self.sqlite3);
|
||||||
let message = if message.is_null() {
|
let message = if message.is_null() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,6 +123,13 @@ pub(crate) fn error_to_result(code: std::os::raw::c_int) -> Result<()> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Connection {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { sqlite3_close(self.sqlite3) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
@ -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]
|
#[test]
|
||||||
fn backup_works() {
|
fn backup_works() {
|
||||||
let connection1 = Connection::open_memory("backup_works");
|
let connection1 = Connection::open_memory("backup_works");
|
||||||
|
|
|
@ -8,11 +8,11 @@ impl Connection {
|
||||||
// point is released.
|
// point is released.
|
||||||
pub fn with_savepoint<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<R>
|
pub fn with_savepoint<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<R>
|
||||||
where
|
where
|
||||||
F: FnOnce(&Connection) -> Result<R>,
|
F: FnOnce() -> Result<R>,
|
||||||
{
|
{
|
||||||
let name = name.as_ref().to_owned();
|
let name = name.as_ref().to_owned();
|
||||||
self.exec(format!("SAVEPOINT {}", &name))?;
|
self.exec(format!("SAVEPOINT {}", &name))?;
|
||||||
let result = f(self);
|
let result = f();
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.exec(format!("RELEASE {}", name))?;
|
self.exec(format!("RELEASE {}", name))?;
|
||||||
|
@ -30,11 +30,11 @@ impl Connection {
|
||||||
// point is released.
|
// point is released.
|
||||||
pub fn with_savepoint_rollback<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<Option<R>>
|
pub fn with_savepoint_rollback<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<Option<R>>
|
||||||
where
|
where
|
||||||
F: FnOnce(&Connection) -> Result<Option<R>>,
|
F: FnOnce() -> Result<Option<R>>,
|
||||||
{
|
{
|
||||||
let name = name.as_ref().to_owned();
|
let name = name.as_ref().to_owned();
|
||||||
self.exec(format!("SAVEPOINT {}", &name))?;
|
self.exec(format!("SAVEPOINT {}", &name))?;
|
||||||
let result = f(self);
|
let result = f();
|
||||||
match result {
|
match result {
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
self.exec(format!("RELEASE {}", name))?;
|
self.exec(format!("RELEASE {}", name))?;
|
||||||
|
@ -69,21 +69,21 @@ mod tests {
|
||||||
let save1_text = "test save1";
|
let save1_text = "test save1";
|
||||||
let save2_text = "test save2";
|
let save2_text = "test save2";
|
||||||
|
|
||||||
connection.with_savepoint("first", |save1| {
|
connection.with_savepoint("first", || {
|
||||||
save1
|
connection
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
||||||
.with_bindings((save1_text, 1))?
|
.with_bindings((save1_text, 1))?
|
||||||
.exec()?;
|
.exec()?;
|
||||||
|
|
||||||
assert!(save1
|
assert!(connection
|
||||||
.with_savepoint("second", |save2| -> Result<Option<()>, anyhow::Error> {
|
.with_savepoint("second", || -> Result<Option<()>, anyhow::Error> {
|
||||||
save2
|
connection
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
||||||
.with_bindings((save2_text, 2))?
|
.with_bindings((save2_text, 2))?
|
||||||
.exec()?;
|
.exec()?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
save2
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
||||||
.rows::<String>()?,
|
.rows::<String>()?,
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
|
@ -95,20 +95,20 @@ mod tests {
|
||||||
.is_some());
|
.is_some());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
save1
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
||||||
.rows::<String>()?,
|
.rows::<String>()?,
|
||||||
vec![save1_text],
|
vec![save1_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
save1.with_savepoint_rollback::<(), _>("second", |save2| {
|
connection.with_savepoint_rollback::<(), _>("second", || {
|
||||||
save2
|
connection
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
||||||
.with_bindings((save2_text, 2))?
|
.with_bindings((save2_text, 2))?
|
||||||
.exec()?;
|
.exec()?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
save2
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
||||||
.rows::<String>()?,
|
.rows::<String>()?,
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
|
@ -118,20 +118,20 @@ mod tests {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
save1
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
||||||
.rows::<String>()?,
|
.rows::<String>()?,
|
||||||
vec![save1_text],
|
vec![save1_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
save1.with_savepoint_rollback("second", |save2| {
|
connection.with_savepoint_rollback("second", || {
|
||||||
save2
|
connection
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
||||||
.with_bindings((save2_text, 2))?
|
.with_bindings((save2_text, 2))?
|
||||||
.exec()?;
|
.exec()?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
save2
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
||||||
.rows::<String>()?,
|
.rows::<String>()?,
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
|
@ -141,7 +141,7 @@ mod tests {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
save1
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
||||||
.rows::<String>()?,
|
.rows::<String>()?,
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
|
|
|
@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use libsqlite3_sys::*;
|
use libsqlite3_sys::*;
|
||||||
|
|
||||||
use crate::bindable::{Bind, Column};
|
use crate::bindable::{Bind, Column};
|
||||||
use crate::connection::{error_to_result, Connection};
|
use crate::connection::Connection;
|
||||||
|
|
||||||
pub struct Statement<'a> {
|
pub struct Statement<'a> {
|
||||||
raw_statement: *mut sqlite3_stmt,
|
raw_statement: *mut sqlite3_stmt,
|
||||||
|
@ -48,7 +48,9 @@ impl<'a> Statement<'a> {
|
||||||
0 as *mut _,
|
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)
|
Ok(statement)
|
||||||
|
@ -309,10 +311,7 @@ impl<'a> Statement<'a> {
|
||||||
|
|
||||||
impl<'a> Drop for Statement<'a> {
|
impl<'a> Drop for Statement<'a> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe { sqlite3_finalize(self.raw_statement) };
|
||||||
let error = sqlite3_finalize(self.raw_statement);
|
|
||||||
error_to_result(error).expect("failed error");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,4 +351,41 @@ mod test {
|
||||||
let mut read = connection1.prepare("SELECT * FROM blobs;").unwrap();
|
let mut read = connection1.prepare("SELECT * FROM blobs;").unwrap();
|
||||||
assert_eq!(read.step().unwrap(), StepResult::Done);
|
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::<String>()
|
||||||
|
.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::<String>()
|
||||||
|
.unwrap(),
|
||||||
|
Some(text_to_insert.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,6 +204,16 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<T, U>(option: Option<(T, U)>) -> (Option<T>, Option<U>) {
|
||||||
|
match option {
|
||||||
|
Some((a, b)) => (Some(a), Some(b)),
|
||||||
|
None => (None, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! iife {
|
macro_rules! iife {
|
||||||
($block:block) => {
|
($block:block) => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue