Skip to content

Commit 605ca0a

Browse files
committed
GH-5691 CLI for running and storing query explanations
1 parent 665e95c commit 605ca0a

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

core/queryrender/src/main/java/org/eclipse/rdf4j/queryrender/sparql/TupleExprToIrConverter.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,8 @@ private static Normalized normalize(final TupleExpr root, final boolean peelScop
923923
// Keep BIND chains inside WHERE: stop peeling when we hit the first nested Extension, otherwise peel and
924924
// remember bindings for reinsertion later.
925925
if (cur instanceof Extension) {
926-
if (((Extension) cur).getArg() instanceof Extension) {
926+
if (((Extension) cur).getArg() instanceof Extension
927+
&& !extensionChainLeadsToHavingFilter((Extension) cur)) {
927928
break;
928929
}
929930
final Extension ext = (Extension) cur;
@@ -1460,6 +1461,22 @@ private static boolean isAnonHavingName(String name) {
14601461
return name != null && name.startsWith("_anon_having_");
14611462
}
14621463

1464+
private static boolean extensionChainLeadsToHavingFilter(Extension ext) {
1465+
TupleExpr cur = ext;
1466+
while (cur instanceof Extension) {
1467+
cur = ((Extension) cur).getArg();
1468+
}
1469+
if (!(cur instanceof Filter)) {
1470+
return false;
1471+
}
1472+
for (String name : freeVars(((Filter) cur).getCondition())) {
1473+
if (isAnonHavingName(name)) {
1474+
return true;
1475+
}
1476+
}
1477+
return false;
1478+
}
1479+
14631480
// Render expressions for HAVING with substitution of _anon_having_* variables
14641481
private String renderExprForHaving(final ValueExpr e, final Normalized n) {
14651482
return renderExprWithSubstitution(e, n == null ? null : n.selectAssignments);

core/queryrender/src/test/java/org/eclipse/rdf4j/queryrender/TupleExprIRRendererTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@
2727

2828
import org.eclipse.rdf4j.query.MalformedQueryException;
2929
import org.eclipse.rdf4j.query.QueryLanguage;
30+
import org.eclipse.rdf4j.query.algebra.Extension;
31+
import org.eclipse.rdf4j.query.algebra.ExtensionElem;
32+
import org.eclipse.rdf4j.query.algebra.Filter;
33+
import org.eclipse.rdf4j.query.algebra.Projection;
34+
import org.eclipse.rdf4j.query.algebra.QueryRoot;
3035
import org.eclipse.rdf4j.query.algebra.TupleExpr;
3136
import org.eclipse.rdf4j.query.parser.ParsedQuery;
3237
import org.eclipse.rdf4j.query.parser.QueryParserUtil;
3338
import org.eclipse.rdf4j.queryrender.sparql.TupleExprIRRenderer;
3439
import org.junit.jupiter.api.BeforeEach;
3540
import org.junit.jupiter.api.RepeatedTest;
41+
import org.junit.jupiter.api.Test;
3642
import org.junit.jupiter.api.TestInfo;
3743
import org.junit.jupiter.api.parallel.Execution;
3844
import org.junit.jupiter.api.parallel.ExecutionMode;
@@ -687,6 +693,21 @@ void aggregates_min_max_avg_sample_having() {
687693
assertSameSparqlQuery(q, cfg(), false);
688694
}
689695

696+
@Test
697+
void having_detected_when_optimized_extension_wraps_filter() {
698+
String q = "SELECT ?s (COUNT(?o) AS ?c) WHERE {\n" +
699+
" ?s ?p ?o .\n" +
700+
"}\n" +
701+
"GROUP BY ?s\n" +
702+
"HAVING (COUNT(?o) >= 2)";
703+
704+
TupleExpr optimizedShape = liftInnerExtensionAboveHavingFilter(parseAlgebra(SPARQL_PREFIX + q));
705+
String rendered = new TupleExprIRRenderer(cfg()).render(optimizedShape, null).trim();
706+
707+
assertThat(rendered).contains("HAVING");
708+
assertThat(rendered).doesNotContain("_anon_having_");
709+
}
710+
690711
// --- Subquery with aggregate and scope ---
691712

692713
@RepeatedTest(10)
@@ -703,6 +724,31 @@ void subquery_with_aggregate_and_having() {
703724
assertSameSparqlQuery(q, cfg(), false);
704725
}
705726

727+
private TupleExpr liftInnerExtensionAboveHavingFilter(TupleExpr tupleExpr) {
728+
TupleExpr copy = tupleExpr.clone();
729+
TupleExpr cur = copy;
730+
if (cur instanceof QueryRoot) {
731+
cur = ((QueryRoot) cur).getArg();
732+
}
733+
734+
assertThat(cur).isInstanceOf(Projection.class);
735+
Projection projection = (Projection) cur;
736+
assertThat(projection.getArg()).isInstanceOf(Extension.class);
737+
Extension outer = (Extension) projection.getArg();
738+
assertThat(outer.getArg()).isInstanceOf(Filter.class);
739+
Filter filter = (Filter) outer.getArg();
740+
assertThat(filter.getArg()).isInstanceOf(Extension.class);
741+
Extension inner = (Extension) filter.getArg();
742+
743+
filter.setArg(inner.getArg());
744+
Extension lifted = new Extension(filter);
745+
for (ExtensionElem elem : inner.getElements()) {
746+
lifted.addElement(elem.clone());
747+
}
748+
outer.setArg(lifted);
749+
return copy;
750+
}
751+
706752
// --- GRAPH with IRI and variable ---
707753

708754
@RepeatedTest(10)

0 commit comments

Comments
 (0)