Skip to content

Commit ff49579

Browse files
committed
Fix handling of EXISTS inside BIND
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
1 parent 57f993d commit ff49579

File tree

4 files changed

+264
-5
lines changed

4 files changed

+264
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ For strings with many escape sequences the parsing speed seems to be almost 4 ti
1414

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

17+
### Other fixes
18+
19+
- Fixed the handling of `EXISTS` inside `BIND` for SPARQL.
20+
This was raising an exception during evaluation before but is now correctly handled.
21+
1722
### Deprecated Functions
1823

1924
Marked the following functions as deprecated:

rdflib/plugins/sparql/algebra.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ def translateGroupGraphPattern(graphPattern):
306306
elif p.name in ("BGP", "Extend"):
307307
G = Join(p1=G, p2=p)
308308
elif p.name == "Bind":
309-
G = Extend(G, p.expr, p.var)
309+
# translateExists will translate the expression if it is EXISTS, and otherwise return
310+
# the expression as is. This is needed because EXISTS has a graph pattern
311+
# which must be translated to work properly during evaluation.
312+
G = Extend(G, translateExists(p.expr), p.var)
310313

311314
else:
312315
raise Exception(

test/test_sparql/test_sparql.py

Lines changed: 253 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import logging
12
from test.testutils import eq_
2-
from typing import Any, Callable, Type
3+
from typing import Any, Callable, Mapping, Sequence, Type
34

45
import pytest
56
from pytest import MonkeyPatch
@@ -11,11 +12,14 @@
1112
from rdflib.compare import isomorphic
1213
from rdflib.namespace import RDF, RDFS, Namespace
1314
from rdflib.plugins.sparql import prepareQuery, sparql
15+
from rdflib.plugins.sparql.algebra import translateQuery
1416
from rdflib.plugins.sparql.evaluate import evalPart
1517
from rdflib.plugins.sparql.evalutils import _eval
18+
from rdflib.plugins.sparql.parser import parseQuery
19+
from rdflib.plugins.sparql.parserutils import prettify_parsetree
1620
from rdflib.plugins.sparql.sparql import SPARQLError
1721
from rdflib.query import Result
18-
from rdflib.term import Variable
22+
from rdflib.term import Identifier, Variable
1923

2024

2125
def test_graph_prefix():
@@ -474,3 +478,250 @@ def thrower(*args: Any, **kwargs: Any) -> None:
474478
with pytest.raises(exception_type) as excinfo:
475479
result_consumer(result)
476480
assert str(excinfo.value) == "TEST ERROR"
481+
482+
483+
@pytest.mark.parametrize(
484+
["query_string", "expected_bindings"],
485+
[
486+
pytest.param(
487+
"""
488+
SELECT ?label ?deprecated WHERE {
489+
?s rdfs:label "Class"
490+
OPTIONAL {
491+
?s
492+
rdfs:comment
493+
?label
494+
}
495+
OPTIONAL {
496+
?s
497+
owl:deprecated
498+
?deprecated
499+
}
500+
}
501+
""",
502+
[{Variable('label'): Literal("The class of classes.")}],
503+
id="select-optional",
504+
),
505+
pytest.param(
506+
"""
507+
SELECT * WHERE {
508+
BIND( SHA256("abc") as ?bound )
509+
}
510+
""",
511+
[
512+
{
513+
Variable('bound'): Literal(
514+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
515+
)
516+
}
517+
],
518+
id="select-bind-sha256",
519+
),
520+
pytest.param(
521+
"""
522+
SELECT * WHERE {
523+
BIND( (1+2) as ?bound )
524+
}
525+
""",
526+
[{Variable('bound'): Literal(3)}],
527+
id="select-bind-plus",
528+
),
529+
pytest.param(
530+
"""
531+
SELECT * WHERE {
532+
OPTIONAL {
533+
<http://example.com/a>
534+
<http://example.com/b>
535+
<http://example.com/c>
536+
}
537+
}
538+
""",
539+
[{}],
540+
id="select-optional-const",
541+
),
542+
pytest.param(
543+
"""
544+
SELECT * WHERE {
545+
?s rdfs:label "Class" .
546+
FILTER EXISTS {
547+
<http://example.com/a>
548+
<http://example.com/b>
549+
<http://example.com/c>
550+
}
551+
}
552+
""",
553+
[],
554+
id="select-filter-exists-const-false",
555+
),
556+
pytest.param(
557+
"""
558+
SELECT * WHERE {
559+
?s rdfs:label "Class" .
560+
FILTER NOT EXISTS {
561+
<http://example.com/a>
562+
<http://example.com/b>
563+
<http://example.com/c>
564+
}
565+
}
566+
""",
567+
[{Variable("s"): RDFS.Class}],
568+
id="select-filter-notexists-const-false",
569+
),
570+
pytest.param(
571+
"""
572+
SELECT * WHERE {
573+
?s rdfs:label "Class"
574+
FILTER EXISTS {
575+
rdfs:Class rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
576+
}
577+
}
578+
""",
579+
[{Variable("s"): RDFS.Class}],
580+
id="select-filter-exists-const-true",
581+
),
582+
pytest.param(
583+
"""
584+
SELECT * WHERE {
585+
?s rdfs:label "Class"
586+
FILTER NOT EXISTS {
587+
rdfs:Class rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
588+
}
589+
}
590+
""",
591+
[],
592+
id="select-filter-notexists-const-true",
593+
),
594+
pytest.param(
595+
"""
596+
SELECT * WHERE {
597+
?s rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
598+
FILTER EXISTS {
599+
?s rdfs:label "MISSING" .
600+
}
601+
}
602+
""",
603+
[],
604+
id="select-filter-exists-var-false",
605+
),
606+
pytest.param(
607+
"""
608+
SELECT * WHERE {
609+
?s rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#>
610+
FILTER EXISTS {
611+
?s rdfs:label "Class" .
612+
}
613+
}
614+
""",
615+
[{Variable("s"): RDFS.Class}],
616+
id="select-filter-exists-var-true",
617+
),
618+
pytest.param(
619+
"""
620+
SELECT * WHERE {
621+
BIND(
622+
EXISTS {
623+
<http://example.com/a>
624+
<http://example.com/b>
625+
<http://example.com/c>
626+
}
627+
AS ?bound
628+
)
629+
}
630+
""",
631+
[{Variable('bound'): Literal(False)}],
632+
id="select-bind-exists-const-false",
633+
),
634+
pytest.param(
635+
"""
636+
SELECT * WHERE {
637+
BIND(
638+
EXISTS {
639+
rdfs:Class rdfs:label "Class"
640+
}
641+
AS ?bound
642+
)
643+
}
644+
""",
645+
[{Variable('bound'): Literal(True)}],
646+
id="select-bind-exists-const-true",
647+
),
648+
pytest.param(
649+
"""
650+
SELECT * WHERE {
651+
?s rdfs:comment "The class of classes."
652+
BIND(
653+
EXISTS {
654+
?s rdfs:label "Class"
655+
}
656+
AS ?bound
657+
)
658+
}
659+
""",
660+
[{Variable("s"): RDFS.Class, Variable('bound'): Literal(True)}],
661+
id="select-bind-exists-var-true",
662+
),
663+
pytest.param(
664+
"""
665+
SELECT * WHERE {
666+
?s rdfs:comment "The class of classes."
667+
BIND(
668+
EXISTS {
669+
?s rdfs:label "Property"
670+
}
671+
AS ?bound
672+
)
673+
}
674+
""",
675+
[{Variable("s"): RDFS.Class, Variable('bound'): Literal(False)}],
676+
id="select-bind-exists-var-false",
677+
),
678+
pytest.param(
679+
"""
680+
SELECT * WHERE {
681+
BIND(
682+
NOT EXISTS {
683+
<http://example.com/a>
684+
<http://example.com/b>
685+
<http://example.com/c>
686+
}
687+
AS ?bound
688+
)
689+
}
690+
""",
691+
[{Variable('bound'): Literal(True)}],
692+
id="select-bind-notexists-const-false",
693+
),
694+
pytest.param(
695+
"""
696+
SELECT * WHERE {
697+
BIND(
698+
NOT EXISTS {
699+
rdfs:Class rdfs:label "Class"
700+
}
701+
AS ?bound
702+
)
703+
}
704+
""",
705+
[{Variable('bound'): Literal(False)}],
706+
id="select-bind-notexists-const-true",
707+
),
708+
],
709+
)
710+
def test_queries(
711+
query_string: str,
712+
expected_bindings: Sequence[Mapping["Variable", "Identifier"]],
713+
rdfs_graph: Graph,
714+
) -> None:
715+
"""
716+
Results of queries against the rdfs.ttl return the expected values.
717+
"""
718+
query_tree = parseQuery(query_string)
719+
720+
logging.debug("query_tree = %s", prettify_parsetree(query_tree))
721+
logging.debug("query_tree = %s", query_tree)
722+
query = translateQuery(query_tree)
723+
logging.debug("query = %s", query)
724+
query._original_args = (query_string, {}, None)
725+
result = rdfs_graph.query(query)
726+
logging.debug("result = %s", result)
727+
assert expected_bindings == result.bindings

tox.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,5 @@ commands =
4747
[pytest]
4848
# log_cli = true
4949
# log_cli_level = DEBUG
50-
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
51-
log_cli_date_format=%Y-%m-%d %H:%M:%S
50+
log_cli_format = %(asctime)s %(levelname)-8s %(name)-12s %(filename)s:%(lineno)s:%(funcName)s %(message)s
51+
log_cli_date_format=%Y-%m-%dT%H:%M:%S

0 commit comments

Comments
 (0)