This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 6a86b5d1869c575142391e3e8bcc8f00f1a75132
Author: Andy Seaborne <[email protected]>
AuthorDate: Mon Mar 3 08:18:29 2025 +0000

    GH-2799: Query transform (variables and modified project)
---
 .../syntaxtransform/ElementTransformSubst.java     |  60 +++++++++++-
 .../syntax/syntaxtransform/QueryTransformOps.java  |  59 ++++++-----
 .../org/apache/jena/sparql/syntax/TS_Syntax.java   |   6 --
 .../syntaxtransform/TestQuerySyntaxSubstitute.java | 109 ++++++++++++++++-----
 4 files changed, 172 insertions(+), 62 deletions(-)

diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/ElementTransformSubst.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/ElementTransformSubst.java
index 8e98b50509..f94a0a301e 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/ElementTransformSubst.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/ElementTransformSubst.java
@@ -18,31 +18,40 @@
 
 package org.apache.jena.sparql.syntax.syntaxtransform;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Node_Variable;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.query.Query;
 import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.core.TriplePath;
 import org.apache.jena.sparql.core.Var;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.engine.binding.BindingBuilder;
 import org.apache.jena.sparql.graph.NodeTransform;
-import org.apache.jena.sparql.syntax.Element;
-import org.apache.jena.sparql.syntax.ElementPathBlock;
-import org.apache.jena.sparql.syntax.ElementTriplesBlock;
+import org.apache.jena.sparql.syntax.*;
 
 /**
  * An {@link ElementTransform} which replaces occurrences of a variable with a 
Node value.
  * Because a {@link Var} is a subclass of {@link Node_Variable} which is a 
{@link Node},
  * this includes variable renaming.
  * <p>
- * This is a transformation on the syntax - all occurrences of a variable are 
replaced, even if
- * inside sub-select's and not project (which means it is effectively a 
different variable).
+ * This is a transformation on the syntax - all occurrences of a variable are 
replaced, even
+ * inside sub-select's regardless of being in a projection
+ * (which means it is effectively a different variable).
+ * <p>
+ * This class does no validity checking.
+ * See {@link QuerySyntaxSubstituteScope} for checks.
  */
 public class ElementTransformSubst extends ElementTransformCopyBase {
     private final NodeTransform nodeTransform;
+    private final Map<Var, ? extends Node> mapping;
 
     public ElementTransformSubst(Map<Var, ? extends Node> mapping) {
+        this.mapping = mapping;
         this.nodeTransform = new NodeTransformSubst(mapping);
     }
 
@@ -122,7 +131,48 @@ public class ElementTransformSubst extends 
ElementTransformCopyBase {
         return Quad.create(g1, s1, p1, o1);
     }
 
+    @Override
+    public ElementSubQuery transform(ElementSubQuery subQuery, Query newQuery) 
{
+        return subQuery;
+    }
+
+
+    // VALUES : Only var->var is supported.
+    // var -> const should have been spotted by 
QuerySyntaxSubstituteScope.scopeCheck
+
+    @Override
+    public ElementData transform(ElementData data) {
+        // Check for var-var. If none, no work to do.
+        List<Var> vars = data.getVars();
+        boolean workToDo = vars.stream().anyMatch(v->mapping.containsKey(v));
+        if ( ! workToDo )
+            return data;
+
+        List<Var> vars2 = vars.stream().map(v->transformVar(v)).toList();
+
+        List<Binding> rows = data.getRows();
+        List<Binding> rows2 = new ArrayList<>();
+
+        BindingBuilder bb = BindingBuilder.create();
+        rows.forEach(binding -> {
+            bb.reset();
+            binding.forEach((v,n)->{
+                Var v2 = transformVar(v);
+                bb.add(v2, n);
+            });
+            rows2.add(bb.build());
+        });
+        return new ElementData(vars2, rows2);
+    }
+
     private Node transform(Node n) {
         return nodeTransform.apply(n);
     }
+
+    private Var transformVar(Var var) {
+        Node n = nodeTransform.apply(var);
+        if ( n instanceof Var v)
+            return v;
+        return var;
+    }
 }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/QueryTransformOps.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/QueryTransformOps.java
index e6823219f8..3067f3a938 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/QueryTransformOps.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/syntax/syntaxtransform/QueryTransformOps.java
@@ -18,8 +18,6 @@
 
 package org.apache.jena.sparql.syntax.syntaxtransform;
 
-import static 
org.apache.jena.sparql.syntax.syntaxtransform.QuerySyntaxSubstituteScope.scopeCheck;
-
 import java.util.*;
 
 import org.apache.jena.graph.Node;
@@ -46,43 +44,48 @@ public class QueryTransformOps {
     /**
      * Replace variables in a query by RDF terms.
      * The replacements are added to the return queries SELECT clause (if a 
SELECT query).
-     * <p>
+     *
      * @throws QueryScopeException if the query contains variables used in a
      *   way that does not allow substitution (.e.g {@code AS ?var} or used in
      *   {@code VALUES}).
+     *
+     *  @see #replaceVars(Query, Map) to replace variables without adding the 
replacements to the SELECT clause.
      */
-    public static Query syntaxSubstitute(Query input, Map<Var, Node> 
substitutions) {
-        scopeCheck(input, substitutions.keySet());
-        Query output = transformTopLevel(input, substitutions);
-        return output;
-    }
-
-    // Call transform, add in the substitutions as top-level SELECT 
expressions/
-    private static Query transformTopLevel(Query query, Map<Var, Node> 
substitutions) {
-        Query query2 = transformSubstitute(query, substitutions);
+    public static Query syntaxSubstitute(Query input, Map<Var, ? extends Node> 
substitutions) {
+        Query query2 = transformSubstitute(input, substitutions);
         // Include substitutions
-        if ( query.isSelectType() ) {
+        if ( input.isSelectType() ) {
             query2.setQueryResultStar(false);
+            List<Var> projectVars = query2.getProjectVars();
             substitutions.forEach((v, n) -> {
-                var nv = NodeValue.makeNode(n);
-                query2.getProject().update(v, NodeValue.makeNode(n));
+                if ( ! projectVars.contains(v) ) {
+                    var nv = NodeValue.makeNode(n);
+                    query2.getProject().update(v, NodeValue.makeNode(n));
+                }
             });
         }
         return query2;
     }
 
-    /** @deprecated Use {@link #queryReplaceVars} */
+    /** @deprecated Use {@link #replaceVars} */
     @Deprecated
     public static Query transform(Query query, Map<Var, ? extends Node> 
substitutions) {
         return replaceVars(query, substitutions);
     }
 
-    /** Transform a query based on a mapping from {@link Var} variable to 
replacement {@link Node}. */
+    /**
+     * Transform a query based on a mapping from {@link Var} variable to 
replacement {@link Node}.
+     * The replacement can be a constant or another variable.
+     * This operation does not record the substitution made.
+     *
+     * See {@link #syntaxSubstitute(Query,Map)}
+     * @throws QueryScopeException if the query contains variables used in a
+     *   way that does not allow constant substitution.
+     */
     public static Query replaceVars(Query query, Map<Var, ? extends Node> 
substitutions) {
         return transformSubstitute(query, substitutions);
     }
 
-
     /** @deprecated Use {@link #queryReplaceVars} */
     @Deprecated
     public static Query transformQuery(Query query, Map<String, ? extends 
RDFNode> substitutions)    {
@@ -100,7 +103,15 @@ public class QueryTransformOps {
     }
 
     private static Query transformSubstitute(Query query, Map<Var, ? extends 
Node> substitutions) {
-        scopeCheck(query, substitutions.keySet());
+        // Those variables that are mapped to constants, not variables.
+        // Replacing a variable by another variable is always possible - no 
scoping issues.
+        Set<Var> varsForConst = new HashSet<>();
+        substitutions.forEach((var,node) -> {
+          if (! ( node instanceof Var ) )
+              varsForConst.add(var);
+        });
+        QuerySyntaxSubstituteScope.scopeCheck(query, varsForConst);
+
         ElementTransform eltrans = new ElementTransformSubst(substitutions);
         NodeTransform nodeTransform = new NodeTransformSubst(substitutions);
         ExprTransform exprTrans = new ExprTransformNodeElement(nodeTransform, 
eltrans);
@@ -109,6 +120,11 @@ public class QueryTransformOps {
 
     // ----------------
 
+    public static Query transform(Query query, ElementTransform transform) {
+        ExprTransform noop = new ExprTransformApplyElementTransform(transform);
+        return transform(query, transform, noop);
+    }
+
     /**
      * Transform a query using {@link ElementTransform} and {@link 
ExprTransform}.
      * It is the responsibility of these transforms to transform to a legal 
SPARQL query.
@@ -199,11 +215,6 @@ public class QueryTransformOps {
         }
     }
 
-    public static Query transform(Query query, ElementTransform transform) {
-        ExprTransform noop = new ExprTransformApplyElementTransform(transform);
-        return transform(query, transform, noop);
-    }
-
     // Transform CONSTRUCT query template
     private static void mutateConstruct(Query query, Query query2, 
ElementTransform transform) {
         if ( query.isConstructQuad() ) {
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TS_Syntax.java 
b/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TS_Syntax.java
index 4394c0e035..a67c6562be 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TS_Syntax.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TS_Syntax.java
@@ -25,12 +25,6 @@ import org.apache.jena.sparql.syntax.syntaxtransform.*;
 
 @Suite
 @SelectClasses({
-
-//import org.junit.runner.RunWith;
-//import org.junit.runners.Suite;
-//import org.junit.runners.Suite.SuiteClasses;
-//@RunWith(Suite.class)
-//@SuiteClasses({
     TestQueryParser.class
     , TestSerialization.class
     , TestQueryShallowCopy.class
diff --git 
a/jena-arq/src/test/java/org/apache/jena/sparql/syntax/syntaxtransform/TestQuerySyntaxSubstitute.java
 
b/jena-arq/src/test/java/org/apache/jena/sparql/syntax/syntaxtransform/TestQuerySyntaxSubstitute.java
index 16f7c473d7..8b6e54a721 100644
--- 
a/jena-arq/src/test/java/org/apache/jena/sparql/syntax/syntaxtransform/TestQuerySyntaxSubstitute.java
+++ 
b/jena-arq/src/test/java/org/apache/jena/sparql/syntax/syntaxtransform/TestQuerySyntaxSubstitute.java
@@ -21,6 +21,7 @@ package org.apache.jena.sparql.syntax.syntaxtransform;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
@@ -34,8 +35,17 @@ import org.apache.jena.sparql.core.Var;
 public class TestQuerySyntaxSubstitute {
 
     private static Map<Var, Node> substitutions1 = Map.of(Var.alloc("x"), 
NodeFactory.createURI("http://example/xxx";));
-    private static Map<Var, Node> substitutions2 = Map.of(Var.alloc("x"), 
NodeFactory.createURI("http://example/xxx";),
-                                                          Var.alloc("y"), 
NodeFactory.createURI("http://example/yyy";));
+    private static Map<Var, Node> substitutions2 = 
orderedMapOf(Var.alloc("x"), NodeFactory.createURI("http://example/xxx";),
+                                                                
Var.alloc("y"), NodeFactory.createURI("http://example/yyy";));
+    private static Map<Var, Var> varRenames = Map.of(Var.alloc("x"), 
Var.alloc("x1"),
+                                                     Var.alloc("y"), 
Var.alloc("y1"));
+
+    private static <K, V> Map<K, V> orderedMapOf(K k1, V v1, K k2, V v2) {
+        Map<K, V> map = new LinkedHashMap<>();
+        map.put(k1,v1);
+        map.put(k2, v2);
+        return map;
+    }
 
     @Test public void syntaxSubstitute_01() {
         testSubstitute("SELECT * { ?x :p ?z }", substitutions1,
@@ -74,46 +84,91 @@ public class TestQuerySyntaxSubstitute {
     }
 
     // GH-2799: Sub-queries not yet ready.
-//    // Sub-query visible variable.
-//    @Test public void syntaxSubstitute_12() {
-//        testSubstitute("SELECT * { ?s ?p ?o { SELECT ?x { ?x :p ?y } } }", 
substitutions1,
-//                      "SELECT (:yyy AS ?y) ?p (:xxx AS ?x) { ?s ?p ?o { 
SELECT * { :xxx :p ?y } }}"
-//                      );
-//    }
-//
-//    // Sub-query hidden variable.
-//    @Test public void syntaxSubstitute_13() {
-//        testSubstitute("SELECT * { ?s ?p ?o { SELECT ?y { ?x :p ?y } } }", 
substitutions1,
-//                      "SELECT ?s ?p ?o (:xxx AS ?x) { ?s ?p ?o { SELECT * { 
:xxx :p ?y } }}"
-//                      );
-//    }
-//
-//    // Multi-level variable.
-//    @Test public void syntaxSubstitute_14() {
-//        testSubstitute("SELECT * { ?x ?p ?o { SELECT * { ?x :p ?y } } }", 
substitutions2,
-//                      "" //"SELECT (:yyy AS ?y) ?p (:xxx AS ?x) { ?s ?p ?o { 
SELECT * { :xxx :p ?y } }}"
-//                      );
-//    }
-
-    @Test public void syntaxSubstitute_50() {
+    // Sub-query with a visible variable and a hidden variable
+    @Test public void syntaxSubstitute_12() {
+        testSubstitute("SELECT * { ?s ?p ?o { SELECT ?x { ?x :p ?y } } }", 
substitutions2,
+                       "SELECT ?s ?p ?o ?x (:yyy AS ?y) { ?s ?p ?o { SELECT 
(:xxx AS ?x) { :xxx :p :yyy } }}"
+                );
+    }
+
+    @Test public void syntaxSubstitute_13() {
+        testSubstitute("SELECT * { ?s ?p ?o { SELECT * { ?s ?p ?o . ?x :p ?y } 
} }", substitutions2,
+                       "SELECT ?s ?p ?o (:xxx AS ?x) (:yyy AS ?y) { ?s ?p ?o { 
SELECT * { ?s ?p ?o . :xxx :p :yyy } }}"
+                );
+    }
+
+    // Multi-level variable.
+    @Test public void syntaxSubstitute_14() {
+        testSubstitute("SELECT * { ?x ?p ?o { SELECT * { ?x :p ?z } } }", 
substitutions2,
+                       "SELECT ?p ?o ?z (:xxx AS ?x) (:yyy AS ?y) { :xxx ?p ?o 
{ SELECT * { :xxx :p ?z } }}"
+                      );
+    }
+
+    // ==== Variable-variable renaming.
+    // This is always possible so no scoping checks are done.
+
+    @Test public void syntaxSubstituteVarToVar_01() {
+        testSubstituteVars("SELECT ?x { ?x :p ?z }", varRenames, "SELECT ?x1 { 
?x1 :p ?z }");
+    }
+
+    @Test public void syntaxSubstituteVarToVar_02() {
+        testSubstituteVars("SELECT ?x { ?x :p ?y }", varRenames, "SELECT ?x1 { 
?x1 :p ?y1 }");
+    }
+
+    @Test public void syntaxSubstituteVarToVar_03() {
+        testSubstituteVars("SELECT ?z ?y { ?a ?b ?z { SELECT ?y { :s :p ?y } 
GROUP BY ?y} }", varRenames,
+                           "SELECT ?z ?y1 { ?a ?b ?z { SELECT ?y1 { :s :p ?y1 
} GROUP BY ?y1} }" );
+    }
+
+    // Var to var where var to constant is not allowed.
+    @Test public void syntaxSubstituteVarToVar_10() {
+        testSubstituteVars("SELECT ?x { ?x ?c ?d FILTER( ?x != ?b ) }", 
varRenames,
+                           "SELECT ?x1 { ?x1 ?c ?d FILTER( ?x1 != ?b ) }");
+    }
+
+    @Test public void syntaxSubstituteVarToVar_11() {
+        // Still hitting the scope check.
+        testSubstituteVars("SELECT * { BIND (:q AS ?x) BIND (?x + 99 AS ?A) 
}", varRenames,
+                           "SELECT * { BIND (:q AS ?x1) BIND (?x1 + 99 AS ?A) 
}");
+    }
+
+    @Test public void syntaxSubstituteVarToVar_12() {
+        // Still hitting the scope check.
+        testSubstituteVars("SELECT * { VALUES ?x { :q } }", varRenames,
+                           "SELECT * { VALUES ?x1 { :q } }");
+    }
+
+    // ==== Scope failures.
+
+    @Test public void syntaxSubstituteScopeEx_01() {
         assertThrows(QueryScopeException.class, ()->
             testSubstitute("SELECT (456 AS ?x) { ?y :p ?z }",  substitutions1,
                           ""
                           ));
     }
 
-    @Test public void syntaxSubstitute_51() {
+    @Test public void syntaxSubstituteScopeEx_02() {
         assertThrows(QueryScopeException.class, ()->
             testSubstitute("SELECT * { ?y :p ?z BIND(789 AS ?x)}", 
substitutions1,
                           ""
                           ));
     }
 
-    private void testSubstitute(String qs, Map<Var, Node> substitutions, 
String outcome) {
+    private void testSubstitute(String qs, Map<Var, ? extends Node> 
substitutions, String outcome) {
+        String prologue = "PREFIX : <http://example/> ";
+        String queryString = prologue+qs;
+        Query query = QueryFactory.create(queryString);
+        Query query2 = QueryTransformOps.syntaxSubstitute(query, 
substitutions);    // syntaxSubstitute, including modifying the SELECT clause
+        String queryOutcomeString = prologue+outcome;
+        Query queryOutcome = QueryFactory.create(queryOutcomeString);
+        assertEquals(queryOutcome,  query2);
+    }
+
+    private void testSubstituteVars(String qs, Map<Var, ? extends Node> 
substitutions, String outcome) {
         String prologue = "PREFIX : <http://example/> ";
         String queryString = prologue+qs;
         Query query = QueryFactory.create(queryString);
-        Query query2 = QueryTransformOps.syntaxSubstitute(query, 
substitutions);
+        Query query2 = QueryTransformOps.replaceVars(query, substitutions);    
 // replaceVars
         Query queryOutcome = QueryFactory.create(prologue+outcome);
         assertEquals(queryOutcome,  query2);
     }

Reply via email to