mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
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:
parent
62c30a3a50
commit
d9dc4cbab5
23 changed files with 3498 additions and 3 deletions
205
tests/unit/test_query/test_sparql_parser.py
Normal file
205
tests/unit/test_query/test_sparql_parser.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
"""
|
||||
Tests for the SPARQL parser module.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from trustgraph.query.sparql.parser import (
|
||||
parse_sparql, ParseError, rdflib_term_to_term, term_to_rdflib,
|
||||
)
|
||||
from trustgraph.schema import Term, IRI, LITERAL, BLANK
|
||||
|
||||
|
||||
class TestParseSparql:
|
||||
"""Tests for parse_sparql function."""
|
||||
|
||||
def test_select_query_type(self):
|
||||
parsed = parse_sparql("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
||||
assert parsed.query_type == "select"
|
||||
|
||||
def test_select_variables(self):
|
||||
parsed = parse_sparql("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
||||
assert parsed.variables == ["s", "p", "o"]
|
||||
|
||||
def test_select_subset_variables(self):
|
||||
parsed = parse_sparql("SELECT ?s ?o WHERE { ?s ?p ?o }")
|
||||
assert parsed.variables == ["s", "o"]
|
||||
|
||||
def test_ask_query_type(self):
|
||||
parsed = parse_sparql(
|
||||
"ASK { <http://example.com/a> ?p ?o }"
|
||||
)
|
||||
assert parsed.query_type == "ask"
|
||||
|
||||
def test_ask_no_variables(self):
|
||||
parsed = parse_sparql(
|
||||
"ASK { <http://example.com/a> ?p ?o }"
|
||||
)
|
||||
assert parsed.variables == []
|
||||
|
||||
def test_construct_query_type(self):
|
||||
parsed = parse_sparql(
|
||||
"CONSTRUCT { ?s <http://example.com/knows> ?o } "
|
||||
"WHERE { ?s <http://example.com/friendOf> ?o }"
|
||||
)
|
||||
assert parsed.query_type == "construct"
|
||||
|
||||
def test_describe_query_type(self):
|
||||
parsed = parse_sparql(
|
||||
"DESCRIBE <http://example.com/alice>"
|
||||
)
|
||||
assert parsed.query_type == "describe"
|
||||
|
||||
def test_select_with_limit(self):
|
||||
parsed = parse_sparql(
|
||||
"SELECT ?s WHERE { ?s ?p ?o } LIMIT 10"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
assert parsed.variables == ["s"]
|
||||
|
||||
def test_select_with_distinct(self):
|
||||
parsed = parse_sparql(
|
||||
"SELECT DISTINCT ?s WHERE { ?s ?p ?o }"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
assert parsed.variables == ["s"]
|
||||
|
||||
def test_select_with_filter(self):
|
||||
parsed = parse_sparql(
|
||||
'SELECT ?s ?label WHERE { '
|
||||
' ?s <http://www.w3.org/2000/01/rdf-schema#label> ?label . '
|
||||
' FILTER(CONTAINS(STR(?label), "test")) '
|
||||
'}'
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
assert parsed.variables == ["s", "label"]
|
||||
|
||||
def test_select_with_optional(self):
|
||||
parsed = parse_sparql(
|
||||
"SELECT ?s ?p ?o ?label WHERE { "
|
||||
" ?s ?p ?o . "
|
||||
" OPTIONAL { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?label } "
|
||||
"}"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
assert set(parsed.variables) == {"s", "p", "o", "label"}
|
||||
|
||||
def test_select_with_union(self):
|
||||
parsed = parse_sparql(
|
||||
"SELECT ?s ?label WHERE { "
|
||||
" { ?s <http://example.com/name> ?label } "
|
||||
" UNION "
|
||||
" { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?label } "
|
||||
"}"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
|
||||
def test_select_with_order_by(self):
|
||||
parsed = parse_sparql(
|
||||
"SELECT ?s ?label WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?label } "
|
||||
"ORDER BY ?label"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
|
||||
def test_select_with_group_by(self):
|
||||
parsed = parse_sparql(
|
||||
"SELECT ?p (COUNT(?o) AS ?count) WHERE { ?s ?p ?o } "
|
||||
"GROUP BY ?p ORDER BY DESC(?count)"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
|
||||
def test_select_with_prefixes(self):
|
||||
parsed = parse_sparql(
|
||||
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> "
|
||||
"SELECT ?s ?label WHERE { ?s rdfs:label ?label }"
|
||||
)
|
||||
assert parsed.query_type == "select"
|
||||
assert parsed.variables == ["s", "label"]
|
||||
|
||||
def test_algebra_not_none(self):
|
||||
parsed = parse_sparql("SELECT ?s WHERE { ?s ?p ?o }")
|
||||
assert parsed.algebra is not None
|
||||
|
||||
def test_parse_error_invalid_sparql(self):
|
||||
with pytest.raises(ParseError):
|
||||
parse_sparql("NOT VALID SPARQL AT ALL")
|
||||
|
||||
def test_parse_error_incomplete_query(self):
|
||||
with pytest.raises(ParseError):
|
||||
parse_sparql("SELECT ?s WHERE {")
|
||||
|
||||
def test_parse_error_message(self):
|
||||
with pytest.raises(ParseError, match="SPARQL parse error"):
|
||||
parse_sparql("GIBBERISH")
|
||||
|
||||
|
||||
class TestRdflibTermToTerm:
|
||||
"""Tests for rdflib-to-Term conversion."""
|
||||
|
||||
def test_uriref_to_term(self):
|
||||
from rdflib import URIRef
|
||||
term = rdflib_term_to_term(URIRef("http://example.com/alice"))
|
||||
assert term.type == IRI
|
||||
assert term.iri == "http://example.com/alice"
|
||||
|
||||
def test_literal_to_term(self):
|
||||
from rdflib import Literal
|
||||
term = rdflib_term_to_term(Literal("hello"))
|
||||
assert term.type == LITERAL
|
||||
assert term.value == "hello"
|
||||
|
||||
def test_typed_literal_to_term(self):
|
||||
from rdflib import Literal, URIRef
|
||||
term = rdflib_term_to_term(
|
||||
Literal("42", datatype=URIRef("http://www.w3.org/2001/XMLSchema#integer"))
|
||||
)
|
||||
assert term.type == LITERAL
|
||||
assert term.value == "42"
|
||||
assert term.datatype == "http://www.w3.org/2001/XMLSchema#integer"
|
||||
|
||||
def test_lang_literal_to_term(self):
|
||||
from rdflib import Literal
|
||||
term = rdflib_term_to_term(Literal("hello", lang="en"))
|
||||
assert term.type == LITERAL
|
||||
assert term.value == "hello"
|
||||
assert term.language == "en"
|
||||
|
||||
def test_bnode_to_term(self):
|
||||
from rdflib import BNode
|
||||
term = rdflib_term_to_term(BNode("b1"))
|
||||
assert term.type == BLANK
|
||||
assert term.id == "b1"
|
||||
|
||||
|
||||
class TestTermToRdflib:
|
||||
"""Tests for Term-to-rdflib conversion."""
|
||||
|
||||
def test_iri_term_to_uriref(self):
|
||||
from rdflib import URIRef
|
||||
result = term_to_rdflib(Term(type=IRI, iri="http://example.com/x"))
|
||||
assert isinstance(result, URIRef)
|
||||
assert str(result) == "http://example.com/x"
|
||||
|
||||
def test_literal_term_to_literal(self):
|
||||
from rdflib import Literal
|
||||
result = term_to_rdflib(Term(type=LITERAL, value="hello"))
|
||||
assert isinstance(result, Literal)
|
||||
assert str(result) == "hello"
|
||||
|
||||
def test_typed_literal_roundtrip(self):
|
||||
from rdflib import URIRef
|
||||
original = Term(
|
||||
type=LITERAL, value="42",
|
||||
datatype="http://www.w3.org/2001/XMLSchema#integer"
|
||||
)
|
||||
rdflib_term = term_to_rdflib(original)
|
||||
assert rdflib_term.datatype == URIRef("http://www.w3.org/2001/XMLSchema#integer")
|
||||
|
||||
def test_lang_literal_roundtrip(self):
|
||||
original = Term(type=LITERAL, value="bonjour", language="fr")
|
||||
rdflib_term = term_to_rdflib(original)
|
||||
assert rdflib_term.language == "fr"
|
||||
|
||||
def test_blank_term_to_bnode(self):
|
||||
from rdflib import BNode
|
||||
result = term_to_rdflib(Term(type=BLANK, id="b1"))
|
||||
assert isinstance(result, BNode)
|
||||
Loading…
Add table
Add a link
Reference in a new issue