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

commit a1c0264a0247c7dd4f0338aa69753f408340779c
Author: Andy Seaborne <[email protected]>
AuthorDate: Tue Jun 30 21:37:30 2026 +0100

    GH-4034: Deep substitute/bNode rewrite for triple terms
---
 .../org/apache/jena/sparql/core/Substitute.java    | 27 ++++----
 .../org/apache/jena/sparql/modify/TemplateLib.java | 79 ++++++++++------------
 .../apache/jena/rdf12/AbstratTestSPARQL12Eval.java | 46 +++++++++++++
 3 files changed, 94 insertions(+), 58 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
index e071038102..e2f3a11d99 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
@@ -118,10 +118,9 @@ public class Substitute {
         Node p1 = substitute(p, binding);
         Node o1 = substitute(o, binding);
 
-        Triple t = triple;
-        if ( s1 != s || p1 != p || o1 != o )
-            t = Triple.create(s1, p1, o1);
-        return t;
+        if ( s1 == s && p1 == p && o1 == o )
+            return triple;
+        return Triple.create(s1, p1, o1);
     }
 
     public static TriplePath substitute(TriplePath triplePath, Binding 
binding) {
@@ -136,10 +135,9 @@ public class Substitute {
         Node s1 = substitute(s, binding);
         Node o1 = substitute(o, binding);
 
-        TriplePath tp = triplePath;
-        if ( s1 != s || o1 != o )
-            tp = new TriplePath(s1, triplePath.getPath(), o1);
-        return tp;
+        if ( s1 == s && o1 == o )
+            return triplePath;
+        return new TriplePath(s1, triplePath.getPath(), o1);
     }
 
     public static Quad substitute(Quad quad, Binding binding) {
@@ -158,10 +156,9 @@ public class Substitute {
         Node p1 = substitute(p, binding);
         Node o1 = substitute(o, binding);
 
-        Quad q = quad;
-        if ( s1 != s || p1 != p || o1 != o || g1 != g )
-            q = Quad.create(g1, s1, p1, o1);
-        return q;
+        if ( s1 == s && p1 == p && o1 == o && g1 == g )
+            return quad;
+        return Quad.create(g1, s1, p1, o1);
     }
 
     public static Node substitute(Node n, Binding binding) {
@@ -187,9 +184,10 @@ public class Substitute {
         // No change - return original
         if ( s1 == s && o1 == o && p1 == p )
             return n;
+        // Change happened. Create new.
+        Node n2 =  NodeFactory.createTripleTerm(s1, p1, o1);
+        return n2;
 
-        // Change. Create new.
-        return NodeFactory.createTripleTerm(s1, p1, o1);
     }
 
     /** Substitute for a node that makes up a triple in a Node_Triple. 
Recursively. */
@@ -198,7 +196,6 @@ public class Substitute {
             if ( ! n.isConcrete() )
                 n = substitute(n, binding);
         } else if ( Var.isVar(n) ) {
-            Var var = Var.alloc(n);
             n = Var.lookup(binding::get, n);
         }
         return n;
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java
index 262f39b2e0..828034c600 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java
@@ -124,62 +124,55 @@ public class TemplateLib {
         Node p = quad.getPredicate();
         Node o = quad.getObject();
 
-        Node g1 = g;
-        Node s1 = s;
-        Node p1 = p;
-        Node o1 = o;
-
-        // replace blank nodes.
-        if ( g1.isBlank() || Var.isBlankNodeVar(g1) )
-            g1 = newBlank(g1, bNodeMap);
-
-        if ( s1.isBlank() || Var.isBlankNodeVar(s1) )
-            s1 = newBlank(s1, bNodeMap);
-
-        if ( p1.isBlank() || Var.isBlankNodeVar(p1) )
-            p1 = newBlank(p1, bNodeMap);
-
-        if ( o1.isBlank() || Var.isBlankNodeVar(o1) )
-            o1 = newBlank(o1, bNodeMap);
-
-        Quad q = quad;
-        if ( s1 != s || p1 != p || o1 != o || g1 != g )
-            q = Quad.create(g1, s1, p1, o1);
-        Quad q2 = Substitute.substitute(q, b);
-        return q2;
+        Node g1 = subst(g, b, bNodeMap, false);
+        Node s1 = subst(s, b, bNodeMap, true);
+        Node p1 = subst(p, b, bNodeMap, false);
+        Node o1 = subst(o, b, bNodeMap, true);
+
+      if ( s1 == s && p1 == p && o1 == o && g1 == g )
+          return quad;
+      return Quad.create(g1, s1, p1, o1);
     }
 
     /** Substitute into a triple, with rewriting of bNodes */
     public static Triple subst(Triple triple, Binding b, Map<Node, Node> 
bNodeMap) {
+        // Transfer to quads.
         Node s = triple.getSubject();
         Node p = triple.getPredicate();
         Node o = triple.getObject();
 
-        Node s1 = s;
-        Node p1 = p;
-        Node o1 = o;
+        Node s1 = subst(s, b, bNodeMap, true);
+        Node p1 = subst(p, b, bNodeMap, false);
+        Node o1 = subst(o, b, bNodeMap, true);
 
-        if ( s1.isBlank() || Var.isBlankNodeVar(s1) )
-            s1 = newBlank(s1, bNodeMap);
-
-        if ( p1.isBlank() || Var.isBlankNodeVar(p1) )
-            p1 = newBlank(p1, bNodeMap);
-
-        if ( o1.isBlank() || Var.isBlankNodeVar(o1) )
-            o1 = newBlank(o1, bNodeMap);
+        if ( s1 == s && p1 == p && o1 == o )
+            return triple;
+        return Triple.create(s1, p1, o1);
+    }
 
-        Triple t = triple;
-        if ( s1 != s || p1 != p || o1 != o )
-            t = Triple.create(s1, p1, o1);
-        Triple t2 = Substitute.substitute(t, b);
-        return t2;
+    // Deep node processing, with blank node rewrite and variable substitution.
+    public static Node subst(Node n, Binding b, Map<Node, Node> bNodeMap, 
boolean allowTripleTerms) {
+        if ( n.isURI() || n.isLiteral() )
+            return n;
+        if ( Var.isVar(n))
+            return Substitute.substitute(Var.alloc(n), b);
+        if ( n.isBlank() || Var.isBlankNodeVar(n) )
+            return newBlank(n, bNodeMap);
+        if ( allowTripleTerms && n.isTripleTerm() ) {
+            Triple t1 = subst(n.getTriple(), b, bNodeMap);
+            return NodeFactory.createTripleTerm(t1);
+        }
+        return n;
     }
 
-    /** generate a blank node consistently */
+    /** Generate a blank node consistently */
     private static Node newBlank(Node n, Map<Node, Node> bNodeMap) {
-        if ( !bNodeMap.containsKey(n) )
-            bNodeMap.put(n, NodeFactory.createBlankNode());
-        return bNodeMap.get(n);
+        Node n2 = bNodeMap.get(n);
+        if ( n2 != null )
+            return n2;
+        Node bNew = NodeFactory.createBlankNode();
+        bNodeMap.put(n, bNew);
+        return bNew;
     }
 
     // ---- Template to query pattern
diff --git 
a/jena-arq/src/test/java/org/apache/jena/rdf12/AbstratTestSPARQL12Eval.java 
b/jena-arq/src/test/java/org/apache/jena/rdf12/AbstratTestSPARQL12Eval.java
index effc88a8cf..0ad75de7e6 100644
--- a/jena-arq/src/test/java/org/apache/jena/rdf12/AbstratTestSPARQL12Eval.java
+++ b/jena-arq/src/test/java/org/apache/jena/rdf12/AbstratTestSPARQL12Eval.java
@@ -22,10 +22,15 @@
 package org.apache.jena.rdf12;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
 
 import org.junit.jupiter.api.Test;
 
 import org.apache.jena.graph.Graph;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFFormat;
+import org.apache.jena.riot.RDFParser;
+import org.apache.jena.riot.RDFWriter;
 import org.apache.jena.sparql.core.DatasetGraph;
 import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.exec.QueryExec;
@@ -33,6 +38,7 @@ import org.apache.jena.sparql.exec.RowSet;
 import org.apache.jena.sparql.exec.RowSetOps;
 import org.apache.jena.sparql.sse.SSE;
 import org.apache.jena.sparql.sse.SSE_ParseException;
+import org.apache.jena.sparql.util.IsoMatcher;
 import org.apache.jena.system.Txn;
 
 /**
@@ -88,6 +94,29 @@ public abstract class AbstratTestSPARQL12Eval {
         test("SELECT * { << ?x ?y ?z >> . }", 1);
     }
 
+
+    @Test public void eval_sparql12_construct1() {
+        String s = """
+                PREFIX : <http://example/>
+                :s :p <<( _:b0 :q :x2 )>> .
+                :s :p <<( _:b1 :q :x1 )>> .
+                """;
+        Graph expected = RDFParser.fromString(s, Lang.TTL).toGraph();
+        testConstruct("PREFIX : <http://example/> CONSTRUCT { :s :p <<( _:b :q 
?x )>> } WHERE { VALUES ?x { :x1 :x2 } }",
+                      expected);
+    }
+
+    @Test public void eval_sparql12_construct2() {
+        String s = """
+                PREFIX : <http://example/>
+                << _:b1 :q :x1 >> .
+                << _:b2 :q :x2 >> .
+                """;
+        Graph expected = RDFParser.fromString(s, Lang.TTL).toGraph();
+        testConstruct("PREFIX : <http://example/> CONSTRUCT { << _:b :q ?x >> 
} WHERE { VALUES ?x { :x1 :x2 } }",
+                      expected);
+    }
+
     private void test(String queryString, int expected) {
         DatasetGraph dsg = dsg();
         Txn.executeRead(dsg, ()->{
@@ -99,6 +128,23 @@ public abstract class AbstratTestSPARQL12Eval {
         });
     }
 
+    private void testConstruct(String queryString, Graph expected) {
+        DatasetGraph dsg = dsg();
+        Txn.executeRead(dsg, ()->{
+            try( QueryExec qExec = 
QueryExec.dataset(dsg).query(queryString).build() ) {
+                Graph actual = qExec.construct();
+                boolean b = IsoMatcher.isomorphic(expected, actual);
+                if ( !b ) {
+                    System.err.println("==== Expected");
+                    
RDFWriter.source(expected).format(RDFFormat.TURTLE_FLAT).output(System.err);
+                    System.err.println("==== Actual");
+                    
RDFWriter.source(actual).format(RDFFormat.TURTLE_FLAT).output(System.err);
+                    System.err.println("====");
+                    fail("CONSTRUCT results are not isomorphic to expecte 
results");
+                }
+            }
+        });
+    }
 
 
 }

Reply via email to