Add support for Set-Returning Functions (SRF) to HQL parser and query rendering.

Signed-off-by: oscarfanchin <oscar.fanchin@gmail.com>

Closes: #3864
Original pull request: #3879
This commit is contained in:
oscarfanchin
2025-05-02 15:38:12 +02:00
committed by Mark Paluch
parent 85fcbcd2fb
commit 2ebe4caba6
8 changed files with 395 additions and 4 deletions

View File

@@ -114,8 +114,13 @@ joinSpecifier
fromRoot
: entityName variable?
| LATERAL? '(' subquery ')' variable?
| functionCallAsFromSource variable?
;
functionCallAsFromSource
: identifier '(' (expression (',' expression)*)? ')'
;
join
: joinType JOIN FETCH? joinTarget joinRestriction? // Spec BNF says joinType isn't optional, but text says that it is.
;
@@ -123,6 +128,11 @@ join
joinTarget
: path variable? # JoinPath
| LATERAL? '(' subquery ')' variable? # JoinSubquery
| functionCallAsJoinTarget variable? # JoinFunctionCall
;
functionCallAsJoinTarget
: identifier '(' (expression (',' expression)*)? ')'
;
// https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-update
@@ -1878,4 +1888,4 @@ ESCAPE_SEQUENCE
QUOTED_IDENTIFIER
: BACKTICK ( ESCAPE_SEQUENCE | '\\' BACKTICK | ~([`]) )* BACKTICK
;
;

View File

@@ -23,19 +23,29 @@ import org.jspecify.annotations.Nullable;
* Hibernate-specific query details capturing common table expression details.
*
* @author Mark Paluch
* @author oscar.fanchin
* @since 3.5
*/
class HibernateQueryInformation extends QueryInformation {
private final boolean hasCte;
private final boolean hasFromFunction;
public HibernateQueryInformation(@Nullable String alias, List<QueryToken> projection,
boolean hasConstructorExpression, boolean hasCte) {
boolean hasConstructorExpression, boolean hasCte,boolean hasFromFunction) {
super(alias, projection, hasConstructorExpression);
this.hasCte = hasCte;
this.hasFromFunction = hasFromFunction;
}
public boolean hasCte() {
return hasCte;
}
public boolean hasFromFunction() {
return hasFromFunction;
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.data.jpa.repository.query.QueryTransformers.CountSele
* @author Greg Turnquist
* @author Christoph Strobl
* @author Mark Paluch
* @author oscar.fanchin
* @since 3.1
*/
@SuppressWarnings("ConstantValue")
@@ -38,11 +39,13 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
private final @Nullable String countProjection;
private final @Nullable String primaryFromAlias;
private final boolean containsCTE;
private final boolean containsFromFunction;
HqlCountQueryTransformer(@Nullable String countProjection, HibernateQueryInformation queryInformation) {
this.countProjection = countProjection;
this.primaryFromAlias = queryInformation.getAlias();
this.containsCTE = queryInformation.hasCte();
this.containsFromFunction = queryInformation.hasFromFunction();
}
@Override
@@ -156,11 +159,19 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
builder.appendExpression(nested);
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
}
} else if (ctx.functionCallAsFromSource() != null) {
builder.appendExpression(visit(ctx.functionCallAsFromSource()));
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
}
}
return builder;
}
@@ -204,7 +215,7 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
} else {
// with CTE primary alias fails with hibernate (WITH entities AS (…) SELECT count(c) FROM entities c)
if (containsCTE) {
if (containsCTE || containsFromFunction) {
nested.append(QueryTokens.token("*"));
} else {

View File

@@ -29,6 +29,7 @@ import org.jspecify.annotations.Nullable;
* {@link ParsedQueryIntrospector} for HQL queries.
*
* @author Mark Paluch
* @author oscar.fanchin
*/
@SuppressWarnings({ "UnreachableCode", "ConstantValue" })
class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIntrospector<HibernateQueryInformation> {
@@ -40,11 +41,12 @@ class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIn
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
private boolean hasCte = false;
private boolean hasFromFunction = false;
@Override
public HibernateQueryInformation getParsedQueryInformation() {
return new HibernateQueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection,
hasConstructorExpression, hasCte);
hasConstructorExpression, hasCte, hasFromFunction);
}
@Override
@@ -63,6 +65,12 @@ class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIn
this.hasCte = true;
return super.visitCte(ctx);
}
@Override
public Void visitFunctionCallAsFromSource(HqlParser.FunctionCallAsFromSourceContext ctx) {
this.hasFromFunction = true;
return super.visitFunctionCallAsFromSource(ctx);
}
@Override
public Void visitFromRoot(HqlParser.FromRootContext ctx) {

View File

@@ -31,6 +31,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Greg Turnquist
* @author Christoph Strobl
* @author Oscar Fanchin
* @since 3.1
*/
@SuppressWarnings({ "ConstantConditions", "DuplicatedCode", "UnreachableCode" })
@@ -63,6 +64,24 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
return visit(ctx.ql_statement());
}
@Override
public QueryTokenStream visitFunctionCallAsFromSource(HqlParser.FunctionCallAsFromSourceContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(visit(ctx.identifier()));
builder.append(TOKEN_OPEN_PAREN);
if (!ctx.expression().isEmpty()) {
builder.append(QueryTokenStream.concatExpressions(ctx.expression(), this::visit, TOKEN_COMMA));
}
builder.append(TOKEN_CLOSE_PAREN);
return builder;
}
@Override
public QueryTokenStream visitQl_statement(HqlParser.Ql_statementContext ctx) {
@@ -376,6 +395,14 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
builder.appendExpression(nested);
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
}
} else if (ctx.functionCallAsFromSource() != null) {
builder.appendExpression(visit(ctx.functionCallAsFromSource()));
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
}
@@ -442,6 +469,39 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
return builder;
}
@Override
public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(visit(ctx.functionCallAsJoinTarget()));
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
}
return builder;
}
@Override
public QueryTokenStream visitFunctionCallAsJoinTarget(HqlParser.FunctionCallAsJoinTargetContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(visit(ctx.identifier()));
builder.append(TOKEN_OPEN_PAREN);
if (!ctx.expression().isEmpty()) {
builder.append(QueryTokenStream.concatExpressions(ctx.expression(), this::visit, TOKEN_COMMA));
}
builder.append(TOKEN_CLOSE_PAREN);
return builder;
}
@Override
public QueryTokenStream visitUpdateStatement(HqlParser.UpdateStatementContext ctx) {

View File

@@ -32,6 +32,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Greg Turnquist
* @author Christoph Strobl
* @author oscar.fanchin
* @since 3.1
*/
@SuppressWarnings("ConstantValue")
@@ -122,6 +123,19 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
return tokens;
}
@Override
public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) {
QueryTokenStream tokens = super.visitJoinFunctionCall(ctx);
if (ctx.variable() != null && !tokens.isEmpty()) {
transformerSupport.registerAlias(tokens.getLast());
}
return tokens;
}
@Override
public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) {

View File

@@ -36,6 +36,7 @@ import org.junit.jupiter.params.provider.ValueSource;
* @author Christoph Strobl
* @author Mark Paluch
* @author Yannick Brandt
* @author oscar.fanchin
* @since 3.1
*/
class HqlQueryRendererTests {
@@ -2375,4 +2376,254 @@ class HqlQueryRendererTests {
assertQuery("select ie from ItemExample ie left join ie.object io where io.object = :externalId");
assertQuery("select ie from ItemExample ie where ie.status = com.app.domain.object.Status.UP");
}
@Test // GH-3864 - Added support for Set Return function (SRF) support H7 parsing and
// rendering
void fromSRFWithAlias() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue ) d
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date ) d
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function() d
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue , :longValue ) d
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void fromSRFWithoutAlias() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue )
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date )
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function()
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue , :longValue )
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinEntityToSRFWithFunctionAlias() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function(:date , :integerValue ) d on (e.id = d.idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function(:date ) d on (e.id = d.idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function() d on (e.id = d.idFunction)
""");
assertQuery(
"""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function(:date , :integerValue , :longValue ) d on (e.id = d.idFunction)
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinEntityToSRFWithoutFunctionAlias() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function(:date , :integerValue ) on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function(:date ) on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function() on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from EntityClass e join some_function(:date , :integerValue , :longValue ) on (e.id = idFunction)
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinSRFToEntityWithoutFunctionWithAlias() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue ) d join EntityClass e on (e.id = d.idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date ) d join EntityClass e on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function() d join EntityClass e on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue , :longValue ) d join EntityClass e on (e.id = d.idFunction)
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinSRFToEntityWithoutFunctionWithoutAlias() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue ) join EntityClass e on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date ) join EntityClass e on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function() join EntityClass e on (e.id = idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue , :longValue ) join EntityClass e on (e.id = idFunction)
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void selectSRFIntoSubquery() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date , :integerValue ) x) d
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date ) x) d
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function() x) d
""");
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date , :integerValue , :longValue ) x) d
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinEntityToSRFIntoSubquery() {
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
inner join (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date , :integerValue ) x ) d on (k.id = d.idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
inner join (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date ) x ) d on (k.id = d.idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
inner join (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function() x ) d on (k.id = d.idFunction)
""");
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
inner join (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date , :integerValue , :longValue ) x ) d on (k.id = d.idFunction)
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinLateralEntityToSRF() {
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
join lateral (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date , :integerValue ) x where x.idFunction = k.id ) d
""");
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
join lateral (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date ) x where x.idFunction = k.id ) d
""");
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
join lateral (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function() x where x.idFunction = k.id ) d
""");
assertQuery("""
select new com.example.dto.SampleDto(k.id, d.nameFunction)
from EntityClass k
join lateral (select x.idFunction idFunction, x.nameFunction nameFunction
from some_function(:date , :integerValue , :longValue ) x where x.idFunction = k.id ) d
""");
}
@Test // GH-3864 - Added support for Set Return function support H7 parsing and
// rendering
void joinTwoFunctions() {
assertQuery("""
select new com.example.dto.SampleDto(d.idFunction, d.nameFunction)
from some_function(:date , :integerValue ) d
inner join some_function_single_param(:date ) k on (d.idFunction = k.idFunctionSP)
""");
}
}

View File

@@ -1133,6 +1133,33 @@ class HqlQueryTransformerTests {
assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee",
"select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee");
}
@Test // GH-3864
void testCountFromFunctionWithAlias() {
// given
var original = "select x.id, x.value from some_function(:date , :integerValue ) x";
// when
var results = createCountQueryFor(original);
// then
assertThat(results).contains("select count(*) from some_function(:date , :integerValue ) x");
}
@Test // GH-3864
void testCountFromFunctionNoAlias() {
// given
var original = "select id, value from some_function(:date , :integerValue )";
// when
var results = createCountQueryFor(original);
// then
assertThat(results).contains("select count(*) from some_function(:date , :integerValue )");
}
@Test // GH-3427
void sortShouldBeAppendedWithSpacingInCaseOfSetOperator() {