#![cfg_attr(doc, doc = include_str!("../README.md"))]
#![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
mod dataset;
mod error;
mod eval;
mod expression;
mod model;
mod service;
mod update;
#[cfg(feature = "sparql-12")]
pub use crate::dataset::ExpressionTriple;
pub use crate::dataset::{ExpressionTerm, InternalQuad, QueryableDataset};
pub use crate::error::QueryEvaluationError;
pub use crate::eval::CancellationToken;
use crate::eval::{EvalNodeWithStats, SimpleEvaluator, Timer};
use crate::expression::{
CustomFunctionRegistry, ExpressionEvaluatorContext, build_expression_evaluator,
};
pub use crate::model::{QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter};
use crate::service::ServiceHandlerRegistry;
pub use crate::service::{DefaultServiceHandler, ServiceHandler};
pub use crate::update::{DeleteInsertIter, DeleteInsertQuad};
use json_event_parser::{JsonEvent, WriterJsonSerializer};
use oxiri::Iri;
use oxrdf::{GraphName, Literal, NamedNode, NamedOrBlankNode, Term, Variable};
use oxsdatatypes::{DateTime, DayTimeDuration, Float};
use spargebra::Query;
use spargebra::algebra::QueryDataset;
use spargebra::term::{GroundQuadPattern, QuadPattern};
use sparopt::Optimizer;
use sparopt::algebra::GraphPattern;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::{fmt, io};
#[derive(Clone, Default)]
pub struct QueryEvaluator {
service_handler: ServiceHandlerRegistry,
custom_functions: CustomFunctionRegistry,
custom_aggregate_functions: CustomAggregateFunctionRegistry,
without_optimizations: bool,
run_stats: bool,
cancellation_token: Option<CancellationToken>,
}
impl QueryEvaluator {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn prepare<'a>(&'a self, query: &'a Query) -> PreparedQuery<'a> {
let dataset = query.dataset().cloned().map(Into::into).unwrap_or_default();
PreparedQuery {
evaluator: self,
query,
dataset,
substitutions: HashMap::new(),
}
}
#[deprecated(since = "0.2.1", note = "Use prepare instead")]
#[expect(deprecated)]
pub fn execute<'a>(
&self,
dataset: impl QueryableDataset<'a>,
query: &Query,
) -> Result<QueryResults<'a>, QueryEvaluationError> {
self.execute_with_substituted_variables(dataset, query, [])
}
#[deprecated(since = "0.2.1", note = "Use prepare instead")]
#[expect(deprecated)]
pub fn execute_with_substituted_variables<'a>(
&self,
dataset: impl QueryableDataset<'a>,
query: &Query,
substitutions: impl IntoIterator<Item = (Variable, Term)>,
) -> Result<QueryResults<'a>, QueryEvaluationError> {
self.explain_with_substituted_variables(dataset, query, substitutions)
.0
}
#[deprecated(since = "0.2.1", note = "Use prepare instead")]
#[expect(deprecated)]
pub fn explain<'a>(
&self,
dataset: impl QueryableDataset<'a>,
query: &Query,
) -> (
Result<QueryResults<'a>, QueryEvaluationError>,
QueryExplanation,
) {
self.explain_with_substituted_variables(dataset, query, [])
}
#[deprecated(since = "0.2.1", note = "Use prepare instead")]
pub fn explain_with_substituted_variables<'a>(
&self,
dataset: impl QueryableDataset<'a>,
query: &Query,
substitutions: impl IntoIterator<Item = (Variable, Term)>,
) -> (
Result<QueryResults<'a>, QueryEvaluationError>,
QueryExplanation,
) {
let mut prepared = PreparedQuery {
evaluator: self,
query,
dataset: QueryDatasetSpecification::new(),
substitutions: HashMap::new(),
};
for (variable, term) in substitutions {
prepared = prepared.substitute_variable(variable, term);
}
prepared.explain(dataset)
}
#[inline]
#[must_use]
pub fn with_service_handler(
mut self,
service_name: impl Into<NamedNode>,
handler: impl ServiceHandler + 'static,
) -> Self {
self.service_handler = self
.service_handler
.with_handler(service_name.into(), handler);
self
}
#[inline]
#[must_use]
pub fn with_default_service_handler(
mut self,
handler: impl DefaultServiceHandler + 'static,
) -> Self {
self.service_handler = self.service_handler.with_default_handler(handler);
self
}
#[inline]
#[must_use]
pub fn has_default_service_handler(&self) -> bool {
self.service_handler.has_default_handler()
}
#[inline]
#[must_use]
pub fn with_custom_function(
mut self,
name: NamedNode,
evaluator: impl Fn(&[Term]) -> Option<Term> + Send + Sync + 'static,
) -> Self {
self.custom_functions.insert(name, Arc::new(evaluator));
self
}
#[inline]
#[must_use]
pub fn with_custom_aggregate_function(
mut self,
name: NamedNode,
evaluator: impl Fn() -> Box<dyn AggregateFunctionAccumulator + Send + Sync>
+ Send
+ Sync
+ 'static,
) -> Self {
self.custom_aggregate_functions
.insert(name, Arc::new(evaluator));
self
}
#[inline]
#[must_use]
pub fn without_optimizations(mut self) -> Self {
self.without_optimizations = true;
self
}
#[inline]
#[must_use]
pub fn compute_statistics(mut self) -> Self {
self.run_stats = true;
self
}
#[must_use]
pub fn with_cancellation_token(mut self, cancellation_token: CancellationToken) -> Self {
self.cancellation_token = Some(cancellation_token);
self
}
fn eval_expression_term_with_substitutions<'a>(
&self,
expression: &sparopt::algebra::Expression,
substitutions: impl IntoIterator<Item = (&'a Variable, Term)>,
) -> Option<ExpressionTerm> {
struct Context<'a> {
now: Option<DateTime>,
custom_functions: &'a CustomFunctionRegistry,
}
impl<'a> ExpressionEvaluatorContext<'a> for Context<'a> {
type Term = Term;
type Tuple = HashMap<&'a Variable, Term>;
type Error = QueryEvaluationError;
fn build_variable_lookup(
&mut self,
variable: &Variable,
) -> impl Fn(&HashMap<&'a Variable, Term>) -> Option<Term> + 'a {
let variable = variable.clone();
move |tuple| tuple.get(&variable).cloned()
}
fn build_is_variable_bound(
&mut self,
variable: &Variable,
) -> impl Fn(&HashMap<&'a Variable, Term>) -> bool + 'a {
let variable = variable.clone();
move |tuple| tuple.contains_key(&variable)
}
fn build_exists(
&mut self,
_: &GraphPattern,
) -> Result<impl Fn(&HashMap<&'a Variable, Term>) -> bool + 'a, QueryEvaluationError>
{
Err::<fn(&HashMap<&'a Variable, Term>) -> bool, _>(
QueryEvaluationError::Unexpected(
"EXISTS is not supported by the SPARQL expression evaluator".into(),
),
)
}
fn internalize_named_node(
&mut self,
term: &NamedNode,
) -> Result<Term, QueryEvaluationError> {
Ok(term.clone().into())
}
fn internalize_literal(
&mut self,
term: &Literal,
) -> Result<Term, QueryEvaluationError> {
Ok(term.clone().into())
}
fn build_internalize_expression_term(
&mut self,
) -> impl Fn(ExpressionTerm) -> Option<Term> + 'a {
|t| Some(t.into())
}
fn build_externalize_expression_term(
&mut self,
) -> impl Fn(Term) -> Option<ExpressionTerm> + 'a {
|t| Some(t.into())
}
fn now(&mut self) -> DateTime {
*self.now.get_or_insert_with(DateTime::now)
}
fn base_iri(&mut self) -> Option<Arc<Iri<String>>> {
None
}
fn custom_functions(&mut self) -> &CustomFunctionRegistry {
self.custom_functions
}
}
build_expression_evaluator(
expression,
&mut Context {
now: None,
custom_functions: &self.custom_functions,
},
)
.ok()?(&substitutions.into_iter().collect::<HashMap<_, _>>())
}
pub fn evaluate_expression<'a>(
&self,
expression: &sparopt::algebra::Expression,
substitutions: impl IntoIterator<Item = (&'a Variable, Term)>,
) -> Option<Term> {
self.eval_expression_term_with_substitutions(expression, substitutions)
.map(Into::into)
}
pub fn evaluate_effective_boolean_value_expression<'a>(
&self,
expression: &sparopt::algebra::Expression,
substitutions: impl IntoIterator<Item = (&'a Variable, Term)>,
) -> Option<bool> {
self.eval_expression_term_with_substitutions(expression, substitutions)?
.effective_boolean_value()
}
pub fn prepare_delete_insert<'a>(
&'a self,
delete: Vec<GroundQuadPattern>,
insert: Vec<QuadPattern>,
base_iri: Option<Iri<String>>,
using: Option<QueryDataset>,
pattern: &'a spargebra::algebra::GraphPattern,
) -> PreparedDeleteInsertUpdate<'a> {
PreparedDeleteInsertUpdate {
evaluator: self,
pattern,
delete,
insert,
base_iri,
dataset: using.map(Into::into).unwrap_or_default(),
}
}
fn simple_evaluator<'a, D: QueryableDataset<'a>>(
&self,
dataset: D,
dataset_spec: QueryDatasetSpecification,
base_iri: &Option<Iri<String>>,
) -> Result<SimpleEvaluator<'a, D>, QueryEvaluationError> {
SimpleEvaluator::new(
dataset,
base_iri.clone().map(Arc::new),
Rc::new(self.service_handler.clone()),
Rc::new(self.custom_functions.clone()),
Rc::new(self.custom_aggregate_functions.clone()),
self.cancellation_token.clone().unwrap_or_default(),
dataset_spec,
self.run_stats,
)
}
}
#[derive(Clone)]
#[must_use]
pub struct PreparedQuery<'a> {
evaluator: &'a QueryEvaluator,
query: &'a Query,
dataset: QueryDatasetSpecification,
substitutions: HashMap<Variable, Term>,
}
impl PreparedQuery<'_> {
#[inline]
pub fn substitute_variable(
mut self,
variable: impl Into<Variable>,
term: impl Into<Term>,
) -> Self {
self.substitutions.insert(variable.into(), term.into());
self
}
#[inline]
pub fn dataset(&self) -> &QueryDatasetSpecification {
&self.dataset
}
#[inline]
pub fn dataset_mut(&mut self) -> &mut QueryDatasetSpecification {
&mut self.dataset
}
pub fn execute<'b>(
self,
dataset: impl QueryableDataset<'b>,
) -> Result<QueryResults<'b>, QueryEvaluationError> {
self.explain(dataset).0
}
pub fn explain<'b>(
self,
dataset: impl QueryableDataset<'b>,
) -> (
Result<QueryResults<'b>, QueryEvaluationError>,
QueryExplanation,
) {
let start_planning = Timer::now();
let (results, plan_node_with_stats, planning_duration) = match self.query {
Query::Select {
pattern, base_iri, ..
} => {
let mut pattern = GraphPattern::from(pattern);
if !self.evaluator.without_optimizations {
pattern = Optimizer::optimize_graph_pattern(pattern);
}
let planning_duration = start_planning.elapsed();
let (results, explanation) =
match self
.evaluator
.simple_evaluator(dataset, self.dataset, base_iri)
{
Ok(evaluator) => evaluator.evaluate_select(&pattern, self.substitutions),
Err(e) => (Err(e), Rc::new(EvalNodeWithStats::empty())),
};
(
results.map(QueryResults::Solutions),
explanation,
planning_duration,
)
}
Query::Ask {
pattern, base_iri, ..
} => {
let mut pattern = GraphPattern::from(pattern);
if !self.evaluator.without_optimizations {
pattern = Optimizer::optimize_graph_pattern(pattern);
}
let planning_duration = start_planning.elapsed();
let (results, explanation) =
match self
.evaluator
.simple_evaluator(dataset, self.dataset, base_iri)
{
Ok(evaluator) => evaluator.evaluate_ask(&pattern, self.substitutions),
Err(e) => (Err(e), Rc::new(EvalNodeWithStats::empty())),
};
(
results.map(QueryResults::Boolean),
explanation,
planning_duration,
)
}
Query::Construct {
template,
pattern,
base_iri,
..
} => {
let mut pattern = GraphPattern::from(pattern);
if !self.evaluator.without_optimizations {
pattern = Optimizer::optimize_graph_pattern(pattern);
}
let planning_duration = start_planning.elapsed();
let (results, explanation) =
match self
.evaluator
.simple_evaluator(dataset, self.dataset, base_iri)
{
Ok(evaluator) => {
evaluator.evaluate_construct(&pattern, template, self.substitutions)
}
Err(e) => (Err(e), Rc::new(EvalNodeWithStats::empty())),
};
(
results.map(QueryResults::Graph),
explanation,
planning_duration,
)
}
Query::Describe {
pattern, base_iri, ..
} => {
let mut pattern = GraphPattern::from(pattern);
if !self.evaluator.without_optimizations {
pattern = Optimizer::optimize_graph_pattern(pattern);
}
let planning_duration = start_planning.elapsed();
let (results, explanation) =
match self
.evaluator
.simple_evaluator(dataset, self.dataset, base_iri)
{
Ok(evaluator) => evaluator.evaluate_describe(&pattern, self.substitutions),
Err(e) => (Err(e), Rc::new(EvalNodeWithStats::empty())),
};
(
results.map(QueryResults::Graph),
explanation,
planning_duration,
)
}
};
let explanation = QueryExplanation {
inner: plan_node_with_stats,
with_stats: self.evaluator.run_stats,
planning_duration,
};
(results, explanation)
}
}
#[derive(Clone)]
#[must_use]
pub struct PreparedDeleteInsertUpdate<'a> {
evaluator: &'a QueryEvaluator,
pattern: &'a spargebra::algebra::GraphPattern,
delete: Vec<GroundQuadPattern>,
insert: Vec<QuadPattern>,
base_iri: Option<Iri<String>>,
dataset: QueryDatasetSpecification,
}
impl PreparedDeleteInsertUpdate<'_> {
#[inline]
pub fn dataset(&self) -> &QueryDatasetSpecification {
&self.dataset
}
#[inline]
pub fn dataset_mut(&mut self) -> &mut QueryDatasetSpecification {
&mut self.dataset
}
pub fn execute<'b>(
self,
dataset: impl QueryableDataset<'b>,
) -> Result<DeleteInsertIter<'b>, QueryEvaluationError> {
let mut pattern = GraphPattern::from(self.pattern);
if !self.evaluator.without_optimizations {
pattern = Optimizer::optimize_graph_pattern(pattern);
}
let (solutions, _) = self
.evaluator
.simple_evaluator(dataset, self.dataset, &self.base_iri)?
.evaluate_select(&pattern, []);
Ok(DeleteInsertIter::new(solutions?, self.delete, self.insert))
}
}
pub(crate) type CustomAggregateFunctionRegistry = HashMap<
NamedNode,
Arc<dyn (Fn() -> Box<dyn AggregateFunctionAccumulator + Send + Sync>) + Send + Sync>,
>;
pub trait AggregateFunctionAccumulator {
fn accumulate(&mut self, element: Term);
fn finish(&mut self) -> Option<Term>;
}
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct QueryDatasetSpecification {
default: Option<Vec<GraphName>>,
named: Option<Vec<NamedOrBlankNode>>,
}
impl QueryDatasetSpecification {
pub fn new() -> Self {
Self {
default: Some(vec![GraphName::DefaultGraph]),
named: None,
}
}
pub fn is_default_dataset(&self) -> bool {
self.default
.as_ref()
.is_some_and(|t| t == &[GraphName::DefaultGraph])
&& self.named.is_none()
}
pub fn default_graph_graphs(&self) -> Option<&[GraphName]> {
self.default.as_deref()
}
pub fn set_default_graph_as_union(&mut self) {
self.default = None;
}
pub fn set_default_graph(&mut self, graphs: Vec<GraphName>) {
self.default = Some(graphs)
}
pub fn available_named_graphs(&self) -> Option<&[NamedOrBlankNode]> {
self.named.as_deref()
}
pub fn set_available_named_graphs(&mut self, named_graphs: Vec<NamedOrBlankNode>) {
self.named = Some(named_graphs);
}
}
impl Default for QueryDatasetSpecification {
fn default() -> Self {
Self::new()
}
}
impl From<QueryDataset> for QueryDatasetSpecification {
fn from(dataset: QueryDataset) -> Self {
Self {
default: Some(dataset.default.into_iter().map(Into::into).collect()),
named: dataset
.named
.map(|named| named.into_iter().map(Into::into).collect()),
}
}
}
#[derive(Clone)]
pub struct QueryExplanation {
inner: Rc<EvalNodeWithStats>,
with_stats: bool,
planning_duration: Option<DayTimeDuration>,
}
impl QueryExplanation {
pub fn write_in_json(&self, writer: impl io::Write) -> io::Result<()> {
let mut serializer = WriterJsonSerializer::new(writer);
serializer.serialize_event(JsonEvent::StartObject)?;
if let Some(planning_duration) = self.planning_duration {
serializer
.serialize_event(JsonEvent::ObjectKey("planning duration in seconds".into()))?;
serializer.serialize_event(JsonEvent::Number(
planning_duration.as_seconds().to_string().into(),
))?;
}
serializer.serialize_event(JsonEvent::ObjectKey("plan".into()))?;
self.inner.json_node(&mut serializer, self.with_stats)?;
serializer.serialize_event(JsonEvent::EndObject)
}
}
impl fmt::Debug for QueryExplanation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut obj = f.debug_struct("QueryExplanation");
if let Some(planning_duration) = self.planning_duration {
obj.field(
"planning duration in seconds",
&f32::from(Float::from(planning_duration.as_seconds())),
);
}
obj.field("tree", &self.inner);
obj.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxrdf::vocab::xsd;
use oxrdf::{Literal, Term};
use sparopt::algebra::{Expression, GraphPattern};
#[test]
fn evaluate_expression_literal_and_arithmetic() {
let evaluator = QueryEvaluator::new();
let expr = Expression::from(Literal::from(3_i32));
let term = evaluator.evaluate_expression(&expr, std::iter::empty());
assert_eq!(term, Some(Term::from(Literal::from(3_i32))));
let add = Expression::Add(
Box::new(Expression::from(Literal::from(1_i32))),
Box::new(Expression::from(Literal::from(2_i32))),
);
let term = evaluator.evaluate_expression(&add, std::iter::empty());
assert_eq!(term, Some(Term::from(Literal::from(3_i32))));
}
#[test]
fn evaluate_expression_with_variable_substitution() {
let evaluator = QueryEvaluator::new();
let x = Variable::new("x").unwrap();
let expr = Expression::Add(
Box::new(Expression::from(x.clone())),
Box::new(Expression::from(Literal::from(2_i32))),
);
let one: Term = Literal::from(1_i32).into();
let result = evaluator.evaluate_expression(&expr, [(&x, one)]);
assert_eq!(result, Some(Term::from(Literal::from(3_i32))));
}
#[test]
fn evaluate_expression_with_unbound_variable_returns_none() {
let evaluator = QueryEvaluator::new();
let x = Variable::new("x").unwrap();
let expr = Expression::from(x);
let result = evaluator.evaluate_expression(&expr, std::iter::empty());
assert!(result.is_none());
}
#[test]
fn evaluate_effective_boolean_value_expression_basic() {
let evaluator = QueryEvaluator::new();
let zero = Expression::from(Literal::from(0_i32));
let five = Expression::from(Literal::from(5_i32));
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&zero, std::iter::empty()),
Some(false)
);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&five, std::iter::empty()),
Some(true)
);
let empty_str = Expression::from(Literal::from(""));
let non_empty_str = Expression::from(Literal::from("a"));
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&empty_str, std::iter::empty()),
Some(false)
);
assert_eq!(
evaluator
.evaluate_effective_boolean_value_expression(&non_empty_str, std::iter::empty()),
Some(true)
);
}
#[test]
fn evaluate_effective_boolean_value_expression_exists() {
let evaluator = QueryEvaluator::new();
let exists_empty = Expression::exists(GraphPattern::empty());
assert_eq!(
evaluator
.evaluate_effective_boolean_value_expression(&exists_empty, std::iter::empty()),
Some(false)
);
let exists_unit = Expression::exists(GraphPattern::empty_singleton());
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&exists_unit, std::iter::empty()),
Some(true)
);
}
#[test]
fn evaluate_effective_boolean_value_expression_non_boolean_term() {
let evaluator = QueryEvaluator::new();
let iri = NamedNode::new("http://example.com/").unwrap();
let nn = Expression::from(iri);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&nn, std::iter::empty()),
None
);
let dt = Literal::new_typed_literal("2020-01-01T00:00:00Z", xsd::DATE_TIME);
let expr = Expression::from(dt);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&expr, std::iter::empty()),
None
);
}
#[test]
fn evaluate_effective_boolean_value_expression_boolean_lexical_forms() {
let evaluator = QueryEvaluator::new();
let one = Expression::from(Literal::new_typed_literal("1", xsd::BOOLEAN));
let zero = Expression::from(Literal::new_typed_literal("0", xsd::BOOLEAN));
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&one, std::iter::empty()),
Some(true)
);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&zero, std::iter::empty()),
Some(false)
);
}
#[test]
fn evaluate_effective_boolean_value_expression_logic_with_errors() {
let evaluator = QueryEvaluator::new();
let errorish = Expression::from(NamedNode::new("http://e/iri").unwrap());
let or_expr = Expression::or_all([errorish, Expression::from(Literal::from(false))]);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&or_expr, std::iter::empty()),
None
);
let errorish = Expression::from(NamedNode::new("http://e/iri2").unwrap());
let and_expr = Expression::and_all([Expression::from(Literal::from(false)), errorish]);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&and_expr, std::iter::empty()),
Some(false)
);
let errorish = Expression::from(NamedNode::new("http://e/iri3").unwrap());
let and_expr = Expression::and_all([Expression::from(Literal::from(true)), errorish]);
assert_eq!(
evaluator.evaluate_effective_boolean_value_expression(&and_expr, std::iter::empty()),
None
);
}
#[test]
fn evaluate_expression_equality_returns_boolean_literal() {
let evaluator = QueryEvaluator::new();
let eq = Expression::equal(
Expression::from(Literal::from(1_i32)),
Expression::from(Literal::from(1_i32)),
);
let term = evaluator.evaluate_expression(&eq, std::iter::empty());
assert_eq!(term, Some(Term::from(Literal::from(true))));
}
#[test]
fn evaluate_expression_arithmetic_with_unbound_variable_is_none() {
let evaluator = QueryEvaluator::new();
let x = Variable::new("x").unwrap();
let expr = Expression::Add(
Box::new(Expression::from(Literal::from(2_i32))),
Box::new(Expression::from(x)),
);
let result = evaluator.evaluate_expression(&expr, std::iter::empty());
assert!(result.is_none());
}
}