Introduce del implemented only for default keys; includes tests

This commit is contained in:
Josh Hansen 2023-08-22 17:29:46 -07:00
parent 26d777e483
commit 7ad5563c12
8 changed files with 329 additions and 47 deletions

View file

@ -6,11 +6,12 @@ use rustyline::DefaultEditor;
use thiserror::Error; use thiserror::Error;
use ghee::{ use ghee::{
cmd::{cp_or_mv, get, idx, init, ins, ls, rm, set, CopyOrMove}, cmd::{cp_or_mv, del, get, idx, init, ins, ls, rm, set, CopyOrMove},
parser::{ parser::{
assignment::{Assignment, AssignmentParser}, assignment::{Assignment, AssignmentParser},
key::Key, key::Key,
predicate::{Predicate, PredicateParser}, predicate::{Predicate, PredicateParser},
value::Value,
xattr::Xattr, xattr::Xattr,
}, },
APP_NAME, PKG_NAME, XDG_DIRS, APP_NAME, PKG_NAME, XDG_DIRS,
@ -101,6 +102,18 @@ enum Commands {
verbose: bool, verbose: bool,
}, },
/// Remove record from a table, updating related indices
Del {
#[arg(help = "Path of the table to delete from")]
table_path: PathBuf,
#[arg(name = "where", short, long, help = "WHERE clauses specifying what to delete", value_parser = ValueParser::new(PredicateParser{}))]
where_: Vec<Predicate>,
#[arg(help = "Primary key values, subkeys space separated")]
key: Vec<Value>,
#[arg(short, long)]
verbose: bool,
},
/// Index tables /// Index tables
Idx { Idx {
#[arg(help = "Path to recursively index")] #[arg(help = "Path to recursively index")]
@ -255,6 +268,16 @@ fn run_command(cmd: &Commands) {
) )
}); });
} }
Commands::Del {
table_path,
where_,
key,
verbose,
} => {
del(table_path, where_, key, *verbose).unwrap_or_else(|e| {
panic!("Error deleting record from {}: {}", table_path.display(), e)
});
}
Commands::Idx { Commands::Idx {
src, src,
dest, dest,

199
src/cmd/del.rs Normal file
View file

@ -0,0 +1,199 @@
use anyhow::Result;
use thiserror::Error;
use walkdir::WalkDir;
use crate::{
get_index_info, get_key, list_xattrs,
parser::{key::Key, predicate::Predicate, relation::Relation, value::Value, xattr::Xattr},
xattr_values,
};
use std::{collections::HashMap, fs::remove_file, io::Write, path::PathBuf};
#[derive(Error, Debug)]
pub enum DelErr {
#[error("Key not found at {0}")]
KeyNotFound(PathBuf),
#[error("Path {0} not found")]
PathNotFound(PathBuf),
#[error("An IO error occurred: {0}")]
IoError(std::io::Error),
#[error("A JSON error occurred: {0}")]
JsonError(serde_json::Error),
#[error("Raw (unnamed) json value not supported: {0}")]
RawJsonValue(serde_json::Value),
#[error(
"Either where clauses or values for the table's primary key should be provided, not both"
)]
WhereClausesOrKeyValuesNotBoth,
#[error(
"Incorrect number of primary key values provided; expected {expected} but got {provided}"
)]
WrongNumberOfPrimaryKeyValues { expected: usize, provided: usize },
}
/// Delete all instantiations of a record across tables
fn unlink_record(
indices: &HashMap<Key, PathBuf>,
record_path: &PathBuf,
verbose: bool,
) -> Result<()> {
let record = xattr_values(record_path)?;
for (key, path) in indices {
let key_value = key.value_for_record(&record)?;
let record_path = {
let mut path = path.clone();
for value in key_value {
path.push(value.to_string());
}
path
};
remove_file(&record_path).map_err(DelErr::IoError)?;
if verbose {
eprintln!("Removed {}", record_path.display());
}
}
Ok(())
}
pub fn del(
table_path: &PathBuf,
where_: &Vec<Predicate>,
key: &Vec<Value>,
verbose: bool,
) -> Result<()> {
if !(where_.is_empty() ^ key.is_empty()) {
return Err(DelErr::WhereClausesOrKeyValuesNotBoth.into());
}
let info = get_index_info(table_path)?;
let main_key = get_key(table_path)?.ok_or(DelErr::KeyNotFound(table_path.clone()))?;
let all_indices = {
let mut indices = info.related_indices.clone();
indices.insert(main_key.clone(), table_path.clone());
indices
};
eprintln!("All indices: {:?}", all_indices);
if key.is_empty() {
debug_assert!(!where_.is_empty());
panic!("WHERE not implemented");
} else {
if main_key.subkeys.len() != key.len() {
return Err(DelErr::WrongNumberOfPrimaryKeyValues {
expected: main_key.subkeys.len(),
provided: key.len(),
}
.into());
}
// An individual record is uniquely identified by the provided primary key values
let record_path = {
let mut path = table_path.clone();
for subkey_value in key {
path.push(subkey_value.to_string());
}
path
};
unlink_record(&all_indices, &record_path, verbose)?;
}
Ok(())
}
#[cfg(test)]
mod test {
use crate::{parser::value::Value, record_count, test_support::Scenario};
use super::del;
#[test]
fn test_del() {
{
// Delete from dir1 using default key
let s: Scenario = Default::default();
{
let dir1count = record_count(&s.dir1);
let dir2count = record_count(&s.dir2);
assert_eq!(dir1count, 1);
assert_eq!(dir2count, 1);
}
del(&s.dir1, &vec![], &vec![Value::Number(0f64)], true).unwrap();
{
let dir1count = record_count(&s.dir1);
let dir2count = record_count(&s.dir2);
assert_eq!(dir1count, 0);
assert_eq!(dir2count, 0);
}
}
{
// Delete from dir1 using bogus key
let s: Scenario = Default::default();
del(&s.dir1, &vec![], &vec![], false)
.expect_err("failed due to too few key components");
del(
&s.dir1,
&vec![],
&vec![Value::String("abc".into()), Value::Number(-10f64)],
false,
)
.expect_err("failed due to too many key components");
}
{
// Delete from dir2 using default key
let s: Scenario = Default::default();
{
let dir1count = record_count(&s.dir1);
let dir2count = record_count(&s.dir2);
assert_eq!(dir1count, 1);
assert_eq!(dir2count, 1);
}
del(
&s.dir2,
&vec![],
&vec![Value::Number(1f64), Value::Number(2f64)],
true,
)
.unwrap();
{
let dir1count = record_count(&s.dir1);
let dir2count = record_count(&s.dir2);
assert_eq!(dir1count, 0);
assert_eq!(dir2count, 0);
}
}
{
// Delete from dir1 using predicate
let s: Scenario = Default::default();
//TODO
}
{
// Delete from dir2 using predicate
let s: Scenario = Default::default();
//TODO
}
}
}

View file

@ -120,51 +120,16 @@ pub fn ins(table_path: &PathBuf, records_path: &Option<PathBuf>, verbose: bool)
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{fs::File, io::Write}; use crate::{parser::value::Value, test_support::Scenario, xattr_values};
use tempdir::TempDir;
use crate::{
cmd::{idx, init},
parser::{key::Key, value::Value},
xattr_values,
};
use super::ins;
#[test] #[test]
fn test_ins() { fn test_ins() {
let records_dir = TempDir::new("ghee-test-ins:records").unwrap().into_path(); let s: Scenario = Default::default();
let mut records_path = records_dir.clone();
records_path.push("records.json");
{ let mut record_path = s.dir1.clone();
let mut w = File::create(&records_path).unwrap();
w.write_all(br#"{"test1": 0, "test2": 1, "test3": 2}"#)
.unwrap();
}
let dir1 = TempDir::new("ghee-test-ins:1").unwrap().into_path();
let dir2 = TempDir::new("ghee-test-ins:2").unwrap().into_path();
let key1 = Key::from(vec!["test1"]);
let xattr1 = key1.subkeys[0].clone();
let key2 = Key::from(vec!["test2", "test3"]);
let xattr2 = key2.subkeys[0].clone();
let xattr3 = key2.subkeys[1].clone();
init(&dir1, &key1, false).unwrap();
idx(&dir1, Some(&dir2), &key2, false);
ins(&dir1, &Some(records_path), false).unwrap();
let mut record_path = dir1.clone();
record_path.push("0"); record_path.push("0");
let mut indexed_record_path = dir2.clone(); let mut indexed_record_path = s.dir2.clone();
indexed_record_path.push("1"); indexed_record_path.push("1");
indexed_record_path.push("2"); indexed_record_path.push("2");
@ -174,15 +139,15 @@ mod test {
let xattrs = xattr_values(&record_path).unwrap(); let xattrs = xattr_values(&record_path).unwrap();
assert_eq!(xattrs.len(), 3); assert_eq!(xattrs.len(), 3);
assert_eq!(xattrs[&xattr1].clone(), Value::Number(0f64)); assert_eq!(xattrs[&s.xattr1].clone(), Value::Number(0f64));
assert_eq!(xattrs[&xattr2].clone(), Value::Number(1f64)); assert_eq!(xattrs[&s.xattr2].clone(), Value::Number(1f64));
assert_eq!(xattrs[&xattr3].clone(), Value::Number(2f64)); assert_eq!(xattrs[&s.xattr3].clone(), Value::Number(2f64));
let indexed_xattrs = xattr_values(&indexed_record_path).unwrap(); let indexed_xattrs = xattr_values(&indexed_record_path).unwrap();
assert_eq!(xattrs.len(), 3); assert_eq!(xattrs.len(), 3);
assert_eq!(indexed_xattrs[&xattr1].clone(), Value::Number(0f64)); assert_eq!(indexed_xattrs[&s.xattr1].clone(), Value::Number(0f64));
assert_eq!(indexed_xattrs[&xattr2].clone(), Value::Number(1f64)); assert_eq!(indexed_xattrs[&s.xattr2].clone(), Value::Number(1f64));
assert_eq!(indexed_xattrs[&xattr3].clone(), Value::Number(2f64)); assert_eq!(indexed_xattrs[&s.xattr3].clone(), Value::Number(2f64));
} }
} }

View file

@ -1,4 +1,5 @@
mod cp_or_mv; mod cp_or_mv;
mod del;
mod get; mod get;
mod idx; mod idx;
mod init; mod init;
@ -7,6 +8,7 @@ mod ls;
mod rm; mod rm;
mod set; mod set;
pub use cp_or_mv::{cp_or_mv, CopyOrMove}; pub use cp_or_mv::{cp_or_mv, CopyOrMove};
pub use del::del;
pub use get::get; pub use get::get;
pub use idx::idx; pub use idx::idx;
pub use init::init; pub use init::init;

View file

@ -5,6 +5,8 @@ pub mod cli;
pub mod cmd; pub mod cmd;
pub mod parser; pub mod parser;
pub mod paths; pub mod paths;
#[cfg(test)]
mod test_support;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -20,6 +22,7 @@ use parser::{
use anyhow::Result; use anyhow::Result;
use thiserror::Error; use thiserror::Error;
use walkdir::{DirEntry, WalkDir};
use xdg::BaseDirectories; use xdg::BaseDirectories;
/// Uppercase the first character in a string /// Uppercase the first character in a string
@ -49,6 +52,32 @@ lazy_static! {
xdg::BaseDirectories::with_prefix(APP_NAME.as_str()).unwrap(); xdg::BaseDirectories::with_prefix(APP_NAME.as_str()).unwrap();
} }
/// Whether a path should be skipped when iterating over records
pub fn is_hidden(entry: &DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| s.starts_with(":"))
.unwrap_or(false)
}
/// The number of records recursively found in a directory
///
/// Ignores nested indexes
pub fn record_count(dir: &PathBuf) -> usize {
WalkDir::new(dir)
.into_iter()
.filter_entry(|e| !is_hidden(e))
.filter(|e| {
if let Ok(entry) = e {
entry.file_type().is_file()
} else {
false
}
})
.count()
}
// Like xattr::list but parsed into our format // Like xattr::list but parsed into our format
pub fn list_xattrs<P: AsRef<Path>>(path: P) -> Vec<Xattr> { pub fn list_xattrs<P: AsRef<Path>>(path: P) -> Vec<Xattr> {
let path: &Path = path.as_ref(); let path: &Path = path.as_ref();

View file

@ -25,7 +25,7 @@ pub enum KeyErr {
RecordNotObject, RecordNotObject,
} }
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Key { pub struct Key {
pub subkeys: Vec<Xattr>, pub subkeys: Vec<Xattr>,
} }

View file

@ -20,6 +20,9 @@ pub struct Predicate {
} }
impl Predicate { impl Predicate {
pub fn new(xattr: Xattr, relation: Relation) -> Self {
Self { xattr, relation }
}
pub fn satisfied(&self, values: &HashMap<Xattr, Value>) -> bool { pub fn satisfied(&self, values: &HashMap<Xattr, Value>) -> bool {
values values
.get(&self.xattr) .get(&self.xattr)

61
src/test_support.rs Normal file
View file

@ -0,0 +1,61 @@
use std::{fs::File, io::Write, path::PathBuf};
use tempdir::TempDir;
use crate::{
cmd::{idx, init, ins},
parser::{key::Key, xattr::Xattr},
};
pub struct Scenario {
/// Table indexed by xattr1
pub dir1: PathBuf,
/// Table indexed by (xattr2,xattr3)
pub dir2: PathBuf,
pub key1: Key,
pub key2: Key,
pub xattr1: Xattr,
pub xattr2: Xattr,
pub xattr3: Xattr,
}
impl Default for Scenario {
fn default() -> Self {
let records_dir = TempDir::new("ghee-test-ins:records").unwrap().into_path();
let mut records_path = records_dir.clone();
records_path.push("records.json");
{
let mut w = File::create(&records_path).unwrap();
w.write_all(br#"{"test1": 0, "test2": 1, "test3": 2}"#)
.unwrap();
}
let dir1 = TempDir::new("ghee-test-ins:1").unwrap().into_path();
let dir2 = TempDir::new("ghee-test-ins:2").unwrap().into_path();
let key1 = Key::from(vec!["test1"]);
let xattr1 = key1.subkeys[0].clone();
let key2 = Key::from(vec!["test2", "test3"]);
let xattr2 = key2.subkeys[0].clone();
let xattr3 = key2.subkeys[1].clone();
init(&dir1, &key1, false).unwrap();
idx(&dir1, Some(&dir2), &key2, false);
ins(&dir1, &Some(records_path), false).unwrap();
Self {
dir1,
dir2,
key1,
key2,
xattr1,
xattr2,
xattr3,
}
}
}