use std::collections::HashMap;
use crate::model::{StarTerm, StarTriple};
pub type Binding = HashMap<String, StarTerm>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddedTriplePattern {
pub subject: StarTerm,
pub predicate: StarTerm,
pub object: StarTerm,
}
impl EmbeddedTriplePattern {
pub fn new(subject: StarTerm, predicate: StarTerm, object: StarTerm) -> Self {
Self {
subject,
predicate,
object,
}
}
pub fn is_fully_unbound(&self) -> bool {
self.subject.is_variable() && self.predicate.is_variable() && self.object.is_variable()
}
pub fn is_ground(&self) -> bool {
!self.subject.is_variable() && !self.predicate.is_variable() && !self.object.is_variable()
}
pub fn unify(&self, triple: &StarTriple) -> Option<Binding> {
let mut bindings = Binding::new();
unify_term(&self.subject, &triple.subject, &mut bindings)?;
unify_term(&self.predicate, &triple.predicate, &mut bindings)?;
unify_term(&self.object, &triple.object, &mut bindings)?;
Some(bindings)
}
pub fn unify_embedded(&self, term: &StarTerm) -> Option<Binding> {
if let StarTerm::QuotedTriple(inner) = term {
self.unify(inner)
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnotationPattern {
pub embedded_subject: StarTerm,
pub embedded_predicate: StarTerm,
pub embedded_object: StarTerm,
pub annotation_predicate: StarTerm,
pub annotation_object: StarTerm,
}
impl AnnotationPattern {
pub fn new(
embedded_subject: StarTerm,
embedded_predicate: StarTerm,
embedded_object: StarTerm,
annotation_predicate: StarTerm,
annotation_object: StarTerm,
) -> Self {
Self {
embedded_subject,
embedded_predicate,
embedded_object,
annotation_predicate,
annotation_object,
}
}
pub fn match_triple(&self, outer: &StarTriple) -> Option<Binding> {
let inner = match &outer.subject {
StarTerm::QuotedTriple(inner) => inner.as_ref(),
_ => return None,
};
let mut bindings = Binding::new();
unify_term(&self.embedded_subject, &inner.subject, &mut bindings)?;
unify_term(&self.embedded_predicate, &inner.predicate, &mut bindings)?;
unify_term(&self.embedded_object, &inner.object, &mut bindings)?;
unify_term(&self.annotation_predicate, &outer.predicate, &mut bindings)?;
unify_term(&self.annotation_object, &outer.object, &mut bindings)?;
Some(bindings)
}
}
fn unify_term(pattern_term: &StarTerm, data_term: &StarTerm, bindings: &mut Binding) -> Option<()> {
match pattern_term {
StarTerm::Variable(var) => {
if let Some(existing) = bindings.get(&var.name) {
if existing != data_term {
return None;
}
} else {
bindings.insert(var.name.clone(), data_term.clone());
}
Some(())
}
StarTerm::QuotedTriple(pattern_inner) => {
if let StarTerm::QuotedTriple(data_inner) = data_term {
unify_term(&pattern_inner.subject, &data_inner.subject, bindings)?;
unify_term(&pattern_inner.predicate, &data_inner.predicate, bindings)?;
unify_term(&pattern_inner.object, &data_inner.object, bindings)?;
Some(())
} else {
None
}
}
concrete => {
if concrete == data_term {
Some(())
} else {
None
}
}
}
}
pub struct SparqlStarEvaluator {
pub recurse_nested: bool,
}
impl Default for SparqlStarEvaluator {
fn default() -> Self {
Self::new()
}
}
impl SparqlStarEvaluator {
pub fn new() -> Self {
Self {
recurse_nested: false,
}
}
pub fn with_recursion() -> Self {
Self {
recurse_nested: true,
}
}
pub fn evaluate_embedded_pattern(
&self,
pattern: &EmbeddedTriplePattern,
graph: &[StarTriple],
) -> Vec<Binding> {
let mut results = Vec::new();
for triple in graph {
if let Some(bindings) = pattern.unify_embedded(&triple.subject) {
results.push(bindings);
}
if let Some(bindings) = pattern.unify_embedded(&triple.object) {
results.push(bindings);
}
if self.recurse_nested {
self.recurse_embedded(&triple.subject, pattern, &mut results);
self.recurse_embedded(&triple.object, pattern, &mut results);
}
}
results
}
fn recurse_embedded(
&self,
term: &StarTerm,
pattern: &EmbeddedTriplePattern,
results: &mut Vec<Binding>,
) {
if let StarTerm::QuotedTriple(inner) = term {
if let Some(bindings) = pattern.unify(inner) {
results.push(bindings);
}
self.recurse_embedded(&inner.subject, pattern, results);
self.recurse_embedded(&inner.predicate, pattern, results);
self.recurse_embedded(&inner.object, pattern, results);
}
}
pub fn evaluate_annotation_pattern(
&self,
pattern: &AnnotationPattern,
graph: &[StarTriple],
) -> Vec<Binding> {
let mut results = Vec::new();
for triple in graph {
if let Some(bindings) = pattern.match_triple(triple) {
results.push(bindings);
}
}
results
}
pub fn evaluate_direct_pattern(
&self,
pattern: &EmbeddedTriplePattern,
graph: &[StarTriple],
) -> Vec<Binding> {
let mut results = Vec::new();
for triple in graph {
if let Some(bindings) = pattern.unify(triple) {
results.push(bindings);
}
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::StarTriple;
fn iri(s: &str) -> StarTerm {
StarTerm::iri(s).expect("iri")
}
fn lit(s: &str) -> StarTerm {
StarTerm::literal(s).expect("lit")
}
fn var(name: &str) -> StarTerm {
StarTerm::variable(name).expect("var")
}
fn simple_triple(s: &str, p: &str, o: &str) -> StarTriple {
StarTriple::new(iri(s), iri(p), iri(o))
}
fn quoted_triple(s: &str, p: &str, o: &str) -> StarTerm {
StarTerm::quoted_triple(simple_triple(s, p, o))
}
#[test]
fn test_unify_fully_variable_pattern() {
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let triple = simple_triple("http://a", "http://b", "http://c");
let bindings = pattern.unify(&triple).expect("should match");
assert_eq!(bindings.get("s"), Some(&iri("http://a")));
assert_eq!(bindings.get("p"), Some(&iri("http://b")));
assert_eq!(bindings.get("o"), Some(&iri("http://c")));
}
#[test]
fn test_unify_concrete_predicate_match() {
let pattern = EmbeddedTriplePattern::new(var("s"), iri("http://p"), var("o"));
let triple = simple_triple("http://a", "http://p", "http://c");
assert!(pattern.unify(&triple).is_some());
}
#[test]
fn test_unify_concrete_predicate_mismatch() {
let pattern = EmbeddedTriplePattern::new(var("s"), iri("http://x"), var("o"));
let triple = simple_triple("http://a", "http://p", "http://c");
assert!(pattern.unify(&triple).is_none());
}
#[test]
fn test_unify_all_concrete_match() {
let pattern = EmbeddedTriplePattern::new(iri("http://a"), iri("http://p"), iri("http://c"));
let triple = simple_triple("http://a", "http://p", "http://c");
assert!(pattern.unify(&triple).is_some());
}
#[test]
fn test_unify_all_concrete_no_match() {
let pattern =
EmbeddedTriplePattern::new(iri("http://a"), iri("http://p"), iri("http://DIFFERENT"));
let triple = simple_triple("http://a", "http://p", "http://c");
assert!(pattern.unify(&triple).is_none());
}
#[test]
fn test_unify_literal_object() {
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), lit("hello"));
let triple = StarTriple::new(iri("http://a"), iri("http://p"), lit("hello"));
assert!(pattern.unify(&triple).is_some());
}
#[test]
fn test_unify_literal_mismatch() {
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), lit("hello"));
let triple = StarTriple::new(iri("http://a"), iri("http://p"), lit("world"));
assert!(pattern.unify(&triple).is_none());
}
#[test]
fn test_unify_same_variable_in_subject_and_object_consistent() {
let pattern = EmbeddedTriplePattern::new(var("x"), iri("http://p"), var("x"));
let same = StarTriple::new(iri("http://a"), iri("http://p"), iri("http://a"));
assert!(pattern.unify(&same).is_some());
let different = StarTriple::new(iri("http://a"), iri("http://p"), iri("http://b"));
assert!(pattern.unify(&different).is_none());
}
#[test]
fn test_embedded_triple_pattern_is_ground() {
let ground = EmbeddedTriplePattern::new(iri("http://a"), iri("http://p"), iri("http://o"));
assert!(ground.is_ground());
assert!(!ground.is_fully_unbound());
}
#[test]
fn test_embedded_triple_pattern_is_fully_unbound() {
let unbound = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
assert!(unbound.is_fully_unbound());
assert!(!unbound.is_ground());
}
#[test]
fn test_unify_embedded_matches_quoted_subject() {
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let embedded = quoted_triple("http://a", "http://p", "http://c");
let bindings = pattern.unify_embedded(&embedded).expect("should match");
assert!(bindings.contains_key("s"));
}
#[test]
fn test_unify_embedded_non_quoted_term_returns_none() {
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let plain = iri("http://example.org/plain");
assert!(pattern.unify_embedded(&plain).is_none());
}
#[test]
fn test_evaluate_embedded_pattern_empty_graph() {
let evaluator = SparqlStarEvaluator::new();
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let results = evaluator.evaluate_embedded_pattern(&pattern, &[]);
assert!(results.is_empty());
}
#[test]
fn test_evaluate_embedded_pattern_finds_quoted_subject() {
let evaluator = SparqlStarEvaluator::new();
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let inner = simple_triple("http://alice", "http://age", "http://v");
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://certainty"),
lit("high"),
);
let results = evaluator.evaluate_embedded_pattern(&pattern, &[outer]);
assert_eq!(results.len(), 1);
assert!(results[0].contains_key("s"));
}
#[test]
fn test_evaluate_embedded_pattern_with_predicate_filter() {
let evaluator = SparqlStarEvaluator::new();
let pattern = EmbeddedTriplePattern::new(var("s"), iri("http://age"), var("o"));
let matching_inner = simple_triple("http://alice", "http://age", "http://v");
let non_matching_inner = simple_triple("http://alice", "http://name", "http://v");
let t1 = StarTriple::new(
StarTerm::quoted_triple(matching_inner),
iri("http://cert"),
lit("hi"),
);
let t2 = StarTriple::new(
StarTerm::quoted_triple(non_matching_inner),
iri("http://cert"),
lit("hi"),
);
let results = evaluator.evaluate_embedded_pattern(&pattern, &[t1, t2]);
assert_eq!(results.len(), 1, "only the age triple should match");
}
#[test]
fn test_evaluate_embedded_pattern_multiple_matches() {
let evaluator = SparqlStarEvaluator::new();
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let t1 = StarTriple::new(
quoted_triple("http://a", "http://p", "http://b"),
iri("http://meta"),
lit("x"),
);
let t2 = StarTriple::new(
quoted_triple("http://c", "http://p", "http://d"),
iri("http://meta"),
lit("y"),
);
let results = evaluator.evaluate_embedded_pattern(&pattern, &[t1, t2]);
assert_eq!(results.len(), 2);
}
#[test]
fn test_annotation_pattern_matches_valid_annotation() {
let pattern = AnnotationPattern::new(
var("s"),
iri("http://age"),
var("age_val"),
iri("http://certainty"),
var("cert"),
);
let inner = StarTriple::new(iri("http://alice"), iri("http://age"), lit("30"));
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://certainty"),
lit("0.9"),
);
let bindings = pattern.match_triple(&outer).expect("should match");
assert_eq!(bindings.get("s"), Some(&iri("http://alice")));
assert_eq!(bindings.get("age_val"), Some(&lit("30")));
assert_eq!(bindings.get("cert"), Some(&lit("0.9")));
}
#[test]
fn test_annotation_pattern_no_match_plain_subject() {
let pattern =
AnnotationPattern::new(var("s"), var("p"), var("o"), var("ann_p"), var("ann_o"));
let plain_outer = StarTriple::new(iri("http://alice"), iri("http://certainty"), lit("0.9"));
assert!(pattern.match_triple(&plain_outer).is_none());
}
#[test]
fn test_annotation_pattern_predicate_filter() {
let pattern = AnnotationPattern::new(
var("s"),
var("p"),
var("o"),
iri("http://source"), var("ann_o"),
);
let inner = simple_triple("http://a", "http://p", "http://b");
let outer_match = StarTriple::new(
StarTerm::quoted_triple(inner.clone()),
iri("http://source"),
lit("db"),
);
assert!(pattern.match_triple(&outer_match).is_some());
let outer_no_match = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://certainty"), lit("db"),
);
assert!(pattern.match_triple(&outer_no_match).is_none());
}
#[test]
fn test_evaluate_annotation_pattern_empty_graph() {
let evaluator = SparqlStarEvaluator::new();
let pattern = AnnotationPattern::new(var("s"), var("p"), var("o"), var("ap"), var("ao"));
let results = evaluator.evaluate_annotation_pattern(&pattern, &[]);
assert!(results.is_empty());
}
#[test]
fn test_evaluate_annotation_pattern_finds_annotations() {
let evaluator = SparqlStarEvaluator::new();
let pattern = AnnotationPattern::new(
var("s"),
var("p"),
var("o"),
iri("http://certainty"),
var("cert"),
);
let inner = simple_triple("http://alice", "http://age", "http://v");
let annotated = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://certainty"),
lit("high"),
);
let results = evaluator.evaluate_annotation_pattern(&pattern, &[annotated]);
assert_eq!(results.len(), 1);
assert_eq!(results[0].get("cert"), Some(&lit("high")));
}
#[test]
fn test_evaluate_direct_pattern_matches_plain_triples() {
let evaluator = SparqlStarEvaluator::new();
let pattern = EmbeddedTriplePattern::new(var("s"), iri("http://age"), var("o"));
let t1 = StarTriple::new(iri("http://alice"), iri("http://age"), lit("30"));
let t2 = StarTriple::new(iri("http://bob"), iri("http://name"), lit("Bob"));
let results = evaluator.evaluate_direct_pattern(&pattern, &[t1, t2]);
assert_eq!(results.len(), 1, "only the age triple matches");
assert_eq!(results[0].get("s"), Some(&iri("http://alice")));
}
#[test]
fn test_evaluate_direct_pattern_empty_graph() {
let evaluator = SparqlStarEvaluator::new();
let pattern = EmbeddedTriplePattern::new(var("s"), var("p"), var("o"));
let results = evaluator.evaluate_direct_pattern(&pattern, &[]);
assert!(results.is_empty());
}
}