Skip to content

Commit

Permalink
Fix handling of EXISTS inside BIND
Browse files Browse the repository at this point in the history
This patch fixes an issue with `BIND( EXISTS ... )` in SPARQL,
for example:

```sparql
SELECT * WHERE {
    BIND(
	EXISTS {
	    <http://example.com/a>
	    <http://example.com/b>
	    <http://example.com/c>
	}
	AS ?bound
    )
}
```

The graph pattern of `EXISTS` needs to be translated for it to operate
correctly during evaluation, but this was not happening. This patch
corrects that so that the graph pattern is translated as part of
translating `BIND`.

This patch also adds a bunch of tests for EXISTS to ensure there is no
regression and that various EXISTS cases function correctly.

Fixes #1472
  • Loading branch information
aucampia committed Apr 13, 2022
1 parent cdaee27 commit f1ab6b9
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -14,6 +14,11 @@ For strings with many escape sequences the parsing speed seems to be almost 4 ti

Fixes [issue #1655](https://github.com/RDFLib/rdflib/issues/1655).

### Other fixes

- Fixed the handling of `EXISTS` inside `BIND` for SPARQL.
This was raising an exception during evaluation before but is now correctly handled.

### Deprecated Functions

Marked the following functions as deprecated:
Expand Down
5 changes: 4 additions & 1 deletion rdflib/plugins/sparql/algebra.py
Expand Up @@ -312,7 +312,10 @@ def translateGroupGraphPattern(graphPattern):
elif p.name in ("BGP", "Extend"):
G = Join(p1=G, p2=p)
elif p.name == "Bind":
G = Extend(G, p.expr, p.var)
# translateExists will translate the expression if it is EXISTS, and otherwise return
# the expression as is. This is needed because EXISTS has a graph pattern
# which must be translated to work properly during evaluation.
G = Extend(G, translateExists(p.expr), p.var)

else:
raise Exception(
Expand Down
254 changes: 253 additions & 1 deletion test/test_sparql/test_sparql.py
@@ -1,18 +1,23 @@
from typing import Any, Callable, Type
import logging
from typing import Mapping, Sequence
from rdflib.plugins.sparql import sparql, prepareQuery
from rdflib.plugins.sparql.sparql import SPARQLError
from rdflib import Graph, URIRef, Literal, BNode, ConjunctiveGraph
from rdflib.namespace import Namespace, RDF, RDFS
from rdflib.compare import isomorphic
from rdflib.query import Result
from rdflib.term import Variable
from rdflib.term import Variable, Identifier
from rdflib.plugins.sparql.evaluate import evalPart
from rdflib.plugins.sparql.evalutils import _eval
import pytest
from pytest import MonkeyPatch
import rdflib.plugins.sparql.operators
import rdflib.plugins.sparql.parser
import rdflib.plugins.sparql
from rdflib.plugins.sparql.algebra import translateQuery
from rdflib.plugins.sparql.parser import parseQuery
from rdflib.plugins.sparql.parserutils import prettify_parsetree

from test.testutils import eq_

Expand Down Expand Up @@ -491,3 +496,250 @@ def thrower(*args: Any, **kwargs: Any) -> None:
with pytest.raises(exception_type) as excinfo:
result_consumer(result)
assert str(excinfo.value) == "TEST ERROR"


@pytest.mark.parametrize(
["query_string", "expected_bindings"],
[
pytest.param(
"""
SELECT ?label ?deprecated WHERE {
?s rdfs:label "Class"
OPTIONAL {
?s
rdfs:comment
?label
}
OPTIONAL {
?s
owl:deprecated
?deprecated
}
}
""",
[{Variable('label'): Literal("The class of classes.")}],
id="select-optional",
),
pytest.param(
"""
SELECT * WHERE {
BIND( SHA256("abc") as ?bound )
}
""",
[
{
Variable('bound'): Literal(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
)
}
],
id="select-bind-sha256",
),
pytest.param(
"""
SELECT * WHERE {
BIND( (1+2) as ?bound )
}
""",
[{Variable('bound'): Literal(3)}],
id="select-bind-plus",
),
pytest.param(
"""
SELECT * WHERE {
OPTIONAL {
<http://example.com/a>
<http://example.com/b>
<http://example.com/c>
}
}
""",
[{}],
id="select-optional-const",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:label "Class" .
FILTER EXISTS {
<http://example.com/a>
<http://example.com/b>
<http://example.com/c>
}
}
""",
[],
id="select-filter-exists-const-false",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:label "Class" .
FILTER NOT EXISTS {
<http://example.com/a>
<http://example.com/b>
<http://example.com/c>
}
}
""",
[{Variable("s"): RDFS.Class}],
id="select-filter-notexists-const-false",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:label "Class"
FILTER EXISTS {
rdfs:Class rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
}
}
""",
[{Variable("s"): RDFS.Class}],
id="select-filter-exists-const-true",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:label "Class"
FILTER NOT EXISTS {
rdfs:Class rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
}
}
""",
[],
id="select-filter-notexists-const-true",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
FILTER EXISTS {
?s rdfs:label "MISSING" .
}
}
""",
[],
id="select-filter-exists-var-false",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
FILTER EXISTS {
?s rdfs:label "Class" .
}
}
""",
[{Variable("s"): RDFS.Class}],
id="select-filter-exists-var-true",
),
pytest.param(
"""
SELECT * WHERE {
BIND(
EXISTS {
<http://example.com/a>
<http://example.com/b>
<http://example.com/c>
}
AS ?bound
)
}
""",
[{Variable('bound'): Literal(False)}],
id="select-bind-exists-const-false",
),
pytest.param(
"""
SELECT * WHERE {
BIND(
EXISTS {
rdfs:Class rdfs:label "Class"
}
AS ?bound
)
}
""",
[{Variable('bound'): Literal(True)}],
id="select-bind-exists-const-true",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:comment "The class of classes."
BIND(
EXISTS {
?s rdfs:label "Class"
}
AS ?bound
)
}
""",
[{Variable("s"): RDFS.Class, Variable('bound'): Literal(True)}],
id="select-bind-exists-var-true",
),
pytest.param(
"""
SELECT * WHERE {
?s rdfs:comment "The class of classes."
BIND(
EXISTS {
?s rdfs:label "Property"
}
AS ?bound
)
}
""",
[{Variable("s"): RDFS.Class, Variable('bound'): Literal(False)}],
id="select-bind-exists-var-false",
),
pytest.param(
"""
SELECT * WHERE {
BIND(
NOT EXISTS {
<http://example.com/a>
<http://example.com/b>
<http://example.com/c>
}
AS ?bound
)
}
""",
[{Variable('bound'): Literal(True)}],
id="select-bind-notexists-const-false",
),
pytest.param(
"""
SELECT * WHERE {
BIND(
NOT EXISTS {
rdfs:Class rdfs:label "Class"
}
AS ?bound
)
}
""",
[{Variable('bound'): Literal(False)}],
id="select-bind-notexists-const-true",
),
],
)
def test_queries(
query_string: str,
expected_bindings: Sequence[Mapping["Variable", "Identifier"]],
rdfs_graph: Graph,
) -> None:
"""
Results of queries against the rdfs.ttl return the expected values.
"""
query_tree = parseQuery(query_string)

logging.debug("query_tree = %s", prettify_parsetree(query_tree))
logging.debug("query_tree = %s", query_tree)
query = translateQuery(query_tree)
logging.debug("query = %s", query)
query._original_args = (query_string, {}, None)
result = rdfs_graph.query(query)
logging.debug("result = %s", result)
assert expected_bindings == result.bindings
4 changes: 2 additions & 2 deletions tox.ini
Expand Up @@ -47,5 +47,5 @@ commands =
[pytest]
# log_cli = true
# log_cli_level = DEBUG
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format=%Y-%m-%d %H:%M:%S
log_cli_format = %(asctime)s %(levelname)-8s %(name)-12s %(filename)s:%(lineno)s:%(funcName)s %(message)s
log_cli_date_format=%Y-%m-%dT%H:%M:%S

0 comments on commit f1ab6b9

Please sign in to comment.