Skip to content

Commit 341267b

Browse files
committed
HHH-3356 Support for normal and lateral subquery in from clause
1 parent 4947af9 commit 341267b

File tree

63 files changed

+4687
-148
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+4687
-148
lines changed

documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,21 @@ This behavior may be slightly adjusted using the `@Polymorphism` annotation.
14701470
14711471
See <<chapters/domain/inheritance.adoc#entity-inheritance-polymorphism>> for more.
14721472
1473+
[[hql-derived-root]]
1474+
==== Derived root
1475+
1476+
As of Hibernate 6.1, HQL allows to declare derived roots, based on a sub query in the `from` clause.
1477+
1478+
[[hql-derived-root-example]]
1479+
====
1480+
[source, JAVA, indent=0]
1481+
----
1482+
include::{sourcedir}/HQLTest.java[tags=hql-derived-root-example, indent=0]
1483+
----
1484+
====
1485+
1486+
This can be used to split up more complicated queries into smaller parts.
1487+
14731488
[[hql-join]]
14741489
=== Declaring joined entities
14751490
@@ -1626,6 +1641,31 @@ This allows the attribute `cardNumber` declared by the subtype `CreditCardPaymen
16261641
16271642
See <<hql-treat-type>> for more information about `treat()`.
16281643
1644+
[[hql-join-derived]]
1645+
==== Join subquery
1646+
1647+
As of Hibernate 6.1, HQL allows against to join sub queries.
1648+
1649+
[[hql-derived-join-example]]
1650+
====
1651+
[source, JAVA, indent=0]
1652+
----
1653+
include::{sourcedir}/HQLTest.java[tags=hql-derived-join-example, indent=0]
1654+
----
1655+
====
1656+
1657+
This is very similar to defining a <<hql-derived-root,derived root>>, but the particular interesting part here,
1658+
is the use of the `lateral` keyword, which allows to refer to previous from clause nodes within the subquery.
1659+
1660+
This is particularly useful for computing top-N elements of multiple groups.
1661+
1662+
[NOTE]
1663+
====
1664+
Most databases support lateral natively, but for certain databases it is necessary to emulate this feature.
1665+
Beware that the emulation is neither very efficient, nor does it support all possible query shapes,
1666+
so be sure to test such queries against your desired target database.
1667+
====
1668+
16291669
[[hql-implicit-join]]
16301670
==== Implicit joins (path expressions)
16311671

documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_select_bnf.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,20 @@ query
1313

1414
queryOrder
1515
: orderByClause limitClause? offsetClause? fetchClause?
16+
17+
fromClause
18+
: FROM entityWithJoins (COMMA entityWithJoins)*
19+
20+
entityWithJoins
21+
: fromRoot (join | crossJoin | jpaCollectionJoin)*
22+
23+
fromRoot
24+
: entityName variable?
25+
| LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable?
26+
27+
join
28+
: joinType JOIN FETCH? joinTarget joinRestriction?
29+
30+
joinTarget
31+
: path variable?
32+
| LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable?

documentation/src/test/java/org/hibernate/userguide/hql/HQLTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.hibernate.testing.SkipForDialect;
5252
import org.hibernate.testing.DialectChecks;
5353
import org.hibernate.testing.RequiresDialectFeature;
54+
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
5455
import org.junit.Before;
5556
import org.junit.Test;
5657

@@ -3043,4 +3044,47 @@ public void test_hql_read_only_entities_native_example() {
30433044
//end::hql-read-only-entities-native-example[]
30443045
});
30453046
}
3047+
3048+
@Test
3049+
public void test_hql_derived_root_example() {
3050+
3051+
doInJPA(this::entityManagerFactory, entityManager -> {
3052+
//tag::hql-derived-root-example[]
3053+
List<Tuple> calls = entityManager.createQuery(
3054+
"select d.owner, d.payed " +
3055+
"from (" +
3056+
" select p.person as owner, c.payment is not null as payed " +
3057+
" from Call c " +
3058+
" join c.phone p " +
3059+
" where p.number = :phoneNumber) d",
3060+
Tuple.class)
3061+
.setParameter("phoneNumber", "123-456-7890")
3062+
.getResultList();
3063+
//end::hql-derived-root-example[]
3064+
});
3065+
}
3066+
3067+
@Test
3068+
@SkipForDialect(value = TiDBDialect.class, comment = "TiDB db does not support subqueries for ON condition")
3069+
@RequiresDialectFeature(DialectChecks.SupportsOrderByInCorrelatedSubquery.class)
3070+
public void test_hql_derived_join_example() {
3071+
3072+
doInJPA(this::entityManagerFactory, entityManager -> {
3073+
//tag::hql-derived-join-example[]
3074+
List<Tuple> calls = entityManager.createQuery(
3075+
"select longest.duration " +
3076+
"from Phone p " +
3077+
"left join lateral (" +
3078+
" select c.duration as duration " +
3079+
" from p.calls c" +
3080+
" order by c.duration desc" +
3081+
" limit 1 " +
3082+
" ) longest " +
3083+
"where p.number = :phoneNumber",
3084+
Tuple.class)
3085+
.setParameter("phoneNumber", "123-456-7890")
3086+
.getResultList();
3087+
//end::hql-derived-join-example[]
3088+
});
3089+
}
30463090
}

hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ IS : [iI] [sS];
207207
JOIN : [jJ] [oO] [iI] [nN];
208208
KEY : [kK] [eE] [yY];
209209
LAST : [lL] [aA] [sS] [tT];
210+
LATERAL : [lL] [aA] [tT] [eE] [rR] [aA] [lL];
210211
LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG];
211212
LEFT : [lL] [eE] [fF] [tT];
212213
LIKE : [lL] [iI] [kK] [eE];

hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,15 @@ fromClause
169169
* The declaration of a root entity in 'from' clause, along with its joins
170170
*/
171171
entityWithJoins
172-
: rootEntity (join | crossJoin | jpaCollectionJoin)*
172+
: fromRoot (join | crossJoin | jpaCollectionJoin)*
173173
;
174174

175175
/**
176176
* A root entity declaration in the 'from' clause, with optional identification variable
177177
*/
178-
rootEntity
179-
: entityName variable?
178+
fromRoot
179+
: entityName variable? # RootEntity
180+
| LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? # RootSubquery
180181
;
181182

182183
/**
@@ -212,7 +213,7 @@ jpaCollectionJoin
212213
* A 'join', with an optional 'on' or 'with' clause
213214
*/
214215
join
215-
: joinType JOIN FETCH? joinPath joinRestriction?
216+
: joinType JOIN FETCH? joinTarget joinRestriction?
216217
;
217218

218219
/**
@@ -226,8 +227,9 @@ joinType
226227
/**
227228
* The joined path, with an optional identification variable
228229
*/
229-
joinPath
230-
: path variable?
230+
joinTarget
231+
: path variable? #JoinPath
232+
| LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? #JoinSubquery
231233
;
232234

233235
/**

hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,8 @@ public boolean supportsValuesListForInsert() {
10811081

10821082
@Override
10831083
public boolean supportsOrderByInSubquery() {
1084-
return false;
1084+
// Seems to work, though I don't know as of which version
1085+
return true;
10851086
}
10861087

10871088
@Override

hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,8 @@ public boolean supportsTupleDistinctCounts() {
577577

578578
@Override
579579
public boolean supportsOrderByInSubquery() {
580-
return false;
580+
// As of version 10.5 Derby supports OFFSET and FETCH as well as ORDER BY in subqueries
581+
return getVersion().isSameOrAfter( 10, 5 );
581582
}
582583

583584
@Override

hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
*/
3232
public class HANASqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
3333

34+
private boolean inLateral;
35+
3436
public HANASqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) {
3537
super( sessionFactory, statement );
3638
}
@@ -64,7 +66,20 @@ public void visitQuerySpec(QuerySpec querySpec) {
6466

6567
@Override
6668
public void visitQueryPartTableReference(QueryPartTableReference tableReference) {
67-
emulateQueryPartTableReferenceColumnAliasing( tableReference );
69+
if ( tableReference.isLateral() && !inLateral ) {
70+
inLateral = true;
71+
emulateQueryPartTableReferenceColumnAliasing( tableReference );
72+
inLateral = false;
73+
}
74+
else {
75+
emulateQueryPartTableReferenceColumnAliasing( tableReference );
76+
}
77+
}
78+
79+
@Override
80+
protected SqlAstNodeRenderingMode getParameterRenderingMode() {
81+
// HANA does not support parameters in lateral sub queries for some reason, so inline all the parameters in this case
82+
return inLateral ? SqlAstNodeRenderingMode.INLINE_ALL_PARAMETERS : super.getParameterRenderingMode();
6883
}
6984

7085
@Override

hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.function.Consumer;
1111

1212
import org.hibernate.engine.spi.SessionFactoryImplementor;
13+
import org.hibernate.metamodel.mapping.JdbcMapping;
14+
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
1315
import org.hibernate.query.sqm.BinaryArithmeticOperator;
1416
import org.hibernate.query.sqm.ComparisonOperator;
1517
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
@@ -213,10 +215,15 @@ protected void renderSelectTupleComparison(
213215
}
214216

215217
protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) {
218+
final JdbcMappingContainer lhsExpressionType = lhs.getExpressionType();
219+
if ( lhsExpressionType == null ) {
220+
renderComparisonStandard( lhs, operator, rhs );
221+
return;
222+
}
216223
switch ( operator ) {
217224
case DISTINCT_FROM:
218225
case NOT_DISTINCT_FROM:
219-
if ( lhs.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType() instanceof ArrayJdbcType ) {
226+
if ( lhsExpressionType.getJdbcMappings().get( 0 ).getJdbcType() instanceof ArrayJdbcType ) {
220227
// HSQL implements distinct from semantics for arrays
221228
lhs.accept( this );
222229
appendSql( operator == ComparisonOperator.DISTINCT_FROM ? "<>" : "=" );

hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() {
132132
return false;
133133
}
134134

135+
@Override
136+
protected boolean supportsIntersect() {
137+
return getDialect().getVersion().isSameOrAfter( 10, 3 );
138+
}
139+
140+
@Override
141+
protected boolean supportsDistinctFromPredicate() {
142+
// It supports a proprietary operator
143+
return true;
144+
}
145+
135146
private boolean supportsWindowFunctions() {
136147
return getDialect().getVersion().isSameOrAfter( 10, 2 );
137148
}

hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,17 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() {
149149
return false;
150150
}
151151

152+
@Override
153+
protected boolean supportsIntersect() {
154+
return false;
155+
}
156+
157+
@Override
158+
protected boolean supportsDistinctFromPredicate() {
159+
// It supports a proprietary operator
160+
return true;
161+
}
162+
152163
@Override
153164
protected String getFromDual() {
154165
return " from dual";

hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.hibernate.engine.spi.SessionFactoryImplementor;
1212
import org.hibernate.internal.util.collections.Stack;
1313
import org.hibernate.metamodel.mapping.JdbcMapping;
14+
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
1415
import org.hibernate.query.sqm.BinaryArithmeticOperator;
1516
import org.hibernate.query.sqm.ComparisonOperator;
1617
import org.hibernate.query.sqm.FetchClauseType;
@@ -354,12 +355,12 @@ public void visitOver(Over<?> over) {
354355

355356
@Override
356357
protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) {
357-
if ( lhs.getExpressionType() == null ) {
358+
final JdbcMappingContainer lhsExpressionType = lhs.getExpressionType();
359+
if ( lhsExpressionType == null ) {
358360
renderComparisonEmulateDecode( lhs, operator, rhs );
359361
return;
360362
}
361-
final JdbcMapping lhsMapping = lhs.getExpressionType().getJdbcMappings().get( 0 );
362-
switch ( lhsMapping.getJdbcType().getJdbcTypeCode() ) {
363+
switch ( lhsExpressionType.getJdbcMappings().get( 0 ).getJdbcType().getJdbcTypeCode() ) {
363364
case SqlTypes.SQLXML:
364365
// In Oracle, XMLTYPE is not "comparable", so we have to use the xmldiff function for this purpose
365366
switch ( operator ) {

hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.hibernate.sql.ast.tree.expression.Expression;
1616
import org.hibernate.sql.ast.tree.expression.Literal;
1717
import org.hibernate.sql.ast.tree.expression.Summarization;
18+
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
19+
import org.hibernate.sql.ast.tree.from.ValuesTableReference;
1820
import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate;
1921
import org.hibernate.sql.ast.tree.select.QueryGroup;
2022
import org.hibernate.sql.ast.tree.select.QueryPart;
@@ -76,6 +78,16 @@ public void visitQuerySpec(QuerySpec querySpec) {
7678
}
7779
}
7880

81+
@Override
82+
public void visitValuesTableReference(ValuesTableReference tableReference) {
83+
emulateValuesTableReferenceColumnAliasing( tableReference );
84+
}
85+
86+
@Override
87+
public void visitQueryPartTableReference(QueryPartTableReference tableReference) {
88+
emulateQueryPartTableReferenceColumnAliasing( tableReference );
89+
}
90+
7991
@Override
8092
public void visitOffsetFetchClause(QueryPart queryPart) {
8193
if ( !isRowNumberingCurrentQueryPart() ) {

0 commit comments

Comments
 (0)