SPARQL query service (#754)

SPARQL 1.1 query service wrapping pub/sub triples interface

Add a backend-agnostic SPARQL query service that parses SPARQL
queries using rdflib, decomposes them into triple pattern lookups
via the existing TriplesClient pub/sub interface, and performs
in-memory joins, filters, and projections.

Includes:
- SPARQL parser, algebra evaluator, expression evaluator, solution
  sequence operations (BGP, JOIN, OPTIONAL, UNION, FILTER, BIND,
  VALUES, GROUP BY, ORDER BY, LIMIT/OFFSET, DISTINCT, aggregates)
- FlowProcessor service with TriplesClientSpec
- Gateway dispatcher, request/response translators, API spec
- Python SDK method (FlowInstance.sparql_query)
- CLI command (tg-invoke-sparql-query)
- Tech spec (docs/tech-specs/sparql-query.md)

New unit tests for SPARQL query
This commit is contained in:
cybermaggedon 2026-04-02 17:21:39 +01:00 committed by GitHub
parent 62c30a3a50
commit d9dc4cbab5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 3498 additions and 3 deletions

View file

@ -0,0 +1,424 @@
"""
Tests for SPARQL FILTER expression evaluator.
"""
import pytest
from trustgraph.schema import Term, IRI, LITERAL, BLANK
from trustgraph.query.sparql.expressions import (
evaluate_expression, _effective_boolean, _to_string, _to_numeric,
_comparable_value,
)
# --- Helpers ---
def iri(v):
return Term(type=IRI, iri=v)
def lit(v, datatype="", language=""):
return Term(type=LITERAL, value=v, datatype=datatype, language=language)
def blank(v):
return Term(type=BLANK, id=v)
XSD = "http://www.w3.org/2001/XMLSchema#"
class TestEvaluateExpression:
"""Test expression evaluation with rdflib algebra nodes."""
def test_variable_bound(self):
from rdflib.term import Variable
result = evaluate_expression(Variable("x"), {"x": lit("hello")})
assert result.value == "hello"
def test_variable_unbound(self):
from rdflib.term import Variable
result = evaluate_expression(Variable("x"), {})
assert result is None
def test_uriref_constant(self):
from rdflib import URIRef
result = evaluate_expression(
URIRef("http://example.com/a"), {}
)
assert result.type == IRI
assert result.iri == "http://example.com/a"
def test_literal_constant(self):
from rdflib import Literal
result = evaluate_expression(Literal("hello"), {})
assert result.type == LITERAL
assert result.value == "hello"
def test_boolean_constant(self):
assert evaluate_expression(True, {}) is True
assert evaluate_expression(False, {}) is False
def test_numeric_constant(self):
assert evaluate_expression(42, {}) == 42
assert evaluate_expression(3.14, {}) == 3.14
def test_none_returns_true(self):
assert evaluate_expression(None, {}) is True
class TestRelationalExpressions:
"""Test comparison operators via CompValue nodes."""
def _make_relational(self, left, op, right):
from rdflib.plugins.sparql.parserutils import CompValue
return CompValue("RelationalExpression",
expr=left, op=op, other=right)
def test_equal_literals(self):
from rdflib import Literal
expr = self._make_relational(Literal("a"), "=", Literal("a"))
assert evaluate_expression(expr, {}) is True
def test_not_equal_literals(self):
from rdflib import Literal
expr = self._make_relational(Literal("a"), "!=", Literal("b"))
assert evaluate_expression(expr, {}) is True
def test_less_than(self):
from rdflib import Literal
expr = self._make_relational(Literal("a"), "<", Literal("b"))
assert evaluate_expression(expr, {}) is True
def test_greater_than(self):
from rdflib import Literal
expr = self._make_relational(Literal("b"), ">", Literal("a"))
assert evaluate_expression(expr, {}) is True
def test_equal_with_variables(self):
from rdflib.term import Variable
expr = self._make_relational(Variable("x"), "=", Variable("y"))
sol = {"x": lit("same"), "y": lit("same")}
assert evaluate_expression(expr, sol) is True
def test_unequal_with_variables(self):
from rdflib.term import Variable
expr = self._make_relational(Variable("x"), "=", Variable("y"))
sol = {"x": lit("one"), "y": lit("two")}
assert evaluate_expression(expr, sol) is False
def test_none_operand_returns_false(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_relational(Variable("x"), "=", Literal("a"))
assert evaluate_expression(expr, {}) is False
class TestLogicalExpressions:
def _make_and(self, exprs):
from rdflib.plugins.sparql.parserutils import CompValue
return CompValue("ConditionalAndExpression",
expr=exprs[0], other=exprs[1:])
def _make_or(self, exprs):
from rdflib.plugins.sparql.parserutils import CompValue
return CompValue("ConditionalOrExpression",
expr=exprs[0], other=exprs[1:])
def _make_not(self, expr):
from rdflib.plugins.sparql.parserutils import CompValue
return CompValue("UnaryNot", expr=expr)
def test_and_true_true(self):
result = evaluate_expression(self._make_and([True, True]), {})
assert result is True
def test_and_true_false(self):
result = evaluate_expression(self._make_and([True, False]), {})
assert result is False
def test_or_false_true(self):
result = evaluate_expression(self._make_or([False, True]), {})
assert result is True
def test_or_false_false(self):
result = evaluate_expression(self._make_or([False, False]), {})
assert result is False
def test_not_true(self):
result = evaluate_expression(self._make_not(True), {})
assert result is False
def test_not_false(self):
result = evaluate_expression(self._make_not(False), {})
assert result is True
class TestBuiltinFunctions:
def _make_builtin(self, name, **kwargs):
from rdflib.plugins.sparql.parserutils import CompValue
return CompValue(f"Builtin_{name}", **kwargs)
def test_bound_true(self):
from rdflib.term import Variable
expr = self._make_builtin("BOUND", arg=Variable("x"))
assert evaluate_expression(expr, {"x": lit("hi")}) is True
def test_bound_false(self):
from rdflib.term import Variable
expr = self._make_builtin("BOUND", arg=Variable("x"))
assert evaluate_expression(expr, {}) is False
def test_isiri_true(self):
from rdflib.term import Variable
expr = self._make_builtin("isIRI", arg=Variable("x"))
assert evaluate_expression(expr, {"x": iri("http://x")}) is True
def test_isiri_false(self):
from rdflib.term import Variable
expr = self._make_builtin("isIRI", arg=Variable("x"))
assert evaluate_expression(expr, {"x": lit("hello")}) is False
def test_isliteral_true(self):
from rdflib.term import Variable
expr = self._make_builtin("isLITERAL", arg=Variable("x"))
assert evaluate_expression(expr, {"x": lit("hello")}) is True
def test_isliteral_false(self):
from rdflib.term import Variable
expr = self._make_builtin("isLITERAL", arg=Variable("x"))
assert evaluate_expression(expr, {"x": iri("http://x")}) is False
def test_isblank_true(self):
from rdflib.term import Variable
expr = self._make_builtin("isBLANK", arg=Variable("x"))
assert evaluate_expression(expr, {"x": blank("b1")}) is True
def test_isblank_false(self):
from rdflib.term import Variable
expr = self._make_builtin("isBLANK", arg=Variable("x"))
assert evaluate_expression(expr, {"x": iri("http://x")}) is False
def test_str(self):
from rdflib.term import Variable
expr = self._make_builtin("STR", arg=Variable("x"))
result = evaluate_expression(expr, {"x": iri("http://example.com/a")})
assert result.type == LITERAL
assert result.value == "http://example.com/a"
def test_lang(self):
from rdflib.term import Variable
expr = self._make_builtin("LANG", arg=Variable("x"))
result = evaluate_expression(
expr, {"x": lit("hello", language="en")}
)
assert result.value == "en"
def test_lang_no_tag(self):
from rdflib.term import Variable
expr = self._make_builtin("LANG", arg=Variable("x"))
result = evaluate_expression(expr, {"x": lit("hello")})
assert result.value == ""
def test_datatype(self):
from rdflib.term import Variable
expr = self._make_builtin("DATATYPE", arg=Variable("x"))
result = evaluate_expression(
expr, {"x": lit("42", datatype=XSD + "integer")}
)
assert result.type == IRI
assert result.iri == XSD + "integer"
def test_strlen(self):
from rdflib.term import Variable
expr = self._make_builtin("STRLEN", arg=Variable("x"))
result = evaluate_expression(expr, {"x": lit("hello")})
assert result == 5
def test_ucase(self):
from rdflib.term import Variable
expr = self._make_builtin("UCASE", arg=Variable("x"))
result = evaluate_expression(expr, {"x": lit("hello")})
assert result.value == "HELLO"
def test_lcase(self):
from rdflib.term import Variable
expr = self._make_builtin("LCASE", arg=Variable("x"))
result = evaluate_expression(expr, {"x": lit("HELLO")})
assert result.value == "hello"
def test_contains_true(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("CONTAINS",
arg1=Variable("x"), arg2=Literal("ell"))
assert evaluate_expression(expr, {"x": lit("hello")}) is True
def test_contains_false(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("CONTAINS",
arg1=Variable("x"), arg2=Literal("xyz"))
assert evaluate_expression(expr, {"x": lit("hello")}) is False
def test_strstarts_true(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("STRSTARTS",
arg1=Variable("x"), arg2=Literal("hel"))
assert evaluate_expression(expr, {"x": lit("hello")}) is True
def test_strends_true(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("STRENDS",
arg1=Variable("x"), arg2=Literal("llo"))
assert evaluate_expression(expr, {"x": lit("hello")}) is True
def test_regex_match(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("REGEX",
text=Variable("x"),
pattern=Literal("^hel"),
flags=None)
assert evaluate_expression(expr, {"x": lit("hello")}) is True
def test_regex_case_insensitive(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("REGEX",
text=Variable("x"),
pattern=Literal("HELLO"),
flags=Literal("i"))
assert evaluate_expression(expr, {"x": lit("hello")}) is True
def test_regex_no_match(self):
from rdflib.term import Variable
from rdflib import Literal
expr = self._make_builtin("REGEX",
text=Variable("x"),
pattern=Literal("^world"),
flags=None)
assert evaluate_expression(expr, {"x": lit("hello")}) is False
class TestEffectiveBoolean:
def test_true(self):
assert _effective_boolean(True) is True
def test_false(self):
assert _effective_boolean(False) is False
def test_none(self):
assert _effective_boolean(None) is False
def test_nonzero_int(self):
assert _effective_boolean(42) is True
def test_zero_int(self):
assert _effective_boolean(0) is False
def test_nonempty_string(self):
assert _effective_boolean("hello") is True
def test_empty_string(self):
assert _effective_boolean("") is False
def test_iri_term(self):
assert _effective_boolean(iri("http://x")) is True
def test_nonempty_literal(self):
assert _effective_boolean(lit("hello")) is True
def test_empty_literal(self):
assert _effective_boolean(lit("")) is False
def test_boolean_literal_true(self):
assert _effective_boolean(
lit("true", datatype=XSD + "boolean")
) is True
def test_boolean_literal_false(self):
assert _effective_boolean(
lit("false", datatype=XSD + "boolean")
) is False
def test_numeric_literal_nonzero(self):
assert _effective_boolean(
lit("42", datatype=XSD + "integer")
) is True
def test_numeric_literal_zero(self):
assert _effective_boolean(
lit("0", datatype=XSD + "integer")
) is False
class TestToString:
def test_none(self):
assert _to_string(None) == ""
def test_string(self):
assert _to_string("hello") == "hello"
def test_iri_term(self):
assert _to_string(iri("http://example.com")) == "http://example.com"
def test_literal_term(self):
assert _to_string(lit("hello")) == "hello"
def test_blank_term(self):
assert _to_string(blank("b1")) == "b1"
class TestToNumeric:
def test_none(self):
assert _to_numeric(None) is None
def test_int(self):
assert _to_numeric(42) == 42
def test_float(self):
assert _to_numeric(3.14) == 3.14
def test_integer_literal(self):
assert _to_numeric(lit("42")) == 42
def test_decimal_literal(self):
assert _to_numeric(lit("3.14")) == 3.14
def test_non_numeric_literal(self):
assert _to_numeric(lit("hello")) is None
def test_numeric_string(self):
assert _to_numeric("42") == 42
def test_non_numeric_string(self):
assert _to_numeric("abc") is None
class TestComparableValue:
def test_none(self):
assert _comparable_value(None) == (0, "")
def test_int(self):
assert _comparable_value(42) == (2, 42)
def test_iri(self):
assert _comparable_value(iri("http://x")) == (4, "http://x")
def test_literal(self):
assert _comparable_value(lit("hello")) == (3, "hello")
def test_numeric_literal(self):
assert _comparable_value(lit("42")) == (2, 42)
def test_ordering(self):
vals = [lit("b"), lit("a"), lit("c")]
sorted_vals = sorted(vals, key=_comparable_value)
assert sorted_vals[0].value == "a"
assert sorted_vals[1].value == "b"
assert sorted_vals[2].value == "c"