#[allow(unused_imports)]
use crate::algebra::{Algebra, EvaluationContext, Term, TriplePattern, Variable};
use crate::executor::ExecutionContext;
use oxirs_core::model::{BlankNode, GraphName, Literal as CoreLiteral, NamedNode, Quad};
use oxirs_core::OxirsError;
use oxirs_core::Store;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum UpdateOperation {
InsertData { data: Vec<QuadPattern> },
DeleteData { data: Vec<QuadPattern> },
DeleteWhere { pattern: Box<Algebra> },
InsertWhere {
pattern: Box<Algebra>,
template: Vec<QuadPattern>,
},
DeleteInsertWhere {
delete_template: Vec<QuadPattern>,
insert_template: Vec<QuadPattern>,
pattern: Box<Algebra>,
using: Option<Vec<GraphReference>>,
},
Clear { target: GraphTarget, silent: bool },
Drop { target: GraphTarget, silent: bool },
Create { graph: GraphReference, silent: bool },
Copy {
from: GraphTarget,
to: GraphTarget,
silent: bool,
},
Move {
from: GraphTarget,
to: GraphTarget,
silent: bool,
},
Add {
from: GraphTarget,
to: GraphTarget,
silent: bool,
},
Load {
source: String,
graph: Option<GraphReference>,
silent: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QuadPattern {
pub subject: Term,
pub predicate: Term,
pub object: Term,
pub graph: Option<GraphReference>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GraphReference {
Iri(String),
Default,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GraphTarget {
Graph(GraphReference),
All,
Named,
Default,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateResult {
pub inserted: usize,
pub deleted: usize,
pub graphs_created: Vec<String>,
pub graphs_dropped: Vec<String>,
}
pub struct UpdateExecutor<'a> {
store: &'a mut dyn Store,
#[allow(dead_code)]
context: ExecutionContext,
transaction_mode: bool,
batch_size: usize,
stats: UpdateStatistics,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateStatistics {
pub total_operations: usize,
pub total_execution_time: std::time::Duration,
pub operations_per_second: f64,
pub memory_usage: usize,
pub batch_count: usize,
}
impl<'a> UpdateExecutor<'a> {
pub fn new(store: &'a mut dyn Store) -> Self {
UpdateExecutor {
store,
context: ExecutionContext::default(),
transaction_mode: false,
batch_size: 10000, stats: UpdateStatistics::default(),
}
}
pub fn with_transaction(store: &'a mut dyn Store) -> Self {
UpdateExecutor {
store,
context: ExecutionContext::default(),
transaction_mode: true,
batch_size: 10000,
stats: UpdateStatistics::default(),
}
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size.max(1);
self
}
pub fn is_transactional(&self) -> bool {
self.transaction_mode
}
pub fn statistics(&self) -> &UpdateStatistics {
&self.stats
}
pub fn reset_statistics(&mut self) {
self.stats = UpdateStatistics::default();
}
pub fn execute(&mut self, operation: &UpdateOperation) -> Result<UpdateResult, OxirsError> {
let start_time = std::time::Instant::now();
self.stats.total_operations += 1;
let result = match operation {
UpdateOperation::InsertData { data } => self.execute_insert_data_enhanced(data),
UpdateOperation::DeleteData { data } => self.execute_delete_data_enhanced(data),
UpdateOperation::DeleteWhere { pattern } => self.execute_delete_where(pattern),
UpdateOperation::InsertWhere { pattern, template } => {
self.execute_insert_where(pattern, template)
}
UpdateOperation::DeleteInsertWhere {
delete_template,
insert_template,
pattern,
using,
} => self.execute_delete_insert_where(delete_template, insert_template, pattern, using),
UpdateOperation::Clear { target, silent } => self.execute_clear(target, *silent),
UpdateOperation::Drop { target, silent } => self.execute_drop(target, *silent),
UpdateOperation::Create { graph, silent } => self.execute_create(graph, *silent),
UpdateOperation::Copy { from, to, silent } => self.execute_copy(from, to, *silent),
UpdateOperation::Move { from, to, silent } => self.execute_move(from, to, *silent),
UpdateOperation::Add { from, to, silent } => self.execute_add(from, to, *silent),
UpdateOperation::Load {
source,
graph,
silent,
} => self.execute_load(source, graph.as_ref(), *silent),
};
let execution_time = start_time.elapsed();
self.stats.total_execution_time += execution_time;
self.stats.operations_per_second =
self.stats.total_operations as f64 / self.stats.total_execution_time.as_secs_f64();
result
}
#[allow(dead_code)]
fn execute_insert_data(&mut self, data: &[QuadPattern]) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
for pattern in data {
let quad = self.pattern_to_quad(pattern)?;
self.store.insert(&quad)?;
result.inserted += 1;
}
Ok(result)
}
fn execute_insert_data_enhanced(
&mut self,
data: &[QuadPattern],
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
if data.is_empty() {
return Ok(result);
}
if data.len() > self.batch_size {
for chunk in data.chunks(self.batch_size) {
let batch_result = self.execute_insert_data_batch(chunk)?;
result.inserted += batch_result.inserted;
self.stats.batch_count += 1;
}
} else {
for pattern in data {
self.validate_quad_pattern(pattern)?;
let quad = self.pattern_to_quad(pattern)?;
self.store.insert(&quad)?;
result.inserted += 1;
}
}
Ok(result)
}
fn execute_insert_data_batch(
&mut self,
batch: &[QuadPattern],
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
let mut quads_to_insert = Vec::with_capacity(batch.len());
for pattern in batch {
self.validate_quad_pattern(pattern)?;
let quad = self.pattern_to_quad(pattern)?;
quads_to_insert.push(quad);
}
for quad in quads_to_insert {
self.store.insert(&quad)?;
result.inserted += 1;
}
Ok(result)
}
fn validate_quad_pattern(&self, pattern: &QuadPattern) -> Result<(), OxirsError> {
if matches!(pattern.subject, Term::Variable(_)) {
return Err(OxirsError::Query(
"Variables not allowed in INSERT DATA subject".to_string(),
));
}
if matches!(pattern.predicate, Term::Variable(_)) {
return Err(OxirsError::Query(
"Variables not allowed in INSERT DATA predicate".to_string(),
));
}
if matches!(pattern.object, Term::Variable(_)) {
return Err(OxirsError::Query(
"Variables not allowed in INSERT DATA object".to_string(),
));
}
if let Term::Iri(iri) = &pattern.subject {
if iri.as_str().is_empty() {
return Err(OxirsError::Query("Empty IRI in subject".to_string()));
}
}
if let Term::Iri(iri) = &pattern.predicate {
if iri.as_str().is_empty() {
return Err(OxirsError::Query("Empty IRI in predicate".to_string()));
}
}
if let Term::Iri(iri) = &pattern.object {
if iri.as_str().is_empty() {
return Err(OxirsError::Query("Empty IRI in object".to_string()));
}
}
Ok(())
}
#[allow(dead_code)]
fn execute_delete_data(&mut self, data: &[QuadPattern]) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
for pattern in data {
let quad = self.pattern_to_quad(pattern)?;
if self.store.remove(&quad)? {
result.deleted += 1;
}
}
Ok(result)
}
fn execute_delete_data_enhanced(
&mut self,
data: &[QuadPattern],
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
if data.is_empty() {
return Ok(result);
}
if data.len() > self.batch_size {
for chunk in data.chunks(self.batch_size) {
let batch_result = self.execute_delete_data_batch(chunk)?;
result.deleted += batch_result.deleted;
self.stats.batch_count += 1;
}
} else {
for pattern in data {
self.validate_quad_pattern(pattern)?;
let quad = self.pattern_to_quad(pattern)?;
if self.store.remove(&quad)? {
result.deleted += 1;
}
}
}
Ok(result)
}
fn execute_delete_data_batch(
&mut self,
batch: &[QuadPattern],
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
let mut quads_to_delete = Vec::with_capacity(batch.len());
for pattern in batch {
self.validate_quad_pattern(pattern)?;
let quad = self.pattern_to_quad(pattern)?;
quads_to_delete.push(quad);
}
for quad in quads_to_delete {
if self.store.remove(&quad)? {
result.deleted += 1;
}
}
Ok(result)
}
fn execute_delete_where(&mut self, pattern: &Algebra) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
let bindings = self.evaluate_pattern(pattern)?;
if bindings.is_empty() {
return Ok(result); }
let mut quads_to_delete = Vec::new();
for binding in &bindings {
let quads = self.apply_binding_to_pattern(pattern, binding)?;
quads_to_delete.extend(quads);
}
quads_to_delete.sort();
quads_to_delete.dedup();
if quads_to_delete.len() > self.batch_size {
for chunk in quads_to_delete.chunks(self.batch_size) {
for quad in chunk {
if self.store.remove(quad)? {
result.deleted += 1;
}
}
self.stats.batch_count += 1;
}
} else {
for quad in quads_to_delete {
if self.store.remove(&quad)? {
result.deleted += 1;
}
}
}
Ok(result)
}
fn execute_insert_where(
&mut self,
pattern: &Algebra,
template: &[QuadPattern],
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
if template.is_empty() {
return Ok(result); }
let bindings = self.evaluate_pattern(pattern)?;
if bindings.is_empty() {
return Ok(result); }
let mut quads_to_insert = Vec::new();
for binding in &bindings {
for quad_pattern in template {
if let Some(quad) = self.instantiate_template(quad_pattern, binding)? {
quads_to_insert.push(quad);
}
}
}
quads_to_insert.sort();
quads_to_insert.dedup();
if quads_to_insert.len() > self.batch_size {
for chunk in quads_to_insert.chunks(self.batch_size) {
for quad in chunk {
self.store.insert(quad)?;
result.inserted += 1;
}
self.stats.batch_count += 1;
}
} else {
for quad in quads_to_insert {
self.store.insert(&quad)?;
result.inserted += 1;
}
}
Ok(result)
}
fn execute_delete_insert_where(
&mut self,
delete_template: &[QuadPattern],
insert_template: &[QuadPattern],
pattern: &Algebra,
using: &Option<Vec<GraphReference>>,
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
if delete_template.is_empty() && insert_template.is_empty() {
return Ok(result); }
let bindings = self.evaluate_pattern_with_using(pattern, using.as_deref())?;
if bindings.is_empty() {
return Ok(result); }
let mut quads_to_delete = Vec::new();
if !delete_template.is_empty() {
for binding in &bindings {
for quad_pattern in delete_template {
if let Some(quad) = self.instantiate_template(quad_pattern, binding)? {
quads_to_delete.push(quad);
}
}
}
quads_to_delete.sort();
quads_to_delete.dedup();
}
let mut quads_to_insert = Vec::new();
if !insert_template.is_empty() {
for binding in &bindings {
for quad_pattern in insert_template {
if let Some(quad) = self.instantiate_template(quad_pattern, binding)? {
quads_to_insert.push(quad);
}
}
}
quads_to_insert.sort();
quads_to_insert.dedup();
}
let mut applied_deletes: Vec<&Quad> = Vec::new();
let mut applied_inserts: Vec<&Quad> = Vec::new();
for quad in &quads_to_delete {
match self.store.remove(quad) {
Ok(true) => {
applied_deletes.push(quad);
result.deleted += 1;
}
Ok(false) => {
}
Err(e) => {
Self::rollback_delete_insert(self.store, &applied_deletes, &applied_inserts);
return Err(e);
}
}
}
for quad in &quads_to_insert {
match self.store.insert(quad) {
Ok(()) => {
applied_inserts.push(quad);
result.inserted += 1;
}
Err(e) => {
Self::rollback_delete_insert(self.store, &applied_deletes, &applied_inserts);
return Err(e);
}
}
}
if quads_to_delete.len() > self.batch_size || quads_to_insert.len() > self.batch_size {
self.stats.batch_count += 1;
}
Ok(result)
}
fn rollback_delete_insert(
store: &mut dyn Store,
applied_deletes: &[&Quad],
applied_inserts: &[&Quad],
) {
for quad in applied_inserts.iter().rev() {
let _ = store.remove(quad);
}
for quad in applied_deletes.iter().rev() {
let _ = store.insert(quad);
}
}
fn execute_clear(
&mut self,
target: &GraphTarget,
silent: bool,
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
match target {
GraphTarget::All => {
result.deleted = self.store.clear_all()?;
}
GraphTarget::Named => {
result.deleted = self.store.clear_named_graphs()?;
}
GraphTarget::Default => {
result.deleted = self.store.clear_default_graph()?;
}
GraphTarget::Graph(graph_ref) => {
let graph_name = self.graph_ref_to_named_node(graph_ref)?;
match self
.store
.clear_graph(Some(&GraphName::NamedNode(graph_name)))
{
Ok(count) => result.deleted = count,
Err(e) if !silent => return Err(e),
_ => {} }
}
}
Ok(result)
}
fn execute_drop(
&mut self,
target: &GraphTarget,
silent: bool,
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
match target {
GraphTarget::All => {
let graphs = self.store.graphs()?;
for graph in graphs {
self.store
.drop_graph(Some(&GraphName::NamedNode(graph.clone())))?;
result.graphs_dropped.push(graph.as_str().to_string());
}
}
GraphTarget::Named => {
let graphs = self.store.named_graphs()?;
for graph in graphs {
self.store
.drop_graph(Some(&GraphName::NamedNode(graph.clone())))?;
result.graphs_dropped.push(graph.as_str().to_string());
}
}
GraphTarget::Default => {
if !silent {
return Err(OxirsError::Query("Cannot DROP DEFAULT graph".to_string()));
}
}
GraphTarget::Graph(graph_ref) => {
let graph_name = self.graph_ref_to_named_node(graph_ref)?;
match self
.store
.drop_graph(Some(&GraphName::NamedNode(graph_name.clone())))
{
Ok(_) => result.graphs_dropped.push(graph_name.as_str().to_string()),
Err(e) if !silent => return Err(e),
_ => {} }
}
}
Ok(result)
}
fn execute_create(
&mut self,
graph: &GraphReference,
silent: bool,
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
let graph_name = self.graph_ref_to_named_node(graph)?;
match self.store.create_graph(Some(&graph_name)) {
Ok(_) => {
result.graphs_created.push(graph_name.as_str().to_string());
}
Err(e) => {
if !silent {
return Err(e);
}
}
}
Ok(result)
}
fn execute_copy(
&mut self,
from: &GraphTarget,
to: &GraphTarget,
silent: bool,
) -> Result<UpdateResult, OxirsError> {
self.execute_clear(to, silent)?;
self.execute_add(from, to, silent)
}
fn execute_move(
&mut self,
from: &GraphTarget,
to: &GraphTarget,
silent: bool,
) -> Result<UpdateResult, OxirsError> {
let result = self.execute_copy(from, to, silent)?;
self.execute_clear(from, silent)?;
Ok(result)
}
fn execute_add(
&mut self,
from: &GraphTarget,
to: &GraphTarget,
_silent: bool,
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
let source_quads = self.get_quads_from_target(from)?;
let target_graph = match to {
GraphTarget::Graph(g) => Some(self.graph_ref_to_named_node(g)?),
GraphTarget::Default => None,
_ => {
return Err(OxirsError::Query(
"Invalid target for ADD operation".to_string(),
))
}
};
for quad in source_quads {
let new_quad = if let Some(ref target) = target_graph {
Quad::new(
quad.subject().clone(),
quad.predicate().clone(),
quad.object().clone(),
GraphName::NamedNode(target.clone()),
)
} else {
Quad::new(
quad.subject().clone(),
quad.predicate().clone(),
quad.object().clone(),
GraphName::DefaultGraph,
)
};
self.store.insert(&new_quad)?;
result.inserted += 1;
}
Ok(result)
}
fn execute_load(
&mut self,
source: &str,
graph: Option<&GraphReference>,
silent: bool,
) -> Result<UpdateResult, OxirsError> {
let mut result = UpdateResult::default();
let target_graph = graph.map(|g| self.graph_ref_to_named_node(g)).transpose()?;
match self.store.load_from_url(source, target_graph.as_ref()) {
Ok(count) => {
result.inserted = count;
}
Err(e) if !silent => return Err(e),
_ => {} }
Ok(result)
}
fn pattern_to_quad(&self, pattern: &QuadPattern) -> Result<Quad, OxirsError> {
let subject = self.term_to_subject(&pattern.subject)?;
let predicate = self.term_to_predicate(&pattern.predicate)?;
let object = self.term_to_object(&pattern.object)?;
let graph_name = pattern
.graph
.as_ref()
.map(|g| self.graph_ref_to_named_node(g))
.transpose()?
.map(GraphName::NamedNode)
.unwrap_or(GraphName::DefaultGraph);
Ok(Quad::new(subject, predicate, object, graph_name))
}
fn term_to_subject(&self, term: &Term) -> Result<oxirs_core::model::Subject, OxirsError> {
match term {
Term::Iri(iri) => Ok(NamedNode::new(iri.as_str())?.into()),
Term::BlankNode(id) => Ok(BlankNode::new(id)?.into()),
Term::Variable(_) => Err(OxirsError::Query(
"Variables not allowed in concrete data".to_string(),
)),
Term::Literal(_) => Err(OxirsError::Query(
"Literals cannot be used as subjects".to_string(),
)),
Term::QuotedTriple(_) => Err(OxirsError::Query(
"Quoted triples not yet supported as subjects in concrete data".to_string(),
)),
Term::PropertyPath(_) => Err(OxirsError::Query(
"Property paths not allowed as subjects in concrete data".to_string(),
)),
}
}
fn term_to_predicate(&self, term: &Term) -> Result<NamedNode, OxirsError> {
match term {
Term::Iri(iri) => NamedNode::new(iri.as_str()),
Term::Variable(_) => Err(OxirsError::Query(
"Variables not allowed in concrete data".to_string(),
)),
Term::BlankNode(_) => Err(OxirsError::Query(
"Blank nodes cannot be used as predicates in most RDF contexts".to_string(),
)),
Term::Literal(_) => Err(OxirsError::Query(
"Literals cannot be used as predicates".to_string(),
)),
Term::QuotedTriple(_) => Err(OxirsError::Query(
"Quoted triples not supported as predicates in concrete data".to_string(),
)),
Term::PropertyPath(_) => Err(OxirsError::Query(
"Property paths not allowed as predicates in concrete data".to_string(),
)),
}
}
fn term_to_object(&self, term: &Term) -> Result<oxirs_core::model::Object, OxirsError> {
match term {
Term::Iri(iri) => Ok(NamedNode::new(iri.as_str())?.into()),
Term::BlankNode(id) => Ok(BlankNode::new(id)?.into()),
Term::Literal(lit) => {
let literal = if let Some(lang) = &lit.language {
CoreLiteral::new_language_tagged_literal(&lit.value, lang)?
} else if let Some(dt) = &lit.datatype {
CoreLiteral::new_typed(&lit.value, dt.clone())
} else {
CoreLiteral::new(&lit.value)
};
Ok(literal.into())
}
Term::Variable(_) => Err(OxirsError::Query(
"Variables not allowed in concrete data".to_string(),
)),
Term::QuotedTriple(_) => Err(OxirsError::Query(
"Quoted triples not yet supported in concrete data".to_string(),
)),
Term::PropertyPath(_) => Err(OxirsError::Query(
"Property paths not allowed in concrete data".to_string(),
)),
}
}
fn graph_ref_to_named_node(&self, graph_ref: &GraphReference) -> Result<NamedNode, OxirsError> {
match graph_ref {
GraphReference::Iri(iri) => NamedNode::new(iri),
GraphReference::Default => Err(OxirsError::Query(
"DEFAULT is not a valid graph IRI".to_string(),
)),
}
}
fn evaluate_pattern(
&mut self,
pattern: &Algebra,
) -> Result<Vec<HashMap<String, oxirs_core::model::Term>>, OxirsError> {
self.evaluate_pattern_with_using(pattern, None)
}
fn evaluate_pattern_with_using(
&mut self,
pattern: &Algebra,
using: Option<&[GraphReference]>,
) -> Result<Vec<HashMap<String, oxirs_core::model::Term>>, OxirsError> {
use crate::executor::dataset::DatasetView;
use crate::executor::{QueryExecutor, StoreRefDataset};
let using_graphs: Vec<NamedNode> = match using {
Some(refs) if !refs.is_empty() => refs
.iter()
.map(|graph_ref| match graph_ref {
GraphReference::Iri(iri) => NamedNode::new(iri).map_err(|e| {
OxirsError::Query(format!("Invalid USING graph IRI `{iri}`: {e}"))
}),
GraphReference::Default => Err(OxirsError::Query(
"USING requires a graph IRI; the default graph cannot be a USING target"
.to_string(),
)),
})
.collect::<Result<Vec<_>, _>>()?,
_ => Vec::new(),
};
let solution = {
let store_ref: &dyn Store = &*self.store;
let base = StoreRefDataset::new(store_ref);
let mut executor = QueryExecutor::new();
if using_graphs.is_empty() {
let (solution, _stats) = executor
.execute(pattern, &base)
.map_err(|e| OxirsError::Query(e.to_string()))?;
solution
} else {
let dataset = DatasetView::new(&base, using_graphs, Vec::new());
let (solution, _stats) = executor
.execute(pattern, &dataset)
.map_err(|e| OxirsError::Query(e.to_string()))?;
solution
}
};
let mut bindings = Vec::new();
for binding in solution {
let mut converted_binding = HashMap::new();
for (var, term) in binding {
let arq_term = crate::term::Term::from_algebra_term(&term);
let core_term = self.convert_term_to_core(&arq_term)?;
converted_binding.insert(var.as_str().to_string(), core_term);
}
bindings.push(converted_binding);
}
Ok(bindings)
}
fn apply_binding_to_pattern(
&self,
pattern: &Algebra,
binding: &HashMap<String, oxirs_core::model::Term>,
) -> Result<Vec<Quad>, OxirsError> {
let mut quads = Vec::new();
let mut triple_patterns: Vec<(TriplePattern, Option<NamedNode>)> = Vec::new();
self.extract_triple_patterns_with_graph(pattern, None, &mut triple_patterns)?;
for (triple_pattern, graph) in triple_patterns {
let subject_term = self.instantiate_algebra_term(&triple_pattern.subject, binding)?;
let predicate_term =
self.instantiate_algebra_term(&triple_pattern.predicate, binding)?;
let object_term = self.instantiate_algebra_term(&triple_pattern.object, binding)?;
let subject = self.core_term_to_subject(subject_term)?;
let predicate = self.core_term_to_predicate(predicate_term)?;
let object = self.core_term_to_object(object_term)?;
let graph_name = match graph {
Some(node) => GraphName::NamedNode(node),
None => GraphName::DefaultGraph,
};
let quad = Quad::new(subject, predicate, object, graph_name);
quads.push(quad);
}
Ok(quads)
}
fn instantiate_template(
&self,
template: &QuadPattern,
binding: &HashMap<String, oxirs_core::model::Term>,
) -> Result<Option<Quad>, OxirsError> {
let (Some(subject), Some(predicate), Some(object)) = (
self.instantiate_term(&template.subject, binding)?,
self.instantiate_term(&template.predicate, binding)?,
self.instantiate_term(&template.object, binding)?,
) else {
return Ok(None);
};
let subject = self.term_to_subject(&subject)?;
let predicate = self.term_to_predicate(&predicate)?;
let object = self.term_to_object(&object)?;
let graph_name = template
.graph
.as_ref()
.map(|g| self.graph_ref_to_named_node(g))
.transpose()?
.map(GraphName::NamedNode)
.unwrap_or(GraphName::DefaultGraph);
Ok(Some(Quad::new(subject, predicate, object, graph_name)))
}
fn instantiate_term(
&self,
term: &Term,
binding: &HashMap<String, oxirs_core::model::Term>,
) -> Result<Option<Term>, OxirsError> {
match term {
Term::Variable(var) => match binding.get(var.as_str()) {
Some(t) => self.core_term_to_arq_term(t).map(Some),
None => Ok(None),
},
_ => Ok(Some(term.clone())),
}
}
fn core_term_to_arq_term(&self, term: &oxirs_core::model::Term) -> Result<Term, OxirsError> {
use oxirs_core::model::Term as CoreTerm;
match term {
CoreTerm::NamedNode(n) => Ok(Term::Iri(n.clone())),
CoreTerm::BlankNode(b) => Ok(Term::BlankNode(b.as_str().to_string())),
CoreTerm::Literal(l) => {
let lit = crate::algebra::Literal {
value: l.value().to_string(),
language: l.language().map(|s| s.to_string()),
datatype: Some(l.datatype().into()),
};
Ok(Term::Literal(lit))
}
CoreTerm::QuotedTriple(qt) => {
let subject =
self.core_term_to_arq_term(&CoreTerm::from_subject(qt.subject()))?;
let predicate =
self.core_term_to_arq_term(&CoreTerm::from_predicate(qt.predicate()))?;
let object = self.core_term_to_arq_term(&CoreTerm::from_object(qt.object()))?;
Ok(Term::QuotedTriple(Box::new(crate::algebra::TriplePattern {
subject,
predicate,
object,
})))
}
CoreTerm::Variable(v) => Err(OxirsError::Query(format!(
"Variable term `{v}` is not valid in stored RDF data for update template instantiation"
))),
}
}
fn get_quads_from_target(&self, target: &GraphTarget) -> Result<Vec<Quad>, OxirsError> {
match target {
GraphTarget::All => self.store.quads(),
GraphTarget::Named => self.store.named_graph_quads(),
GraphTarget::Default => self.store.default_graph_quads(),
GraphTarget::Graph(graph_ref) => {
let graph = self.graph_ref_to_named_node(graph_ref)?;
self.store.graph_quads(Some(&graph))
}
}
}
fn convert_term_to_core(
&self,
term: &crate::term::Term,
) -> Result<oxirs_core::model::Term, OxirsError> {
use oxirs_core::model::Term as CoreTerm;
match term {
crate::term::Term::Iri(iri) => Ok(CoreTerm::NamedNode(NamedNode::new(iri)?)),
crate::term::Term::BlankNode(id) => Ok(CoreTerm::BlankNode(BlankNode::new(id)?)),
crate::term::Term::Literal(lit) => {
let core_literal = if let Some(lang) = &lit.language_tag {
CoreLiteral::new_language_tagged_literal(&lit.lexical_form, lang)?
} else if lit.datatype != "http://www.w3.org/2001/XMLSchema#string" {
CoreLiteral::new_typed(&lit.lexical_form, NamedNode::new(&lit.datatype)?)
} else {
CoreLiteral::new_simple_literal(&lit.lexical_form)
};
Ok(CoreTerm::Literal(core_literal))
}
crate::term::Term::Variable(_) => Err(OxirsError::Query(
"Cannot convert variable to concrete term".to_string(),
)),
crate::term::Term::QuotedTriple(_) => Err(OxirsError::Query(
"Cannot convert quoted triple to concrete term".to_string(),
)),
crate::term::Term::PropertyPath(_) => Err(OxirsError::Query(
"Cannot convert property path to concrete term".to_string(),
)),
}
}
#[allow(clippy::only_used_in_recursion)]
#[allow(clippy::only_used_in_recursion)]
fn extract_triple_patterns_with_graph(
&self,
algebra: &Algebra,
current_graph: Option<&NamedNode>,
out: &mut Vec<(TriplePattern, Option<NamedNode>)>,
) -> Result<(), OxirsError> {
match algebra {
Algebra::Bgp(bgp_patterns) => {
for p in bgp_patterns {
out.push((p.clone(), current_graph.cloned()));
}
}
Algebra::Graph { graph, pattern } => match graph {
Term::Iri(node) => {
self.extract_triple_patterns_with_graph(pattern, Some(node), out)?;
}
_ => {
self.extract_triple_patterns_with_graph(pattern, current_graph, out)?;
}
},
Algebra::Join { left, right }
| Algebra::Union { left, right }
| Algebra::Minus { left, right } => {
self.extract_triple_patterns_with_graph(left, current_graph, out)?;
self.extract_triple_patterns_with_graph(right, current_graph, out)?;
}
Algebra::LeftJoin { left, right, .. } => {
self.extract_triple_patterns_with_graph(left, current_graph, out)?;
self.extract_triple_patterns_with_graph(right, current_graph, out)?;
}
Algebra::Filter { pattern, .. }
| Algebra::Extend { pattern, .. }
| Algebra::Service { pattern, .. } => {
self.extract_triple_patterns_with_graph(pattern, current_graph, out)?;
}
_ => {}
}
Ok(())
}
fn instantiate_algebra_term(
&self,
term: &Term,
binding: &HashMap<String, oxirs_core::model::Term>,
) -> Result<oxirs_core::model::Term, OxirsError> {
match term {
Term::Variable(var) => binding
.get(var.as_str())
.cloned()
.ok_or_else(|| OxirsError::Query(format!("Unbound variable: {var}"))),
_ => {
let arq_term = crate::term::Term::from_algebra_term(term);
self.convert_term_to_core(&arq_term)
}
}
}
fn core_term_to_subject(
&self,
term: oxirs_core::model::Term,
) -> Result<oxirs_core::Subject, OxirsError> {
match term {
oxirs_core::model::Term::NamedNode(node) => Ok(oxirs_core::Subject::NamedNode(node)),
oxirs_core::model::Term::BlankNode(node) => Ok(oxirs_core::Subject::BlankNode(node)),
oxirs_core::model::Term::Variable(var) => Ok(oxirs_core::Subject::Variable(var)),
_ => Err(OxirsError::Query("Invalid subject term type".to_string())),
}
}
fn core_term_to_predicate(
&self,
term: oxirs_core::model::Term,
) -> Result<oxirs_core::Predicate, OxirsError> {
match term {
oxirs_core::model::Term::NamedNode(node) => Ok(oxirs_core::Predicate::NamedNode(node)),
oxirs_core::model::Term::Variable(var) => Ok(oxirs_core::Predicate::Variable(var)),
_ => Err(OxirsError::Query("Invalid predicate term type".to_string())),
}
}
fn core_term_to_object(
&self,
term: oxirs_core::model::Term,
) -> Result<oxirs_core::Object, OxirsError> {
match term {
oxirs_core::model::Term::NamedNode(node) => Ok(oxirs_core::Object::NamedNode(node)),
oxirs_core::model::Term::BlankNode(node) => Ok(oxirs_core::Object::BlankNode(node)),
oxirs_core::model::Term::Literal(lit) => Ok(oxirs_core::Object::Literal(lit)),
oxirs_core::model::Term::Variable(var) => Ok(oxirs_core::Object::Variable(var)),
oxirs_core::model::Term::QuotedTriple(qt) => Ok(oxirs_core::Object::QuotedTriple(qt)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_result_default() {
let result = UpdateResult::default();
assert_eq!(result.inserted, 0);
assert_eq!(result.deleted, 0);
assert!(result.graphs_created.is_empty());
assert!(result.graphs_dropped.is_empty());
}
#[test]
fn test_graph_reference() {
let iri_ref = GraphReference::Iri("http://example.org/graph".to_string());
let default_ref = GraphReference::Default;
assert_ne!(iri_ref, default_ref);
}
#[test]
fn test_quad_pattern() {
let pattern = QuadPattern {
subject: Term::Variable(Variable::new("s").unwrap()),
predicate: Term::Iri(NamedNode::new("http://example.org/pred").unwrap()),
object: Term::Literal(crate::algebra::Literal {
value: "test".to_string(),
language: None,
datatype: None,
}),
graph: None,
};
assert_eq!(pattern.subject, Term::Variable(Variable::new("s").unwrap()));
}
#[test]
fn test_enhanced_update_operations() {
let insert_data = UpdateOperation::InsertData {
data: vec![QuadPattern {
subject: Term::Iri(NamedNode::new("http://example.org/subject").unwrap()),
predicate: Term::Iri(NamedNode::new("http://example.org/predicate").unwrap()),
object: Term::Literal(crate::algebra::Literal {
value: "test_value".to_string(),
language: None,
datatype: None,
}),
graph: None,
}],
};
let delete_data = UpdateOperation::DeleteData {
data: vec![QuadPattern {
subject: Term::Iri(NamedNode::new("http://example.org/subject").unwrap()),
predicate: Term::Iri(NamedNode::new("http://example.org/predicate").unwrap()),
object: Term::Literal(crate::algebra::Literal {
value: "test_value".to_string(),
language: None,
datatype: None,
}),
graph: None,
}],
};
match insert_data {
UpdateOperation::InsertData { data } => {
assert_eq!(data.len(), 1);
}
_ => panic!("Expected InsertData operation"),
}
match delete_data {
UpdateOperation::DeleteData { data } => {
assert_eq!(data.len(), 1);
}
_ => panic!("Expected DeleteData operation"),
}
}
#[test]
fn test_update_statistics() {
let stats = UpdateStatistics::default();
assert_eq!(stats.total_operations, 0);
assert_eq!(stats.batch_count, 0);
assert_eq!(stats.operations_per_second, 0.0);
}
#[test]
fn delete_where_roundtrip_on_real_store() {
use oxirs_core::model::{Literal as CoreLiteral, NamedNode, Predicate, Quad};
use oxirs_core::rdf_store::{ConcreteStore, Store};
let mut store = ConcreteStore::new().expect("create store");
let s1 = NamedNode::new("http://example.org/s1").expect("iri");
let s2 = NamedNode::new("http://example.org/s2").expect("iri");
let p = NamedNode::new("http://example.org/p").expect("iri");
let q = NamedNode::new("http://example.org/q").expect("iri");
store
.insert(&Quad::new(
s1.clone(),
p.clone(),
CoreLiteral::new("v1"),
GraphName::DefaultGraph,
))
.expect("insert s1");
store
.insert(&Quad::new(
s2.clone(),
q.clone(),
CoreLiteral::new("v2"),
GraphName::DefaultGraph,
))
.expect("insert s2");
let pattern = Algebra::Bgp(vec![TriplePattern {
subject: Term::Variable(Variable::new("s").expect("var")),
predicate: Term::Iri(p.clone()),
object: Term::Variable(Variable::new("o").expect("var")),
}]);
let op = UpdateOperation::DeleteWhere {
pattern: Box::new(pattern),
};
let deleted = {
let mut executor = UpdateExecutor::new(&mut store);
executor.execute(&op).expect("delete where").deleted
};
assert_eq!(deleted, 1, "exactly one matching triple must be deleted");
let remaining_p = store
.find_quads(None, Some(&Predicate::NamedNode(p)), None, None)
.expect("find p");
assert!(remaining_p.is_empty(), "the (s1 p v1) triple must be gone");
let remaining_q = store
.find_quads(None, Some(&Predicate::NamedNode(q)), None, None)
.expect("find q");
assert_eq!(remaining_q.len(), 1, "the (s2 q v2) triple must survive");
}
#[test]
fn delete_where_matches_typed_literal_on_real_store() {
use oxirs_core::model::{Literal as CoreLiteral, NamedNode, Predicate, Quad};
use oxirs_core::rdf_store::{ConcreteStore, Store};
let mut store = ConcreteStore::new().expect("create store");
let s = NamedNode::new("http://example.org/s").expect("iri");
let age = NamedNode::new("http://example.org/age").expect("iri");
let xsd_int = NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("iri");
store
.insert(&Quad::new(
s.clone(),
age.clone(),
CoreLiteral::new_typed("25", xsd_int.clone()),
GraphName::DefaultGraph,
))
.expect("insert 25");
store
.insert(&Quad::new(
s.clone(),
age.clone(),
CoreLiteral::new_typed("30", xsd_int.clone()),
GraphName::DefaultGraph,
))
.expect("insert 30");
let pattern = Algebra::Bgp(vec![TriplePattern {
subject: Term::Variable(Variable::new("s").expect("var")),
predicate: Term::Iri(age.clone()),
object: Term::Literal(crate::algebra::Literal {
value: "25".to_string(),
language: None,
datatype: Some(xsd_int.clone()),
}),
}]);
let op = UpdateOperation::DeleteWhere {
pattern: Box::new(pattern),
};
let deleted = {
let mut executor = UpdateExecutor::new(&mut store);
executor.execute(&op).expect("delete where").deleted
};
assert_eq!(
deleted, 1,
"typed-literal pattern must match exactly one triple"
);
let remaining = store
.find_quads(None, Some(&Predicate::NamedNode(age)), None, None)
.expect("find age");
assert_eq!(remaining.len(), 1, "only the age=30 triple must survive");
}
#[test]
fn test_validation_errors() {
let _invalid_pattern = QuadPattern {
subject: Term::Variable(Variable::new("s").unwrap()),
predicate: Term::Iri(NamedNode::new("http://example.org/predicate").unwrap()),
object: Term::Literal(crate::algebra::Literal {
value: "test".to_string(),
language: None,
datatype: None,
}),
graph: None,
};
let validation_result = std::panic::catch_unwind(|| {
});
assert!(validation_result.is_ok());
}
#[test]
fn regression_core_term_to_arq_term_quoted_triple_no_panic() {
use oxirs_core::model::star::QuotedTriple;
use oxirs_core::model::{Literal as CoreLiteral, NamedNode as CoreNamedNode, Triple};
use oxirs_core::rdf_store::ConcreteStore;
let mut store = ConcreteStore::new().expect("create store");
let executor = UpdateExecutor::new(&mut store);
let inner = Triple::new(
CoreNamedNode::new("http://ex/s").expect("s"),
CoreNamedNode::new("http://ex/p").expect("p"),
CoreLiteral::new("o"),
);
let quoted = oxirs_core::model::Term::QuotedTriple(Box::new(QuotedTriple::new(inner)));
let converted = executor
.core_term_to_arq_term("ed)
.expect("quoted triple must convert without panicking");
assert!(matches!(converted, Term::QuotedTriple(_)));
let var_term =
oxirs_core::model::Term::Variable(oxirs_core::model::Variable::new("x").expect("var"));
assert!(executor.core_term_to_arq_term(&var_term).is_err());
}
#[test]
fn regression_delete_insert_where_honors_using_clause() {
use oxirs_core::model::{Literal as CoreLiteral, NamedNode, Quad};
use oxirs_core::rdf_store::{ConcreteStore, Store};
let mut store = ConcreteStore::new().expect("create store");
let s1 = NamedNode::new("http://ex/s1").expect("iri");
let s2 = NamedNode::new("http://ex/s2").expect("iri");
let p = NamedNode::new("http://ex/p").expect("iri");
let g1 = NamedNode::new("http://ex/g1").expect("iri");
let g2 = NamedNode::new("http://ex/g2").expect("iri");
store
.insert(&Quad::new(
s1.clone(),
p.clone(),
CoreLiteral::new("v1"),
GraphName::NamedNode(g1.clone()),
))
.expect("insert g1");
store
.insert(&Quad::new(
s2.clone(),
p.clone(),
CoreLiteral::new("v2"),
GraphName::NamedNode(g2.clone()),
))
.expect("insert g2");
let pattern = Algebra::Bgp(vec![TriplePattern {
subject: Term::Variable(Variable::new("s").expect("var")),
predicate: Term::Iri(p.clone()),
object: Term::Variable(Variable::new("o").expect("var")),
}]);
let mut executor = UpdateExecutor::new(&mut store);
let using_g1 = [GraphReference::Iri("http://ex/g1".to_string())];
let bindings = executor
.evaluate_pattern_with_using(&pattern, Some(&using_g1))
.expect("using g1");
assert_eq!(bindings.len(), 1, "USING <g1> must scope WHERE to g1 only");
let using_g2 = [GraphReference::Iri("http://ex/g2".to_string())];
let bindings = executor
.evaluate_pattern_with_using(&pattern, Some(&using_g2))
.expect("using g2");
assert_eq!(bindings.len(), 1, "USING <g2> must scope WHERE to g2 only");
let bindings = executor
.evaluate_pattern_with_using(&pattern, None)
.expect("no using");
assert!(
bindings.is_empty(),
"without USING the default graph is empty here"
);
let using_default = [GraphReference::Default];
assert!(executor
.evaluate_pattern_with_using(&pattern, Some(&using_default))
.is_err());
}
}