Skip to content

Commit 65cae4e

Browse files
authored
Merge pull request #1794 from aucampia/iwana-20220329T2225-sparql_exists_as
Fix handling of EXISTS inside BIND
2 parents c2d85b4 + ff49579 commit 65cae4e

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)