Skip to content

Commit 6c5a783

Browse files
mgbergajnelson-nistashleysommer
authored
Fix bug preventing nested FILTER statements from working (RDFLib#709) (RDFLib#2822)
* test: Start tests for nested FILTER statements (RDFLib#709) This patch adds a single test, self-authored, recreating an issue I encountered when attempting to use a query with a nested `FILTER NOT EXISTS` statement. This test is known to currently fail, but is expected to pass as written with a corrected implementation. References: * RDFLib#709 Signed-off-by: Alex Nelson <[email protected]> * test: Add nested FILTER statement test (RDFLib#709) @mgberg wrote the graph and query for this test last year. This patch puts his work, with his permission, into the new test. This test is known to currently fail, but is expected to pass as written with a corrected implementation. References: * RDFLib#709 (comment) Co-authored-by: Matt Goldberg <[email protected]> Signed-off-by: Alex Nelson <[email protected]> * sparql algebra: Prevent graph patterns from being translated more than once to enable nested filters to work * Fix lint errors in test_nested_filters.py --------- Signed-off-by: Alex Nelson <[email protected]> Co-authored-by: Alex Nelson <[email protected]> Co-authored-by: Ashley Sommer <[email protected]>
1 parent 4cb7d5e commit 6c5a783

File tree

2 files changed

+356
-0
lines changed

2 files changed

+356
-0
lines changed

rdflib/plugins/sparql/algebra.py

+8
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ def translateGroupGraphPattern(graphPattern: CompValue) -> CompValue:
328328
http://www.w3.org/TR/sparql11-query/#convertGraphPattern
329329
"""
330330

331+
if graphPattern.translated:
332+
# This occurs if it is attempted to translate a group graph pattern twice,
333+
# which occurs with nested (NOT) EXISTS filters. Simply return the already
334+
# translated pattern instead.
335+
return graphPattern
331336
if graphPattern.name == "SubSelect":
332337
# The first output from translate cannot be None for a subselect query
333338
# as it can only be None for certain DESCRIBE queries.
@@ -384,6 +389,9 @@ def translateGroupGraphPattern(graphPattern: CompValue) -> CompValue:
384389
if filters:
385390
G = Filter(expr=filters, p=G)
386391

392+
# Mark this graph pattern as translated
393+
G.translated = True
394+
387395
return G
388396

389397

+348
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
#!/usr/bin/env python3
2+
3+
# Portions of this script contributed by NIST are governed by the
4+
# following license:
5+
#
6+
# This software was developed at the National Institute of Standards
7+
# and Technology by employees of the Federal Government in the course
8+
# of their official duties. Pursuant to title 17 Section 105 of the
9+
# United States Code this software is not subject to copyright
10+
# protection and is in the public domain. NIST assumes no
11+
# responsibility whatsoever for its use by other parties, and makes
12+
# no guarantees, expressed or implied, about its quality,
13+
# reliability, or any other characteristic.
14+
#
15+
# We would appreciate acknowledgement if the software is used.
16+
17+
from __future__ import annotations
18+
19+
import logging
20+
from typing import Set, Tuple
21+
22+
from rdflib import Graph, URIRef
23+
from rdflib.query import ResultRow
24+
25+
26+
def test_nested_filter_outer_binding_propagation() -> None:
27+
expected: Set[URIRef] = {
28+
URIRef("http://example.org/Superclass"),
29+
}
30+
computed: Set[URIRef] = set()
31+
graph_data = """\
32+
@prefix ex: <http://example.org/> .
33+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
34+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
35+
36+
ex:Superclass
37+
a owl:Class ;
38+
.
39+
ex:Subclass1
40+
a owl:Class ;
41+
rdfs:subClassOf ex:Superclass ;
42+
.
43+
ex:Subclass2
44+
a owl:Class ;
45+
rdfs:subClassOf ex:Superclass ;
46+
owl:deprecated true ;
47+
.
48+
"""
49+
query = """\
50+
SELECT ?class
51+
WHERE {
52+
?class a owl:Class .
53+
FILTER EXISTS {
54+
?subclass rdfs:subClassOf ?class .
55+
FILTER NOT EXISTS { ?subclass owl:deprecated true }
56+
}
57+
}
58+
"""
59+
graph = Graph()
60+
graph.parse(data=graph_data)
61+
for result in graph.query(query):
62+
assert isinstance(result, ResultRow)
63+
assert isinstance(result[0], URIRef)
64+
computed.add(result[0])
65+
assert expected == computed
66+
67+
68+
def test_nested_filter_outermost_binding_propagation() -> None:
69+
"""
70+
This test implements a query that requires functionality of nested FILTER NOT EXISTS query components.
71+
72+
It encodes a single ground truth positive query result, a tuple where:
73+
* The first member is a HistoricAction,
74+
* The second member is a wholly redundant HistoricRecord in consideration of latter HistoricRecords that cover all non-HistoricRecord inputs to the Action, and
75+
* The third member is the superseding record.
76+
"""
77+
expected: Set[Tuple[URIRef, URIRef, URIRef]] = {
78+
(
79+
URIRef("http://example.org/kb/action-1-2"),
80+
URIRef("http://example.org/kb/record-123-1"),
81+
URIRef("http://example.org/kb/record-1-2"),
82+
)
83+
}
84+
computed: Set[Tuple[URIRef, URIRef, URIRef]] = set()
85+
86+
historic_ontology_graph_data = """\
87+
@prefix case-investigation: <https://ontology.caseontology.org/case/investigation/> .
88+
@prefix ex: <http://example.org/ontology/> .
89+
@prefix kb: <http://example.org/kb/> .
90+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
91+
@prefix prov: <http://www.w3.org/ns/prov#> .
92+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
93+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
94+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
95+
96+
<http://example.org/ontology>
97+
a owl:Ontology ;
98+
rdfs:comment "This example ontology represents a history-analyzing application, where notes of things' handling are created and accompany the things as they are used in actions. For the sake of demonstration, classes and properties implemented here are simplifications of other ontologies' classes and properties. Otherwise, this ontology is narrowly similar to an application of the CASE and PROV-O ontologies."@en ;
99+
rdfs:seeAlso <https://github.com/casework/CASE-Implementation-PROV-O> ;
100+
.
101+
102+
# Begin ontology (TBox).
103+
104+
ex:HistoricThing
105+
a owl:Class ;
106+
rdfs:subClassOf owl:Thing ;
107+
rdfs:comment "A thing generated by some HistoricAction with an accompanying HistoricRecord, and is the input to other HistoricActions. When a HistoricThing is the input to a HistoricAction, a new HistoricRecord should be emitted by the HistoricAction."@en ;
108+
rdfs:seeAlso prov:Entity ;
109+
.
110+
111+
ex:HistoricRecord
112+
a owl:Class ;
113+
rdfs:subClassOf ex:HistoricThing ;
114+
rdfs:comment
115+
"An example class analagous to PROV-O's Collection and CASE's ProvenanceRecord."@en ,
116+
"Only the latest HistoricRecord for an object should be an input to a HistoricAction."@en
117+
;
118+
rdfs:seeAlso
119+
case-investigation:ProvenanceRecord ,
120+
prov:Collection
121+
;
122+
.
123+
124+
ex:HistoricAction
125+
a owl:Class ;
126+
rdfs:subClassOf owl:Thing ;
127+
rdfs:comment "An example class analagous to PROV-O's Activity and CASE's InvestigativeAction."@en ;
128+
rdfs:seeAlso
129+
case-investigation:InvestigativeAction ,
130+
prov:Activity
131+
;
132+
owl:disjointWith ex:HistoricThing ;
133+
.
134+
135+
ex:hadMember
136+
a owl:ObjectProperty ;
137+
rdfs:domain ex:HistoricRecord ;
138+
rdfs:range ex:HistoricThing ;
139+
rdfs:seeAlso prov:hadMember ;
140+
.
141+
142+
ex:generated
143+
a owl:ObjectProperty ;
144+
rdfs:domain ex:HistoricAction ;
145+
rdfs:range ex:HistoricThing ;
146+
rdfs:seeAlso prov:wasGeneratedBy ;
147+
.
148+
149+
ex:used
150+
a owl:ObjectProperty ;
151+
rdfs:domain ex:HistoricAction ;
152+
rdfs:range ex:HistoricThing ;
153+
rdfs:seeAlso prov:used ;
154+
.
155+
156+
ex:wasDerivedFrom
157+
a owl:ObjectProperty ;
158+
rdfs:domain owl:Thing ;
159+
rdfs:range owl:Thing ;
160+
rdfs:seeAlso prov:wasDerivedFrom ;
161+
.
162+
163+
# Begin knowledge base (ABox).
164+
165+
kb:record-123-1
166+
a ex:HistoricRecord ;
167+
rdfs:comment "This is a first record of having handled thing-1, thing-2, and thing-3."@en ;
168+
ex:hadMember
169+
kb:thing-1 ,
170+
kb:thing-2
171+
;
172+
.
173+
174+
kb:record-1-2
175+
a ex:HistoricRecord ;
176+
rdfs:comment "This is a second record of having handled thing-1."@en ;
177+
ex:hadMember kb:thing-1 ;
178+
ex:wasDerivedFrom kb:record-123-1 ;
179+
.
180+
181+
kb:record-2-2
182+
a ex:HistoricRecord ;
183+
rdfs:comment "This is a second record of having handled thing-2."@en ;
184+
ex:hadMember kb:thing-2 ;
185+
ex:wasDerivedFrom kb:record-123-1 ;
186+
.
187+
188+
kb:record-4-1
189+
a ex:HistoricRecord ;
190+
rdfs:comment "This is a first record of having handled thing-4. thing-4 is independent in history of thing-1 and thing-2."@en ;
191+
ex:hadMember kb:thing-4 ;
192+
.
193+
194+
kb:thing-1
195+
a ex:HistoricThing ;
196+
.
197+
198+
kb:thing-2
199+
a ex:HistoricThing ;
200+
.
201+
202+
kb:thing-3
203+
a ex:HistoricThing ;
204+
.
205+
206+
kb:thing-4
207+
a ex:HistoricThing ;
208+
.
209+
210+
kb:action-123-0
211+
a ex:HistoricAction ;
212+
rdfs:comment "Generate things 1, 2, and 3."@en ;
213+
ex:generated
214+
kb:record-123-1 ,
215+
kb:thing-1 ,
216+
kb:thing-2 ,
217+
kb:thing-3
218+
.
219+
220+
kb:action-4-0
221+
a ex:HistoricAction ;
222+
rdfs:comment "Generate thing 4."@en ;
223+
ex:generated
224+
kb:record-4-1 ,
225+
kb:thing-4
226+
.
227+
228+
kb:action-1-1
229+
a ex:HistoricAction ;
230+
rdfs:comment "Handle thing-1."@en ;
231+
ex:used
232+
kb:record-123-1 ,
233+
kb:thing-1
234+
;
235+
ex:generated kb:record-1-2 ;
236+
.
237+
238+
kb:action-2-1
239+
a ex:HistoricAction ;
240+
rdfs:comment "Handle thing-2."@en ;
241+
ex:used
242+
kb:record-123-1 ,
243+
kb:thing-2
244+
;
245+
ex:generated kb:record-2-2 ;
246+
.
247+
248+
kb:action-1-2
249+
a ex:HistoricAction ;
250+
rdfs:comment "This node SHOULD be found by the query. record-123-1 is wholly redundant with record-1-2 with respect to the collective whole of action inputs."@en ;
251+
ex:used
252+
kb:record-123-1 ,
253+
kb:record-1-2 ,
254+
kb:thing-1
255+
;
256+
.
257+
258+
kb:action-12-2
259+
a ex:HistoricAction ;
260+
rdfs:comment "This node SHOULD NOT be found by the query. record-123-1 is partially, but not wholly, redundant with record-1-2, due to to thing-2 having record-123-1 as its only accompanying historic record."@en ;
261+
ex:used
262+
kb:record-123-1 ,
263+
kb:record-1-2 ,
264+
kb:thing-1 ,
265+
kb:thing-2
266+
;
267+
.
268+
269+
kb:action-123-2
270+
a ex:HistoricAction ;
271+
rdfs:comment "This node SHOULD NOT be found by the query. record-123-1 is partially, but not wholly, redundant with record-1-2 and record-2-2, due to thing-3 having record-123-1 as its only accompanying historic record."@en ;
272+
ex:used
273+
kb:record-123-1 ,
274+
kb:record-1-2 ,
275+
kb:record-2-2 ,
276+
kb:thing-1 ,
277+
kb:thing-2 ,
278+
kb:thing-3
279+
;
280+
.
281+
282+
kb:action-1234-2
283+
a ex:HistoricAction ;
284+
rdfs:comment "This node SHOULD NOT be found by the query. record-123-1 is partially, but not wholly, redundant with record-1-2 and record-2-2, due to thing-3 having record-123-1 as its only accompanying historic record. thing-4 also has no shared history with thing-1, -2, or -3."@en ;
285+
ex:used
286+
kb:record-123-1 ,
287+
kb:record-1-2 ,
288+
kb:record-2-2 ,
289+
kb:record-4-1 ,
290+
kb:thing-1 ,
291+
kb:thing-2 ,
292+
kb:thing-3 ,
293+
kb:thing-4
294+
;
295+
.
296+
"""
297+
298+
# See 'TEST OBJECTIVE' annotation.
299+
query = """\
300+
PREFIX ex: <http://example.org/ontology/>
301+
SELECT ?nAction ?nRedundantRecord ?nSupersedingRecord
302+
WHERE {
303+
?nAction
304+
ex:used
305+
?nThing1 ,
306+
?nRedundantRecord ,
307+
?nSupersedingRecord
308+
;
309+
.
310+
?nRedundantRecord
311+
a ex:HistoricRecord ;
312+
ex:hadMember ?nThing1 ;
313+
.
314+
?nSupersedingRecord
315+
a ex:HistoricRecord ;
316+
ex:wasDerivedFrom+ ?nRedundantRecord ;
317+
ex:hadMember ?nThing1 ;
318+
.
319+
FILTER NOT EXISTS {
320+
?nAction ex:used ?nThing2 .
321+
?nRedundantRecord ex:hadMember ?nThing2 .
322+
FILTER ( ?nThing1 != ?nThing2 )
323+
FILTER NOT EXISTS {
324+
####
325+
#
326+
# TEST OBJECTIVE:
327+
# nThing2 must be passed from the outermost context.
328+
#
329+
####
330+
?nSupersedingRecord ex:hadMember ?nThing2 .
331+
}
332+
}
333+
}
334+
"""
335+
336+
graph = Graph()
337+
graph.parse(data=historic_ontology_graph_data)
338+
logging.debug(len(graph))
339+
340+
for result in graph.query(query):
341+
assert isinstance(result, ResultRow)
342+
assert isinstance(result[0], URIRef)
343+
assert isinstance(result[1], URIRef)
344+
assert isinstance(result[2], URIRef)
345+
346+
computed.add((result[0], result[1], result[2]))
347+
348+
assert expected == computed

0 commit comments

Comments
 (0)