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"); + } + } + }); + } }
