pub use crate::algebra::{
Aggregate, Algebra, BinaryOperator, Binding, Expression, GroupCondition, Literal,
OrderCondition, PropertyPath, Term, TriplePattern, UnaryOperator, Variable,
};
pub use crate::executor::{Dataset, InMemoryDataset, QueryExecutor};
use anyhow::Result;
pub use oxirs_core::model::NamedNode;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug)]
pub struct ConformanceTestError {
pub test_name: String,
pub group: ConformanceGroup,
pub expected: String,
pub actual: String,
pub message: String,
}
impl fmt::Display for ConformanceTestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{:?}/{}] FAILED: {}\n Expected: {}\n Actual: {}",
self.group, self.test_name, self.message, self.expected, self.actual
)
}
}
impl std::error::Error for ConformanceTestError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConformanceGroup {
BasicPatterns,
FilterExpressions,
Optional,
Union,
Aggregates,
GroupBy,
Subquery,
PropertyPaths,
NamedGraphs,
Update,
Negation,
Bind,
Values,
StringFunctions,
DateFunctions,
MathFunctions,
TypeSystem,
Entailment,
Construct,
Describe,
}
#[derive(Debug, Clone)]
pub enum ConformanceResult {
SelectResults(Vec<HashMap<String, String>>),
AskResult(bool),
ConstructGraph(Vec<(String, String, String)>),
UpdateSuccess,
Error,
OrderedSelectResults(Vec<HashMap<String, String>>),
ResultCount(usize),
}
pub struct ConformanceTest {
pub name: &'static str,
pub group: ConformanceGroup,
pub algebra: Algebra,
pub dataset: InMemoryDataset,
pub expected: ConformanceResult,
}
impl ConformanceTest {
pub fn new(
name: &'static str,
group: ConformanceGroup,
algebra: Algebra,
dataset: InMemoryDataset,
expected: ConformanceResult,
) -> Self {
Self {
name,
group,
algebra,
dataset,
expected,
}
}
}
pub struct ConformanceTestRunner {
executor: QueryExecutor,
}
impl ConformanceTestRunner {
pub fn new() -> Self {
Self {
executor: QueryExecutor::new(),
}
}
pub fn run_test(&mut self, test: &ConformanceTest) -> Result<(), ConformanceTestError> {
let (solution, _stats) = self
.executor
.execute(&test.algebra, &test.dataset)
.map_err(|e| ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: format!("{:?}", test.expected),
actual: format!("Error: {e}"),
message: "Query execution failed".to_string(),
})?;
self.verify_result(test, &solution)
}
fn verify_result(
&self,
test: &ConformanceTest,
solution: &[Binding],
) -> Result<(), ConformanceTestError> {
match &test.expected {
ConformanceResult::ResultCount(expected_count) => {
if solution.len() != *expected_count {
return Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: expected_count.to_string(),
actual: solution.len().to_string(),
message: format!(
"Expected {} results, got {}",
expected_count,
solution.len()
),
});
}
Ok(())
}
ConformanceResult::AskResult(expected_bool) => {
let has_results = !solution.is_empty();
if has_results != *expected_bool {
return Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: expected_bool.to_string(),
actual: has_results.to_string(),
message: format!(
"ASK result mismatch: expected {expected_bool}, got {has_results}"
),
});
}
Ok(())
}
ConformanceResult::SelectResults(expected_rows) => {
self.verify_select_results(test, solution, expected_rows, false)
}
ConformanceResult::OrderedSelectResults(expected_rows) => {
self.verify_select_results(test, solution, expected_rows, true)
}
ConformanceResult::ConstructGraph(expected_triples) => {
if solution.len() != expected_triples.len() {
return Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: format!("{} triples", expected_triples.len()),
actual: format!("{} solutions", solution.len()),
message: "CONSTRUCT result count mismatch".to_string(),
});
}
Ok(())
}
ConformanceResult::UpdateSuccess => Ok(()),
ConformanceResult::Error => {
Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: "Error".to_string(),
actual: format!("{} results", solution.len()),
message: "Expected error was not raised".to_string(),
})
}
}
}
fn verify_select_results(
&self,
test: &ConformanceTest,
solution: &[Binding],
expected_rows: &[HashMap<String, String>],
ordered: bool,
) -> Result<(), ConformanceTestError> {
if solution.len() != expected_rows.len() {
return Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: format!("{} rows", expected_rows.len()),
actual: format!("{} rows", solution.len()),
message: format!(
"Result count mismatch: expected {} rows, got {}",
expected_rows.len(),
solution.len()
),
});
}
if ordered {
for (i, (actual_binding, expected_row)) in
solution.iter().zip(expected_rows.iter()).enumerate()
{
if let Err(e) = self.check_row_match(test, actual_binding, expected_row) {
return Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: format!("row {i}: {expected_row:?}"),
actual: format!("row {i}: {e}"),
message: format!("Ordered result mismatch at row {i}"),
});
}
}
} else {
let mut matched = vec![false; solution.len()];
for expected_row in expected_rows {
let mut found = false;
for (idx, actual_binding) in solution.iter().enumerate() {
if !matched[idx]
&& self
.check_row_match(test, actual_binding, expected_row)
.is_ok()
{
matched[idx] = true;
found = true;
break;
}
}
if !found {
return Err(ConformanceTestError {
test_name: test.name.to_string(),
group: test.group,
expected: format!("{expected_row:?}"),
actual: format!(
"{:?}",
solution
.iter()
.map(|b| b
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<_, _>>())
.collect::<Vec<_>>()
),
message: format!("Expected row not found in results: {expected_row:?}"),
});
}
}
}
Ok(())
}
fn check_row_match(
&self,
_test: &ConformanceTest,
actual: &Binding,
expected: &HashMap<String, String>,
) -> Result<(), String> {
for (var_name, expected_value) in expected {
let var = Variable::new(var_name).map_err(|e| format!("Invalid variable: {e}"))?;
match actual.get(&var) {
Some(term) => {
let actual_str = term_to_check_string(term);
if actual_str != *expected_value {
return Err(format!(
"Variable ?{var_name}: expected '{expected_value}', got '{actual_str}'"
));
}
}
None => {
if !expected_value.is_empty() {
return Err(format!(
"Variable ?{var_name} is unbound, expected '{expected_value}'"
));
}
}
}
}
Ok(())
}
}
impl Default for ConformanceTestRunner {
fn default() -> Self {
Self::new()
}
}
fn term_to_check_string(term: &Term) -> String {
match term {
Term::Iri(iri) => iri.as_str().to_string(),
Term::Literal(lit) => lit.value.clone(),
Term::BlankNode(id) => format!("_:{id}"),
Term::Variable(v) => format!("?{v}"),
Term::QuotedTriple(t) => format!("<<{} {} {}>>", t.subject, t.predicate, t.object),
Term::PropertyPath(_) => "<path>".to_string(),
}
}
pub fn iri(s: &str) -> Term {
Term::Iri(NamedNode::new_unchecked(s))
}
pub fn named_node(s: &str) -> NamedNode {
NamedNode::new_unchecked(s)
}
pub fn var(name: &str) -> Term {
Term::Variable(Variable::new(name).expect("valid variable name"))
}
pub fn variable(name: &str) -> Variable {
Variable::new(name).expect("valid variable name")
}
pub fn str_lit(s: &str) -> Term {
Term::Literal(Literal {
value: s.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#string",
)),
})
}
pub fn int_lit(n: i64) -> Term {
Term::Literal(Literal {
value: n.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#integer",
)),
})
}
pub fn dec_lit(n: f64) -> Term {
Term::Literal(Literal {
value: n.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#decimal",
)),
})
}
pub fn bool_lit(b: bool) -> Term {
Term::Literal(Literal {
value: b.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#boolean",
)),
})
}
pub fn lang_lit(s: &str, lang: &str) -> Term {
Term::Literal(Literal {
value: s.to_string(),
language: Some(lang.to_string()),
datatype: None,
})
}
pub fn triple(s: Term, p: Term, o: Term) -> TriplePattern {
TriplePattern::new(s, p, o)
}
pub fn bgp(patterns: Vec<TriplePattern>) -> Algebra {
Algebra::Bgp(patterns)
}
pub fn join(left: Algebra, right: Algebra) -> Algebra {
Algebra::Join {
left: Box::new(left),
right: Box::new(right),
}
}
pub fn left_join(left: Algebra, right: Algebra, filter: Option<Expression>) -> Algebra {
Algebra::LeftJoin {
left: Box::new(left),
right: Box::new(right),
filter,
}
}
pub fn union(left: Algebra, right: Algebra) -> Algebra {
Algebra::Union {
left: Box::new(left),
right: Box::new(right),
}
}
pub fn filter(pattern: Algebra, condition: Expression) -> Algebra {
Algebra::Filter {
pattern: Box::new(pattern),
condition,
}
}
pub fn project(pattern: Algebra, variables: Vec<Variable>) -> Algebra {
Algebra::Project {
pattern: Box::new(pattern),
variables,
}
}
pub fn distinct(pattern: Algebra) -> Algebra {
Algebra::Distinct {
pattern: Box::new(pattern),
}
}
pub fn slice(pattern: Algebra, offset: Option<usize>, limit: Option<usize>) -> Algebra {
Algebra::Slice {
pattern: Box::new(pattern),
offset,
limit,
}
}
pub fn order_by(pattern: Algebra, conditions: Vec<OrderCondition>) -> Algebra {
Algebra::OrderBy {
pattern: Box::new(pattern),
conditions,
}
}
pub fn asc_cond(expr: Expression) -> OrderCondition {
OrderCondition {
expr,
ascending: true,
}
}
pub fn desc_cond(expr: Expression) -> OrderCondition {
OrderCondition {
expr,
ascending: false,
}
}
pub fn group(
pattern: Algebra,
variables: Vec<GroupCondition>,
aggregates: Vec<(Variable, Aggregate)>,
) -> Algebra {
Algebra::Group {
pattern: Box::new(pattern),
variables,
aggregates,
}
}
pub fn group_var(var_name: &str) -> GroupCondition {
GroupCondition {
expr: Expression::Variable(variable(var_name)),
alias: None,
}
}
pub fn values(variables: Vec<Variable>, bindings: Vec<Binding>) -> Algebra {
Algebra::Values {
variables,
bindings,
}
}
pub fn minus(left: Algebra, right: Algebra) -> Algebra {
Algebra::Minus {
left: Box::new(left),
right: Box::new(right),
}
}
pub fn extend(pattern: Algebra, variable: Variable, expr: Expression) -> Algebra {
Algebra::Extend {
pattern: Box::new(pattern),
variable,
expr,
}
}
pub fn expr_var(name: &str) -> Expression {
Expression::Variable(variable(name))
}
pub fn expr_lit(lit: Literal) -> Expression {
Expression::Literal(lit)
}
pub fn expr_iri(s: &str) -> Expression {
Expression::Iri(NamedNode::new_unchecked(s))
}
pub fn expr_binary(op: BinaryOperator, left: Expression, right: Expression) -> Expression {
Expression::Binary {
op,
left: Box::new(left),
right: Box::new(right),
}
}
pub fn expr_eq(left: Expression, right: Expression) -> Expression {
expr_binary(BinaryOperator::Equal, left, right)
}
pub fn expr_lt(left: Expression, right: Expression) -> Expression {
expr_binary(BinaryOperator::Less, left, right)
}
pub fn expr_gt(left: Expression, right: Expression) -> Expression {
expr_binary(BinaryOperator::Greater, left, right)
}
pub fn expr_and(left: Expression, right: Expression) -> Expression {
expr_binary(BinaryOperator::And, left, right)
}
pub fn expr_or(left: Expression, right: Expression) -> Expression {
expr_binary(BinaryOperator::Or, left, right)
}
pub fn expr_fn(name: &str, args: Vec<Expression>) -> Expression {
Expression::Function {
name: name.to_string(),
args,
}
}
pub fn agg_count_star() -> Aggregate {
Aggregate::Count {
distinct: false,
expr: None,
}
}
pub fn agg_count(var_name: &str) -> Aggregate {
Aggregate::Count {
distinct: false,
expr: Some(expr_var(var_name)),
}
}
pub fn agg_count_distinct(var_name: &str) -> Aggregate {
Aggregate::Count {
distinct: true,
expr: Some(expr_var(var_name)),
}
}
pub fn agg_sum(var_name: &str) -> Aggregate {
Aggregate::Sum {
distinct: false,
expr: expr_var(var_name),
}
}
pub fn agg_avg(var_name: &str) -> Aggregate {
Aggregate::Avg {
distinct: false,
expr: expr_var(var_name),
}
}
pub fn agg_min(var_name: &str) -> Aggregate {
Aggregate::Min {
distinct: false,
expr: expr_var(var_name),
}
}
pub fn agg_max(var_name: &str) -> Aggregate {
Aggregate::Max {
distinct: false,
expr: expr_var(var_name),
}
}
pub fn agg_group_concat(var_name: &str, separator: Option<String>) -> Aggregate {
Aggregate::GroupConcat {
distinct: false,
expr: expr_var(var_name),
separator,
}
}
pub fn property_path(subject: Term, path: PropertyPath, object: Term) -> Algebra {
Algebra::PropertyPath {
subject,
path,
object,
}
}
pub fn path_seq(left: PropertyPath, right: PropertyPath) -> PropertyPath {
PropertyPath::Sequence(Box::new(left), Box::new(right))
}
pub fn path_alt(left: PropertyPath, right: PropertyPath) -> PropertyPath {
PropertyPath::Alternative(Box::new(left), Box::new(right))
}
pub fn path_star(p: PropertyPath) -> PropertyPath {
PropertyPath::ZeroOrMore(Box::new(p))
}
pub fn path_plus(p: PropertyPath) -> PropertyPath {
PropertyPath::OneOrMore(Box::new(p))
}
pub fn path_opt(p: PropertyPath) -> PropertyPath {
PropertyPath::ZeroOrOne(Box::new(p))
}
pub fn path_inv(p: PropertyPath) -> PropertyPath {
PropertyPath::Inverse(Box::new(p))
}
pub fn path_iri(s: &str) -> PropertyPath {
PropertyPath::Iri(NamedNode::new_unchecked(s))
}
pub fn path_neg(paths: Vec<PropertyPath>) -> PropertyPath {
PropertyPath::NegatedPropertySet(paths)
}
pub fn row(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
pub fn lit_int(n: i64) -> Literal {
Literal {
value: n.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#integer",
)),
}
}
pub fn lit_str(s: &str) -> Literal {
Literal {
value: s.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#string",
)),
}
}
pub fn lit_bool(b: bool) -> Literal {
Literal {
value: b.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#boolean",
)),
}
}
pub fn lit_dec(n: f64) -> Literal {
Literal {
value: n.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(
"http://www.w3.org/2001/XMLSchema#decimal",
)),
}
}