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