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 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
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)]
|
#[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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
29
src/lib.rs
29
src/lib.rs
|
@ -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();
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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