use crate::{
model::{GraphName, NamedNode, Quad},
query::algebra::{Expression, GraphPattern, GraphTarget, QuadPattern, Update, UpdateOperation},
query::{AlgebraTriplePattern, TermPattern},
vocab::xsd,
OxirsError, Result, Store,
};
use std::collections::HashMap;
const BATCH_THRESHOLD: usize = 100;
const MAX_BATCH_SIZE: usize = 10_000;
pub struct UpdateExecutor<'a> {
store: &'a dyn Store,
batch_enabled: bool,
batch_size: usize,
}
impl<'a> UpdateExecutor<'a> {
pub fn new(store: &'a dyn Store) -> Self {
Self {
store,
batch_enabled: true,
batch_size: BATCH_THRESHOLD,
}
}
pub fn with_batch_size(store: &'a dyn Store, batch_size: usize) -> Self {
Self {
store,
batch_enabled: true,
batch_size: batch_size.min(MAX_BATCH_SIZE),
}
}
pub fn without_batching(store: &'a dyn Store) -> Self {
Self {
store,
batch_enabled: false,
batch_size: 1,
}
}
pub fn execute(&self, update: &Update) -> Result<()> {
for operation in &update.operations {
self.execute_operation(operation)?;
}
Ok(())
}
fn execute_operation(&self, operation: &UpdateOperation) -> Result<()> {
match operation {
UpdateOperation::InsertData { data } => self.execute_insert_data(data),
UpdateOperation::DeleteData { data } => self.execute_delete_data(data),
UpdateOperation::DeleteWhere { pattern } => self.execute_delete_where(pattern),
UpdateOperation::Modify {
delete,
insert,
where_clause,
using: _,
} => self.execute_modify(delete, insert, where_clause),
UpdateOperation::Load {
source,
destination,
silent,
} => self.execute_load(source, destination, *silent),
UpdateOperation::Clear { graph, silent } => self.execute_clear(graph, *silent),
UpdateOperation::Create { graph, silent } => self.execute_create(graph, *silent),
UpdateOperation::Drop { graph, silent } => self.execute_drop(graph, *silent),
UpdateOperation::Copy {
source,
destination,
silent,
} => self.execute_copy(source, destination, *silent),
UpdateOperation::Move {
source,
destination,
silent,
} => self.execute_move(source, destination, *silent),
UpdateOperation::Add {
source,
destination,
silent,
} => self.execute_add(source, destination, *silent),
}
}
fn execute_insert_data(&self, data: &[Quad]) -> Result<()> {
if !self.batch_enabled || data.len() < self.batch_size {
for quad in data {
self.store.insert_quad(quad.clone())?;
}
return Ok(());
}
self.batch_insert_quads(data)
}
fn execute_delete_data(&self, data: &[Quad]) -> Result<()> {
if !self.batch_enabled || data.len() < self.batch_size {
for quad in data {
self.store.remove_quad(quad)?;
}
return Ok(());
}
self.batch_delete_quads(data)
}
fn batch_insert_quads(&self, quads: &[Quad]) -> Result<()> {
for chunk in quads.chunks(self.batch_size) {
for quad in chunk {
self.store.insert_quad(quad.clone())?;
}
}
Ok(())
}
fn batch_delete_quads(&self, quads: &[Quad]) -> Result<()> {
for chunk in quads.chunks(self.batch_size) {
for quad in chunk {
self.store.remove_quad(quad)?;
}
}
Ok(())
}
fn execute_delete_where(&self, patterns: &[QuadPattern]) -> Result<()> {
let mut all_matching_quads = Vec::new();
for pattern in patterns {
let matching_quads = self.find_matching_quads(pattern)?;
all_matching_quads.extend(matching_quads);
}
if self.batch_enabled && all_matching_quads.len() >= self.batch_size {
self.batch_delete_quads(&all_matching_quads)
} else {
for quad in all_matching_quads {
self.store.remove_quad(&quad)?;
}
Ok(())
}
}
fn execute_modify(
&self,
delete_patterns: &Option<Vec<QuadPattern>>,
insert_patterns: &Option<Vec<QuadPattern>>,
where_clause: &GraphPattern,
) -> Result<()> {
let solutions = self.evaluate_graph_pattern(where_clause)?;
let mut quads_to_delete = Vec::new();
let mut quads_to_insert = Vec::new();
for solution in solutions {
if let Some(delete_patterns) = delete_patterns {
for pattern in delete_patterns {
if let Some(quad) = self.instantiate_quad_pattern(pattern, &solution)? {
quads_to_delete.push(quad);
}
}
}
if let Some(insert_patterns) = insert_patterns {
for pattern in insert_patterns {
if let Some(quad) = self.instantiate_quad_pattern(pattern, &solution)? {
quads_to_insert.push(quad);
}
}
}
}
if !quads_to_delete.is_empty() {
if self.batch_enabled && quads_to_delete.len() >= self.batch_size {
self.batch_delete_quads(&quads_to_delete)?;
} else {
for quad in quads_to_delete {
self.store.remove_quad(&quad)?;
}
}
}
if !quads_to_insert.is_empty() {
if self.batch_enabled && quads_to_insert.len() >= self.batch_size {
self.batch_insert_quads(&quads_to_insert)?;
} else {
for quad in quads_to_insert {
self.store.insert_quad(quad)?;
}
}
}
Ok(())
}
fn execute_load(
&self,
source: &NamedNode,
destination: &Option<NamedNode>,
silent: bool,
) -> Result<()> {
match self.load_from_source(source, destination) {
Ok(()) => Ok(()),
Err(e) => {
if silent {
tracing::warn!("LOAD SILENT failed for source {}: {}", source.as_str(), e);
Ok(())
} else {
Err(e)
}
}
}
}
fn load_from_source(&self, source: &NamedNode, destination: &Option<NamedNode>) -> Result<()> {
use crate::parser::{detect_format_from_content, Parser, RdfFormat};
let iri = source.as_str();
let (content, content_type, extension): (String, Option<String>, Option<String>) =
if let Some(path) = iri.strip_prefix("file://") {
let local_path = path.strip_prefix("localhost").unwrap_or(path);
let local_path = if let Some(rest) = local_path.strip_prefix('/') {
format!("/{rest}")
} else {
local_path.to_string()
};
let text = std::fs::read_to_string(&local_path).map_err(|e| {
OxirsError::Update(format!("LOAD failed reading {local_path}: {e}"))
})?;
let ext = Self::extension_from_path(&local_path);
(text, None, ext)
} else if iri.starts_with("http://") || iri.starts_with("https://") {
let (text, content_type) = Self::fetch_http(iri)?;
(text, content_type, Self::extension_from_path(iri))
} else if !iri.contains("://") {
let text = std::fs::read_to_string(iri)
.map_err(|e| OxirsError::Update(format!("LOAD failed reading {iri}: {e}")))?;
let ext = Self::extension_from_path(iri);
(text, None, ext)
} else {
return Err(OxirsError::Update(format!(
"LOAD does not support the URI scheme of source: {iri}"
)));
};
let format = content_type
.as_deref()
.and_then(Self::media_type_to_format)
.or_else(|| extension.as_deref().and_then(RdfFormat::from_extension))
.or_else(|| detect_format_from_content(&content))
.ok_or_else(|| {
OxirsError::Update(format!(
"LOAD could not determine the RDF format of source: {iri}"
))
})?;
let parser = Parser::new(format);
let quads = parser
.parse_str_to_quads(&content)
.map_err(|e| OxirsError::Update(format!("LOAD failed parsing {iri}: {e}")))?;
let target_graph = destination.clone().map(GraphName::NamedNode);
for quad in quads {
let final_quad = if let Some(ref target) = target_graph {
Quad::new(
quad.subject().clone(),
quad.predicate().clone(),
quad.object().clone(),
target.clone(),
)
} else {
quad
};
self.store.insert_quad(final_quad)?;
}
Ok(())
}
fn fetch_http(url: &str) -> Result<(String, Option<String>)> {
let url_owned = url.to_string();
std::thread::scope(|scope| {
let handle = scope.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
OxirsError::Update(format!("LOAD failed to create HTTP runtime: {e}"))
})?;
runtime.block_on(async {
let response = reqwest::get(&url_owned).await.map_err(|e| {
OxirsError::Update(format!("LOAD failed to fetch {url_owned}: {e}"))
})?;
if !response.status().is_success() {
return Err(OxirsError::Update(format!(
"LOAD received HTTP {} fetching {url_owned}",
response.status()
)));
}
let content_type = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|s| s.split(';').next().unwrap_or(s).trim().to_string());
let text = response.text().await.map_err(|e| {
OxirsError::Update(format!("LOAD failed reading body of {url_owned}: {e}"))
})?;
Ok::<_, OxirsError>((text, content_type))
})
});
handle
.join()
.map_err(|_| OxirsError::Update("LOAD HTTP fetch thread panicked".to_string()))?
})
}
fn media_type_to_format(media_type: &str) -> Option<crate::parser::RdfFormat> {
use crate::parser::RdfFormat;
match media_type.to_lowercase().trim() {
"text/turtle" | "application/x-turtle" => Some(RdfFormat::Turtle),
"application/n-triples" | "text/plain" => Some(RdfFormat::NTriples),
"application/trig" | "application/x-trig" => Some(RdfFormat::TriG),
"application/n-quads" | "text/x-nquads" => Some(RdfFormat::NQuads),
"application/rdf+xml" | "application/xml" | "text/xml" => Some(RdfFormat::RdfXml),
"application/ld+json" | "application/json" => Some(RdfFormat::JsonLd),
_ => None,
}
}
fn extension_from_path(path: &str) -> Option<String> {
let path = path.split(['?', '#']).next().unwrap_or(path);
let last_segment = path.rsplit('/').next().unwrap_or(path);
last_segment
.rsplit_once('.')
.map(|(_, ext)| ext.to_lowercase())
.filter(|ext| !ext.is_empty())
}
fn execute_clear(&self, graph: &GraphTarget, _silent: bool) -> Result<()> {
match graph {
GraphTarget::Default => {
let default_graph = GraphName::DefaultGraph;
let quads = self
.store
.find_quads(None, None, None, Some(&default_graph))?;
for quad in quads {
self.store.remove_quad(&quad)?;
}
}
GraphTarget::Named(graph_name) => {
let graph = GraphName::NamedNode(graph_name.clone());
let quads = self.store.find_quads(None, None, None, Some(&graph))?;
for quad in quads {
self.store.remove_quad(&quad)?;
}
}
GraphTarget::All => {
let quads = self.store.find_quads(None, None, None, None)?;
for quad in quads {
self.store.remove_quad(&quad)?;
}
}
}
Ok(())
}
fn execute_create(&self, graph: &NamedNode, silent: bool) -> Result<()> {
match self.store.create_graph(Some(graph)) {
Ok(()) => Ok(()),
Err(OxirsError::NotSupported(_)) => Ok(()),
Err(e) => {
if silent {
Ok(())
} else {
Err(e)
}
}
}
}
fn execute_drop(&self, graph: &GraphTarget, silent: bool) -> Result<()> {
let graph_name = match graph {
GraphTarget::Default => Some(GraphName::DefaultGraph),
GraphTarget::Named(name) => Some(GraphName::NamedNode(name.clone())),
GraphTarget::All => None,
};
if let Some(ref gn) = graph_name {
match self.store.drop_graph(Some(gn)) {
Ok(()) => return Ok(()),
Err(OxirsError::NotSupported(_)) => {}
Err(e) => {
return if silent { Ok(()) } else { Err(e) };
}
}
}
self.execute_clear(graph, silent)
}
fn execute_copy(
&self,
source: &GraphTarget,
destination: &GraphTarget,
_silent: bool,
) -> Result<()> {
self.execute_clear(destination, true)?;
let source_quads = self.get_quads_from_target(source)?;
for quad in source_quads {
let dest_quad = self.move_quad_to_target(&quad, destination)?;
self.store.insert_quad(dest_quad)?;
}
Ok(())
}
fn execute_move(
&self,
source: &GraphTarget,
destination: &GraphTarget,
_silent: bool,
) -> Result<()> {
self.execute_copy(source, destination, true)?;
self.execute_drop(source, true)?;
Ok(())
}
fn execute_add(
&self,
source: &GraphTarget,
destination: &GraphTarget,
_silent: bool,
) -> Result<()> {
let source_quads = self.get_quads_from_target(source)?;
for quad in source_quads {
let dest_quad = self.move_quad_to_target(&quad, destination)?;
self.store.insert_quad(dest_quad)?;
}
Ok(())
}
fn find_matching_quads(&self, pattern: &QuadPattern) -> Result<Vec<Quad>> {
let subject = self.term_pattern_to_subject(&pattern.subject)?;
let predicate = self.term_pattern_to_predicate(&pattern.predicate)?;
let object = self.term_pattern_to_object(&pattern.object)?;
let graph = pattern
.graph
.as_ref()
.map(|g| self.term_pattern_to_graph_name(g))
.transpose()?;
self.store.find_quads(
subject.as_ref(),
predicate.as_ref(),
object.as_ref(),
graph.as_ref(),
)
}
fn term_pattern_to_subject(
&self,
pattern: &TermPattern,
) -> Result<Option<crate::model::Subject>> {
match pattern {
TermPattern::NamedNode(n) => Ok(Some(crate::model::Subject::NamedNode(n.clone()))),
TermPattern::BlankNode(b) => Ok(Some(crate::model::Subject::BlankNode(b.clone()))),
TermPattern::Variable(_) => Ok(None), TermPattern::Literal(_) => Err(OxirsError::Update(
"Subject cannot be a literal".to_string(),
)),
TermPattern::QuotedTriple(_) => Err(OxirsError::Update(
"RDF-star quoted triples as subjects not yet fully implemented".to_string(),
)),
}
}
fn term_pattern_to_predicate(
&self,
pattern: &TermPattern,
) -> Result<Option<crate::model::Predicate>> {
match pattern {
TermPattern::NamedNode(n) => Ok(Some(crate::model::Predicate::NamedNode(n.clone()))),
TermPattern::Variable(_) => Ok(None), TermPattern::BlankNode(_) | TermPattern::Literal(_) => Err(OxirsError::Update(
"Predicate must be a named node".to_string(),
)),
TermPattern::QuotedTriple(_) => Err(OxirsError::Update(
"Quoted triples cannot be predicates".to_string(),
)),
}
}
fn term_pattern_to_object(
&self,
pattern: &TermPattern,
) -> Result<Option<crate::model::Object>> {
match pattern {
TermPattern::NamedNode(n) => Ok(Some(crate::model::Object::NamedNode(n.clone()))),
TermPattern::BlankNode(b) => Ok(Some(crate::model::Object::BlankNode(b.clone()))),
TermPattern::Literal(l) => Ok(Some(crate::model::Object::Literal(l.clone()))),
TermPattern::Variable(_) => Ok(None), TermPattern::QuotedTriple(_) => Err(OxirsError::Update(
"RDF-star quoted triples as objects not yet fully implemented".to_string(),
)),
}
}
fn term_pattern_to_graph_name(&self, pattern: &TermPattern) -> Result<GraphName> {
match pattern {
TermPattern::NamedNode(n) => Ok(GraphName::NamedNode(n.clone())),
TermPattern::Variable(_) => Ok(GraphName::DefaultGraph), TermPattern::BlankNode(_) | TermPattern::Literal(_) => Err(OxirsError::Update(
"Graph name must be a named node".to_string(),
)),
TermPattern::QuotedTriple(_) => Err(OxirsError::Update(
"Graph names cannot be quoted triples".to_string(),
)),
}
}
fn evaluate_graph_pattern(
&self,
pattern: &GraphPattern,
) -> Result<Vec<HashMap<String, crate::model::Term>>> {
use crate::query::{QueryEngine, QueryResult};
let query_engine = QueryEngine::new();
let sparql_query = self.graph_pattern_to_sparql(pattern)?;
match query_engine.query(&sparql_query, self.store)? {
QueryResult::Select {
variables: _,
bindings,
} => {
let mut solutions = Vec::new();
for binding in bindings {
let mut solution = HashMap::new();
for (var_name, term) in binding {
solution.insert(var_name, term);
}
solutions.push(solution);
}
Ok(solutions)
}
_ => Err(OxirsError::Update(
"Expected SELECT query result for WHERE clause evaluation".to_string(),
)),
}
}
fn instantiate_quad_pattern(
&self,
pattern: &QuadPattern,
solution: &HashMap<String, crate::model::Term>,
) -> Result<Option<Quad>> {
use crate::model::*;
let subject = match &pattern.subject {
TermPattern::Variable(var) => {
if let Some(term) = solution.get(var.name()) {
match term {
Term::NamedNode(n) => Subject::NamedNode(n.clone()),
Term::BlankNode(b) => Subject::BlankNode(b.clone()),
_ => return Ok(None), }
} else {
return Ok(None); }
}
TermPattern::NamedNode(n) => Subject::NamedNode(n.clone()),
TermPattern::BlankNode(b) => Subject::BlankNode(b.clone()),
TermPattern::Literal(_) => return Ok(None), TermPattern::QuotedTriple(_) => return Ok(None), };
let predicate = match &pattern.predicate {
TermPattern::Variable(var) => {
if let Some(Term::NamedNode(n)) = solution.get(var.name()) {
Predicate::NamedNode(n.clone())
} else {
return Ok(None); }
}
TermPattern::NamedNode(n) => Predicate::NamedNode(n.clone()),
_ => return Ok(None), };
let object = match &pattern.object {
TermPattern::Variable(var) => {
if let Some(term) = solution.get(var.name()) {
match term {
Term::NamedNode(n) => Object::NamedNode(n.clone()),
Term::BlankNode(b) => Object::BlankNode(b.clone()),
Term::Literal(l) => Object::Literal(l.clone()),
_ => return Ok(None), }
} else {
return Ok(None); }
}
TermPattern::NamedNode(n) => Object::NamedNode(n.clone()),
TermPattern::BlankNode(b) => Object::BlankNode(b.clone()),
TermPattern::Literal(l) => Object::Literal(l.clone()),
TermPattern::QuotedTriple(_) => return Ok(None), };
let graph_name = match &pattern.graph {
Some(graph_pattern) => match graph_pattern {
TermPattern::Variable(var) => {
if let Some(Term::NamedNode(n)) = solution.get(var.name()) {
GraphName::NamedNode(n.clone())
} else {
return Ok(None); }
}
TermPattern::NamedNode(n) => GraphName::NamedNode(n.clone()),
_ => return Ok(None), },
None => GraphName::DefaultGraph,
};
Ok(Some(Quad::new(subject, predicate, object, graph_name)))
}
fn get_quads_from_target(&self, target: &GraphTarget) -> Result<Vec<Quad>> {
match target {
GraphTarget::Default => {
let graph = GraphName::DefaultGraph;
self.store.find_quads(None, None, None, Some(&graph))
}
GraphTarget::Named(graph_name) => {
let graph = GraphName::NamedNode(graph_name.clone());
self.store.find_quads(None, None, None, Some(&graph))
}
GraphTarget::All => self.store.find_quads(None, None, None, None),
}
}
fn move_quad_to_target(&self, quad: &Quad, target: &GraphTarget) -> Result<Quad> {
match target {
GraphTarget::Default => Ok(Quad::new(
quad.subject().clone(),
quad.predicate().clone(),
quad.object().clone(),
GraphName::DefaultGraph,
)),
GraphTarget::Named(graph_name) => Ok(Quad::new(
quad.subject().clone(),
quad.predicate().clone(),
quad.object().clone(),
GraphName::NamedNode(graph_name.clone()),
)),
GraphTarget::All => {
Ok(quad.clone())
}
}
}
fn graph_pattern_to_sparql(&self, pattern: &GraphPattern) -> Result<String> {
use crate::query::algebra::*;
match pattern {
GraphPattern::Bgp(triple_patterns) => {
let mut sparql = String::from("SELECT * WHERE { ");
for (i, triple_pattern) in triple_patterns.iter().enumerate() {
if i > 0 {
sparql.push_str(" . ");
}
sparql.push_str(&self.triple_pattern_to_sparql(triple_pattern)?);
}
sparql.push_str(" }");
Ok(sparql)
}
GraphPattern::Join(left, right) => {
let left_sparql = self.graph_pattern_to_sparql(left)?;
let right_sparql = self.graph_pattern_to_sparql(right)?;
let left_where = self.extract_where_clause(&left_sparql)?;
let right_where = self.extract_where_clause(&right_sparql)?;
Ok(format!("SELECT * WHERE {{ {left_where} . {right_where} }}"))
}
GraphPattern::Filter { expr, inner } => {
let inner_sparql = self.graph_pattern_to_sparql(inner)?;
let inner_where = self.extract_where_clause(&inner_sparql)?;
let filter_expr = self.expression_to_sparql(expr)?;
Ok(format!(
"SELECT * WHERE {{ {inner_where} FILTER ({filter_expr}) }}"
))
}
GraphPattern::Union(left, right) => {
let left_sparql = self.graph_pattern_to_sparql(left)?;
let right_sparql = self.graph_pattern_to_sparql(right)?;
let left_where = self.extract_where_clause(&left_sparql)?;
let right_where = self.extract_where_clause(&right_sparql)?;
Ok(format!(
"SELECT * WHERE {{ {{ {left_where} }} UNION {{ {right_where} }} }}"
))
}
_ => Err(OxirsError::Update(format!(
"Graph pattern type not yet supported in SPARQL conversion: {pattern:?}"
))),
}
}
fn triple_pattern_to_sparql(&self, pattern: &AlgebraTriplePattern) -> Result<String> {
let subject = self.term_pattern_to_sparql(&pattern.subject)?;
let predicate = self.term_pattern_to_sparql(&pattern.predicate)?;
let object = self.term_pattern_to_sparql(&pattern.object)?;
Ok(format!("{subject} {predicate} {object}"))
}
fn term_pattern_to_sparql(&self, pattern: &TermPattern) -> Result<String> {
match pattern {
TermPattern::Variable(var) => Ok(format!("?{}", var.name())),
TermPattern::NamedNode(node) => Ok(format!("<{}>", node.as_str())),
TermPattern::BlankNode(blank) => Ok(format!("_:{}", blank.as_str())),
TermPattern::Literal(literal) => {
if let Some(lang) = literal.language() {
Ok(format!("\"{}\"@{}", literal.value(), lang))
} else if literal.datatype() != xsd::STRING.as_ref() {
Ok(format!("\"{}\"^^<{}>", literal.value(), literal.datatype()))
} else {
Ok(format!("\"{}\"", literal.value()))
}
}
TermPattern::QuotedTriple(_) => Err(OxirsError::Update(
"RDF-star quoted triples not yet fully supported in SPARQL conversion".to_string(),
)),
}
}
#[allow(clippy::only_used_in_recursion)]
fn expression_to_sparql(&self, expr: &Expression) -> Result<String> {
match expr {
Expression::Variable(var) => Ok(format!("?{}", var.name())),
Expression::Term(term) => match term {
crate::model::Term::NamedNode(n) => Ok(format!("<{}>", n.as_str())),
crate::model::Term::BlankNode(b) => Ok(format!("_:{}", b.as_str())),
crate::model::Term::Literal(l) => {
if let Some(lang) = l.language() {
Ok(format!("\"{}\"@{}", l.value(), lang))
} else if l.datatype() != xsd::STRING.as_ref() {
Ok(format!("\"{}\"^^<{}>", l.value(), l.datatype()))
} else {
Ok(format!("\"{}\"", l.value()))
}
}
_ => Err(OxirsError::Update(
"Unsupported term type in expression".to_string(),
)),
},
Expression::Equal(left, right) => {
let left_sparql = self.expression_to_sparql(left)?;
let right_sparql = self.expression_to_sparql(right)?;
Ok(format!("({left_sparql} = {right_sparql})"))
}
Expression::And(left, right) => {
let left_sparql = self.expression_to_sparql(left)?;
let right_sparql = self.expression_to_sparql(right)?;
Ok(format!("({left_sparql} && {right_sparql})"))
}
Expression::Or(left, right) => {
let left_sparql = self.expression_to_sparql(left)?;
let right_sparql = self.expression_to_sparql(right)?;
Ok(format!("({left_sparql} || {right_sparql})"))
}
Expression::Not(inner) => {
let inner_sparql = self.expression_to_sparql(inner)?;
Ok(format!("(!{inner_sparql})"))
}
_ => Err(OxirsError::Update(format!(
"Expression type not yet supported in SPARQL conversion: {expr:?}"
))),
}
}
fn extract_where_clause(&self, sparql: &str) -> Result<String> {
if let Some(start) = sparql.find("WHERE {") {
let where_start = start + 7; if let Some(end) = sparql.rfind('}') {
let where_content = &sparql[where_start..end].trim();
Ok(where_content.to_string())
} else {
Err(OxirsError::Update(
"Malformed SPARQL query: missing closing brace".to_string(),
))
}
} else {
Err(OxirsError::Update(
"Malformed SPARQL query: missing WHERE clause".to_string(),
))
}
}
}
#[derive(Default)]
pub struct UpdateParser;
impl UpdateParser {
pub fn new() -> Self {
Self
}
pub fn parse(&self, update_str: &str) -> Result<Update> {
let trimmed = update_str.trim();
let (prefixes, remaining) = self.extract_prefixes(trimmed)?;
if remaining.contains("INSERT DATA") {
self.parse_insert_data(&remaining, prefixes)
} else if remaining.contains("DELETE DATA") {
self.parse_delete_data(&remaining, prefixes)
} else if self.is_delete_where_shorthand(&remaining) {
self.parse_delete_where(&remaining, prefixes)
} else if remaining.contains("DELETE") && remaining.contains("WHERE") {
self.parse_delete_modify(&remaining, prefixes)
} else if remaining.contains("INSERT") && remaining.contains("WHERE") {
self.parse_insert_where(&remaining, prefixes)
} else if remaining.contains("CLEAR") {
self.parse_clear(&remaining, prefixes)
} else {
Err(OxirsError::Parse(format!(
"Unsupported UPDATE operation: {}",
remaining
)))
}
}
fn is_delete_where_shorthand(&self, update_str: &str) -> bool {
if let Some(delete_pos) = update_str.find("DELETE") {
if let Some(where_pos) = update_str.find("WHERE") {
let between = &update_str[delete_pos + 6..where_pos];
return !between.contains('{');
}
}
false
}
fn extract_prefixes(&self, update_str: &str) -> Result<(HashMap<String, NamedNode>, String)> {
let mut prefixes = HashMap::new();
let mut remaining = update_str.to_string();
loop {
let trimmed = remaining.trim();
if let Some(prefix_start) = trimmed.find("PREFIX") {
if prefix_start == 0 || trimmed[..prefix_start].chars().all(|c| c.is_whitespace()) {
let after_prefix = &trimmed[prefix_start + 6..];
if let Some(colon_pos) = after_prefix.find(':') {
if let Some(iri_start) = after_prefix.find('<') {
if let Some(iri_end) = after_prefix.find('>') {
let prefix = after_prefix[..colon_pos].trim().to_string();
let iri_str = &after_prefix[iri_start + 1..iri_end];
let iri_node = NamedNode::new(iri_str).map_err(|e| {
OxirsError::Parse(format!("Invalid prefix IRI: {e}"))
})?;
prefixes.insert(prefix, iri_node);
remaining = after_prefix[iri_end + 1..].to_string();
continue;
}
}
}
}
}
break;
}
Ok((prefixes, remaining.trim().to_string()))
}
fn parse_insert_data(
&self,
update_str: &str,
prefixes: HashMap<String, NamedNode>,
) -> Result<Update> {
use crate::query::algebra::UpdateOperation;
let data_start = update_str.find('{');
let data_end = update_str.rfind('}');
if let (Some(start), Some(end)) = (data_start, data_end) {
let data_block = update_str[start + 1..end].trim();
let quads = self.parse_quad_data(data_block, &prefixes)?;
Ok(Update {
base: None,
prefixes,
operations: vec![UpdateOperation::InsertData { data: quads }],
})
} else {
Err(OxirsError::Parse(
"Malformed INSERT DATA: missing data block".to_string(),
))
}
}
fn parse_delete_data(
&self,
update_str: &str,
prefixes: HashMap<String, NamedNode>,
) -> Result<Update> {
use crate::query::algebra::UpdateOperation;
let data_start = update_str.find('{');
let data_end = update_str.rfind('}');
if let (Some(start), Some(end)) = (data_start, data_end) {
let data_block = update_str[start + 1..end].trim();
let quads = self.parse_quad_data(data_block, &prefixes)?;
Ok(Update {
base: None,
prefixes,
operations: vec![UpdateOperation::DeleteData { data: quads }],
})
} else {
Err(OxirsError::Parse(
"Malformed DELETE DATA: missing data block".to_string(),
))
}
}
fn parse_quad_data(
&self,
data_block: &str,
prefixes: &HashMap<String, NamedNode>,
) -> Result<Vec<Quad>> {
use crate::format::format::RdfFormat;
use crate::format::RdfParser;
use std::io::Cursor;
let mut turtle_doc = String::new();
for (prefix, iri) in prefixes {
turtle_doc.push_str(&format!("@prefix {}: <{}> .\n", prefix, iri.as_str()));
}
turtle_doc.push('\n');
turtle_doc.push_str(data_block);
if !data_block.trim().is_empty() && !data_block.trim_end().ends_with('.') {
turtle_doc.push_str(" .\n");
}
let parser = RdfParser::new(RdfFormat::Turtle);
let turtle_bytes = turtle_doc.into_bytes();
let cursor = Cursor::new(turtle_bytes);
let quads: Vec<Quad> = parser
.for_reader(cursor)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| OxirsError::Parse(format!("Failed to parse UPDATE data: {}", e)))?;
Ok(quads)
}
fn parse_delete_modify(
&self,
update_str: &str,
prefixes: HashMap<String, NamedNode>,
) -> Result<Update> {
use crate::query::algebra::UpdateOperation;
let delete_pos = update_str.find("DELETE");
let where_pos = update_str.find("WHERE");
if delete_pos.is_none() || where_pos.is_none() {
return Err(OxirsError::Parse(
"Malformed DELETE/WHERE: missing DELETE or WHERE keyword".to_string(),
));
}
let delete_start =
update_str[delete_pos.expect("delete_pos validated as Some above")..].find('{');
let delete_end =
update_str[delete_pos.expect("delete_pos validated as Some above")..].find('}');
if delete_start.is_none() || delete_end.is_none() {
return Err(OxirsError::Parse(
"Malformed DELETE/WHERE: missing template block".to_string(),
));
}
let template_start = delete_pos.expect("delete_pos validated as Some above")
+ delete_start.expect("delete_start validated as Some above")
+ 1;
let template_end = delete_pos.expect("delete_pos validated as Some above")
+ delete_end.expect("delete_end validated as Some above");
let template_block = update_str[template_start..template_end].trim();
let delete_patterns = self.parse_template_patterns(template_block, &prefixes)?;
let where_start =
update_str[where_pos.expect("where_pos validated as Some above")..].find('{');
let where_end =
update_str[where_pos.expect("where_pos validated as Some above")..].rfind('}');
if where_start.is_none() || where_end.is_none() {
return Err(OxirsError::Parse(
"Malformed DELETE/WHERE: missing WHERE pattern block".to_string(),
));
}
let where_pattern_start = where_pos.expect("where_pos validated as Some above")
+ where_start.expect("where_start validated as Some above")
+ 1;
let where_pattern_end = where_pos.expect("where_pos validated as Some above")
+ where_end.expect("where_end validated as Some above");
let where_block = update_str[where_pattern_start..where_pattern_end].trim();
let where_pattern = self.parse_where_pattern(where_block, &prefixes)?;
Ok(Update {
base: None,
prefixes,
operations: vec![UpdateOperation::Modify {
delete: Some(delete_patterns),
insert: None,
where_clause: Box::new(where_pattern),
using: crate::query::algebra::Dataset {
default: vec![],
named: vec![],
},
}],
})
}
fn parse_delete_where(
&self,
update_str: &str,
prefixes: HashMap<String, NamedNode>,
) -> Result<Update> {
use crate::query::algebra::UpdateOperation;
let pattern_start = update_str.find('{');
let pattern_end = update_str.rfind('}');
if let (Some(start), Some(end)) = (pattern_start, pattern_end) {
let pattern_block = update_str[start + 1..end].trim();
let patterns = self.parse_quad_patterns(pattern_block, &prefixes)?;
Ok(Update {
base: None,
prefixes,
operations: vec![UpdateOperation::DeleteWhere { pattern: patterns }],
})
} else {
Err(OxirsError::Parse(
"Malformed DELETE WHERE: missing pattern block".to_string(),
))
}
}
fn parse_insert_where(
&self,
update_str: &str,
prefixes: HashMap<String, NamedNode>,
) -> Result<Update> {
use crate::query::algebra::UpdateOperation;
let insert_pos = update_str.find("INSERT");
let where_pos = update_str.find("WHERE");
if insert_pos.is_none() || where_pos.is_none() {
return Err(OxirsError::Parse(
"Malformed INSERT WHERE: missing INSERT or WHERE keyword".to_string(),
));
}
let insert_start =
update_str[insert_pos.expect("insert_pos validated as Some above")..].find('{');
let insert_end =
update_str[insert_pos.expect("insert_pos validated as Some above")..].find('}');
if insert_start.is_none() || insert_end.is_none() {
return Err(OxirsError::Parse(
"Malformed INSERT WHERE: missing template block".to_string(),
));
}
let template_start = insert_pos.expect("insert_pos validated as Some above")
+ insert_start.expect("insert_start validated as Some above")
+ 1;
let template_end = insert_pos.expect("insert_pos validated as Some above")
+ insert_end.expect("insert_end validated as Some above");
let template_block = update_str[template_start..template_end].trim();
let insert_patterns = self.parse_template_patterns(template_block, &prefixes)?;
let where_start =
update_str[where_pos.expect("where_pos validated as Some above")..].find('{');
let where_end =
update_str[where_pos.expect("where_pos validated as Some above")..].rfind('}');
if where_start.is_none() || where_end.is_none() {
return Err(OxirsError::Parse(
"Malformed INSERT WHERE: missing WHERE pattern block".to_string(),
));
}
let where_pattern_start = where_pos.expect("where_pos validated as Some above")
+ where_start.expect("where_start validated as Some above")
+ 1;
let where_pattern_end = where_pos.expect("where_pos validated as Some above")
+ where_end.expect("where_end validated as Some above");
let where_block = update_str[where_pattern_start..where_pattern_end].trim();
let where_pattern = self.parse_where_pattern(where_block, &prefixes)?;
Ok(Update {
base: None,
prefixes,
operations: vec![UpdateOperation::Modify {
delete: None,
insert: Some(insert_patterns),
where_clause: Box::new(where_pattern),
using: crate::query::algebra::Dataset {
default: vec![],
named: vec![],
},
}],
})
}
fn parse_quad_patterns(
&self,
pattern_block: &str,
prefixes: &HashMap<String, NamedNode>,
) -> Result<Vec<crate::query::algebra::QuadPattern>> {
use crate::format::format::RdfFormat;
use crate::format::RdfParser;
use crate::query::algebra::QuadPattern;
use std::io::Cursor;
let mut turtle_doc = String::new();
for (prefix, iri) in prefixes {
turtle_doc.push_str(&format!("@prefix {}: <{}> .\n", prefix, iri.as_str()));
}
turtle_doc.push('\n');
turtle_doc.push_str(pattern_block);
let parser = RdfParser::new(RdfFormat::Turtle);
let turtle_bytes = turtle_doc.into_bytes();
let cursor = Cursor::new(turtle_bytes);
let quads: Vec<Quad> = parser
.for_reader(cursor)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| OxirsError::Parse(format!("Failed to parse pattern: {}", e)))?;
let quad_patterns: Vec<QuadPattern> = quads
.into_iter()
.map(|quad| QuadPattern {
subject: self.subject_to_term_pattern(quad.subject()),
predicate: self.predicate_to_term_pattern(quad.predicate()),
object: self.object_to_term_pattern(quad.object()),
graph: Some(self.graph_to_term_pattern(quad.graph_name())),
})
.collect();
Ok(quad_patterns)
}
fn parse_template_patterns(
&self,
template_block: &str,
prefixes: &HashMap<String, NamedNode>,
) -> Result<Vec<crate::query::algebra::QuadPattern>> {
use crate::query::algebra::QuadPattern;
let pattern_lines: Vec<&str> = template_block
.split('.')
.map(|s| s.trim())
.filter(|s| !s.is_empty() && *s != "}")
.collect();
let mut quad_patterns = Vec::new();
for line in pattern_lines {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let subject = self.parse_term_pattern_with_prefix(parts[0], prefixes)?;
let predicate = self.parse_term_pattern_with_prefix(parts[1], prefixes)?;
let object = self.parse_term_pattern_with_prefix(parts[2], prefixes)?;
quad_patterns.push(QuadPattern {
subject,
predicate,
object,
graph: None, });
}
}
Ok(quad_patterns)
}
fn parse_where_pattern(
&self,
pattern_block: &str,
prefixes: &HashMap<String, NamedNode>,
) -> Result<crate::query::algebra::GraphPattern> {
use crate::query::algebra::{AlgebraTriplePattern, GraphPattern};
let pattern_lines: Vec<&str> = pattern_block
.split('.')
.map(|s| s.trim())
.filter(|s| !s.is_empty() && !s.starts_with("FILTER"))
.collect();
let mut triple_patterns = Vec::new();
for line in pattern_lines {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let subject = self.parse_term_pattern_with_prefix(parts[0], prefixes)?;
let predicate = self.parse_term_pattern_with_prefix(parts[1], prefixes)?;
let object = self.parse_term_pattern_with_prefix(parts[2], prefixes)?;
triple_patterns.push(AlgebraTriplePattern {
subject,
predicate,
object,
});
}
}
Ok(GraphPattern::Bgp(triple_patterns))
}
#[allow(dead_code)]
fn parse_term_pattern(&self, term_str: &str) -> Result<TermPattern> {
let trimmed = term_str.trim();
if let Some(var_name) = trimmed.strip_prefix('?') {
let var = crate::model::Variable::new(var_name)
.map_err(|e| OxirsError::Parse(format!("Invalid variable: {e}")))?;
Ok(TermPattern::Variable(var))
} else if trimmed.starts_with('<') && trimmed.ends_with('>') {
let iri = &trimmed[1..trimmed.len() - 1];
let node =
NamedNode::new(iri).map_err(|e| OxirsError::Parse(format!("Invalid IRI: {e}")))?;
Ok(TermPattern::NamedNode(node))
} else if trimmed.starts_with('"') {
let lit_value = trimmed.trim_matches('"');
Ok(TermPattern::Literal(
crate::model::Literal::new_simple_literal(lit_value),
))
} else {
Err(OxirsError::Parse(format!(
"Cannot parse term pattern: {}",
term_str
)))
}
}
fn parse_term_pattern_with_prefix(
&self,
term_str: &str,
prefixes: &HashMap<String, NamedNode>,
) -> Result<TermPattern> {
let trimmed = term_str.trim();
if let Some(var_name) = trimmed.strip_prefix('?') {
let var = crate::model::Variable::new(var_name)
.map_err(|e| OxirsError::Parse(format!("Invalid variable: {e}")))?;
Ok(TermPattern::Variable(var))
} else if trimmed.starts_with('<') && trimmed.ends_with('>') {
let iri = &trimmed[1..trimmed.len() - 1];
let node =
NamedNode::new(iri).map_err(|e| OxirsError::Parse(format!("Invalid IRI: {e}")))?;
Ok(TermPattern::NamedNode(node))
} else if trimmed.starts_with('"') {
let lit_value = trimmed.trim_matches('"');
Ok(TermPattern::Literal(
crate::model::Literal::new_simple_literal(lit_value),
))
} else if trimmed.contains(':') {
let parts: Vec<&str> = trimmed.splitn(2, ':').collect();
if parts.len() == 2 {
let prefix = parts[0];
let local = parts[1];
if let Some(base_iri) = prefixes.get(prefix) {
let full_iri = format!("{}{}", base_iri.as_str(), local);
let node = NamedNode::new(&full_iri)
.map_err(|e| OxirsError::Parse(format!("Invalid expanded IRI: {e}")))?;
Ok(TermPattern::NamedNode(node))
} else {
Err(OxirsError::Parse(format!("Unknown prefix: {}", prefix)))
}
} else {
Err(OxirsError::Parse(format!(
"Invalid prefixed name: {}",
term_str
)))
}
} else {
Err(OxirsError::Parse(format!(
"Cannot parse term pattern: {}",
term_str
)))
}
}
fn subject_to_term_pattern(&self, subject: &crate::model::Subject) -> TermPattern {
use crate::model::Subject;
match subject {
Subject::NamedNode(n) => TermPattern::NamedNode(n.clone()),
Subject::BlankNode(b) => TermPattern::BlankNode(b.clone()),
Subject::Variable(v) => TermPattern::Variable(v.clone()),
Subject::QuotedTriple(_) => {
TermPattern::Variable(
crate::model::Variable::new("quotedTriple")
.expect("quotedTriple is a valid variable name"),
)
}
}
}
fn predicate_to_term_pattern(&self, predicate: &crate::model::Predicate) -> TermPattern {
use crate::model::Predicate;
match predicate {
Predicate::NamedNode(n) => TermPattern::NamedNode(n.clone()),
Predicate::Variable(v) => TermPattern::Variable(v.clone()),
}
}
fn object_to_term_pattern(&self, object: &crate::model::Object) -> TermPattern {
use crate::model::Object;
match object {
Object::NamedNode(n) => TermPattern::NamedNode(n.clone()),
Object::BlankNode(b) => TermPattern::BlankNode(b.clone()),
Object::Literal(l) => TermPattern::Literal(l.clone()),
Object::Variable(v) => TermPattern::Variable(v.clone()),
Object::QuotedTriple(_) => {
TermPattern::Variable(
crate::model::Variable::new("quotedTripleObj")
.expect("quotedTripleObj is a valid variable name"),
)
}
}
}
fn graph_to_term_pattern(&self, graph: &GraphName) -> TermPattern {
match graph {
GraphName::NamedNode(n) => TermPattern::NamedNode(n.clone()),
GraphName::BlankNode(b) => TermPattern::BlankNode(b.clone()),
GraphName::Variable(v) => TermPattern::Variable(v.clone()),
GraphName::DefaultGraph => {
TermPattern::Variable(
crate::model::Variable::new("defaultGraph")
.expect("defaultGraph is a valid variable name"),
)
}
}
}
fn parse_clear(
&self,
update_str: &str,
prefixes: HashMap<String, NamedNode>,
) -> Result<Update> {
use crate::query::algebra::{GraphTarget, UpdateOperation};
let trimmed = update_str.trim();
let silent = trimmed.contains("SILENT");
let graph_target = if trimmed.contains("DEFAULT") {
GraphTarget::Default
} else if trimmed.contains("ALL") {
GraphTarget::All
} else if let Some(graph_start) = trimmed.find("GRAPH") {
let after_graph = &trimmed[graph_start + 5..].trim();
if let Some(iri_start) = after_graph.find('<') {
if let Some(iri_end) = after_graph.find('>') {
let iri_str = &after_graph[iri_start + 1..iri_end];
let graph_node = NamedNode::new(iri_str)
.map_err(|e| OxirsError::Parse(format!("Invalid graph IRI: {e}")))?;
GraphTarget::Named(graph_node)
} else {
return Err(OxirsError::Parse(
"Malformed graph IRI in CLEAR".to_string(),
));
}
} else {
return Err(OxirsError::Parse("Missing graph IRI in CLEAR".to_string()));
}
} else {
GraphTarget::Default };
Ok(Update {
base: None,
prefixes,
operations: vec![UpdateOperation::Clear {
graph: graph_target,
silent,
}],
})
}
}
#[cfg(test)]
mod load_tests {
use super::*;
use crate::model::GraphName;
use crate::query::algebra::{Update, UpdateOperation};
use crate::rdf_store::RdfStore;
use std::io::Write;
fn unique_temp_dir(tag: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut dir = std::env::temp_dir();
dir.push(format!(
"oxirs_update_load_{tag}_{}_{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn load_update(source: NamedNode, destination: Option<NamedNode>, silent: bool) -> Update {
Update {
base: None,
prefixes: HashMap::new(),
operations: vec![UpdateOperation::Load {
source,
destination,
silent,
}],
}
}
#[test]
fn test_load_from_local_file() -> Result<()> {
let store = RdfStore::new()?;
let dir = unique_temp_dir("localfile");
let path = dir.join("data.ttl");
{
let mut f = std::fs::File::create(&path).expect("create ttl file");
writeln!(
f,
"<http://example.org/s> <http://example.org/p> <http://example.org/o> ."
)
.expect("write ttl");
}
let source = NamedNode::new(format!("file://{}", path.display()))?;
UpdateExecutor::new(&store).execute(&load_update(source, None, false))?;
assert_eq!(store.len()?, 1);
let _ = std::fs::remove_dir_all(&dir);
Ok(())
}
#[test]
fn test_load_into_named_graph() -> Result<()> {
let store = RdfStore::new()?;
let dir = unique_temp_dir("namedgraph");
let path = dir.join("data.nt");
{
let mut f = std::fs::File::create(&path).expect("create nt file");
writeln!(
f,
"<http://example.org/s> <http://example.org/p> <http://example.org/o> ."
)
.expect("write nt");
}
let graph = NamedNode::new("http://example.org/g")?;
let source = NamedNode::new(format!("file://{}", path.display()))?;
UpdateExecutor::new(&store).execute(&load_update(source, Some(graph.clone()), false))?;
let graph_name = GraphName::NamedNode(graph);
let in_graph = store.find_quads(None, None, None, Some(&graph_name))?;
assert_eq!(in_graph.len(), 1);
let _ = std::fs::remove_dir_all(&dir);
Ok(())
}
#[test]
fn test_load_unsupported_scheme_errors_unless_silent() -> Result<()> {
let store = RdfStore::new()?;
let source = NamedNode::new("ftp://example.org/data.ttl")?;
let result = UpdateExecutor::new(&store).execute(&load_update(source.clone(), None, false));
assert!(result.is_err(), "non-silent LOAD should return an error");
let silent_result = UpdateExecutor::new(&store).execute(&load_update(source, None, true));
assert!(silent_result.is_ok(), "silent LOAD should succeed");
assert_eq!(store.len()?, 0);
Ok(())
}
#[test]
fn test_load_missing_local_file_silent_ok() -> Result<()> {
let store = RdfStore::new()?;
let missing = unique_temp_dir("missing").join("does_not_exist.ttl");
let source = NamedNode::new(format!("file://{}", missing.display()))?;
assert!(UpdateExecutor::new(&store)
.execute(&load_update(source.clone(), None, false))
.is_err());
assert!(UpdateExecutor::new(&store)
.execute(&load_update(source, None, true))
.is_ok());
Ok(())
}
#[test]
fn test_extension_from_path() {
assert_eq!(
UpdateExecutor::extension_from_path("http://example.org/data.ttl"),
Some("ttl".to_string())
);
assert_eq!(
UpdateExecutor::extension_from_path("http://example.org/data.nt?x=1#frag"),
Some("nt".to_string())
);
assert_eq!(
UpdateExecutor::extension_from_path("http://example.org/nodot"),
None
);
}
}