MemoryStoreUndefBindingQueryTest.java
/**
* ******************************************************************************
* Copyright (c) 2025 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
* ******************************************************************************
*/
package org.eclipse.rdf4j.sail.memory;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.eclipse.rdf4j.model.impl.SimpleNamespace;
import org.eclipse.rdf4j.model.util.Values;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.OWL;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Reproduces the scenario described in the issue: a GRAPH clause containing an UNDEF binding should not cause a
* NullPointerException during evaluation when joined with another pattern. The query should evaluate without error and
* produce one solution binding for mappingProp.
*/
public class MemoryStoreUndefBindingQueryTest {
private SailRepository repository;
@BeforeEach
public void setUp() {
repository = new SailRepository(new MemoryStore());
repository.init();
}
@AfterEach
public void tearDown() {
if (repository != null) {
repository.shutDown();
}
}
@Test
public void testGraphBindUndefDoesNotThrowAndBindsMappingProp() {
try (SailRepositoryConnection conn = repository.getConnection()) {
SimpleNamespace NS1 = new SimpleNamespace("ex1", "http://example.org/");
SimpleNamespace NS2 = new SimpleNamespace("ex2", "http://example.org/2/");
// Add a statement so that the named graph exists
conn.add(Values.iri(NS1, "A1"), OWL.EQUIVALENTCLASS, Values.iri(NS2, "A2"),
Values.iri("http://example.org/"));
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * {\n"
+ " ?mappingProp rdfs:subPropertyOf* owl:equivalentClass .\n"
+ " GRAPH <http://example.org/> {\n"
+ " BIND(?unbound as ?myVar)\n"
+ " # Also reproduces with: VALUES(?myVar) { (UNDEF) }\n"
+ " }\n"
+ "}";
var query = conn.prepareTupleQuery(sparql);
int count = 0;
try (var res = query.evaluate()) {
while (res.hasNext()) {
BindingSet bs = res.next();
assertTrue(bs.hasBinding("mappingProp"), "Expected mappingProp binding");
assertEquals(OWL.EQUIVALENTCLASS, bs.getValue("mappingProp"));
count++;
}
}
assertEquals(1, count, "Expected exactly one result row");
}
}
@Test
public void testSubselect() {
try (SailRepositoryConnection conn = repository.getConnection()) {
SimpleNamespace NS1 = new SimpleNamespace("ex1", "http://example.org/");
SimpleNamespace NS2 = new SimpleNamespace("ex2", "http://example.org/2/");
// Add a statement so that the named graph exists
conn.add(Values.iri(NS1, "A1"), FOAF.KNOWS, Values.iri(NS2, "A2"),
Values.iri("http://example.org/"));
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * WHERE { SELECT * WHERE {\n"
+ " ?a ?prop ?b .\n"
+ "}}";
var query = conn.prepareTupleQuery(sparql);
int count = 0;
try (var res = query.evaluate()) {
while (res.hasNext()) {
BindingSet bs = res.next();
assertFalse(bs.isEmpty(), "Expected non-empty binding set");
assertTrue(bs.hasBinding("a"), "Expected binding, was: " + bs);
count++;
}
}
assertEquals(1, count, "Expected exactly one result row");
}
}
@Test
public void testSubSubselect() {
try (SailRepositoryConnection conn = repository.getConnection()) {
SimpleNamespace NS1 = new SimpleNamespace("ex1", "http://example.org/");
SimpleNamespace NS2 = new SimpleNamespace("ex2", "http://example.org/2/");
// Add a statement so that the named graph exists
conn.add(Values.iri(NS1, "A1"), FOAF.KNOWS, Values.iri(NS2, "A2"),
Values.iri("http://example.org/"));
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * WHERE { SELECT * WHERE { SELECT * WHERE {\n"
+ " ?a ?prop ?b .\n"
+ "}}}";
var query = conn.prepareTupleQuery(sparql);
int count = 0;
try (var res = query.evaluate()) {
while (res.hasNext()) {
BindingSet bs = res.next();
assertFalse(bs.isEmpty(), "Expected non-empty binding set");
assertTrue(bs.hasBinding("a"), "Expected binding, was: " + bs);
count++;
}
}
assertEquals(1, count, "Expected exactly one result row");
}
}
@Test
public void testSubSubselect2() {
try (SailRepositoryConnection conn = repository.getConnection()) {
SimpleNamespace NS1 = new SimpleNamespace("ex1", "http://example.org/");
SimpleNamespace NS2 = new SimpleNamespace("ex2", "http://example.org/2/");
// Add a statement so that the named graph exists
conn.add(Values.iri(NS1, "A1"), FOAF.KNOWS, Values.iri(NS2, "A2"),
Values.iri("http://example.org/"));
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * WHERE { BIND(1 as ?one)\n {SELECT * WHERE { BIND(1 as ?one)\n {SELECT * WHERE { SELECT * WHERE {\n"
+ " ?a ?prop ?b .\n"
+ "}}}}}}";
var query = conn.prepareTupleQuery(sparql);
int count = 0;
try (var res = query.evaluate()) {
while (res.hasNext()) {
BindingSet bs = res.next();
assertFalse(bs.isEmpty(), "Expected non-empty binding set");
assertTrue(bs.hasBinding("a"), "Expected binding, was: " + bs);
count++;
}
}
assertEquals(1, count, "Expected exactly one result row");
}
}
// Temporary helper for debugging the failing test: dump optimized plan
@Test
public void debugExplainSubSubselect() {
try (SailRepositoryConnection conn = repository.getConnection()) {
SimpleNamespace NS1 = new SimpleNamespace("ex1", "http://example.org/");
SimpleNamespace NS2 = new SimpleNamespace("ex2", "http://example.org/2/");
conn.add(Values.iri(NS1, "A1"), FOAF.KNOWS, Values.iri(NS2, "A2"),
Values.iri("http://example.org/"));
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * WHERE { SELECT * WHERE { SELECT * WHERE {\n"
+ " ?a ?prop ?b .\n"
+ "}}}";
var query = conn.prepareTupleQuery(sparql);
System.out.println("\n==== Optimized Plan (debug) ====");
System.out.println(query.explain(org.eclipse.rdf4j.query.explanation.Explanation.Level.Optimized));
System.out.println("==== Executed Plan (debug) ====");
System.out.println(query.explain(org.eclipse.rdf4j.query.explanation.Explanation.Level.Executed));
try (var res = query.evaluate()) {
System.out.println("==== Results (debug subsubselect) ====");
while (res.hasNext()) {
var bs = res.next();
System.out.println(bs);
}
}
}
}
@Test
public void debugExplainSubselect() {
try (SailRepositoryConnection conn = repository.getConnection()) {
SimpleNamespace NS1 = new SimpleNamespace("ex1", "http://example.org/");
SimpleNamespace NS2 = new SimpleNamespace("ex2", "http://example.org/2/");
conn.add(Values.iri(NS1, "A1"), FOAF.KNOWS, Values.iri(NS2, "A2"),
Values.iri("http://example.org/"));
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * WHERE { SELECT * WHERE {\n"
+ " ?a ?prop ?b .\n"
+ "}}";
var query = conn.prepareTupleQuery(sparql);
System.out.println("\n==== Optimized Plan (debug subselect) ====");
System.out.println(query.explain(org.eclipse.rdf4j.query.explanation.Explanation.Level.Optimized));
System.out.println("==== Executed Plan (debug subselect) ====");
System.out.println(query.explain(org.eclipse.rdf4j.query.explanation.Explanation.Level.Executed));
try (var res = query.evaluate()) {
while (res.hasNext()) {
res.next();
}
}
}
}
@Test
public void debugAllVariablesUsedInQueryForSubSubselect() throws Exception {
String sparql = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
+ "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n"
+ "SELECT * WHERE { SELECT * WHERE { SELECT * WHERE {\n"
+ " ?a ?prop ?b .\n"
+ "}}}";
var pq = org.eclipse.rdf4j.query.parser.QueryParserUtil
.parseQuery(org.eclipse.rdf4j.query.QueryLanguage.SPARQL, sparql, null);
var te = pq.getTupleExpr();
org.eclipse.rdf4j.query.algebra.QueryRoot root;
if (te instanceof org.eclipse.rdf4j.query.algebra.QueryRoot) {
root = (org.eclipse.rdf4j.query.algebra.QueryRoot) te;
} else {
root = new org.eclipse.rdf4j.query.algebra.QueryRoot(te);
}
String[] all = org.eclipse.rdf4j.query.algebra.evaluation.impl.ArrayBindingBasedQueryEvaluationContext
.findAllVariablesUsedInQuery(root);
System.out.println("==== allVariables (debug subsubselect) ====");
for (String v : all) {
System.out.println(v);
}
System.out.println("==== tuple expr (raw) ====");
System.out.println(te);
}
}