2727
2828import org .eclipse .rdf4j .query .MalformedQueryException ;
2929import 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 ;
3035import org .eclipse .rdf4j .query .algebra .TupleExpr ;
3136import org .eclipse .rdf4j .query .parser .ParsedQuery ;
3237import org .eclipse .rdf4j .query .parser .QueryParserUtil ;
3338import org .eclipse .rdf4j .queryrender .sparql .TupleExprIRRenderer ;
3439import org .junit .jupiter .api .BeforeEach ;
3540import org .junit .jupiter .api .RepeatedTest ;
41+ import org .junit .jupiter .api .Test ;
3642import org .junit .jupiter .api .TestInfo ;
3743import org .junit .jupiter .api .parallel .Execution ;
3844import 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