All workspace tests passing :D

This commit is contained in:
Mikayla Maki 2022-10-25 15:27:51 -07:00
parent d7bbfb82a3
commit e9ea751f3d
2 changed files with 261 additions and 156 deletions

View file

@ -1,4 +1,4 @@
use std::{fs::File, path::Path}; use std::{fs::File, path::Path, thread::sleep, time::Duration};
const TEST_FILE: &'static str = "test-db.db"; const TEST_FILE: &'static str = "test-db.db";
@ -23,20 +23,28 @@ fn main() -> anyhow::Result<()> {
let workspace_6 = db.workspace_for_worktree_roots(&[]); let workspace_6 = db.workspace_for_worktree_roots(&[]);
let workspace_7 = db.workspace_for_worktree_roots(&[]); let workspace_7 = db.workspace_for_worktree_roots(&[]);
// Order scrambled + sleeps added because sqlite only has 1 second resolution on
// their timestamps
db.update_worktree_roots(&workspace_7.workspace_id, &["/tmp2"])
.unwrap();
sleep(Duration::from_secs(1));
db.update_worktree_roots(&workspace_1.workspace_id, &["/tmp1"]) db.update_worktree_roots(&workspace_1.workspace_id, &["/tmp1"])
.unwrap(); .unwrap();
sleep(Duration::from_secs(1));
db.update_worktree_roots(&workspace_2.workspace_id, &["/tmp1", "/tmp2"]) db.update_worktree_roots(&workspace_2.workspace_id, &["/tmp1", "/tmp2"])
.unwrap(); .unwrap();
sleep(Duration::from_secs(1));
db.update_worktree_roots(&workspace_3.workspace_id, &["/tmp1", "/tmp2", "/tmp3"]) db.update_worktree_roots(&workspace_3.workspace_id, &["/tmp1", "/tmp2", "/tmp3"])
.unwrap(); .unwrap();
sleep(Duration::from_secs(1));
db.update_worktree_roots(&workspace_4.workspace_id, &["/tmp2", "/tmp3"]) db.update_worktree_roots(&workspace_4.workspace_id, &["/tmp2", "/tmp3"])
.unwrap(); .unwrap();
sleep(Duration::from_secs(1));
db.update_worktree_roots(&workspace_5.workspace_id, &["/tmp2", "/tmp3", "/tmp4"]) db.update_worktree_roots(&workspace_5.workspace_id, &["/tmp2", "/tmp3", "/tmp4"])
.unwrap(); .unwrap();
sleep(Duration::from_secs(1));
db.update_worktree_roots(&workspace_6.workspace_id, &["/tmp2", "/tmp4"]) db.update_worktree_roots(&workspace_6.workspace_id, &["/tmp2", "/tmp4"])
.unwrap(); .unwrap();
db.update_worktree_roots(&workspace_7.workspace_id, &["/tmp2"])
.unwrap();
db.write_to(file).ok(); db.write_to(file).ok();

View file

@ -1,23 +1,31 @@
use anyhow::Result; use anyhow::Result;
use rusqlite::params; use rusqlite::{params, Connection};
use std::{path::Path, sync::Arc}; use std::{
path::{Path, PathBuf},
sync::Arc,
};
use crate::pane::{PaneGroupId, PaneId, SerializedPane, SerializedPaneGroup}; use crate::pane::{PaneGroupId, PaneId, SerializedPane, SerializedPaneGroup};
use super::Db; use super::Db;
// TODO for workspace serialization:
// - Update return types to unwrap all of the results into dummy values
// - On database failure to initialize, delete the DB file
// - Update paths to be blobs ( :( https://users.rust-lang.org/t/how-to-safely-store-a-path-osstring-in-a-sqllite-database/79712/10 )
// - Convert hot paths to prepare-cache-execute style
pub(crate) const WORKSPACE_M_1: &str = " pub(crate) const WORKSPACE_M_1: &str = "
CREATE TABLE workspaces( CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP, timestamp TEXT DEFAULT CURRENT_TIMESTAMP
dummy_data INTEGER
) STRICT; ) STRICT;
CREATE TABLE worktree_roots( CREATE TABLE worktree_roots(
worktree_root TEXT NOT NULL, --TODO: Update this to use blobs worktree_root TEXT NOT NULL,
workspace_id INTEGER NOT NULL, workspace_id INTEGER NOT NULL,
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
PRIMARY KEY(worktree_root, workspace_id) PRIMARY KEY(worktree_root, workspace_id)
) STRICT; ) STRICT;
"; ";
@ -76,7 +84,7 @@ impl Db {
self.real() self.real()
.map(|db| { .map(|db| {
let lock = db.connection.lock(); let lock = db.connection.lock();
match lock.execute("INSERT INTO workspaces(dummy_data) VALUES(1);", []) { match lock.execute("INSERT INTO workspaces DEFAULT VALUES;", []) {
Ok(_) => SerializedWorkspace { Ok(_) => SerializedWorkspace {
workspace_id: WorkspaceId(lock.last_insert_rowid()), workspace_id: WorkspaceId(lock.last_insert_rowid()),
}, },
@ -94,6 +102,117 @@ impl Db {
.map(|db| { .map(|db| {
let lock = db.connection.lock(); let lock = db.connection.lock();
get_workspace_id(worktree_roots, &lock)
})
.unwrap_or(Ok(None))
}
// fn get_workspace_row(&self, workspace_id: WorkspaceId) -> WorkspaceRow {
// unimplemented!()
// }
/// Updates the open paths for the given workspace id. Will garbage collect items from
/// any workspace ids which are no replaced by the new workspace id. Updates the timestamps
/// in the workspace id table
pub fn update_worktree_roots<P>(
&self,
workspace_id: &WorkspaceId,
worktree_roots: &[P],
) -> Result<()>
where
P: AsRef<Path>,
{
self.real()
.map(|db| {
let mut lock = db.connection.lock();
let tx = lock.transaction()?;
{
// Lookup any old WorkspaceIds which have the same set of roots, and delete them.
let preexisting_id = get_workspace_id(worktree_roots, &tx)?;
if let Some(preexisting_id) = preexisting_id {
if preexisting_id != *workspace_id {
// Should also delete fields in other tables
tx.execute(
"DELETE FROM workspaces WHERE workspace_id = ?",
[preexisting_id.0],
)?;
}
}
tx.execute(
"DELETE FROM worktree_roots WHERE workspace_id = ?",
[workspace_id.0],
)?;
for root in worktree_roots {
// TODO: Update this to use blobs
let path = root.as_ref().to_string_lossy().to_string();
let mut stmt = tx.prepare_cached("INSERT INTO worktree_roots(workspace_id, worktree_root) VALUES (?, ?)")?;
stmt.execute(params![workspace_id.0, path])?;
}
let mut stmt = tx.prepare_cached("UPDATE workspaces SET timestamp = CURRENT_TIMESTAMP WHERE workspace_id = ?")?;
stmt.execute([workspace_id.0])?;
}
tx.commit()?;
Ok(())
})
.unwrap_or(Ok(()))
}
/// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
pub fn recent_workspaces(&self, limit: usize) -> Result<Vec<(WorkspaceId, Vec<Arc<Path>>)>> {
// Return all the workspace ids and their associated paths ordered by the access timestamp
//ORDER BY timestamps
self.real()
.map(|db| {
let mut lock = db.connection.lock();
let tx = lock.transaction()?;
let result = {
let mut stmt = tx.prepare_cached(
"SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
)?;
let workspace_ids = stmt
.query_map([limit], |row| Ok(WorkspaceId(row.get(0)?)))?
.collect::<Result<Vec<_>, rusqlite::Error>>()?;
let mut result = Vec::new();
let mut stmt = tx.prepare_cached(
"SELECT worktree_root FROM worktree_roots WHERE workspace_id = ?",
)?;
for workspace_id in workspace_ids {
let roots = stmt
.query_map([workspace_id.0], |row| {
let row = row.get::<_, String>(0)?;
Ok(PathBuf::from(Path::new(&row)).into())
})?
.collect::<Result<Vec<_>, rusqlite::Error>>()?;
result.push((workspace_id, roots))
}
result
};
tx.commit()?;
return Ok(result);
})
.unwrap_or_else(|| Ok(Vec::new()))
}
}
fn get_workspace_id<P>(
worktree_roots: &[P],
connection: &Connection,
) -> Result<Option<WorkspaceId>, anyhow::Error>
where
P: AsRef<Path>,
{
// Prepare the array binding string. SQL doesn't have syntax for this, so // Prepare the array binding string. SQL doesn't have syntax for this, so
// we have to do it ourselves. // we have to do it ourselves.
let mut array_binding_stmt = "(".to_string(); let mut array_binding_stmt = "(".to_string();
@ -105,7 +224,6 @@ impl Db {
} }
} }
array_binding_stmt.push(')'); array_binding_stmt.push(')');
// Any workspace can have multiple independent paths, and these paths // Any workspace can have multiple independent paths, and these paths
// can overlap in the database. Take this test data for example: // can overlap in the database. Take this test data for example:
// //
@ -165,9 +283,7 @@ impl Db {
"#, "#,
array_bind = array_binding_stmt array_bind = array_binding_stmt
); );
let mut stmt = connection.prepare_cached(&query)?;
let mut stmt = lock.prepare_cached(&query)?;
// Make sure we bound the parameters correctly // Make sure we bound the parameters correctly
debug_assert!(worktree_roots.len() + 1 == stmt.parameter_count()); debug_assert!(worktree_roots.len() + 1 == stmt.parameter_count());
@ -181,67 +297,24 @@ impl Db {
let mut rows = stmt.raw_query(); let mut rows = stmt.raw_query();
if let Ok(Some(row)) = rows.next() { if let Ok(Some(row)) = rows.next() {
return Ok(Some(WorkspaceId(row.get(0)?))) return Ok(Some(WorkspaceId(row.get(0)?)));
} }
// Ensure that this query only returns one row. The PRIMARY KEY constraint should catch this case
// Ensure that this query only returns one row // but this is here to catch it if someone refactors that constraint out.
debug_assert!(matches!(rows.next(), Ok(None))); debug_assert!(matches!(rows.next(), Ok(None)));
Ok(None) Ok(None)
})
.unwrap_or(Ok(None))
}
fn get_workspace_row(&self, workspace_id: WorkspaceId) -> WorkspaceRow {
unimplemented!()
}
/// Updates the open paths for the given workspace id. Will garbage collect items from
/// any workspace ids which are no replaced by the new workspace id. Updates the timestamps
/// in the workspace id table
pub fn update_worktree_roots<P>(
&self,
workspace_id: &WorkspaceId,
worktree_roots: &[P],
) -> Result<()>
where
P: AsRef<Path>,
{
// Lookup any WorkspaceIds which have the same set of roots, and delete them. (NOTE: this should garbage collect other tables)
// TODO
// Remove the old rows which contain workspace_id
// TODO
// Add rows for the new worktree_roots
self.real()
.map(|db| {
let lock = db.connection.lock();
for root in worktree_roots {
// TODO: Update this to use blobs
let path = root.as_ref().to_string_lossy().to_string();
lock.execute(
"INSERT INTO worktree_roots(workspace_id, worktree_root) VALUES (?, ?)",
params![workspace_id.0, path],
)?;
}
Ok(())
})
.unwrap_or(Ok(()))
}
/// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
pub fn recent_workspaces(&self) -> Vec<(WorkspaceId, Vec<Arc<Path>>)> {
// Return all the workspace ids and their associated paths ordered by the access timestamp
//ORDER BY timestamps
unimplemented!();
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{
path::{Path, PathBuf},
sync::Arc,
thread::sleep,
time::Duration,
};
use crate::Db; use crate::Db;
use super::WorkspaceId; use super::WorkspaceId;
@ -265,32 +338,36 @@ mod tests {
db.update_worktree_roots(workspace_id, entries).unwrap(); db.update_worktree_roots(workspace_id, entries).unwrap();
} }
assert_eq!(WorkspaceId(1), db.workspace_id(&["/tmp1"]).unwrap().unwrap()); assert_eq!(Some(WorkspaceId(1)), db.workspace_id(&["/tmp1"]).unwrap());
assert_eq!( assert_eq!(
WorkspaceId(2), db.workspace_id(&["/tmp1", "/tmp2"]).unwrap(),
db.workspace_id(&["/tmp1", "/tmp2"]).unwrap().unwrap() Some(WorkspaceId(2))
); );
assert_eq!( assert_eq!(
WorkspaceId(3), db.workspace_id(&["/tmp1", "/tmp2", "/tmp3"]).unwrap(),
db.workspace_id(&["/tmp1", "/tmp2", "/tmp3"]).unwrap().unwrap() Some(WorkspaceId(3))
); );
assert_eq!( assert_eq!(
WorkspaceId(4), db.workspace_id(&["/tmp2", "/tmp3"]).unwrap(),
db.workspace_id(&["/tmp2", "/tmp3"]).unwrap().unwrap() Some(WorkspaceId(4))
); );
assert_eq!( assert_eq!(
WorkspaceId(5), db.workspace_id(&["/tmp2", "/tmp3", "/tmp4"]).unwrap(),
db.workspace_id(&["/tmp2", "/tmp3", "/tmp4"]).unwrap().unwrap() Some(WorkspaceId(5))
); );
assert_eq!( assert_eq!(
WorkspaceId(6), db.workspace_id(&["/tmp2", "/tmp4"]).unwrap(),
db.workspace_id(&["/tmp2", "/tmp4"]).unwrap().unwrap() Some(WorkspaceId(6))
); );
assert_eq!(WorkspaceId(7), db.workspace_id(&["/tmp2"]).unwrap().unwrap()); assert_eq!(db.workspace_id(&["/tmp2"]).unwrap(), Some(WorkspaceId(7)));
assert_eq!(None, db.workspace_id(&["/tmp1", "/tmp5"]).unwrap()); assert_eq!(db.workspace_id(&["/tmp1", "/tmp5"]).unwrap(), None);
assert_eq!(None, db.workspace_id(&["/tmp5"]).unwrap()); assert_eq!(db.workspace_id(&["/tmp5"]).unwrap(), None);
assert_eq!(None, db.workspace_id(&["/tmp2", "/tmp3", "/tmp4", "/tmp5"]).unwrap()); assert_eq!(
db.workspace_id(&["/tmp2", "/tmp3", "/tmp4", "/tmp5"])
.unwrap(),
None
);
} }
#[test] #[test]
@ -308,14 +385,21 @@ mod tests {
db.update_worktree_roots(workspace_id, entries).unwrap(); db.update_worktree_roots(workspace_id, entries).unwrap();
} }
assert_eq!(None, db.workspace_id(&["/tmp2"]).unwrap()); assert_eq!(db.workspace_id(&["/tmp2"]).unwrap(), None);
assert_eq!(None, db.workspace_id(&["/tmp2", "/tmp3"]).unwrap()); assert_eq!(db.workspace_id(&["/tmp2", "/tmp3"]).unwrap(), None);
assert_eq!(Some(WorkspaceId(1)), db.workspace_id(&["/tmp"]).unwrap()); assert_eq!(db.workspace_id(&["/tmp"]).unwrap(), Some(WorkspaceId(1)));
assert_eq!(Some(WorkspaceId(2)), db.workspace_id(&["/tmp", "/tmp2"]).unwrap());
assert_eq!( assert_eq!(
Some(WorkspaceId(3)), db.workspace_id(&["/tmp", "/tmp2"]).unwrap(),
db.workspace_id(&["/tmp", "/tmp2", "/tmp3"]).unwrap() Some(WorkspaceId(2))
); );
assert_eq!(
db.workspace_id(&["/tmp", "/tmp2", "/tmp3"]).unwrap(),
Some(WorkspaceId(3))
);
}
fn arc_path(path: &'static str) -> Arc<Path> {
PathBuf::from(path).into()
} }
#[test] #[test]
@ -340,25 +424,38 @@ mod tests {
let db = Db::open_in_memory(); let db = Db::open_in_memory();
// Load in the test data
for (workspace_id, entries) in data { for (workspace_id, entries) in data {
db.update_worktree_roots(workspace_id, entries).unwrap(); //?? db.workspace_for_worktree_roots(&[]);
assert_eq!(&db.workspace_id::<String>(&[]).unwrap(), &Some(*workspace_id)) db.update_worktree_roots(workspace_id, entries).unwrap();
} }
for (workspace_id, entries) in data { // Make sure the timestamp updates
assert_eq!(&db.workspace_id(entries.as_slice()).unwrap(), &Some(*workspace_id)); sleep(Duration::from_secs(1));
} // Execute the update
db.update_worktree_roots(&WorkspaceId(2), &["/tmp2", "/tmp3"])
db.update_worktree_roots(&WorkspaceId(2), &["/tmp2"])
.unwrap(); .unwrap();
// todo!(); // make sure that 3 got garbage collected
assert_eq!(db.workspace_id(&["/tmp2"]).unwrap(), Some(WorkspaceId(2))); // Make sure that workspace 3 doesn't exist
assert_eq!(
db.workspace_id(&["/tmp2", "/tmp3"]).unwrap(),
Some(WorkspaceId(2))
);
// And that workspace 1 was untouched
assert_eq!(db.workspace_id(&["/tmp"]).unwrap(), Some(WorkspaceId(1))); assert_eq!(db.workspace_id(&["/tmp"]).unwrap(), Some(WorkspaceId(1)));
let recent_workspaces = db.recent_workspaces(); // And that workspace 2 is no longer registered under this
assert_eq!(recent_workspaces.get(0).unwrap().0, WorkspaceId(2)); assert_eq!(db.workspace_id(&["/tmp", "/tmp2"]).unwrap(), None);
assert_eq!(recent_workspaces.get(1).unwrap().0, WorkspaceId(3));
assert_eq!(recent_workspaces.get(2).unwrap().0, WorkspaceId(1)); let recent_workspaces = db.recent_workspaces(10).unwrap();
assert_eq!(
recent_workspaces.get(0).unwrap(),
&(WorkspaceId(2), vec![arc_path("/tmp2"), arc_path("/tmp3")])
);
assert_eq!(
recent_workspaces.get(1).unwrap(),
&(WorkspaceId(1), vec![arc_path("/tmp")])
);
} }
} }