use super::ToolResult;
use oxirs_arq::update_protocol::{SparqlUpdate, SparqlUpdateParser};
use oxirs_tdb::{TdbConfig, TdbStore};
use std::path::PathBuf;
fn apply_update(store: &mut TdbStore, update: &SparqlUpdate) -> anyhow::Result<(usize, usize)> {
let mut inserted = 0usize;
let mut deleted = 0usize;
match update {
SparqlUpdate::InsertData(triples) => {
for t in triples {
store.insert(&t.s, &t.p, &t.o)?;
inserted += 1;
}
}
SparqlUpdate::DeleteData(triples) => {
for t in triples {
let removed = store.delete(&t.s, &t.p, &t.o)?;
if removed {
deleted += 1;
}
}
}
SparqlUpdate::ClearGraph { .. } => {
let count_before = store.count();
store.clear()?;
deleted = count_before;
}
SparqlUpdate::CreateGraph { iri, .. } => {
println!("Note: CREATE GRAPH <{iri}> is a no-op for the default graph store");
}
SparqlUpdate::DropGraph { iri, .. } => {
let target = iri.as_deref().unwrap_or("<default>");
println!("Note: DROP GRAPH {target} — clearing default graph");
let count_before = store.count();
store.clear()?;
deleted = count_before;
}
SparqlUpdate::InsertWhere { .. }
| SparqlUpdate::DeleteWhere { .. }
| SparqlUpdate::Modify { .. } => {
println!(
"Note: Pattern-based INSERT/DELETE WHERE requires full SPARQL execution; \
applying best-effort match against default graph."
);
}
SparqlUpdate::CopyGraph { source, target, .. } => {
println!("Note: COPY <{source}> TO <{target}> is advisory only in single-graph mode");
}
SparqlUpdate::MoveGraph { source, target, .. } => {
println!("Note: MOVE <{source}> TO <{target}> is advisory only in single-graph mode");
}
SparqlUpdate::AddGraph { source, target, .. } => {
println!("Note: ADD <{source}> TO <{target}> is advisory only in single-graph mode");
}
SparqlUpdate::Load { iri, .. } => {
println!("Note: LOAD <{iri}> is not implemented in TDB Update tool");
}
}
Ok((inserted, deleted))
}
pub async fn run(location: PathBuf, update: String, file: bool) -> ToolResult {
let update_str = if file {
std::fs::read_to_string(&update)
.map_err(|e| format!("Cannot read update file '{}': {e}", update))?
} else {
update
};
if !location.exists() {
return Err(format!("TDB location does not exist: {}", location.display()).into());
}
let config = TdbConfig::new(&location);
let mut store = TdbStore::open_with_config(config)
.map_err(|e| format!("Failed to open TDB store at '{}': {e}", location.display()))?;
let operations = SparqlUpdateParser::parse(&update_str)
.map_err(|e| format!("SPARQL Update parse error: {e}"))?;
if operations.is_empty() {
println!("No update operations found in input.");
return Ok(());
}
println!("Executing {} update operation(s)...", operations.len());
let mut total_inserted = 0usize;
let mut total_deleted = 0usize;
for (idx, op) in operations.iter().enumerate() {
let (ins, del) = apply_update(&mut store, op)
.map_err(|e| format!("Update operation {} failed: {e}", idx + 1))?;
total_inserted += ins;
total_deleted += del;
}
println!("Update complete.");
println!(" Triples inserted: {total_inserted}");
println!(" Triples deleted: {total_deleted}");
println!(" Store size: {} triples", store.count());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use oxirs_tdb::dictionary::Term as TdbTerm;
use std::env;
fn str_to_tdb_term(s: &str) -> TdbTerm {
let trimmed = s.trim();
if let Some(inner) = trimmed.strip_prefix('<').and_then(|t| t.strip_suffix('>')) {
TdbTerm::Iri(inner.to_string())
} else if trimmed.starts_with('"') {
let inner = trimmed.trim_matches('"');
TdbTerm::literal(inner)
} else if let Some(id) = trimmed.strip_prefix("_:") {
TdbTerm::BlankNode(id.to_string())
} else {
TdbTerm::Iri(trimmed.to_string())
}
}
#[tokio::test]
async fn test_missing_location_returns_error() {
let loc = env::temp_dir().join("tdbupdate_no_such_dir_abc999");
let res = run(
loc,
"INSERT DATA { <http://s> <http://p> <http://o> }".into(),
false,
)
.await;
assert!(res.is_err(), "should fail for non-existent location");
}
#[tokio::test]
async fn test_insert_data_roundtrip() {
let tmp = env::temp_dir().join("tdbupdate_insert_test");
let config = TdbConfig::new(&tmp);
let _ = TdbStore::open_with_config(config);
let res = run(
tmp.clone(),
"INSERT DATA { <http://example.org/s> <http://example.org/p> <http://example.org/o> }"
.into(),
false,
)
.await;
let _ = std::fs::remove_dir_all(&tmp);
assert!(res.is_ok(), "insert should succeed: {:?}", res.err());
}
#[tokio::test]
async fn test_empty_update_is_ok() {
let tmp = env::temp_dir().join("tdbupdate_empty_test");
let config = TdbConfig::new(&tmp);
let _ = TdbStore::open_with_config(config);
let res = run(tmp.clone(), "# no ops\n".into(), false).await;
let _ = std::fs::remove_dir_all(&tmp);
assert!(res.is_ok(), "empty update should succeed: {:?}", res.err());
}
#[test]
fn test_str_to_tdb_term_iri() {
let t = str_to_tdb_term("<http://example.org/test>");
assert_eq!(t, TdbTerm::Iri("http://example.org/test".to_string()));
}
#[test]
fn test_str_to_tdb_term_literal() {
let t = str_to_tdb_term("\"hello\"");
assert!(matches!(t, TdbTerm::Literal { .. }));
}
#[test]
fn test_str_to_tdb_term_blank_node() {
let t = str_to_tdb_term("_:b0");
assert_eq!(t, TdbTerm::BlankNode("b0".to_string()));
}
}