mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-05-27 16:25:12 +02:00
feat: extend SPARQL evaluator with comprehensive function and operator support (#945)
Add 30+ SPARQL 1.1 built-in functions and the MINUS algebra operator to the custom SPARQL query backend. String functions: - SUBSTR (2-arg and 3-arg forms), STRBEFORE, STRAFTER - REPLACE (regex with flags), ENCODE_FOR_URI Numeric functions: - FLOOR, CEIL, ROUND, ABS Date/time accessors: - YEAR, MONTH, DAY, HOURS, MINUTES, SECONDS - NOW, TZ Hash functions: - MD5, SHA1, SHA256, SHA512 Term constructors: - IRI/URI, BNODE, UUID, STRUUID Other functions: - LANGMATCHES, RAND - EXISTS / NOT EXISTS (with async pre-evaluation to bridge the sync expression evaluator and async algebra evaluator) Algebra: - MINUS set-difference operator - HAVING already works via rdflib's Filter mapping (verified) Fix SPARQL ORDER handling Includes 653 lines of new unit tests covering all added functionality across expressions, solutions, and algebra layers.
This commit is contained in:
parent
e57f4669e1
commit
2c3a699af3
6 changed files with 1021 additions and 29 deletions
|
|
@ -84,6 +84,20 @@ def make_distinct(inner):
|
|||
return node
|
||||
|
||||
|
||||
def make_filter(inner, expr):
|
||||
node = CompValue("Filter")
|
||||
node.p = inner
|
||||
node.expr = expr
|
||||
return node
|
||||
|
||||
|
||||
def make_minus(left, right):
|
||||
node = CompValue("Minus")
|
||||
node.p1 = left
|
||||
node.p2 = right
|
||||
return node
|
||||
|
||||
|
||||
class TestQueryPattern:
|
||||
"""Tests for _query_pattern — the leaf that calls TriplesClient."""
|
||||
|
||||
|
|
@ -282,6 +296,177 @@ class TestEvaluate:
|
|||
|
||||
assert len(solutions) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_minus_removes_matching(self):
|
||||
tc = AsyncMock()
|
||||
|
||||
alice = iri("http://example.com/alice")
|
||||
bob = iri("http://example.com/bob")
|
||||
knows = iri("http://example.com/knows")
|
||||
hates = iri("http://example.com/hates")
|
||||
charlie = iri("http://example.com/charlie")
|
||||
|
||||
left_triple = make_triple(alice, knows, bob)
|
||||
right_triple1 = make_triple(alice, knows, bob)
|
||||
right_triple2 = make_triple(alice, hates, charlie)
|
||||
|
||||
left_bgp = make_bgp(
|
||||
(Variable("s"), URIRef("http://example.com/knows"), Variable("o"))
|
||||
)
|
||||
right_bgp = make_bgp(
|
||||
(Variable("s"), URIRef("http://example.com/hates"), Variable("r"))
|
||||
)
|
||||
|
||||
async def mock_query(**kwargs):
|
||||
pred = kwargs.get("p")
|
||||
if pred and pred.iri == "http://example.com/knows":
|
||||
return [left_triple]
|
||||
elif pred and pred.iri == "http://example.com/hates":
|
||||
return [right_triple2]
|
||||
return []
|
||||
|
||||
tc.query.side_effect = mock_query
|
||||
|
||||
tree = make_select(
|
||||
make_project(
|
||||
make_minus(left_bgp, right_bgp),
|
||||
["s", "o"]
|
||||
)
|
||||
)
|
||||
|
||||
solutions = await evaluate(tree, tc, collection="default")
|
||||
|
||||
# alice knows bob, but alice also hates charlie
|
||||
# shared var is "s" (alice), so alice's solution is removed
|
||||
assert len(solutions) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_minus_no_shared_vars_preserves_all(self):
|
||||
tc = AsyncMock()
|
||||
|
||||
alice = iri("http://example.com/alice")
|
||||
bob = iri("http://example.com/bob")
|
||||
|
||||
left_triple = make_triple(alice, iri("http://example.com/p"), bob)
|
||||
|
||||
left_bgp = make_bgp(
|
||||
(Variable("s"), URIRef("http://example.com/p"), Variable("o"))
|
||||
)
|
||||
right_bgp = make_bgp(
|
||||
(Variable("x"), URIRef("http://example.com/q"), Variable("y"))
|
||||
)
|
||||
|
||||
async def mock_query(**kwargs):
|
||||
pred = kwargs.get("p")
|
||||
if pred and pred.iri == "http://example.com/p":
|
||||
return [left_triple]
|
||||
return []
|
||||
|
||||
tc.query.side_effect = mock_query
|
||||
|
||||
tree = make_select(
|
||||
make_project(
|
||||
make_minus(left_bgp, right_bgp),
|
||||
["s", "o"]
|
||||
)
|
||||
)
|
||||
|
||||
solutions = await evaluate(tree, tc, collection="default")
|
||||
|
||||
assert len(solutions) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_exists_keeps_matching(self):
|
||||
tc = AsyncMock()
|
||||
|
||||
alice = iri("http://example.com/alice")
|
||||
bob = iri("http://example.com/bob")
|
||||
charlie = iri("http://example.com/charlie")
|
||||
|
||||
left_triple1 = make_triple(alice, iri("http://example.com/knows"), bob)
|
||||
left_triple2 = make_triple(alice, iri("http://example.com/knows"), charlie)
|
||||
exists_triple = make_triple(bob, iri("http://example.com/likes"), alice)
|
||||
|
||||
left_bgp = make_bgp(
|
||||
(Variable("s"), URIRef("http://example.com/knows"), Variable("o"))
|
||||
)
|
||||
exists_bgp = make_bgp(
|
||||
(Variable("o"), URIRef("http://example.com/likes"), Variable("_any"))
|
||||
)
|
||||
|
||||
async def mock_query(**kwargs):
|
||||
pred = kwargs.get("p")
|
||||
if pred and pred.iri == "http://example.com/knows":
|
||||
return [left_triple1, left_triple2]
|
||||
elif pred and pred.iri == "http://example.com/likes":
|
||||
return [exists_triple]
|
||||
return []
|
||||
|
||||
tc.query.side_effect = mock_query
|
||||
|
||||
exists_expr = CompValue("Builtin_EXISTS")
|
||||
exists_expr.graph = exists_bgp
|
||||
|
||||
tree = make_select(
|
||||
make_project(
|
||||
make_filter(left_bgp, exists_expr),
|
||||
["s", "o"]
|
||||
)
|
||||
)
|
||||
|
||||
solutions = await evaluate(tree, tc, collection="default")
|
||||
|
||||
# Only bob has a "likes" triple, so only the bob solution passes
|
||||
result_objects = [s["o"].iri for s in solutions]
|
||||
assert "http://example.com/bob" in result_objects
|
||||
assert "http://example.com/charlie" not in result_objects
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_not_exists_removes_matching(self):
|
||||
tc = AsyncMock()
|
||||
|
||||
alice = iri("http://example.com/alice")
|
||||
bob = iri("http://example.com/bob")
|
||||
charlie = iri("http://example.com/charlie")
|
||||
|
||||
left_triple1 = make_triple(alice, iri("http://example.com/knows"), bob)
|
||||
left_triple2 = make_triple(alice, iri("http://example.com/knows"), charlie)
|
||||
exists_triple = make_triple(bob, iri("http://example.com/likes"), alice)
|
||||
|
||||
left_bgp = make_bgp(
|
||||
(Variable("s"), URIRef("http://example.com/knows"), Variable("o"))
|
||||
)
|
||||
exists_bgp = make_bgp(
|
||||
(Variable("o"), URIRef("http://example.com/likes"), Variable("_any"))
|
||||
)
|
||||
|
||||
async def mock_query(**kwargs):
|
||||
pred = kwargs.get("p")
|
||||
if pred and pred.iri == "http://example.com/knows":
|
||||
return [left_triple1, left_triple2]
|
||||
elif pred and pred.iri == "http://example.com/likes":
|
||||
return [exists_triple]
|
||||
return []
|
||||
|
||||
tc.query.side_effect = mock_query
|
||||
|
||||
not_exists_expr = CompValue("Builtin_NOTEXISTS")
|
||||
not_exists_expr.graph = exists_bgp
|
||||
|
||||
tree = make_select(
|
||||
make_project(
|
||||
make_filter(left_bgp, not_exists_expr),
|
||||
["s", "o"]
|
||||
)
|
||||
)
|
||||
|
||||
solutions = await evaluate(tree, tc, collection="default")
|
||||
|
||||
# bob has a "likes" triple so is removed; charlie stays
|
||||
result_objects = [s["o"].iri for s in solutions]
|
||||
assert "http://example.com/charlie" in result_objects
|
||||
assert "http://example.com/bob" not in result_objects
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unsupported_node_returns_empty_solution(self):
|
||||
tc = AsyncMock()
|
||||
|
|
|
|||
|
|
@ -300,6 +300,438 @@ class TestBuiltinFunctions:
|
|||
flags=None)
|
||||
assert evaluate_expression(expr, {"x": lit("hello")}) is False
|
||||
|
||||
def test_substr_three_args(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("SUBSTR",
|
||||
arg=Variable("x"),
|
||||
start=Literal(1),
|
||||
length=Literal(4))
|
||||
result = evaluate_expression(expr, {"x": lit("2024-03-15")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "2024"
|
||||
|
||||
def test_substr_two_args(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("SUBSTR",
|
||||
arg=Variable("x"),
|
||||
start=Literal(6),
|
||||
length=None)
|
||||
result = evaluate_expression(expr, {"x": lit("2024-03-15")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "03-15"
|
||||
|
||||
def test_substr_middle(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("SUBSTR",
|
||||
arg=Variable("x"),
|
||||
start=Literal(6),
|
||||
length=Literal(2))
|
||||
result = evaluate_expression(expr, {"x": lit("2024-03-15")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "03"
|
||||
|
||||
def test_substr_null_start(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("SUBSTR",
|
||||
arg=Variable("x"),
|
||||
start=Variable("missing"),
|
||||
length=None)
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result is None
|
||||
|
||||
def test_year(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("YEAR", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15", datatype=XSD + "date")}
|
||||
)
|
||||
assert result == 2024
|
||||
|
||||
def test_month(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("MONTH", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15", datatype=XSD + "date")}
|
||||
)
|
||||
assert result == 3
|
||||
|
||||
def test_day(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("DAY", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15", datatype=XSD + "date")}
|
||||
)
|
||||
assert result == 15
|
||||
|
||||
def test_hours(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("HOURS", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15T10:30:45", datatype=XSD + "dateTime")}
|
||||
)
|
||||
assert result == 10
|
||||
|
||||
def test_minutes(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("MINUTES", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15T10:30:45", datatype=XSD + "dateTime")}
|
||||
)
|
||||
assert result == 30
|
||||
|
||||
def test_seconds(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("SECONDS", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15T10:30:45", datatype=XSD + "dateTime")}
|
||||
)
|
||||
assert result == 45
|
||||
|
||||
def test_year_from_datetime(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("YEAR", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15T10:30:45", datatype=XSD + "dateTime")}
|
||||
)
|
||||
assert result == 2024
|
||||
|
||||
def test_hours_from_date_returns_zero(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("HOURS", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15", datatype=XSD + "date")}
|
||||
)
|
||||
assert result == 0
|
||||
|
||||
def test_year_invalid_date(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("YEAR", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("not-a-date")}
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_floor(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("FLOOR", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("3.7")}) == 3
|
||||
|
||||
def test_floor_negative(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("FLOOR", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("-2.3")}) == -3
|
||||
|
||||
def test_floor_none(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("FLOOR", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("abc")}) is None
|
||||
|
||||
def test_ceil(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("CEIL", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("3.2")}) == 4
|
||||
|
||||
def test_ceil_negative(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("CEIL", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("-2.7")}) == -2
|
||||
|
||||
def test_abs_positive(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ABS", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("42")}) == 42
|
||||
|
||||
def test_abs_negative(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ABS", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("-42")}) == 42
|
||||
|
||||
def test_abs_none(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ABS", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("abc")}) is None
|
||||
|
||||
def test_replace_simple(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("REPLACE",
|
||||
arg=Variable("x"),
|
||||
pattern=Literal(" BC"),
|
||||
replacement=Literal(""),
|
||||
flags=None)
|
||||
result = evaluate_expression(expr, {"x": lit("500 BC")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "500"
|
||||
|
||||
def test_replace_regex(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("REPLACE",
|
||||
arg=Variable("x"),
|
||||
pattern=Literal("[0-9]+"),
|
||||
replacement=Literal("X"),
|
||||
flags=None)
|
||||
result = evaluate_expression(expr, {"x": lit("abc123def456")})
|
||||
assert result.value == "abcXdefX"
|
||||
|
||||
def test_replace_case_insensitive(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("REPLACE",
|
||||
arg=Variable("x"),
|
||||
pattern=Literal("hello"),
|
||||
replacement=Literal("world"),
|
||||
flags=Literal("i"))
|
||||
result = evaluate_expression(expr, {"x": lit("HELLO there")})
|
||||
assert result.value == "world there"
|
||||
|
||||
def test_round_up(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ROUND", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("3.7")}) == 4
|
||||
|
||||
def test_round_down(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ROUND", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("3.2")}) == 3
|
||||
|
||||
def test_round_none(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ROUND", arg=Variable("x"))
|
||||
assert evaluate_expression(expr, {"x": lit("abc")}) is None
|
||||
|
||||
def test_strbefore(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("STRBEFORE",
|
||||
arg1=Variable("x"), arg2=Literal("-"))
|
||||
result = evaluate_expression(expr, {"x": lit("2024-03-15")})
|
||||
assert result.value == "2024"
|
||||
|
||||
def test_strbefore_not_found(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("STRBEFORE",
|
||||
arg1=Variable("x"), arg2=Literal("/"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result.value == ""
|
||||
|
||||
def test_strafter(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("STRAFTER",
|
||||
arg1=Variable("x"), arg2=Literal("-"))
|
||||
result = evaluate_expression(expr, {"x": lit("2024-03-15")})
|
||||
assert result.value == "03-15"
|
||||
|
||||
def test_strafter_not_found(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("STRAFTER",
|
||||
arg1=Variable("x"), arg2=Literal("/"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result.value == ""
|
||||
|
||||
def test_encode_for_uri(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ENCODE_FOR_URI", arg=Variable("x"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello world")})
|
||||
assert result.value == "hello%20world"
|
||||
|
||||
def test_encode_for_uri_special_chars(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("ENCODE_FOR_URI", arg=Variable("x"))
|
||||
result = evaluate_expression(expr, {"x": lit("a/b?c=d&e")})
|
||||
assert result.value == "a%2Fb%3Fc%3Dd%26e"
|
||||
|
||||
def test_langmatches_basic(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("LANGMATCHES",
|
||||
arg1=Literal("en"), arg2=Literal("en"))
|
||||
assert evaluate_expression(expr, {}) is True
|
||||
|
||||
def test_langmatches_subtag(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("LANGMATCHES",
|
||||
arg1=Literal("en-US"), arg2=Literal("en"))
|
||||
assert evaluate_expression(expr, {}) is True
|
||||
|
||||
def test_langmatches_wildcard(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("LANGMATCHES",
|
||||
arg1=Literal("fr"), arg2=Literal("*"))
|
||||
assert evaluate_expression(expr, {}) is True
|
||||
|
||||
def test_langmatches_wildcard_empty(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("LANGMATCHES",
|
||||
arg1=Literal(""), arg2=Literal("*"))
|
||||
assert evaluate_expression(expr, {}) is False
|
||||
|
||||
def test_langmatches_no_match(self):
|
||||
from rdflib.term import Variable
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("LANGMATCHES",
|
||||
arg1=Literal("fr"), arg2=Literal("en"))
|
||||
assert evaluate_expression(expr, {}) is False
|
||||
|
||||
def test_iri_constructor(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("IRI", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("http://example.com/test")}
|
||||
)
|
||||
assert result.type == IRI
|
||||
assert result.iri == "http://example.com/test"
|
||||
|
||||
def test_uri_constructor(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("URI", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("http://example.com/test")}
|
||||
)
|
||||
assert result.type == IRI
|
||||
assert result.iri == "http://example.com/test"
|
||||
|
||||
def test_bnode_no_arg(self):
|
||||
expr = self._make_builtin("BNODE")
|
||||
result = evaluate_expression(expr, {})
|
||||
assert result.type == BLANK
|
||||
assert len(result.id) > 0
|
||||
|
||||
def test_bnode_with_label(self):
|
||||
from rdflib import Literal
|
||||
expr = self._make_builtin("BNODE", arg=Literal("mynode"))
|
||||
result = evaluate_expression(expr, {})
|
||||
assert result.type == BLANK
|
||||
assert result.id == "mynode"
|
||||
|
||||
def test_now(self):
|
||||
import re as re_mod
|
||||
expr = self._make_builtin("NOW")
|
||||
result = evaluate_expression(expr, {})
|
||||
assert result.type == LITERAL
|
||||
assert result.datatype == XSD + "dateTime"
|
||||
assert re_mod.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", result.value)
|
||||
|
||||
def test_tz_with_utc(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("TZ", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15T10:30:45+0000",
|
||||
datatype=XSD + "dateTime")}
|
||||
)
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "+00:00"
|
||||
|
||||
def test_tz_no_timezone(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("TZ", arg=Variable("x"))
|
||||
result = evaluate_expression(
|
||||
expr, {"x": lit("2024-03-15T10:30:45",
|
||||
datatype=XSD + "dateTime")}
|
||||
)
|
||||
assert result.value == ""
|
||||
|
||||
def test_rand(self):
|
||||
expr = self._make_builtin("RAND")
|
||||
result = evaluate_expression(expr, {})
|
||||
assert isinstance(result, float)
|
||||
assert 0.0 <= result < 1.0
|
||||
|
||||
def test_uuid(self):
|
||||
import re as re_mod
|
||||
expr = self._make_builtin("UUID")
|
||||
result = evaluate_expression(expr, {})
|
||||
assert result.type == IRI
|
||||
assert result.iri.startswith("urn:uuid:")
|
||||
uuid_part = result.iri[len("urn:uuid:"):]
|
||||
assert re_mod.match(
|
||||
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
|
||||
uuid_part
|
||||
)
|
||||
|
||||
def test_struuid(self):
|
||||
import re as re_mod
|
||||
expr = self._make_builtin("STRUUID")
|
||||
result = evaluate_expression(expr, {})
|
||||
assert result.type == LITERAL
|
||||
assert re_mod.match(
|
||||
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
|
||||
result.value
|
||||
)
|
||||
|
||||
def test_md5(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("MD5", arg=Variable("x"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "5d41402abc4b2a76b9719d911017c592"
|
||||
|
||||
def test_sha1(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("SHA1", arg=Variable("x"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
|
||||
|
||||
def test_sha256(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("SHA256", arg=Variable("x"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result.type == LITERAL
|
||||
assert result.value == (
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e"
|
||||
"1b161e5c1fa7425e73043362938b9824"
|
||||
)
|
||||
|
||||
def test_sha512(self):
|
||||
from rdflib.term import Variable
|
||||
expr = self._make_builtin("SHA512", arg=Variable("x"))
|
||||
result = evaluate_expression(expr, {"x": lit("hello")})
|
||||
assert result.type == LITERAL
|
||||
assert len(result.value) == 128
|
||||
|
||||
def test_exists_with_callback(self):
|
||||
from rdflib.plugins.sparql.parserutils import CompValue
|
||||
graph = CompValue("BGP")
|
||||
expr = self._make_builtin("EXISTS", graph=graph)
|
||||
cb = lambda g, s: True
|
||||
result = evaluate_expression(expr, {}, exists_cb=cb)
|
||||
assert result is True
|
||||
|
||||
def test_exists_callback_false(self):
|
||||
from rdflib.plugins.sparql.parserutils import CompValue
|
||||
graph = CompValue("BGP")
|
||||
expr = self._make_builtin("EXISTS", graph=graph)
|
||||
cb = lambda g, s: False
|
||||
result = evaluate_expression(expr, {}, exists_cb=cb)
|
||||
assert result is False
|
||||
|
||||
def test_notexists_with_callback(self):
|
||||
from rdflib.plugins.sparql.parserutils import CompValue
|
||||
graph = CompValue("BGP")
|
||||
expr = self._make_builtin("NOTEXISTS", graph=graph)
|
||||
cb = lambda g, s: True
|
||||
result = evaluate_expression(expr, {}, exists_cb=cb)
|
||||
assert result is False
|
||||
|
||||
def test_notexists_callback_false(self):
|
||||
from rdflib.plugins.sparql.parserutils import CompValue
|
||||
graph = CompValue("BGP")
|
||||
expr = self._make_builtin("NOTEXISTS", graph=graph)
|
||||
cb = lambda g, s: False
|
||||
result = evaluate_expression(expr, {}, exists_cb=cb)
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestEffectiveBoolean:
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Tests for SPARQL solution sequence operations.
|
|||
import pytest
|
||||
from trustgraph.schema import Term, IRI, LITERAL
|
||||
from trustgraph.query.sparql.solutions import (
|
||||
hash_join, left_join, union, project, distinct,
|
||||
hash_join, left_join, minus, union, project, distinct,
|
||||
order_by, slice_solutions, _terms_equal, _compatible,
|
||||
)
|
||||
|
||||
|
|
@ -311,6 +311,30 @@ class TestOrderBy:
|
|||
result = order_by(solutions, [])
|
||||
assert len(result) == 1
|
||||
|
||||
def test_order_by_numeric_literals(self):
|
||||
solutions = [
|
||||
{"year": lit("1950")},
|
||||
{"year": lit("700")},
|
||||
{"year": lit("2000")},
|
||||
{"year": lit("450")},
|
||||
{"year": lit("1200")},
|
||||
]
|
||||
key_fns = [(lambda sol: sol.get("year"), True)]
|
||||
result = order_by(solutions, key_fns)
|
||||
values = [s["year"].value for s in result]
|
||||
assert values == ["450", "700", "1200", "1950", "2000"]
|
||||
|
||||
def test_order_by_numeric_descending(self):
|
||||
solutions = [
|
||||
{"year": lit("1950")},
|
||||
{"year": lit("700")},
|
||||
{"year": lit("2000")},
|
||||
]
|
||||
key_fns = [(lambda sol: sol.get("year"), False)]
|
||||
result = order_by(solutions, key_fns)
|
||||
values = [s["year"].value for s in result]
|
||||
assert values == ["2000", "1950", "700"]
|
||||
|
||||
|
||||
class TestSlice:
|
||||
|
||||
|
|
@ -343,3 +367,37 @@ class TestSlice:
|
|||
solutions = [{"s": alice}, {"s": bob}]
|
||||
result = slice_solutions(solutions)
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
class TestMinus:
|
||||
|
||||
def test_removes_compatible(self, alice, bob):
|
||||
left = [{"s": alice}, {"s": bob}]
|
||||
right = [{"s": alice}]
|
||||
result = minus(left, right)
|
||||
assert len(result) == 1
|
||||
assert result[0]["s"].iri == "http://example.com/bob"
|
||||
|
||||
def test_empty_right_preserves_all(self, alice, bob):
|
||||
left = [{"s": alice}, {"s": bob}]
|
||||
result = minus(left, [])
|
||||
assert len(result) == 2
|
||||
|
||||
def test_no_shared_variables_preserves_all(self, alice, bob):
|
||||
left = [{"s": alice}]
|
||||
right = [{"t": bob}]
|
||||
result = minus(left, right)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_all_removed(self, alice):
|
||||
left = [{"s": alice}]
|
||||
right = [{"s": alice}]
|
||||
result = minus(left, right)
|
||||
assert len(result) == 0
|
||||
|
||||
def test_partial_shared_variables(self, alice, bob):
|
||||
left = [{"s": alice, "p": lit("x")}, {"s": bob, "p": lit("y")}]
|
||||
right = [{"s": alice}]
|
||||
result = minus(left, right)
|
||||
assert len(result) == 1
|
||||
assert result[0]["s"].iri == "http://example.com/bob"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue