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 9, 2022
1 parent 1cba9d8 commit cb71508
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 3 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
4 changes: 4 additions & 0 deletions rdflib/plugins/sparql/algebra.py
Expand Up @@ -67,6 +67,10 @@ def Filter(expr, p):


def Extend(p, expr, var):
# this 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.
expr = translateExists(expr)
return CompValue("Extend", p=p, expr=expr, var=var)


Expand Down
254 changes: 253 additions & 1 deletion test/test_sparql/test_sparql.py
@@ -1,8 +1,14 @@
import logging
from typing import Mapping, Sequence
import pytest
from rdflib.plugins.sparql import sparql, prepareQuery
from rdflib import Graph, URIRef, Literal, BNode, ConjunctiveGraph
from rdflib.namespace import Namespace, RDF, RDFS
from rdflib.compare import isomorphic
from rdflib.term import Variable
from rdflib.plugins.sparql.algebra import translateQuery
from rdflib.plugins.sparql.parser import parseQuery
from rdflib.plugins.sparql.parserutils import prettify_parsetree
from rdflib.term import Variable, Identifier

from test.testutils import eq_

Expand Down Expand Up @@ -290,3 +296,249 @@ def test_property_bindings(rdfs_graph: Graph) -> None:

result.bindings = []
assert [] == result.bindings

@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 cb71508

Please sign in to comment.