This is an automated email from the ASF dual-hosted git repository.
afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git
The following commit(s) were added to refs/heads/main by this push:
new 9e6b68a930 GH 1868 zero-length paths must not match nodes absent from
the active graph
9e6b68a930 is described below
commit 9e6b68a930a7a3985c7c927dd38739edc35250d9
Author: Thomas Thelen <[email protected]>
AuthorDate: Tue Jun 30 20:08:06 2026 -0700
GH 1868 zero-length paths must not match nodes absent from the active graph
---
.../java/org/apache/jena/sparql/path/P_Alt.java | 6 ++
.../org/apache/jena/sparql/path/P_FixedLength.java | 3 +
.../org/apache/jena/sparql/path/P_Inverse.java | 6 ++
.../java/org/apache/jena/sparql/path/P_Mod.java | 3 +
.../java/org/apache/jena/sparql/path/P_Seq.java | 5 ++
.../org/apache/jena/sparql/path/P_ZeroOrMore1.java | 3 +
.../org/apache/jena/sparql/path/P_ZeroOrMoreN.java | 3 +
.../org/apache/jena/sparql/path/P_ZeroOrOne.java | 3 +
.../java/org/apache/jena/sparql/path/Path.java | 3 +
.../java/org/apache/jena/sparql/path/PathLib.java | 16 ++++-
.../org/apache/jena/sparql/path/TestPathQuery.java | 74 ++++++++++++++++++++++
11 files changed, 123 insertions(+), 2 deletions(-)
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Alt.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Alt.java
index d942d783dc..cce7160566 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Alt.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Alt.java
@@ -24,10 +24,16 @@ package org.apache.jena.sparql.path;
import org.apache.jena.sparql.util.NodeIsomorphismMap;
public class P_Alt extends P_Path2 {
+ private final boolean hasZeroLength;
+
public P_Alt(Path p1, Path p2) {
super(p1, p2);
+ this.hasZeroLength = p1.hasZeroLengthComponent() ||
p2.hasZeroLengthComponent();
}
+ @Override
+ public boolean hasZeroLengthComponent() { return hasZeroLength; }
+
@Override
public void visit(PathVisitor visitor) {
visitor.visit(this);
diff --git
a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_FixedLength.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_FixedLength.java
index 1985fc54a9..068ceecc05 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_FixedLength.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_FixedLength.java
@@ -42,6 +42,9 @@ public class P_FixedLength extends P_Path1 {
return count;
}
+ @Override
+ public boolean hasZeroLengthComponent() { return count == 0; }
+
@Override
public int hashCode() {
return hashFixedLength ^ (int)count ^ getSubPath().hashCode();
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Inverse.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Inverse.java
index 33acaead04..3e40078a7d 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Inverse.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Inverse.java
@@ -24,10 +24,16 @@ package org.apache.jena.sparql.path;
import org.apache.jena.sparql.util.NodeIsomorphismMap;
public class P_Inverse extends P_Path1 {
+ private final boolean hasZeroLength;
+
public P_Inverse(Path p) {
super(p);
+ this.hasZeroLength = p.hasZeroLengthComponent();
}
+ @Override
+ public boolean hasZeroLengthComponent() { return hasZeroLength; }
+
@Override
public void visit(PathVisitor visitor) {
visitor.visit(this);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Mod.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Mod.java
index 808e256df1..a311fd319d 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Mod.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Mod.java
@@ -62,6 +62,9 @@ public class P_Mod extends P_Path1 {
return false;
}
+ @Override
+ public boolean hasZeroLengthComponent() { return min == 0 || min == UNSET;
}
+
public boolean isFixedLength() {
return max == min && min >= 0;
}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Seq.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Seq.java
index 4ec371a4bc..a67f159c91 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Seq.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_Seq.java
@@ -24,11 +24,16 @@ package org.apache.jena.sparql.path ;
import org.apache.jena.sparql.util.NodeIsomorphismMap ;
public class P_Seq extends P_Path2 {
+ private final boolean hasZeroLength;
public P_Seq(Path p1, Path p2) {
super(p1, p2) ;
+ this.hasZeroLength = p1.hasZeroLengthComponent() &&
p2.hasZeroLengthComponent();
}
+ @Override
+ public boolean hasZeroLengthComponent() { return hasZeroLength; }
+
@Override
public void visit(PathVisitor visitor) {
visitor.visit(this) ;
diff --git
a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMore1.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMore1.java
index dca5742520..6c8bf2f641 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMore1.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMore1.java
@@ -40,6 +40,9 @@ public class P_ZeroOrMore1 extends P_Path1 {
return hashZeroOrMoreN ^ getSubPath().hashCode() ;
}
+ @Override
+ public boolean hasZeroLengthComponent() { return true; }
+
@Override
public void visit(PathVisitor visitor) {
visitor.visit(this) ;
diff --git
a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMoreN.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMoreN.java
index f960d2f07a..f6e5f31365 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMoreN.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrMoreN.java
@@ -40,6 +40,9 @@ public class P_ZeroOrMoreN extends P_Path1 {
return hashZeroOrMoreN ^ getSubPath().hashCode() ;
}
+ @Override
+ public boolean hasZeroLengthComponent() { return true; }
+
@Override
public void visit(PathVisitor visitor) {
visitor.visit(this) ;
diff --git
a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrOne.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrOne.java
index 0bc6779cda..0160f1a028 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrOne.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/P_ZeroOrOne.java
@@ -40,6 +40,9 @@ public class P_ZeroOrOne extends P_Path1 {
return hashZeroOrOne ^ getSubPath().hashCode() ;
}
+ @Override
+ public boolean hasZeroLengthComponent() { return true; }
+
@Override
public void visit(PathVisitor visitor) {
visitor.visit(this) ;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/path/Path.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/Path.java
index b27687cf19..8d511fa91b 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/Path.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/Path.java
@@ -29,4 +29,7 @@ public interface Path
public void visit(PathVisitor visitor) ;
public boolean equalTo(Path path2, NodeIsomorphismMap isoMap) ;
public String toString(Prologue prolog) ;
+
+ // Return true if this path can match in zero steps
+ public default boolean hasZeroLengthComponent() { return false; }
}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/path/PathLib.java
b/jena-arq/src/main/java/org/apache/jena/sparql/path/PathLib.java
index 6523f6b756..fc2fd59195 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/path/PathLib.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/path/PathLib.java
@@ -132,7 +132,7 @@ public class PathLib
// Both constants.
if ( !Var.isVar(s) && !Var.isVar(o) )
- return evalGroundedPath(binding, graph, s, path, o, execCxt);
+ return evalGroundedPath(binding, graph, s, path, o,
path.hasZeroLengthComponent(), execCxt);
// One variable, one constant
if ( Var.isVar(s) ) {
@@ -164,7 +164,7 @@ public class PathLib
// Subject and object are nodes.
private static QueryIterator evalGroundedPath(Binding binding,
Graph graph, Node subject,
Path path, Node object,
- ExecutionContext execCxt) {
+ boolean pathHasZeroLength,
ExecutionContext execCxt) {
Iterator<Node> iter = PathEval.eval(graph, subject, path,
execCxt.getContext()) ;
// Now count the number of matches.
@@ -175,6 +175,12 @@ public class PathLib
count++ ;
}
+ // SPARQL requires zero-length path endpoints to exist in nodes(G);
suppress spurious
+ // zero-step matches when subject is absent from the active graph.
+ if ( count > 0 && subject.sameValueAs(object)
+ && pathHasZeroLength && !isNodeInGraph(graph, subject) )
+ count = 0;
+
return new QueryIterYieldN(count, binding, execCxt) ;
}
@@ -297,4 +303,10 @@ public class PathLib
long x = Iter.count(iter) ;
return (int)x ;
}
+
+ /** Return true if {@code node} appears as a subject or object in {@code
graph}. */
+ private static boolean isNodeInGraph(Graph graph, Node node) {
+ return graph.contains(node, Node.ANY, Node.ANY)
+ || graph.contains(Node.ANY, Node.ANY, node);
+ }
}
diff --git
a/jena-arq/src/test/java/org/apache/jena/sparql/path/TestPathQuery.java
b/jena-arq/src/test/java/org/apache/jena/sparql/path/TestPathQuery.java
index 2e081d33d7..fa3686d392 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/path/TestPathQuery.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/path/TestPathQuery.java
@@ -21,12 +21,16 @@
package org.apache.jena.sparql.path;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.apache.jena.atlas.lib.StrUtils;
import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.GraphMemFactory;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.graph.Triple;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.riot.Lang;
@@ -85,6 +89,76 @@ public class TestPathQuery {
test(graph1, qsPath, qsExpected);
}
+ @Test public void testZeroPath_emptyGraph_01() {
+ // ?v <p>{0} ?v with VALUES ?v {1} on empty graph. Should be 0 rows
returned.
+ String qs = "SELECT * WHERE { VALUES ?v { 1 } ?v
<http://example.com/p>{0} ?v }";
+ assertEquals(0, rowCount(GraphMemFactory.createDefaultGraph(), qs));
+ }
+
+ @Test public void testZeroPath_emptyGraph_02() {
+ // ?v <p>? ?v with VALUES ?v {1} on empty graph. Should be be 0 rows
returned.
+ String qs = "SELECT * WHERE { VALUES ?v { 1 } ?v
<http://example.com/p>? ?v }";
+ assertEquals(0, rowCount(GraphMemFactory.createDefaultGraph(), qs));
+ }
+
+ @Test public void testZeroPath_emptyGraph_03() {
+ // ?v <p>* ?v with VALUES ?v {1} on empty graph. Should be be 0 rows
returned.
+ String qs = "SELECT * WHERE { VALUES ?v { 1 } ?v
<http://example.com/p>* ?v }";
+ assertEquals(0, rowCount(GraphMemFactory.createDefaultGraph(), qs));
+ }
+
+ @Test public void testZeroPath_nodeInGraph_01() {
+ String qs = "PREFIX : <http://example/> SELECT * WHERE { VALUES ?v {
:s } ?v <http://example.com/p>? ?v }";
+ assertEquals(1, rowCount(graph1, qs));
+ }
+
+ @Test public void testZeroPath_nodeInGraph_02() {
+ String qs = "PREFIX : <http://example/> SELECT * WHERE { VALUES ?v {
:s } ?v <http://example.com/p>* ?v }";
+ assertEquals(1, rowCount(graph1, qs));
+ }
+
+ @Test public void testZeroPath_nodeInGraph_03() {
+ String qs = "PREFIX : <http://example/> SELECT * WHERE { VALUES ?v {
:s } ?v <http://example.com/p>{0} ?v }";
+ assertEquals(1, rowCount(graph1, qs));
+ }
+
+ @Test public void testZeroPath_nodeAsObjectOnly_01() {
+ // Node is present only as an object in the graph.
+ // graph1 contains ":s :p 123", so 123 should appear only as an object.
+ String qs = "SELECT * WHERE { VALUES ?v { 123 } ?v
<http://example.com/p>? ?v }";
+ assertEquals(1, rowCount(graph1, qs));
+ }
+
+ @Test public void testZeroPath_emptyGraph_04() {
+ // {0,3} has min=0 so it is a zero-length path.
+ String qs = "SELECT * WHERE { VALUES ?v { 1 } ?v
<http://example.com/p>{0,3} ?v }";
+ assertEquals(0, rowCount(GraphMemFactory.createDefaultGraph(), qs));
+ }
+
+ @Test public void testZeroPath_emptyGraph_05() {
+ // p?|q has a zero-length branch.
+ String qs = "SELECT * WHERE { VALUES ?v { 1 } ?v
(<http://example.com/p>?|<http://example.com/q>) ?v }";
+ assertEquals(0, rowCount(GraphMemFactory.createDefaultGraph(), qs));
+ }
+
+ @Test public void testNonZeroPath_selfLoop_returnsResult() {
+ // A non-zero-length path that reaches s from s via a self-loop
+ // must not be zeroed out by the zero-length fix.
+ Graph g = GraphMemFactory.createDefaultGraph();
+ g.add(Triple.create(
+ NodeFactory.createURI("http://example/s"),
+ NodeFactory.createURI("http://example/p"),
+ NodeFactory.createURI("http://example/s")));
+ String qs = "PREFIX : <http://example/> SELECT * WHERE { VALUES ?v {
:s } ?v :p{1} ?v }";
+ assertEquals(1, rowCount(g, qs));
+ }
+
+ private long rowCount(Graph graph, String queryString) {
+ Query query = QueryFactory.create(queryString);
+ RowSet rs = QueryExec.graph(graph).query(query).select();
+ return RowSetOps.count(rs);
+ }
+
private void test(Graph graph12, String qsPath, String qsExpected) {
RowSetRewindable rs1 = exec(graph1, qsPath);
RowSetRewindable rs2 = exec(graph1, qsExpected);