@@ -134,6 +134,199 @@ public void testEmptyUnion() {
134134 }
135135 }
136136
137+ @ Test
138+ public void complexMinus_rhsOptionalBindOfOuterVar_unsharedBindIgnored () throws IOException {
139+ String ttl = "@prefix : <http://ex/> .\n " +
140+ ":a :p 1 ; :q 1, 2 .\n " +
141+ ":b :p 3 ; :q 4 .\n " +
142+ ":c :p 7 ." ;
143+
144+ try (SailRepositoryConnection conn = repository .getConnection ()) {
145+ List <BindingSet > rows = selectWithData (conn , ttl , RDFFormat .TURTLE ,
146+ "SELECT ?x WHERE {\n " +
147+ " ?x :p ?n .\n " +
148+ " MINUS { OPTIONAL { BIND(?n AS ?k) } ?x :q ?k }\n " +
149+ "} ORDER BY ?x" );
150+
151+ // Only :c lacks :q; binding of out-of-scope ?n in the RHS is ignored for scoping,
152+ // and ?k is bound by ?x :q ?k when available. Thus :a and :b are removed.
153+ assertEquals (setOf ("c" ), names (rows , "x" ));
154+ }
155+ }
156+
157+ @ Test
158+ public void complexMinus_rhsUnionSharedAndUnsharedBranches_onlySharedAffects () throws IOException {
159+ String ttl = "@prefix : <http://ex/> .\n " +
160+ ":a :p 10 ; :q 20 .\n " +
161+ ":b :p 20 ; :q 30 .\n " +
162+ ":c :p 30 .\n " +
163+ ":z :q 999 ." ;
164+
165+ try (SailRepositoryConnection conn = repository .getConnection ()) {
166+ List <BindingSet > rows = selectWithData (conn , ttl , RDFFormat .TURTLE ,
167+ "SELECT ?x WHERE {\n " +
168+ " ?x :p ?v .\n " +
169+ " MINUS { { ?x :q ?v } UNION { ?y :q ?w } }\n " +
170+ "} ORDER BY ?x" );
171+
172+ // Unshared UNION branch must not affect MINUS; only { ?x :q ?v } would remove rows
173+ // where q==p, which does not occur in the dataset. All subjects with :p remain.
174+ assertEquals (setOf ("a" , "b" , "c" ), names (rows , "x" ));
175+ }
176+ }
177+
178+ @ Test
179+ public void complexNotExists_overBareOptional_alwaysFalse () {
180+ try (SailRepositoryConnection conn = repository .getConnection ()) {
181+ String query = "SELECT * WHERE { BIND(1 AS ?d) FILTER NOT EXISTS { OPTIONAL { BIND(1 AS ?z) } } }" ;
182+ TupleQuery tq = conn .prepareTupleQuery (QueryLanguage .SPARQL , query );
183+ try (TupleQueryResult r = tq .evaluate ()) {
184+ assertNotNull (r );
185+ assertFalse (r .hasNext ());
186+ }
187+ }
188+ }
189+
190+ @ Test
191+ public void complexOptionalSubselect_noLeftBindings_emptyOptionalYieldsNoRow () {
192+ try (SailRepositoryConnection conn = repository .getConnection ()) {
193+ String query = "SELECT ?flag WHERE { " +
194+ "OPTIONAL { SELECT ?v WHERE { VALUES ?u { } } } " +
195+ "BIND(IF(BOUND(?v), 'Y','N') AS ?flag) }" ;
196+ TupleQuery tq = conn .prepareTupleQuery (QueryLanguage .SPARQL , query );
197+ try (TupleQueryResult r = tq .evaluate ()) {
198+ assertNotNull (r );
199+ assertFalse (r .hasNext ());
200+ }
201+ }
202+ }
203+
204+ @ Test
205+ public void complexMinus_rhsSubselectOrderLimitBindEquality_removesLimitedMatches () throws IOException {
206+ String ttl = "@prefix : <http://ex/> .\n " +
207+ ":e :p 10 ; :q 10 .\n " +
208+ ":f :p 20 ; :q 20 .\n " +
209+ ":g :p 30 ; :q 99 .\n " +
210+ ":h :p 40 ; :q 40 ." ;
211+
212+ try (SailRepositoryConnection conn = repository .getConnection ()) {
213+ List <BindingSet > rows = selectWithData (conn , ttl , RDFFormat .TURTLE ,
214+ "SELECT ?x WHERE {\n " +
215+ " ?x :p ?v .\n " +
216+ " MINUS { { SELECT ?x ?rv WHERE { ?x :q ?rv } ORDER BY ?rv LIMIT 2 } BIND(?rv AS ?v) }\n " +
217+ "} ORDER BY ?x" );
218+
219+ // The two smallest q-values are 10 and 20; only e and f match q==p among those,
220+ // so they are removed. g and h remain.
221+ assertEquals (setOf ("g" , "h" ), names (rows , "x" ));
222+ }
223+ }
224+
225+ @ Test
226+ public void graphIsolation_sameGraphNoRemovalWhenValuesDiffer () throws IOException {
227+ String trig = "@prefix : <http://ex/> .\n " +
228+ "GRAPH :g1 { :a :p 1 . }\n " +
229+ "GRAPH :g2 { :a :q 1 . :a :p 2 . }" ;
230+
231+ try (SailRepositoryConnection conn = repository .getConnection ()) {
232+ List <BindingSet > rows = selectWithData (conn , trig , RDFFormat .TRIG ,
233+ "SELECT ?g ?x ?n WHERE {\n " +
234+ " GRAPH ?g { ?x :p ?n }\n " +
235+ " MINUS { GRAPH ?g { ?x :q ?n } }\n " +
236+ "} ORDER BY ?g ?x ?n" );
237+ assertEquals (setOf ("g1|a|1" , "g2|a|2" ),
238+ rows .stream ()
239+ .map (bs -> name (bs .getValue ("g" )) + "|" + name (bs .getValue ("x" )) + "|"
240+ + name (bs .getValue ("n" )))
241+ .collect (Collectors .toCollection (LinkedHashSet ::new )));
242+ }
243+ }
244+
245+ @ Test
246+ public void graphIsolation_removesOnlyInGraphWithMatch () throws IOException {
247+ String trig = "@prefix : <http://ex/> .\n " +
248+ "GRAPH :g1 { :a :p 1 . }\n " +
249+ "GRAPH :g2 { :a :q 1 . :a :p 2 . :a :q 2 . }" ;
250+
251+ try (SailRepositoryConnection conn = repository .getConnection ()) {
252+ List <BindingSet > rows = selectWithData (conn , trig , RDFFormat .TRIG ,
253+ "SELECT ?g ?x ?n WHERE {\n " +
254+ " GRAPH ?g { ?x :p ?n }\n " +
255+ " MINUS { GRAPH ?g { ?x :q ?n } }\n " +
256+ "} ORDER BY ?g" );
257+ assertEquals (setOf ("g1|a|1" ),
258+ rows .stream ()
259+ .map (bs -> name (bs .getValue ("g" )) + "|" + name (bs .getValue ("x" )) + "|"
260+ + name (bs .getValue ("n" )))
261+ .collect (Collectors .toCollection (LinkedHashSet ::new )));
262+ }
263+ }
264+
265+ @ Test
266+ public void valuesOnRight_sharedVarRemovesOnlyListedSubjects () throws IOException {
267+ String ttl = "@prefix : <http://ex/> .\n " +
268+ ":a :p 1 . :b :p 2 . :c :p 3 ." ;
269+ try (SailRepositoryConnection conn = repository .getConnection ()) {
270+ List <BindingSet > rows = selectWithData (conn , ttl , RDFFormat .TURTLE ,
271+ "SELECT ?x WHERE { ?x :p ?v MINUS { VALUES ?x { :a :c } } } ORDER BY ?x" );
272+ assertEquals (setOf ("b" ), names (rows , "x" ));
273+ }
274+ }
275+
276+ @ Test
277+ public void nestedNotExistsOverOptionalWithUnion_isAlwaysFalse () {
278+ try (SailRepositoryConnection conn = repository .getConnection ()) {
279+ String q = "SELECT * WHERE { BIND(1 AS ?d) FILTER NOT EXISTS { OPTIONAL { { BIND(1 AS ?z) } UNION { BIND(2 AS ?z) FILTER(?z>1) } } } }" ;
280+ TupleQuery tq = conn .prepareTupleQuery (QueryLanguage .SPARQL , q );
281+ try (TupleQueryResult r = tq .evaluate ()) {
282+ assertNotNull (r );
283+ assertFalse (r .hasNext ());
284+ }
285+ }
286+ }
287+
288+ @ Test
289+ public void optionalSubselectWithLeftBindings_keepsLeftRows () throws IOException {
290+ String ttl = "@prefix : <http://ex/> .\n " +
291+ ":a :p 1 ." ;
292+ try (SailRepositoryConnection conn = repository .getConnection ()) {
293+ List <BindingSet > rows = selectWithData (conn , ttl , RDFFormat .TURTLE ,
294+ "SELECT ?x WHERE { ?x :p ?v OPTIONAL { SELECT ?z WHERE { FILTER(false) } } } ORDER BY ?x" );
295+ assertEquals (setOf ("a" ), names (rows , "x" ));
296+ }
297+ }
298+
299+ @ Test
300+ public void minusRhsUnionSubselects_withGraphs_onlySameGraphBranchRemoves () throws IOException {
301+ String trig = "@prefix : <http://ex/> .\n " +
302+ "GRAPH :g1 { :a :p 1 . :a :q 1 . }\n " +
303+ "GRAPH :g2 { :b :p 2 . :c :q 2 . }" ;
304+ try (SailRepositoryConnection conn = repository .getConnection ()) {
305+ List <BindingSet > rows = selectWithData (conn , trig , RDFFormat .TRIG ,
306+ "SELECT ?g ?x ?n WHERE {\n " +
307+ " GRAPH ?g { ?x :p ?n }\n " +
308+ " MINUS { { GRAPH ?g { ?x :q ?n } } UNION { GRAPH :g2 { ?x :q ?n } } }\n " +
309+ "} ORDER BY ?g ?x ?n" );
310+ assertEquals (setOf ("g2|b|2" ),
311+ rows .stream ()
312+ .map (bs -> name (bs .getValue ("g" )) + "|" + name (bs .getValue ("x" )) + "|"
313+ + name (bs .getValue ("n" )))
314+ .collect (Collectors .toCollection (LinkedHashSet ::new )));
315+ }
316+ }
317+
318+ @ Test
319+ public void bnodeOnRhsViaUnionAndSubselect_cannotMatchDataIds () throws IOException {
320+ String ttl = "@prefix : <http://ex/> .\n " +
321+ "_:b1 a [] . _:b2 a [] .\n " +
322+ ":k :id _:b1 . :l :id _:b2 ." ;
323+ try (SailRepositoryConnection conn = repository .getConnection ()) {
324+ List <BindingSet > rows = selectWithData (conn , ttl , RDFFormat .TURTLE ,
325+ "SELECT ?s WHERE { ?s :id ?id MINUS { { BIND(BNODE() AS ?id) } UNION { SELECT ?id WHERE { BIND(BNODE() AS ?id) } } } } ORDER BY ?s" );
326+ assertEquals (setOf ("k" , "l" ), names (rows , "s" ));
327+ }
328+ }
329+
137330 private List <BindingSet > selectWithData (RepositoryConnection conn , String data , RDFFormat format , String body )
138331 throws IOException {
139332 String sparql = PREFIX + body ;
0 commit comments