Introduce del
implemented only for default keys; includes tests
This commit is contained in:
parent
26d777e483
commit
7ad5563c12
8 changed files with 329 additions and 47 deletions
|
@ -6,11 +6,12 @@ use rustyline::DefaultEditor;
|
|||
use thiserror::Error;
|
||||
|
||||
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::{
|
||||
assignment::{Assignment, AssignmentParser},
|
||||
key::Key,
|
||||
predicate::{Predicate, PredicateParser},
|
||||
value::Value,
|
||||
xattr::Xattr,
|
||||
},
|
||||
APP_NAME, PKG_NAME, XDG_DIRS,
|
||||
|
@ -101,6 +102,18 @@ enum Commands {
|
|||
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
|
||||
Idx {
|
||||
#[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 {
|
||||
src,
|
||||
dest,
|
||||
|
|
199
src/cmd/del.rs
Normal file
199
src/cmd/del.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,51 +120,16 @@ pub fn ins(table_path: &PathBuf, records_path: &Option<PathBuf>, verbose: bool)
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::{
|
||||
cmd::{idx, init},
|
||||
parser::{key::Key, value::Value},
|
||||
xattr_values,
|
||||
};
|
||||
|
||||
use super::ins;
|
||||
use crate::{parser::value::Value, test_support::Scenario, xattr_values};
|
||||
|
||||
#[test]
|
||||
fn test_ins() {
|
||||
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 s: Scenario = Default::default();
|
||||
|
||||
{
|
||||
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();
|
||||
let mut record_path = s.dir1.clone();
|
||||
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("2");
|
||||
|
||||
|
@ -174,15 +139,15 @@ mod test {
|
|||
let xattrs = xattr_values(&record_path).unwrap();
|
||||
assert_eq!(xattrs.len(), 3);
|
||||
|
||||
assert_eq!(xattrs[&xattr1].clone(), Value::Number(0f64));
|
||||
assert_eq!(xattrs[&xattr2].clone(), Value::Number(1f64));
|
||||
assert_eq!(xattrs[&xattr3].clone(), Value::Number(2f64));
|
||||
assert_eq!(xattrs[&s.xattr1].clone(), Value::Number(0f64));
|
||||
assert_eq!(xattrs[&s.xattr2].clone(), Value::Number(1f64));
|
||||
assert_eq!(xattrs[&s.xattr3].clone(), Value::Number(2f64));
|
||||
|
||||
let indexed_xattrs = xattr_values(&indexed_record_path).unwrap();
|
||||
assert_eq!(xattrs.len(), 3);
|
||||
|
||||
assert_eq!(indexed_xattrs[&xattr1].clone(), Value::Number(0f64));
|
||||
assert_eq!(indexed_xattrs[&xattr2].clone(), Value::Number(1f64));
|
||||
assert_eq!(indexed_xattrs[&xattr3].clone(), Value::Number(2f64));
|
||||
assert_eq!(indexed_xattrs[&s.xattr1].clone(), Value::Number(0f64));
|
||||
assert_eq!(indexed_xattrs[&s.xattr2].clone(), Value::Number(1f64));
|
||||
assert_eq!(indexed_xattrs[&s.xattr3].clone(), Value::Number(2f64));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod cp_or_mv;
|
||||
mod del;
|
||||
mod get;
|
||||
mod idx;
|
||||
mod init;
|
||||
|
@ -7,6 +8,7 @@ mod ls;
|
|||
mod rm;
|
||||
mod set;
|
||||
pub use cp_or_mv::{cp_or_mv, CopyOrMove};
|
||||
pub use del::del;
|
||||
pub use get::get;
|
||||
pub use idx::idx;
|
||||
pub use init::init;
|
||||
|
|
29
src/lib.rs
29
src/lib.rs
|
@ -5,6 +5,8 @@ pub mod cli;
|
|||
pub mod cmd;
|
||||
pub mod parser;
|
||||
pub mod paths;
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -20,6 +22,7 @@ use parser::{
|
|||
|
||||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
/// Uppercase the first character in a string
|
||||
|
@ -49,6 +52,32 @@ lazy_static! {
|
|||
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
|
||||
pub fn list_xattrs<P: AsRef<Path>>(path: P) -> Vec<Xattr> {
|
||||
let path: &Path = path.as_ref();
|
||||
|
|
|
@ -25,7 +25,7 @@ pub enum KeyErr {
|
|||
RecordNotObject,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Key {
|
||||
pub subkeys: Vec<Xattr>,
|
||||
}
|
||||
|
|
|
@ -20,6 +20,9 @@ pub struct Predicate {
|
|||
}
|
||||
|
||||
impl Predicate {
|
||||
pub fn new(xattr: Xattr, relation: Relation) -> Self {
|
||||
Self { xattr, relation }
|
||||
}
|
||||
pub fn satisfied(&self, values: &HashMap<Xattr, Value>) -> bool {
|
||||
values
|
||||
.get(&self.xattr)
|
||||
|
|
61
src/test_support.rs
Normal file
61
src/test_support.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue