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);

Reply via email to