diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 849809b71a..80e0cf00e8 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -89,6 +89,7 @@ Most of the complexity here arises from the interplay of set operators (`union`, We'll describe the various clauses of a query later in this chapter, but to summarize, a query might have: +* a `with` clause, specifying <> to be used in the following query, * a `select` list, specifying a <> (the things to return from the query), * a `from` clause and joins, <> the entities involved in the query, and how they're <> to each other, * a `where` clause, specifying a <>, @@ -1559,6 +1560,16 @@ It may not refer to other roots declared in the same `from` clause. A subquery may also occur in a <>, in which case it may be a correlated subquery. +[[hql-from-cte]] +==== Common table expressions in `from` clause + +A _Common table expression (CTE)_ is like a derived root with a name. The big difference is, +that the name can be referred to multiple times. It must declare an identification variable. + +The CTE name can be used for a `from` clause root or a `join`, similar to entity names. + +Refer to the <> chapter for details about CTEs. + [[hql-join]] === Declaring joined entities @@ -2477,3 +2488,132 @@ This _almost certainly_ isn't the behavior you were hoping for, and in general w ==== In the next chapter we'll see a completely different way to write queries in Hibernate. + +[[hql-with-cte]] +==== With clause + +The `with` clause allows to specify _common table expressions (CTEs)_ which can be imagined like named subqueries. +Every uncorrelated subquery can be factored to a CTE in the `with` clause. The semantics are equivalent. + +The `with` clause offers features beyond naming subqueries though: + +* Specify materialization hints +* Recursive querying + +===== Materialization hint + +The materialization hint `MATERIALIZED` or `NOT MATERIALIZED` can be applied to tell the DBMS whether a CTE should +or shouldn't be materialized. Consult the database manual of the respective database for the exact meaning of the hint. + +Usually, one can expect that `MATERIALIZED` will cause the subquery to be executed separately and saved into a temporary table, +whereas `NOT MATERIALIZED` will cause the subquery to be inlined into every use site and considered during optimizations separately. + +[[hql-cte-materialized-example]] +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/HQLTest.java[tags=hql-cte-materialized-example, indent=0] +---- +==== + +===== Recursive querying + +The main use case for the `with` clause is to define a name for a subquery, +such that this subquery can refer to itself, which ultimately enables recursive querying. + +Recursive CTEs must follow a very particular shape, which is + +* Base query part +* `union` or `union all` +* Recursive query part + +[[hql-cte-recursive-example]] +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/HQLTest.java[tags=hql-cte-recursive-example, indent=0] +---- +==== + +The base query part represents the initial set of rows. When fetching a tree of data, +the base query part usually is the tree root. + +The recursive query part is executed again and again until it produces no new rows. +The result of such a CTE is the base query part result _unioned_ together with all recursive query part executions. +Depending on whether `union all` or `union` (`distinct`) is used, duplicate rows are preserved or not. + +Recursive queries additionally can have + +* a `search` clause to hint the DBMS whether to use breadth or depth first searching +* a `cycle` clause to hint the DBMS how to determine that a cycle was reached + +Defining the `search` clause requires specifying a name for an attribute in the `set` sub-clause, +that will be added to the CTE type and allows ordering results according to the search order. + +[[hql-cte-recursive-search-bnf-example]] +==== +[source, antlrv4, indent=0] +---- +searchClause +: "SEARCH" ("BREADTH"|"DEPTH") "FIRST BY" searchSpecifications "SET" identifier +; + +searchSpecifications +: searchSpecification ("," searchSpecification)* +; + +searchSpecification +: identifier sortDirection? nullsPrecedence? +; +---- +==== + +A DBMS has two possible orders when executing the recursive query part + +* Depth first - handle the *newest* produced rows by the recursive query part first +* Breadth first - handle the *oldest* produced rows by the recursive query part first + +[[hql-cte-recursive-search-example]] +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/HQLTest.java[tags=hql-cte-recursive-search-example, indent=0] +---- +==== + +Recursive processing can lead to cycles which might lead to queries executing forever. +The `cycle` clause hints the DBMS which CTE attributes to track for the cycle detection. +It requires specifying a name for a cycle mark attribute in the `set` sub-clause, +that will be added to the CTE type and allows detecting if a cycle occurred for a result. + +By default, the cycle mark attribute will be set to `true` when a cycle is detected and `false` otherwise. +The values to use can be explicitly specified through the `to` and `default` sub-clauses. +Optionally, it's also possible to specify a cycle path attribute name through the `using` clause +The cycle path attribute can be used to understand the traversal path that lead to a result. + +[[hql-cte-recursive-cycle-bnf-example]] +==== +[source, antlrv4, indent=0] +---- +cycleClause + : "CYCLE" cteAttributes "SET" identifier ("TO" literal "DEFAULT" literal)? ("USING" identifier)? + ; +---- +==== + +[[hql-cte-recursive-cycle-example]] +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/HQLTest.java[tags=hql-cte-recursive-cycle-example, indent=0] +---- +==== + +[IMPORTANT] +==== +Hibernate merely translates recursive CTEs but doesn't attempt to emulate the feature. +Therefore, this feature will only work if the database supports recursive CTEs. +Hibernate does emulate the `search` and `cycle` clauses though if necessary, so you can safely use that. + +Note that most modern database versions support recursive CTEs already. +==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_select_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_select_bnf.txt index 2086482d9a..7aff8fbd07 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_select_bnf.txt +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_select_bnf.txt @@ -2,7 +2,7 @@ selectStatement : queryExpression queryExpression - : orderedQuery (setOperator orderedQuery)* + : withClause? orderedQuery (setOperator orderedQuery)* orderedQuery : (query | "(" queryExpression ")") queryOrder? @@ -29,4 +29,32 @@ join joinTarget : path variable? - | "LATERAL"? "(" subquery ")" variable? \ No newline at end of file + | "LATERAL"? "(" subquery ")" variable? + +withClause + : "WITH" cte ("," cte)* + ; + +cte + : identifier AS ("NOT"? "MATERIALIZED")? "(" queryExpression ")" searchClause? cycleClause? + ; + +cteAttributes + : identifier ("," identifier)* + ; + +searchClause + : "SEARCH" ("BREADTH"|"DEPTH") "FIRST BY" searchSpecifications "SET" identifier + ; + +searchSpecifications + : searchSpecification ("," searchSpecification)* + ; + +searchSpecification + : identifier sortDirection? nullsPrecedence? + ; + +cycleClause + : "CYCLE" cteAttributes "SET" identifier ("TO" literal "DEFAULT" literal)? ("USING" identifier)? + ; diff --git a/documentation/src/test/java/org/hibernate/userguide/hql/HQLTest.java b/documentation/src/test/java/org/hibernate/userguide/hql/HQLTest.java index 5912a79127..209e1a7fb7 100644 --- a/documentation/src/test/java/org/hibernate/userguide/hql/HQLTest.java +++ b/documentation/src/test/java/org/hibernate/userguide/hql/HQLTest.java @@ -3133,6 +3133,102 @@ public class HQLTest extends BaseEntityManagerFunctionalTestCase { }); } + @Test + public void test_hql_cte_materialized_example() { + + doInJPA(this::entityManagerFactory, entityManager -> { + //tag::hql-cte-materialized-example[] + List calls = entityManager.createQuery( + "with data as materialized(" + + " select p.person as owner, c.payment is not null as payed " + + " from Call c " + + " join c.phone p " + + " where p.number = :phoneNumber" + + ")" + + "select d.owner, d.payed " + + "from data d", + Tuple.class) + .setParameter("phoneNumber", "123-456-7890") + .getResultList(); + //end::hql-cte-materialized-example[] + }); + } + + @Test + @RequiresDialectFeature( DialectChecks.SupportsRecursiveCtes.class ) + public void test_hql_cte_recursive_example() { + doInJPA(this::entityManagerFactory, entityManager -> { + //tag::hql-cte-recursive-example[] + List calls = entityManager.createQuery( + "with paymentConnectedPersons as(" + + " select a.owner owner " + + " from Account a where a.id = :startId " + + " union all" + + " select a2.owner owner " + + " from paymentConnectedPersons d " + + " join Account a on a.owner = d.owner " + + " join a.payments p " + + " join Account a2 on a2.owner = p.person" + + ")" + + "select d.owner " + + "from paymentConnectedPersons d", + Tuple.class) + .setParameter("startId", 123L) + .getResultList(); + //end::hql-cte-recursive-example[] + }); + } + + @Test + @RequiresDialectFeature( DialectChecks.SupportsRecursiveCtes.class ) + public void test_hql_cte_recursive_search_example() { + doInJPA(this::entityManagerFactory, entityManager -> { + //tag::hql-cte-recursive-search-example[] + List calls = entityManager.createQuery( + "with paymentConnectedPersons as(" + + " select a.owner owner " + + " from Account a where a.id = :startId " + + " union all" + + " select a2.owner owner " + + " from paymentConnectedPersons d " + + " join Account a on a.owner = d.owner " + + " join a.payments p " + + " join Account a2 on a2.owner = p.person" + + ") search breadth first by owner set orderAttr " + + "select d.owner " + + "from paymentConnectedPersons d", + Tuple.class) + .setParameter("startId", 123L) + .getResultList(); + //end::hql-cte-recursive-search-example[] + }); + } + + @Test + @RequiresDialectFeature( DialectChecks.SupportsRecursiveCtes.class ) + public void test_hql_cte_recursive_cycle_example() { + doInJPA(this::entityManagerFactory, entityManager -> { + //tag::hql-cte-recursive-cycle-example[] + List calls = entityManager.createQuery( + "with paymentConnectedPersons as(" + + " select a.owner owner " + + " from Account a where a.id = :startId " + + " union all" + + " select a2.owner owner " + + " from paymentConnectedPersons d " + + " join Account a on a.owner = d.owner " + + " join a.payments p " + + " join Account a2 on a2.owner = p.person" + + ") cycle owner set cycleMark " + + "select d.owner, d.cycleMark " + + "from paymentConnectedPersons d", + Tuple.class) + .setParameter("startId", 123L) + .getResultList(); + //end::hql-cte-recursive-cycle-example[] + }); + } + @Test @RequiresDialectFeature({ DialectChecks.SupportsSubqueryInOnClause.class, diff --git a/gradle/databases.gradle b/gradle/databases.gradle index 53322991ff..a5326cfc7a 100644 --- a/gradle/databases.gradle +++ b/gradle/databases.gradle @@ -155,6 +155,14 @@ ext { 'jdbc.url' : 'jdbc:mysql://' + dbHost + '/hibernate_orm_test', 'connection.init_sql' : '' ], + tidb_ci5 : [ + 'db.dialect' : 'org.hibernate.dialect.TiDBDialect', + 'jdbc.driver': 'com.mysql.jdbc.Driver', + 'jdbc.user' : 'root', + 'jdbc.pass' : '', + 'jdbc.url' : 'jdbc:mysql://' + dbHost + ':4000/test', + 'connection.init_sql' : '' + ], postgis : [ 'db.dialect' : 'org.hibernate.spatial.dialect.postgis.PostgisPG95Dialect', 'jdbc.driver': 'org.postgresql.Driver', diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java index 60284e5170..e409562d87 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java @@ -37,16 +37,6 @@ public class CUBRIDSqlAstTranslator extends AbstractSql renderCombinedLimitClause( queryPart ); } - @Override - protected void renderSearchClause(CteStatement cte) { - // CUBRID does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // CUBRID does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CacheSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CacheSqlAstTranslator.java index a523c92892..788e3ca45d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CacheSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CacheSqlAstTranslator.java @@ -80,16 +80,6 @@ public class CacheSqlAstTranslator extends AbstractSqlA } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Cache does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Cache does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index af1cf5887c..a1c87fcd17 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -468,6 +468,11 @@ public class CockroachLegacyDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 20, 1 ); + } + @Override public String getNoColumnsInsertString() { return "default values"; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java index 9d23e680cd..1fbda61b4e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java @@ -9,6 +9,7 @@ package org.hibernate.community.dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -52,6 +53,26 @@ public class CockroachLegacySqlAstTranslator extends Ab } } + @Override + protected void renderMaterializationHint(CteMaterialization materialization) { + if ( getDialect().getVersion().isSameOrAfter( 20, 2 ) ) { + if ( materialization == CteMaterialization.NOT_MATERIALIZED ) { + appendSql( "not " ); + } + appendSql( "materialized " ); + } + } + + @Override + protected boolean supportsRowConstructor() { + return true; + } + + @Override + protected boolean supportsArrayConstructor() { + return true; + } + @Override protected String getForShare(int timeoutMillis) { return " for share"; @@ -116,16 +137,6 @@ public class CockroachLegacySqlAstTranslator extends Ab } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Cockroach does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Cockroach does not support this, but it can be emulated - } - @Override protected void renderPartitionItem(Expression expression) { if ( expression instanceof Literal ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 452b7e3224..765cb733bc 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -534,6 +534,11 @@ public class DB2LegacyDialect extends Dialect { return false; } + @Override + public boolean requiresCastForConcatenatingNonStrings() { + return true; + } + @Override public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { return selectNullString(sqlType); @@ -756,6 +761,12 @@ public class DB2LegacyDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + // Supported at last since 9.7 + return getDB2Version().isSameOrAfter( 9, 7 ); + } + @Override public boolean supportsOffsetInSubquery() { return true; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index 804a31da2f..32da4db998 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -9,6 +9,7 @@ package org.hibernate.community.dialect; import java.util.List; import java.util.function.Consumer; +import org.hibernate.LockMode; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; @@ -26,6 +27,9 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; import org.hibernate.sql.ast.tree.insert.InsertStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; @@ -45,6 +49,70 @@ public class DB2LegacySqlAstTranslator extends Abstract super( sessionFactory, statement ); } + @Override + protected boolean needsRecursiveKeywordInWithClause() { + return false; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + + @Override + protected void renderTableReferenceJoins(TableGroup tableGroup) { + // When we are in a recursive CTE, we can't render joins on DB2... + // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 + if ( isInRecursiveQueryPart() ) { + final List joins = tableGroup.getTableReferenceJoins(); + if ( joins == null || joins.isEmpty() ) { + return; + } + + for ( TableReferenceJoin tableJoin : joins ) { + switch ( tableJoin.getJoinType() ) { + case CROSS: + case INNER: + break; + default: + throw new UnsupportedOperationException( "Can't emulate '" + tableJoin.getJoinType().getText() + "join' in a DB2 recursive query part" ); + } + appendSql( COMA_SEPARATOR_CHAR ); + + renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + + if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { + addAdditionalWherePredicate( tableJoin.getPredicate() ); + } + } + } + else { + super.renderTableReferenceJoins( tableGroup ); + } + } + + @Override + protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List tableGroupJoinCollector) { + if ( isInRecursiveQueryPart() ) { + switch ( tableGroupJoin.getJoinType() ) { + case CROSS: + case INNER: + break; + default: + throw new UnsupportedOperationException( "Can't emulate '" + tableGroupJoin.getJoinType().getText() + "join' in a DB2 recursive query part" ); + } + appendSql( COMA_SEPARATOR_CHAR ); + + renderTableGroup( tableGroupJoin.getJoinedGroup(), null, tableGroupJoinCollector ); + if ( tableGroupJoin.getPredicate() != null && !tableGroupJoin.getPredicate().isEmpty() ) { + addAdditionalWherePredicate( tableGroupJoin.getPredicate() ); + } + } + else { + super.renderTableGroupJoin( tableGroupJoin, tableGroupJoinCollector ); + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java index df193057db..6a26aa9729 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java @@ -123,6 +123,11 @@ public class DB2iLegacyDialect extends DB2LegacyDialect { return getVersion().isSameOrAfter( 7, 1 ); } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 7, 1 ); + } + @Override public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { return new StandardSqlAstTranslatorFactory() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java index 58260903ff..357df2ede5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java @@ -125,6 +125,11 @@ public class DB2zLegacyDialect extends DB2LegacyDialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 11 ); + } + @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { StringBuilder pattern = new StringBuilder(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java index 300d46dc32..c2fb3b05bc 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java @@ -61,6 +61,10 @@ public class DB2zLegacySqlAstTranslator extends DB2Lega @Override protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { + if ( shouldInlineCte( tableGroup ) ) { + inlineCteTableGroup( tableGroup, lockMode ); + return false; + } final TableReference tableReference = tableGroup.getPrimaryTableReference(); if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java index c7cfabf091..dfd7bb68c6 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java @@ -18,6 +18,7 @@ import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.function.CaseLeastGreatestEmulation; import org.hibernate.dialect.function.CastingConcatFunction; +import org.hibernate.dialect.function.ChrLiteralEmulation; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.CountFunction; import org.hibernate.dialect.function.DerbyLpadEmulation; @@ -286,6 +287,16 @@ public class DerbyLegacyDialect extends Dialect { // AVG by default uses the input type, so we possibly need to cast the argument type, hence a special function functionFactory.avg_castingNonDoubleArguments( this, SqlAstNodeRenderingMode.DEFAULT ); + // Note that Derby does not have chr() / ascii() functions. + // It does have a function named char(), but it's really a + // sort of to_char() function. + + // We register an emulation instead, that can at least translate integer literals + queryEngine.getSqmFunctionRegistry().register( + "chr", + new ChrLiteralEmulation( queryEngine.getTypeConfiguration() ) + ); + functionFactory.concat_pipeOperator(); functionFactory.cot(); functionFactory.chr_char(); @@ -604,6 +615,11 @@ public class DerbyLegacyDialect extends Dialect { return getVersion().isSameOrAfter( 10, 5 ); } + @Override + public boolean requiresCastForConcatenatingNonStrings() { + return true; + } + @Override public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contributeTypes( typeContributions, serviceRegistry ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java index 6f3de6e33e..a17629b0c1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java @@ -41,6 +41,11 @@ public class DerbyLegacySqlAstTranslator extends Abstra super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return false; + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -125,24 +130,6 @@ public class DerbyLegacySqlAstTranslator extends Abstra return " with rs"; } - @Override - public void visitCteContainer(CteContainer cteContainer) { - if ( cteContainer.isWithRecursive() ) { - throw new IllegalArgumentException( "Recursive CTEs can't be emulated" ); - } - super.visitCteContainer( cteContainer ); - } - - @Override - protected void renderSearchClause(CteStatement cte) { - // Derby does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Derby does not support this, but it can be emulated - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { // Derby only supports the OFFSET and FETCH clause with ROWS diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java index 1b79bf0537..bf0457b325 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java @@ -136,16 +136,6 @@ public class FirebirdSqlAstTranslator extends AbstractS } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Firebird does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Firebird does not support this, but it can be emulated - } - @Override protected boolean supportsSimpleQueryGrouping() { // Firebird is quite strict i.e. it requires `select .. union all select * from (select ...)` diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index e27e26b091..74101ca54d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -768,6 +768,11 @@ public class H2LegacyDialect extends Dialect { return getVersion().isSameOrAfter( 1, 4, 200 ); } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 1, 4, 196 ); + } + @Override public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 1, 4, 198 ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java index a51d10c245..122db5d612 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java @@ -17,7 +17,9 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.cte.CteTableGroup; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -46,6 +48,47 @@ public class H2LegacySqlAstTranslator extends AbstractS super( sessionFactory, statement ); } + @Override + public void visitCteContainer(CteContainer cteContainer) { + // H2 has various bugs in different versions that make it impossible to use CTEs with parameters reliably + withParameterRenderingMode( + SqlAstNodeRenderingMode.INLINE_PARAMETERS, + () -> super.visitCteContainer( cteContainer ) + ); + } + + @Override + protected boolean needsCteInlining() { + // CTEs in H2 are just so buggy, that we can't reliably use them + return true; + } + + @Override + protected boolean shouldInlineCte(TableGroup tableGroup) { + return tableGroup instanceof CteTableGroup + && !getCteStatement( tableGroup.getPrimaryTableReference().getTableId() ).isRecursive(); + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + + @Override + protected boolean supportsRowConstructor() { + return getDialect().getVersion().isSameOrAfter( 2 ); + } + + @Override + protected boolean supportsArrayConstructor() { + return getDialect().getVersion().isSameOrAfter( 2 ); + } + + @Override + protected String getArrayContainsFunction() { + return "array_contains"; + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -84,16 +127,6 @@ public class H2LegacySqlAstTranslator extends AbstractS } } - @Override - protected void renderSearchClause(CteStatement cte) { - // H2 does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // H2 does not support this, but it can be emulated - } - @Override protected void renderSelectTupleComparison( List lhsExpressions, diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java index 3c8f514665..c14426442a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java @@ -59,6 +59,39 @@ public class HSQLLegacySqlAstTranslator extends Abstrac } } + @Override + protected boolean supportsArrayConstructor() { + return true; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + // Doesn't support correlations in the WITH clause + return false; + } + + @Override + protected boolean supportsRecursiveClauseArrayAndRowEmulation() { + // Even though HSQL supports the array constructor, it's illegal to use arrays in CTEs + return false; + } + + @Override + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + // HSQL determines the type and size of a column in a recursive CTE based on the expression of the non-recursive part + // Due to that, we have to cast the path in the non-recursive path to a varchar of appropriate size to avoid data truncation errors + if ( sizeEstimate == -1 ) { + super.visitRecursivePath( recursivePath, sizeEstimate ); + } + else { + appendSql( "cast(" ); + recursivePath.accept( this ); + appendSql( " as varchar(" ); + appendSql( sizeEstimate ); + appendSql( "))" ); + } + } + // HSQL does not allow CASE expressions where all result arms contain plain parameters. // At least one result arm must provide some type context for inference, // so we cast the first result arm if we encounter this condition @@ -190,16 +223,6 @@ public class HSQLLegacySqlAstTranslator extends Abstrac } } - @Override - protected void renderSearchClause(CteStatement cte) { - // HSQL does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // HSQL does not support this, but it can be emulated - } - @Override protected void renderSelectExpression(Expression expression) { renderSelectExpressionWithCastedOrInlinedPlainParameters( expression ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java index d5e9e9684d..8c20677347 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java @@ -86,16 +86,6 @@ public class InformixSqlAstTranslator extends AbstractS } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Informix does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Informix does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java index a5a6c75c15..63b94af516 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java @@ -93,16 +93,6 @@ public class IngresSqlAstTranslator extends AbstractSql } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Ingres does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Ingres does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index bab40f01b6..161c68b035 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -50,7 +50,8 @@ public class MariaDBLegacyDialect extends MySQLLegacyDialect { } public MariaDBLegacyDialect(DialectResolutionInfo info) { - super(info); + super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + registerKeywords( info ); } @Override @@ -101,6 +102,17 @@ public class MariaDBLegacyDialect extends MySQLLegacyDialect { return getVersion().isSameOrAfter( 10, 2 ); } + @Override + public boolean supportsLateral() { + // See https://jira.mariadb.org/browse/MDEV-19078 + return false; + } + + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 10, 2 ); + } + @Override public boolean supportsColumnCheck() { return getVersion().isSameOrAfter( 10, 2 ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java index 000de9c9f4..d9fe9108c1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java @@ -32,11 +32,37 @@ public class MariaDBLegacySqlAstTranslator extends Abst super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return getDialect().getVersion().isSameOrAfter( 10, 2 ); + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); } + @Override + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + // MariaDB determines the type and size of a column in a recursive CTE based on the expression of the non-recursive part + // Due to that, we have to cast the path in the non-recursive path to a varchar of appropriate size to avoid data truncation errors + if ( sizeEstimate == -1 ) { + super.visitRecursivePath( recursivePath, sizeEstimate ); + } + else { + appendSql( "cast(" ); + recursivePath.accept( this ); + appendSql( " as char(" ); + appendSql( sizeEstimate ); + appendSql( "))" ); + } + } + @Override public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { final boolean isNegated = booleanExpressionPredicate.isNegated(); @@ -91,16 +117,6 @@ public class MariaDBLegacySqlAstTranslator extends Abst } } - @Override - protected void renderSearchClause(CteStatement cte) { - // MariaDB does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // MariaDB does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonDistinctOperator( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java index 09102b5f69..a87ef8057f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java @@ -38,16 +38,6 @@ public class MaxDBSqlAstTranslator extends AbstractSqlA renderLimitOffsetClause( queryPart ); } - @Override - protected void renderSearchClause(CteStatement cte) { - // MaxDB does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // MaxDB does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java index 44edca3c48..652833630c 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java @@ -38,16 +38,6 @@ public class MimerSQLSqlAstTranslator extends AbstractS renderOffsetFetchClause( queryPart, true ); } - @Override - protected void renderSearchClause(CteStatement cte) { - // MimerSQL does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // MimerSQL does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index e0f4751112..0af0f33373 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -123,17 +123,35 @@ public class MySQLLegacyDialect extends Dialect { } public MySQLLegacyDialect(DatabaseVersion version) { + this( version, 4 ); + } + + public MySQLLegacyDialect(DatabaseVersion version, int bytesPerCharacter) { super( version ); - registerKeyword( "key" ); - maxVarcharLength = maxVarcharLength( getMySQLVersion(), 4 ); //conservative assumption + maxVarcharLength = maxVarcharLength( getMySQLVersion(), bytesPerCharacter ); //conservative assumption maxVarbinaryLength = maxVarbinaryLength( getMySQLVersion() ); } public MySQLLegacyDialect(DialectResolutionInfo info) { - super( info ); - int bytesPerCharacter = getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ); - maxVarcharLength = maxVarcharLength( getMySQLVersion(), bytesPerCharacter ); - maxVarbinaryLength = maxVarbinaryLength( getMySQLVersion() ); + this( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + registerKeywords( info ); + } + + protected static DatabaseVersion createVersion(DialectResolutionInfo info) { + final String versionString = info.getDatabaseVersion(); + final String[] components = versionString.split( "\\." ); + if ( components.length >= 3 ) { + try { + final int majorVersion = Integer.parseInt( components[0] ); + final int minorVersion = Integer.parseInt( components[1] ); + final int patchLevel = Integer.parseInt( components[2] ); + return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return info.makeCopy(); } @Override @@ -518,9 +536,9 @@ public class MySQLLegacyDialect extends Dialect { // MySQL timestamp type defaults to precision 0 (seconds) but // we want the standard default precision of 6 (microseconds) functionFactory.sysdateExplicitMicros(); - if ( getMySQLVersion().isSameOrAfter( 8, 2 ) ) { + if ( getMySQLVersion().isSameOrAfter( 8, 0, 2 ) ) { functionFactory.windowFunctions(); - if ( getMySQLVersion().isSameOrAfter( 8, 11 ) ) { + if ( getMySQLVersion().isSameOrAfter( 8, 0, 11 ) ) { functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); } } @@ -1211,12 +1229,17 @@ public class MySQLLegacyDialect extends Dialect { @Override public boolean supportsWindowFunctions() { - return getMySQLVersion().isSameOrAfter( 8, 2 ); + return getMySQLVersion().isSameOrAfter( 8, 0, 2 ); } @Override public boolean supportsLateral() { - return getMySQLVersion().isSameOrAfter( 8, 14 ); + return getMySQLVersion().isSameOrAfter( 8, 0, 14 ); + } + + @Override + public boolean supportsRecursiveCTE() { + return getMySQLVersion().isSameOrAfter( 8, 0, 14 ); } @Override @@ -1240,6 +1263,12 @@ public class MySQLLegacyDialect extends Dialect { return supportsAliasLocks() ? RowLockStrategy.TABLE : RowLockStrategy.NONE; } + @Override + protected void registerDefaultKeywords() { + super.registerDefaultKeywords(); + registerKeyword( "key" ); + } + boolean supportsForShare() { return getMySQLVersion().isSameOrAfter( 8 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java index 9e39f1ea70..a368ebce7e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java @@ -10,7 +10,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; @@ -38,6 +37,22 @@ public class MySQLLegacySqlAstTranslator extends Abstra expression.accept( this ); } + @Override + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + // MySQL determines the type and size of a column in a recursive CTE based on the expression of the non-recursive part + // Due to that, we have to cast the path in the non-recursive path to a varchar of appropriate size to avoid data truncation errors + if ( sizeEstimate == -1 ) { + super.visitRecursivePath( recursivePath, sizeEstimate ); + } + else { + appendSql( "cast(" ); + recursivePath.accept( this ); + appendSql( " as char(" ); + appendSql( sizeEstimate ); + appendSql( "))" ); + } + } + @Override public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { final boolean isNegated = booleanExpressionPredicate.isNegated(); @@ -103,16 +118,6 @@ public class MySQLLegacySqlAstTranslator extends Abstra } } - @Override - protected void renderSearchClause(CteStatement cte) { - // MySQL does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // MySQL does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonDistinctOperator( lhs, operator, rhs ); @@ -160,6 +165,11 @@ public class MySQLLegacySqlAstTranslator extends Abstra return true; } + @Override + protected boolean supportsWithClause() { + return getDialect().getVersion().isSameOrAfter( 8 ); + } + @Override protected String getFromDual() { return " from dual"; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index f049e41a5a..7ed8adcf3e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -1122,6 +1122,11 @@ public class OracleLegacyDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 11, 2 ); + } + @Override public boolean supportsLateral() { return getVersion().isSameOrAfter( 12, 1 ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java index 01345f5138..7705b03802 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java @@ -21,6 +21,7 @@ import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.Expression; @@ -54,6 +55,37 @@ public class OracleLegacySqlAstTranslator extends Abstr super( sessionFactory, statement ); } + @Override + protected boolean needsRecursiveKeywordInWithClause() { + return false; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + // Oracle has some limitations, see ORA-32034, so we just report false here for simplicity + return false; + } + + @Override + protected boolean supportsRecursiveSearchClause() { + return true; + } + + @Override + protected boolean supportsRecursiveCycleClause() { + return true; + } + + @Override + public void visitSqlSelection(SqlSelection sqlSelection) { + if ( getCurrentCteStatement() != null ) { + if ( getCurrentCteStatement().getMaterialization() == CteMaterialization.MATERIALIZED ) { + appendSql( "/*+ materialize */ " ); + } + } + super.visitSqlSelection( sqlSelection ); + } + @Override protected LockStrategy determineLockingStrategy( QuerySpec querySpec, @@ -169,31 +201,20 @@ public class OracleLegacySqlAstTranslator extends Abstr true, // we need select aliases to avoid ORA-00918: column ambiguously defined () -> { final QueryPart currentQueryPart = getQueryPartStack().getCurrent(); - final boolean needsParenthesis; final boolean needsWrapper; if ( currentQueryPart instanceof QueryGroup ) { - needsParenthesis = false; // visitQuerySpec will add the select wrapper needsWrapper = !currentQueryPart.hasOffsetOrFetchClause(); } else { - needsParenthesis = !querySpec.isRoot(); needsWrapper = true; } if ( needsWrapper ) { - if ( needsParenthesis ) { - appendSql( '(' ); - } - appendSql( "select * from " ); - if ( !needsParenthesis ) { - appendSql( '(' ); - } + appendSql( "select * from (" ); } super.visitQuerySpec( querySpec ); if ( needsWrapper ) { - if ( !needsParenthesis ) { - appendSql( ')' ); - } + appendSql( ')' ); } appendSql( " where rownum<=" ); final Stack clauseStack = getClauseStack(); @@ -209,12 +230,6 @@ public class OracleLegacySqlAstTranslator extends Abstr finally { clauseStack.pop(); } - - if ( needsWrapper ) { - if ( needsParenthesis ) { - appendSql( ')' ); - } - } } ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java index 1e07bffa17..21a3cadb14 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java @@ -60,6 +60,16 @@ public class PostgreSQLLegacySqlAstTranslator extends A } } + @Override + protected boolean supportsRowConstructor() { + return true; + } + + @Override + protected boolean supportsArrayConstructor() { + return true; + } + @Override public boolean supportsFilterClause() { return getDialect().getVersion().isSameOrAfter( 9, 4 ); @@ -117,13 +127,27 @@ public class PostgreSQLLegacySqlAstTranslator extends A } @Override - protected void renderSearchClause(CteStatement cte) { - // PostgreSQL does not support this, but it's just a hint anyway + protected boolean supportsRecursiveSearchClause() { + return getDialect().getVersion().isSameOrAfter( 14 ); } @Override - protected void renderCycleClause(CteStatement cte) { - // PostgreSQL does not support this, but it can be emulated + protected boolean supportsRecursiveCycleClause() { + return getDialect().getVersion().isSameOrAfter( 14 ); + } + + @Override + protected boolean supportsRecursiveCycleUsingClause() { + return getDialect().getVersion().isSameOrAfter( 14 ); + } + + @Override + protected void renderStandardCycleClause(CteStatement cte) { + super.renderStandardCycleClause( cte ); + if ( cte.getCycleMarkColumn() != null && cte.getCyclePathColumn() == null && supportsRecursiveCycleUsingClause() ) { + appendSql( " using " ); + appendSql( determineCyclePathColumnName( cte ) ); + } } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java index c6291f3ec1..307a2f1918 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java @@ -77,16 +77,6 @@ public class RDMSOS2200SqlAstTranslator extends Abstrac return true; } - @Override - protected void renderSearchClause(CteStatement cte) { - // Unisys 2200 does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Unisys 2200 does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index ae52b1bcf2..afa43b875a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -614,6 +614,11 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { return getVersion().isSameOrAfter( 9 ); } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 9 ); + } + @Override public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 11 ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java index 8bb73eabde..f881f2bf05 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java @@ -52,6 +52,16 @@ public class SQLServerLegacySqlAstTranslator extends Ab super( sessionFactory, statement ); } + @Override + protected boolean needsRecursiveKeywordInWithClause() { + return false; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + @Override protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List tableGroupJoinCollector) { appendSql( WHITESPACE ); @@ -87,6 +97,10 @@ public class SQLServerLegacySqlAstTranslator extends Ab } protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { + if ( shouldInlineCte( tableGroup ) ) { + inlineCteTableGroup( tableGroup, lockMode ); + return false; + } final TableReference tableReference = tableGroup.getPrimaryTableReference(); if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); @@ -377,16 +391,6 @@ public class SQLServerLegacySqlAstTranslator extends Ab } } - @Override - protected void renderSearchClause(CteStatement cte) { - // SQL Server does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // SQL Server does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteSqlAstTranslator.java index 06581646d5..e17ba3b41b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteSqlAstTranslator.java @@ -105,16 +105,6 @@ public class SQLiteSqlAstTranslator extends AbstractSql } } - @Override - protected void renderSearchClause(CteStatement cte) { - // SQLite does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // SQLite does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { if ( rhs instanceof Any ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java index c2fe947702..2924f0e4b3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java @@ -49,6 +49,11 @@ public class SybaseASELegacySqlAstTranslator extends Ab super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return false; + } + // Sybase ASE does not allow CASE expressions where all result arms contain plain parameters. // At least one result arm must provide some type context for inference, // so we cast the first result arm if we encounter this condition @@ -152,16 +157,6 @@ public class SybaseASELegacySqlAstTranslator extends Ab super.renderForUpdateClause( querySpec, forUpdateClause ); } - @Override - protected void renderSearchClause(CteStatement cte) { - // Sybase ASE does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Sybase ASE does not support this, but it can be emulated - } - @Override protected void visitSqlSelections(SelectClause selectClause) { if ( supportsTopClause() ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java index 39bf2041be..8d2ebd40fc 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java @@ -157,16 +157,6 @@ public class SybaseAnywhereSqlAstTranslator extends Abs } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Sybase Anywhere does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Sybase Anywhere does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java index b496b097d2..39e6561859 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java @@ -39,6 +39,11 @@ public class SybaseLegacySqlAstTranslator extends Abstr super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return false; + } + // Sybase does not allow CASE expressions where all result arms contain plain parameters. // At least one result arm must provide some type context for inference, // so we cast the first result arm if we encounter this condition @@ -105,16 +110,6 @@ public class SybaseLegacySqlAstTranslator extends Abstr // Sybase does not support the FOR UPDATE clause } - @Override - protected void renderSearchClause(CteStatement cte) { - // Sybase does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Sybase does not support this, but it can be emulated - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { assertRowsOnlyFetchClauseType( queryPart ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataSqlAstTranslator.java index edbad12b8f..717aeb22a5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataSqlAstTranslator.java @@ -101,16 +101,6 @@ public class TeradataSqlAstTranslator extends AbstractS } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Teradata does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Teradata does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java index 177c0a3648..41fc922c81 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java @@ -89,16 +89,6 @@ public class TimesTenSqlAstTranslator extends AbstractS } } - @Override - protected void renderSearchClause(CteStatement cte) { - // TimesTen does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // TimesTen does not support this, but it can be emulated - } - @Override protected void visitSqlSelections(SelectClause selectClause) { renderRowsToClause( (QuerySpec) getQueryPartStack().getCurrent() ); diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index 17ed4e5420..1aa980ec9d 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -149,6 +149,7 @@ ASC : [aA] [sS] [cC]; AVG : [aA] [vV] [gG]; BETWEEN : [bB] [eE] [tT] [wW] [eE] [eE] [nN]; BOTH : [bB] [oO] [tT] [hH]; +BREADTH : [bB] [rR] [eE] [aA] [dD] [tT] [hH]; BY : [bB] [yY]; CASE : [cC] [aA] [sS] [eE]; CAST : [cC] [aA] [sS] [tT]; @@ -161,10 +162,13 @@ CURRENT_DATE : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [dD] [aA] [tT] [eE]; CURRENT_INSTANT : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [iI] [nN] [sS] [tT] [aA] [nN] [tT]; //deprecated legacy CURRENT_TIME : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [tT] [iI] [mM] [eE]; CURRENT_TIMESTAMP : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [tT] [iI] [mM] [eE] [sS] [tT] [aA] [mM] [pP]; +CYCLE : [cC] [yY] [cC] [lL] [eE]; DATE : [dD] [aA] [tT] [eE]; DATETIME : [dD] [aA] [tT] [eE] [tT] [iI] [mM] [eE]; DAY : [dD] [aA] [yY]; +DEFAULT : [dD] [eE] [fF] [aA] [uU] [lL] [tT]; DELETE : [dD] [eE] [lL] [eE] [tT] [eE]; +DEPTH : [dD] [eE] [pP] [tT] [hH]; DESC : [dD] [eE] [sS] [cC]; DISTINCT : [dD] [iI] [sS] [tT] [iI] [nN] [cC] [tT]; ELEMENT : [eE] [lL] [eE] [mM] [eE] [nN] [tT]; @@ -219,6 +223,7 @@ LOCAL_DATE : [lL] [oO] [cC] [aA] [lL] '_' [dD] [aA] [tT] [eE]; LOCAL_DATETIME : [lL] [oO] [cC] [aA] [lL] '_' [dD] [aA] [tT] [eE] [tT] [iI] [mM] [eE]; LOCAL_TIME : [lL] [oO] [cC] [aA] [lL] '_' [tT] [iI] [mM] [eE]; MAP : [mM] [aA] [pP]; +MATERIALIZED : [mM] [aA] [tT] [eE] [rR] [iI] [aA] [lL] [iI] [zZ] [eE] [dD]; MAX : [mM] [aA] [xX]; MAXELEMENT : [mM] [aA] [xX] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; MAXINDEX : [mM] [aA] [xX] [iI] [nN] [dD] [eE] [xX]; @@ -262,6 +267,7 @@ RIGHT : [rR] [iI] [gG] [hH] [tT]; ROLLUP : [rR] [oO] [lL] [lL] [uU] [pP]; ROW : [rR] [oO] [wW]; ROWS : [rR] [oO] [wW] [sS]; +SEARCH : [sS] [eE] [aA] [rR] [cC] [hH]; SECOND : [sS] [eE] [cC] [oO] [nN] [dD]; SELECT : [sS] [eE] [lL] [eE] [cC] [tT]; SET : [sS] [eE] [tT]; @@ -275,6 +281,7 @@ TIME : [tT] [iI] [mM] [eE]; TIMESTAMP : [tT] [iI] [mM] [eE] [sS] [tT] [aA] [mM] [pP]; TIMEZONE_HOUR : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [hH] [oO] [uU] [rR]; TIMEZONE_MINUTE : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [mM] [iI] [nN] [uU] [tT] [eE]; +TO : [tT] [oO]; TRAILING : [tT] [rR] [aA] [iI] [lL] [iI] [nN] [gG]; TREAT : [tT] [rR] [eE] [aA] [tT]; TRIM : [tT] [rR] [iI] [mM]; @@ -283,6 +290,7 @@ TYPE : [tT] [yY] [pP] [eE]; UNBOUNDED : [uU] [nN] [bB] [oO] [uU] [nN] [dD] [eE] [dD]; UNION : [uU] [nN] [iI] [oO] [nN]; UPDATE : [uU] [pP] [dD] [aA] [tT] [eE]; +USING : [uU] [sS] [iI] [nN] [gG]; VALUE : [vV] [aA] [lL] [uU] [eE]; VALUES : [vV] [aA] [lL] [uU] [eE] [sS]; WEEK : [wW] [eE] [eE] [kK]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 279cf84587..c9d85f8e49 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -110,12 +110,40 @@ values // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // QUERY SPEC - general structure of root sqm or sub sqm +withClause + : WITH cte (COMMA cte)* + ; + +cte + : identifier AS (NOT? MATERIALIZED)? LEFT_PAREN queryExpression RIGHT_PAREN searchClause? cycleClause? + ; + +cteAttributes + : identifier (COMMA identifier)* + ; + +searchClause + : SEARCH (BREADTH|DEPTH) FIRST BY searchSpecifications SET identifier + ; + +searchSpecifications + : searchSpecification (COMMA searchSpecification)* + ; + +searchSpecification + : identifier sortDirection? nullsPrecedence? + ; + +cycleClause + : CYCLE cteAttributes SET identifier (TO literal DEFAULT literal)? (USING identifier)? + ; + /** * A toplevel query of subquery, which may be a union or intersection of subqueries */ queryExpression - : orderedQuery # SimpleQueryGroup - | orderedQuery (setOperator orderedQuery)+ # SetQueryGroup + : withClause? orderedQuery # SimpleQueryGroup + | withClause? orderedQuery (setOperator orderedQuery)+ # SetQueryGroup ; /** @@ -1482,6 +1510,7 @@ rollup | AVG | BETWEEN | BOTH + | BREADTH | BY | CASE | CAST @@ -1494,10 +1523,13 @@ rollup | CURRENT_INSTANT | CURRENT_TIME | CURRENT_TIMESTAMP + | CYCLE | DATE | DATETIME | DAY + | DEFAULT | DELETE + | DEPTH | DESC | DISTINCT | ELEMENT @@ -1552,6 +1584,7 @@ rollup | LOCAL_DATETIME | LOCAL_TIME | MAP + | MATERIALIZED | MAX | MAXELEMENT | MAXINDEX @@ -1596,6 +1629,7 @@ rollup | ROLLUP | ROW | ROWS + | SEARCH | SECOND | SELECT | SET @@ -1609,6 +1643,7 @@ rollup | TIMESTAMP | TIMEZONE_HOUR | TIMEZONE_MINUTE + | TO | TRAILING | TREAT | TRIM @@ -1617,6 +1652,7 @@ rollup | UNBOUNDED | UNION | UPDATE + | USING | VALUE | VALUES | VERSION diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/BulkOperationCleanupAction.java b/hibernate-core/src/main/java/org/hibernate/action/internal/BulkOperationCleanupAction.java index 03fd27bd99..8ef40fb966 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/BulkOperationCleanupAction.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/BulkOperationCleanupAction.java @@ -29,6 +29,7 @@ import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.query.sqm.tree.SqmQuery; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.sql.ast.tree.insert.InsertStatement; @@ -151,8 +152,8 @@ public class BulkOperationCleanupAction implements Executable, Serializable { entityPersisters.add( metamodel.getEntityDescriptor( statement.getTarget().getEntityName() ) ); } for ( SqmCteStatement cteStatement : statement.getCteStatements() ) { - final SqmStatement cteDefinition = cteStatement.getCteDefinition(); - if ( cteDefinition instanceof SqmDmlStatement && !( cteDefinition instanceof InsertStatement ) ) { + final SqmQuery cteDefinition = cteStatement.getCteDefinition(); + if ( cteDefinition instanceof SqmDmlStatement ) { entityPersisters.add( metamodel.getEntityDescriptor( ( (SqmDmlStatement) cteDefinition ).getTarget().getEntityName() ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java index 28972252f2..33c2c93e77 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractTransactSQLDialect.java @@ -290,6 +290,11 @@ public abstract class AbstractTransactSQLDialect extends Dialect { return NullOrdering.SMALLEST; } + @Override + public boolean requiresCastForConcatenatingNonStrings() { + return true; + } + @Override public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( EntityMappingType entityDescriptor, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 1b21c809ea..2ae0e0f576 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -474,6 +474,11 @@ public class CockroachDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public String getNoColumnsInsertString() { return "default values"; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java index ee465220fe..5ffef6d983 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java @@ -9,6 +9,7 @@ package org.hibernate.dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -52,6 +53,24 @@ public class CockroachSqlAstTranslator extends Abstract } } + @Override + protected void renderMaterializationHint(CteMaterialization materialization) { + if ( materialization == CteMaterialization.NOT_MATERIALIZED ) { + appendSql( "not " ); + } + appendSql( "materialized " ); + } + + @Override + protected boolean supportsRowConstructor() { + return true; + } + + @Override + protected boolean supportsArrayConstructor() { + return true; + } + @Override protected String getForShare(int timeoutMillis) { return " for share"; @@ -90,16 +109,6 @@ public class CockroachSqlAstTranslator extends Abstract } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Cockroach does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Cockroach does not support this, but it can be emulated - } - @Override protected void renderPartitionItem(Expression expression) { if ( expression instanceof Literal ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 0419b72512..b07c9321ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -507,6 +507,11 @@ public class DB2Dialect extends Dialect { return false; } + @Override + public boolean requiresCastForConcatenatingNonStrings() { + return true; + } + @Override public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { return selectNullString(sqlType); @@ -729,6 +734,12 @@ public class DB2Dialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + // Supported at last since 9.7 + return true; + } + @Override public boolean supportsOffsetInSubquery() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java index 5dcae6028d..ed48628536 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java @@ -9,6 +9,7 @@ package org.hibernate.dialect; import java.util.List; import java.util.function.Consumer; +import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; @@ -25,6 +26,9 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; import org.hibernate.sql.ast.tree.insert.InsertStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; @@ -44,6 +48,70 @@ public class DB2SqlAstTranslator extends AbstractSqlAst super( sessionFactory, statement ); } + @Override + protected boolean needsRecursiveKeywordInWithClause() { + return false; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + + @Override + protected void renderTableReferenceJoins(TableGroup tableGroup) { + // When we are in a recursive CTE, we can't render joins on DB2... + // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 + if ( isInRecursiveQueryPart() ) { + final List joins = tableGroup.getTableReferenceJoins(); + if ( joins == null || joins.isEmpty() ) { + return; + } + + for ( TableReferenceJoin tableJoin : joins ) { + switch ( tableJoin.getJoinType() ) { + case CROSS: + case INNER: + break; + default: + throw new UnsupportedOperationException( "Can't emulate '" + tableJoin.getJoinType().getText() + "join' in a DB2 recursive query part" ); + } + appendSql( COMA_SEPARATOR_CHAR ); + + renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + + if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { + addAdditionalWherePredicate( tableJoin.getPredicate() ); + } + } + } + else { + super.renderTableReferenceJoins( tableGroup ); + } + } + + @Override + protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List tableGroupJoinCollector) { + if ( isInRecursiveQueryPart() ) { + switch ( tableGroupJoin.getJoinType() ) { + case CROSS: + case INNER: + break; + default: + throw new UnsupportedOperationException( "Can't emulate '" + tableGroupJoin.getJoinType().getText() + "join' in a DB2 recursive query part" ); + } + appendSql( COMA_SEPARATOR_CHAR ); + + renderTableGroup( tableGroupJoin.getJoinedGroup(), null, tableGroupJoinCollector ); + if ( tableGroupJoin.getPredicate() != null && !tableGroupJoin.getPredicate().isEmpty() ) { + addAdditionalWherePredicate( tableGroupJoin.getPredicate() ); + } + } + else { + super.renderTableGroupJoin( tableGroupJoin, tableGroupJoinCollector ); + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java index 035b7025c8..463328851e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java @@ -128,6 +128,11 @@ public class DB2iDialect extends DB2Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { return new StandardSqlAstTranslatorFactory() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java index 4a418b6364..b279fae6b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java @@ -123,6 +123,11 @@ public class DB2zDialect extends DB2Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { StringBuilder pattern = new StringBuilder(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java index 90d2dc8e96..f31bb410c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java @@ -54,6 +54,10 @@ public class DB2zSqlAstTranslator extends DB2SqlAstTran @Override protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { + if ( shouldInlineCte( tableGroup ) ) { + inlineCteTableGroup( tableGroup, lockMode ); + return false; + } final TableReference tableReference = tableGroup.getPrimaryTableReference(); if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java index e70f4bb50e..843008cd95 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java @@ -8,6 +8,7 @@ package org.hibernate.dialect; import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.function.CastingConcatFunction; +import org.hibernate.dialect.function.ChrLiteralEmulation; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.CountFunction; import org.hibernate.dialect.function.DerbyLpadEmulation; @@ -256,6 +257,12 @@ public class DerbyDialect extends Dialect { // It does have a function named char(), but it's really a // sort of to_char() function. + // We register an emulation instead, that can at least translate integer literals + queryEngine.getSqmFunctionRegistry().register( + "chr", + new ChrLiteralEmulation( queryEngine.getTypeConfiguration() ) + ); + functionFactory.concat_pipeOperator(); functionFactory.cot(); functionFactory.degrees(); @@ -562,6 +569,11 @@ public class DerbyDialect extends Dialect { return true; } + @Override + public boolean requiresCastForConcatenatingNonStrings() { + return true; + } + @Override public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contributeTypes( typeContributions, serviceRegistry ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java index dfee3489d0..ed66fb4940 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java @@ -41,6 +41,11 @@ public class DerbySqlAstTranslator extends AbstractSqlA super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return false; + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -125,24 +130,6 @@ public class DerbySqlAstTranslator extends AbstractSqlA return " with rs"; } - @Override - public void visitCteContainer(CteContainer cteContainer) { - if ( cteContainer.isWithRecursive() ) { - throw new IllegalArgumentException( "Recursive CTEs can't be emulated" ); - } - super.visitCteContainer( cteContainer ); - } - - @Override - protected void renderSearchClause(CteStatement cte) { - // Derby does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Derby does not support this, but it can be emulated - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { // Derby only supports the OFFSET and FETCH clause with ROWS diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index baaef6a831..ce7a6f96d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -2991,6 +2991,16 @@ public abstract class Dialect implements ConversionContext { return true; } + /** + * Does this dialect/database require casting of non-string arguments in a concat function? + * + * @return {@code true} if casting of non-string arguments in concat is required + * @since 6.2 + */ + public boolean requiresCastForConcatenatingNonStrings() { + return false; + } + /** * Does this dialect require that integer divisions be wrapped in {@code cast()} * calls to tell the db parser the expected type. @@ -3574,6 +3584,16 @@ public abstract class Dialect implements ConversionContext { return false; } + /** + * Does this dialect/database support recursive CTEs (Common Table Expressions)? + * + * @return {@code true} if recursive CTEs are supported + * @since 6.2 + */ + public boolean supportsRecursiveCTE() { + return false; + } + /** * Does this dialect support {@code values} lists of form * {@code VALUES (1), (2), (3)}? diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 8b8d19269c..dad4ac78e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -735,6 +735,11 @@ public class H2Dialect extends Dialect { return getVersion().isSameOrAfter( 1, 4, 200 ); } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 1, 4, 196 ); + } + @Override public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 1, 4, 198 ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 84e47e1df8..f420ea26b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -17,7 +17,9 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.cte.CteTableGroup; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -46,6 +48,47 @@ public class H2SqlAstTranslator extends AbstractSqlAstT super( sessionFactory, statement ); } + @Override + public void visitCteContainer(CteContainer cteContainer) { + // H2 has various bugs in different versions that make it impossible to use CTEs with parameters reliably + withParameterRenderingMode( + SqlAstNodeRenderingMode.INLINE_PARAMETERS, + () -> super.visitCteContainer( cteContainer ) + ); + } + + @Override + protected boolean needsCteInlining() { + // CTEs in H2 are just so buggy, that we can't reliably use them + return true; + } + + @Override + protected boolean shouldInlineCte(TableGroup tableGroup) { + return tableGroup instanceof CteTableGroup + && !getCteStatement( tableGroup.getPrimaryTableReference().getTableId() ).isRecursive(); + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + + @Override + protected boolean supportsRowConstructor() { + return getDialect().getVersion().isSameOrAfter( 2 ); + } + + @Override + protected boolean supportsArrayConstructor() { + return getDialect().getVersion().isSameOrAfter( 2 ); + } + + @Override + protected String getArrayContainsFunction() { + return "array_contains"; + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -84,16 +127,6 @@ public class H2SqlAstTranslator extends AbstractSqlAstT } } - @Override - protected void renderSearchClause(CteStatement cte) { - // H2 does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // H2 does not support this, but it can be emulated - } - @Override protected void renderSelectTupleComparison( List lhsExpressions, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java index 07e2bf47d5..84551fb440 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java @@ -44,6 +44,18 @@ public class HANASqlAstTranslator extends AbstractSqlAs && !isRowsOnlyFetchClauseType( queryPart ); } + @Override + protected boolean supportsWithClauseInSubquery() { + // HANA doesn't seem to support correlation, so we just report false here for simplicity + return false; + } + + @Override + protected boolean isCorrelated(CteStatement cteStatement) { + // Report false here, because apparently HANA does not need the "lateral" keyword to correlate a from clause subquery in a subquery + return false; + } + @Override public void visitQueryGroup(QueryGroup queryGroup) { if ( shouldEmulateFetchClause( queryGroup ) ) { @@ -95,16 +107,6 @@ public class HANASqlAstTranslator extends AbstractSqlAs } } - @Override - protected void renderSearchClause(CteStatement cte) { - // HANA does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // HANA does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index bd52b89988..03e5427467 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -632,6 +632,11 @@ public class HSQLDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return getVersion().isSameOrAfter( 2 ); + } + @Override public boolean requiresFloatCastingOfIntegerDivision() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java index 7ab116a3ee..e9a4674b6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java @@ -17,7 +17,6 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; @@ -58,6 +57,39 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs } } + @Override + protected boolean supportsArrayConstructor() { + return true; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + // Doesn't support correlations in the WITH clause + return false; + } + + @Override + protected boolean supportsRecursiveClauseArrayAndRowEmulation() { + // Even though HSQL supports the array constructor, it's illegal to use arrays in CTEs + return false; + } + + @Override + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + // HSQL determines the type and size of a column in a recursive CTE based on the expression of the non-recursive part + // Due to that, we have to cast the path in the non-recursive path to a varchar of appropriate size to avoid data truncation errors + if ( sizeEstimate == -1 ) { + super.visitRecursivePath( recursivePath, sizeEstimate ); + } + else { + appendSql( "cast(" ); + recursivePath.accept( this ); + appendSql( " as varchar(" ); + appendSql( sizeEstimate ); + appendSql( "))" ); + } + } + // HSQL does not allow CASE expressions where all result arms contain plain parameters. // At least one result arm must provide some type context for inference, // so we cast the first result arm if we encounter this condition @@ -170,16 +202,6 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs } } - @Override - protected void renderSearchClause(CteStatement cte) { - // HSQL does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // HSQL does not support this, but it can be emulated - } - @Override protected void renderSelectExpression(Expression expression) { renderSelectExpressionWithCastedOrInlinedPlainParameters( expression ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index 1f24fd3efe..39d377e27e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -56,7 +56,8 @@ public class MariaDBDialect extends MySQLDialect { } public MariaDBDialect(DialectResolutionInfo info) { - super(info); + super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + registerKeywords( info ); } @Override @@ -149,6 +150,17 @@ public class MariaDBDialect extends MySQLDialect { return true; } + @Override + public boolean supportsLateral() { + // See https://jira.mariadb.org/browse/MDEV-19078 + return false; + } + + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public boolean supportsColumnCheck() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java index ff2f4002c8..370c368e86 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java @@ -32,11 +32,32 @@ public class MariaDBSqlAstTranslator extends AbstractSq super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); } + @Override + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + // MariaDB determines the type and size of a column in a recursive CTE based on the expression of the non-recursive part + // Due to that, we have to cast the path in the non-recursive path to a varchar of appropriate size to avoid data truncation errors + if ( sizeEstimate == -1 ) { + super.visitRecursivePath( recursivePath, sizeEstimate ); + } + else { + appendSql( "cast(" ); + recursivePath.accept( this ); + appendSql( " as char(" ); + appendSql( sizeEstimate ); + appendSql( "))" ); + } + } + @Override public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { final boolean isNegated = booleanExpressionPredicate.isNegated(); @@ -91,16 +112,6 @@ public class MariaDBSqlAstTranslator extends AbstractSq } } - @Override - protected void renderSearchClause(CteStatement cte) { - // MariaDB does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // MariaDB does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonDistinctOperator( lhs, operator, rhs ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index cde1f711eb..e015d470a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -119,17 +119,37 @@ public class MySQLDialect extends Dialect { } public MySQLDialect(DatabaseVersion version) { + this( version, 4 ); + } + + public MySQLDialect(DatabaseVersion version, int bytesPerCharacter) { super( version ); - registerKeyword( "key" ); - maxVarcharLength = maxVarcharLength( getMySQLVersion(), 4 ); //conservative assumption + maxVarcharLength = maxVarcharLength( getMySQLVersion(), bytesPerCharacter ); //conservative assumption maxVarbinaryLength = maxVarbinaryLength( getMySQLVersion() ); } public MySQLDialect(DialectResolutionInfo info) { - super( info ); - int bytesPerCharacter = getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ); - maxVarcharLength = maxVarcharLength( getMySQLVersion(), bytesPerCharacter ); - maxVarbinaryLength = maxVarbinaryLength( getMySQLVersion() ); + this( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + registerKeywords( info ); + } + + protected static DatabaseVersion createVersion(DialectResolutionInfo info) { + final String versionString = info.getDatabaseVersion(); + if ( versionString != null ) { + final String[] components = versionString.split( "\\." ); + if ( components.length >= 3 ) { + try { + final int majorVersion = Integer.parseInt( components[0] ); + final int minorVersion = Integer.parseInt( components[1] ); + final int patchLevel = Integer.parseInt( components[2] ); + return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); + } + catch (NumberFormatException ex) { + // Ignore + } + } + } + return info.makeCopy(); } @Override @@ -532,9 +552,9 @@ public class MySQLDialect extends Dialect { // MySQL timestamp type defaults to precision 0 (seconds) but // we want the standard default precision of 6 (microseconds) functionFactory.sysdateExplicitMicros(); - if ( getMySQLVersion().isSameOrAfter( 8, 2 ) ) { + if ( getMySQLVersion().isSameOrAfter( 8, 0, 2 ) ) { functionFactory.windowFunctions(); - if ( getMySQLVersion().isSameOrAfter( 8, 11 ) ) { + if ( getMySQLVersion().isSameOrAfter( 8, 0, 11 ) ) { functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); } } @@ -1219,12 +1239,17 @@ public class MySQLDialect extends Dialect { @Override public boolean supportsWindowFunctions() { - return getMySQLVersion().isSameOrAfter( 8, 2 ); + return getMySQLVersion().isSameOrAfter( 8, 0, 2 ); } @Override public boolean supportsLateral() { - return getMySQLVersion().isSameOrAfter( 8, 14 ); + return getMySQLVersion().isSameOrAfter( 8, 0, 14 ); + } + + @Override + public boolean supportsRecursiveCTE() { + return getMySQLVersion().isSameOrAfter( 8, 0, 14 ); } @Override @@ -1248,6 +1273,12 @@ public class MySQLDialect extends Dialect { return supportsAliasLocks() ? RowLockStrategy.TABLE : RowLockStrategy.NONE; } + @Override + protected void registerDefaultKeywords() { + super.registerDefaultKeywords(); + registerKeyword( "key" ); + } + boolean supportsForShare() { return getMySQLVersion().isSameOrAfter( 8 ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java index c03167a4ef..d846a091d3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java @@ -10,7 +10,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; @@ -38,6 +37,22 @@ public class MySQLSqlAstTranslator extends AbstractSqlA expression.accept( this ); } + @Override + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + // MySQL determines the type and size of a column in a recursive CTE based on the expression of the non-recursive part + // Due to that, we have to cast the path in the non-recursive path to a varchar of appropriate size to avoid data truncation errors + if ( sizeEstimate == -1 ) { + super.visitRecursivePath( recursivePath, sizeEstimate ); + } + else { + appendSql( "cast(" ); + recursivePath.accept( this ); + appendSql( " as char(" ); + appendSql( sizeEstimate ); + appendSql( "))" ); + } + } + @Override public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { final boolean isNegated = booleanExpressionPredicate.isNegated(); @@ -103,16 +118,6 @@ public class MySQLSqlAstTranslator extends AbstractSqlA } } - @Override - protected void renderSearchClause(CteStatement cte) { - // MySQL does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // MySQL does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonDistinctOperator( lhs, operator, rhs ); @@ -170,6 +175,11 @@ public class MySQLSqlAstTranslator extends AbstractSqlA return false; } + @Override + protected boolean supportsWithClause() { + return getDialect().getVersion().isSameOrAfter( 8 ); + } + @Override protected String getFromDual() { return " from dual"; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 9ce9bad568..110a874e8b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -1073,6 +1073,11 @@ public class OracleDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public boolean supportsLateral() { return getVersion().isSameOrAfter( 12, 1 ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java index 0f4f97ad3e..7b5b70ab0d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -21,6 +21,7 @@ import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.Expression; @@ -54,6 +55,37 @@ public class OracleSqlAstTranslator extends AbstractSql super( sessionFactory, statement ); } + @Override + protected boolean needsRecursiveKeywordInWithClause() { + return false; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + // Oracle has some limitations, see ORA-32034, so we just report false here for simplicity + return false; + } + + @Override + protected boolean supportsRecursiveSearchClause() { + return true; + } + + @Override + protected boolean supportsRecursiveCycleClause() { + return true; + } + + @Override + public void visitSqlSelection(SqlSelection sqlSelection) { + if ( getCurrentCteStatement() != null ) { + if ( getCurrentCteStatement().getMaterialization() == CteMaterialization.MATERIALIZED ) { + appendSql( "/*+ materialize */ " ); + } + } + super.visitSqlSelection( sqlSelection ); + } + @Override protected LockStrategy determineLockingStrategy( QuerySpec querySpec, @@ -169,31 +201,20 @@ public class OracleSqlAstTranslator extends AbstractSql true, // we need select aliases to avoid ORA-00918: column ambiguously defined () -> { final QueryPart currentQueryPart = getQueryPartStack().getCurrent(); - final boolean needsParenthesis; final boolean needsWrapper; if ( currentQueryPart instanceof QueryGroup ) { - needsParenthesis = false; // visitQuerySpec will add the select wrapper needsWrapper = !currentQueryPart.hasOffsetOrFetchClause(); } else { - needsParenthesis = !querySpec.isRoot(); needsWrapper = true; } if ( needsWrapper ) { - if ( needsParenthesis ) { - appendSql( '(' ); - } - appendSql( "select * from " ); - if ( !needsParenthesis ) { - appendSql( '(' ); - } + appendSql( "select * from (" ); } super.visitQuerySpec( querySpec ); if ( needsWrapper ) { - if ( !needsParenthesis ) { - appendSql( ')' ); - } + appendSql( ')' ); } appendSql( " where rownum<=" ); final Stack clauseStack = getClauseStack(); @@ -209,12 +230,6 @@ public class OracleSqlAstTranslator extends AbstractSql finally { clauseStack.pop(); } - - if ( needsWrapper ) { - if ( needsParenthesis ) { - appendSql( ')' ); - } - } } ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 5b9d549785..e7106b6676 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -1184,6 +1184,11 @@ public class PostgreSQLDialect extends Dialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public boolean supportsFetchClause(FetchClauseType type) { switch ( type ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java index b7df470f71..d25a85e89f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java @@ -60,6 +60,16 @@ public class PostgreSQLSqlAstTranslator extends Abstrac } } + @Override + protected boolean supportsRowConstructor() { + return true; + } + + @Override + protected boolean supportsArrayConstructor() { + return true; + } + @Override public boolean supportsFilterClause() { return getDialect().getVersion().isSameOrAfter( 9, 4 ); @@ -117,13 +127,27 @@ public class PostgreSQLSqlAstTranslator extends Abstrac } @Override - protected void renderSearchClause(CteStatement cte) { - // PostgreSQL does not support this, but it's just a hint anyway + protected boolean supportsRecursiveSearchClause() { + return getDialect().getVersion().isSameOrAfter( 14 ); } @Override - protected void renderCycleClause(CteStatement cte) { - // PostgreSQL does not support this, but it can be emulated + protected boolean supportsRecursiveCycleClause() { + return getDialect().getVersion().isSameOrAfter( 14 ); + } + + @Override + protected boolean supportsRecursiveCycleUsingClause() { + return getDialect().getVersion().isSameOrAfter( 14 ); + } + + @Override + protected void renderStandardCycleClause(CteStatement cte) { + super.renderStandardCycleClause( cte ); + if ( cte.getCycleMarkColumn() != null && cte.getCyclePathColumn() == null && supportsRecursiveCycleUsingClause() ) { + appendSql( " using " ); + appendSql( determineCyclePathColumnName( cte ) ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 8dafca5917..b8bc500297 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -614,6 +614,11 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { return true; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 11 ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java index c4d9c75899..cb990bcf70 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java @@ -51,6 +51,16 @@ public class SQLServerSqlAstTranslator extends Abstract super( sessionFactory, statement ); } + @Override + protected boolean needsRecursiveKeywordInWithClause() { + return false; + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + @Override protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List tableGroupJoinCollector) { appendSql( WHITESPACE ); @@ -86,6 +96,10 @@ public class SQLServerSqlAstTranslator extends Abstract } protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { + if ( shouldInlineCte( tableGroup ) ) { + inlineCteTableGroup( tableGroup, lockMode ); + return false; + } final TableReference tableReference = tableGroup.getPrimaryTableReference(); if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); @@ -353,16 +367,6 @@ public class SQLServerSqlAstTranslator extends Abstract } } - @Override - protected void renderSearchClause(CteStatement cte) { - // SQL Server does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // SQL Server does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java index a79241b0b7..3aabc748c3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java @@ -61,16 +61,6 @@ public class SpannerSqlAstTranslator extends AbstractSq renderLimitOffsetClause( queryPart ); } - @Override - protected void renderSearchClause(CteStatement cte) { - // Spanner does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Spanner does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonEmulateIntersect( lhs, operator, rhs ); @@ -121,6 +111,10 @@ public class SpannerSqlAstTranslator extends AbstractSq @Override protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { + if ( shouldInlineCte( tableGroup ) ) { + inlineCteTableGroup( tableGroup, lockMode ); + return false; + } final TableReference tableReference = tableGroup.getPrimaryTableReference(); if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java index 721a11b85d..d877bcde8b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java @@ -49,6 +49,11 @@ public class SybaseASESqlAstTranslator extends Abstract super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return false; + } + // Sybase ASE does not allow CASE expressions where all result arms contain plain parameters. // At least one result arm must provide some type context for inference, // so we cast the first result arm if we encounter this condition @@ -138,16 +143,6 @@ public class SybaseASESqlAstTranslator extends Abstract } } - @Override - protected void renderSearchClause(CteStatement cte) { - // Sybase ASE does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Sybase ASE does not support this, but it can be emulated - } - @Override protected void visitSqlSelections(SelectClause selectClause) { if ( supportsTopClause() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java index ddd1a4dff3..587a194a87 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java @@ -39,6 +39,11 @@ public class SybaseSqlAstTranslator extends AbstractSql super( sessionFactory, statement ); } + @Override + protected boolean supportsWithClause() { + return false; + } + // Sybase does not allow CASE expressions where all result arms contain plain parameters. // At least one result arm must provide some type context for inference, // so we cast the first result arm if we encounter this condition @@ -105,16 +110,6 @@ public class SybaseSqlAstTranslator extends AbstractSql // Sybase does not support the FOR UPDATE clause } - @Override - protected void renderSearchClause(CteStatement cte) { - // Sybase does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // Sybase does not support this, but it can be emulated - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { assertRowsOnlyFetchClauseType( queryPart ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java index f442663fc1..532469f372 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java @@ -39,7 +39,8 @@ public class TiDBDialect extends MySQLDialect { } public TiDBDialect(DialectResolutionInfo info) { - super(info); + super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + registerKeywords( info ); } @Override @@ -98,6 +99,11 @@ public class TiDBDialect extends MySQLDialect { }; } + @Override + public boolean supportsRecursiveCTE() { + return true; + } + @Override public boolean supportsNoWait() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java index 1b66882f85..a5685b2010 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java @@ -95,16 +95,6 @@ public class TiDBSqlAstTranslator extends AbstractSqlAs } } - @Override - protected void renderSearchClause(CteStatement cte) { - // TiDB does not support this, but it's just a hint anyway - } - - @Override - protected void renderCycleClause(CteStatement cte) { - // TiDB does not support this, but it can be emulated - } - @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { renderComparisonDistinctOperator( lhs, operator, rhs ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java index 78fd84d391..0a5441f427 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java @@ -38,6 +38,7 @@ import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.results.internal.ResolvedSqlSelection; import org.hibernate.type.BasicType; @@ -417,7 +418,7 @@ public class AggregateWindowEmulationQueryTransformer implements QueryTransforme final QueryPartTableGroup queryPartTableGroup = new QueryPartTableGroup( navigablePath, null, - subQuerySpec, + new SelectStatement( subQuerySpec ), identifierVariable, columnNames, false, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/ChrLiteralEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/ChrLiteralEmulation.java new file mode 100644 index 0000000000..9f5fb677a9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/ChrLiteralEmulation.java @@ -0,0 +1,74 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function; + +import java.util.List; +import java.util.Locale; + +import org.hibernate.QueryException; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.expression.SqmLiteral; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; + +/** + * A chr implementation that translates integer literals to string literals. + * + * @author Christian Beikov + */ +public class ChrLiteralEmulation extends AbstractSqmSelfRenderingFunctionDescriptor { + + public ChrLiteralEmulation(TypeConfiguration typeConfiguration) { + super( + "chr", + new ArgumentTypesValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + (arguments, functionName, queryEngine) -> { + if ( !( arguments.get( 0 ) instanceof SqmLiteral ) ) { + throw new QueryException( + String.format( + Locale.ROOT, + "Emulation of function chr() supports only integer literals, but %s argument given", + arguments.get( 0 ).getClass().getName() + ) + ); + } + } + ), + INTEGER + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.CHARACTER ) + ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, INTEGER ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + SqlAstTranslator walker) { + @SuppressWarnings("unchecked") + final QueryLiteral literal = (QueryLiteral) arguments.get( 0 ); + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( (char) literal.getLiteralValue().intValue() ); + sqlAppender.appendSql( '\'' ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java index af83a6c844..7bd36dce98 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java @@ -27,6 +27,7 @@ import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Distinct; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Star; @@ -54,7 +55,14 @@ public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor { TypeConfiguration typeConfiguration, SqlAstNodeRenderingMode defaultArgumentRenderingMode, String concatOperator) { - this( dialect, typeConfiguration, defaultArgumentRenderingMode, concatOperator, null, false ); + this( + dialect, + typeConfiguration, + defaultArgumentRenderingMode, + concatOperator, + null, + false + ); } public CountFunction( @@ -142,6 +150,19 @@ public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor { // '' -> \0 + argumentNumber // In the end, the expression looks like the following: // count(distinct coalesce(nullif(coalesce(col1 || '', '\0'), ''), '\01') || '\0' || coalesce(nullif(coalesce(col2 || '', '\0'), ''), '\02')) + final AbstractSqmSelfRenderingFunctionDescriptor chr = + (AbstractSqmSelfRenderingFunctionDescriptor) translator.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "chr" ); + final List chrArguments = List.of( + new QueryLiteral<>( + 0, + translator.getSessionFactory() + .getTypeConfiguration() + .getBasicTypeForJavaType( Integer.class ) + ) + ); if ( caseWrapper ) { translator.getCurrentClauseStack().push( Clause.WHERE ); sqlAppender.appendSql( "case when " ); @@ -161,11 +182,16 @@ public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor { sqlAppender.appendSql( concatOperator ); sqlAppender.appendSql( "''" ); } - sqlAppender.appendSql( ",'\\0'),''),'\\0" ); + sqlAppender.appendSql( SqlAppender.COMA_SEPARATOR_CHAR ); + chr.render( sqlAppender, chrArguments, translator ); + sqlAppender.appendSql( "),'')," ); + chr.render( sqlAppender, chrArguments, translator ); + sqlAppender.appendSql( concatOperator ); + sqlAppender.appendSql( "'" ); sqlAppender.appendSql( argumentNumber ); sqlAppender.appendSql( "')" ); sqlAppender.appendSql( concatOperator ); - sqlAppender.appendSql( "'\\0'" ); + chr.render( sqlAppender, chrArguments, translator ); sqlAppender.appendSql( concatOperator ); sqlAppender.appendSql( "coalesce(nullif(coalesce(" ); needsConcat = renderCastedArgument( sqlAppender, translator, expressions.get( i ) ); @@ -175,7 +201,12 @@ public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor { sqlAppender.appendSql( concatOperator ); sqlAppender.appendSql( "''" ); } - sqlAppender.appendSql( ",'\\0'),''),'\\0" ); + sqlAppender.appendSql( SqlAppender.COMA_SEPARATOR_CHAR ); + chr.render( sqlAppender, chrArguments, translator ); + sqlAppender.appendSql( "),'')," ); + chr.render( sqlAppender, chrArguments, translator ); + sqlAppender.appendSql( concatOperator ); + sqlAppender.appendSql( "'" ); sqlAppender.appendSql( argumentNumber ); sqlAppender.appendSql( "')" ); if ( castDistinctStringConcat ) { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/Stack.java b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/Stack.java index fe44144a18..e4eb4bbef3 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/Stack.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/Stack.java @@ -32,6 +32,11 @@ public interface Stack { */ T getCurrent(); + /** + * The element currently at the bottom of the stack + */ + T getRoot(); + /** * How many elements are currently on the stack? */ diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/StandardStack.java b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/StandardStack.java index 499a8a42d2..5766946e29 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/StandardStack.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/StandardStack.java @@ -66,7 +66,15 @@ public final class StandardStack implements Stack { if ( internalStack == null ) { return null; } - return convert( internalStack.peek() ); + return convert( internalStack.peekFirst() ); + } + + @Override + public T getRoot() { + if ( internalStack == null ) { + return null; + } + return convert( internalStack.peekLast() ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 459d0e8ea4..291358d250 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -31,6 +31,7 @@ import jakarta.persistence.criteria.Selection; import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.criteria.Subquery; +import org.hibernate.Incubating; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -104,6 +105,36 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { JpaCriteriaQuery except(boolean all, CriteriaQuery query1, CriteriaQuery... queries); + default JpaSubQuery unionAll(Subquery query1, Subquery... queries) { + return union( true, query1, queries ); + } + + default JpaSubQuery union(Subquery query1, Subquery... queries) { + return union( false, query1, queries ); + } + + JpaSubQuery union(boolean all, Subquery query1, Subquery... queries); + + default JpaSubQuery intersectAll(Subquery query1, Subquery... queries) { + return intersect( true, query1, queries ); + } + + default JpaSubQuery intersect(Subquery query1, Subquery... queries) { + return intersect( false, query1, queries ); + } + + JpaSubQuery intersect(boolean all, Subquery query1, Subquery... queries); + + default JpaSubQuery exceptAll(Subquery query1, Subquery... queries) { + return except( true, query1, queries ); + } + + default JpaSubQuery except(Subquery query1, Subquery... queries) { + return except( false, query1, queries ); + } + + JpaSubQuery except(boolean all, Subquery query1, Subquery... queries); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // JPA 3.1 @@ -798,4 +829,65 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { * @return descending ordering corresponding to the expression */ JpaOrder desc(Expression x, boolean nullsFirst); + + /** + * Create a search ordering based on the sort order and null precedence of the value of the CTE attribute. + * @param cteAttribute CTE attribute used to define the ordering + * @param sortOrder The sort order + * @param nullPrecedence The null precedence + * @return ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder search(JpaCteCriteriaAttribute cteAttribute, SortOrder sortOrder, NullPrecedence nullPrecedence); + + /** + * Create a search ordering based on the sort order of the value of the CTE attribute. + * @param cteAttribute CTE attribute used to define the ordering + * @param sortOrder The sort order + * @return ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder search(JpaCteCriteriaAttribute cteAttribute, SortOrder sortOrder); + + /** + * Create a search ordering based on the ascending value of the CTE attribute. + * @param cteAttribute CTE attribute used to define the ordering + * @return ascending ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder search(JpaCteCriteriaAttribute cteAttribute); + + /** + * Create a search ordering by the ascending value of the CTE attribute. + * @param x CTE attribute used to define the ordering + * @return ascending ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder asc(JpaCteCriteriaAttribute x); + + /** + * Create a search ordering by the descending value of the CTE attribute. + * @param x CTE attribute used to define the ordering + * @return descending ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder desc(JpaCteCriteriaAttribute x); + + /** + * Create a search ordering by the ascending value of the CTE attribute. + * @param x CTE attribute used to define the ordering + * @param nullsFirst Whether null should be sorted first + * @return ascending ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder asc(JpaCteCriteriaAttribute x, boolean nullsFirst); + + /** + * Create a search ordering by the descending value of the CTE attribute. + * @param x CTE attribute used to define the ordering + * @param nullsFirst Whether null should be sorted first + * @return descending ordering corresponding to the CTE attribute + */ + @Incubating + JpaSearchOrder desc(JpaCteCriteriaAttribute x, boolean nullsFirst); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteContainer.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteContainer.java new file mode 100644 index 0000000000..606fcefd7c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteContainer.java @@ -0,0 +1,77 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import java.util.Collection; +import java.util.function.Function; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.tree.SqmJoinType; + +import jakarta.persistence.criteria.AbstractQuery; +import jakarta.persistence.criteria.CriteriaQuery; + +/** + * Common contract for criteria parts that can hold CTEs (common table expressions). + */ +@Incubating +public interface JpaCteContainer extends JpaCriteriaNode { + + /** + * Returns the CTEs that are registered on this container. + */ + Collection> getCteCriterias(); + + /** + * Returns a CTE that is registered by the given name on this container, or any of its parents. + */ + JpaCteCriteria getCteCriteria(String cteName); + + /** + * Registers the given {@link CriteriaQuery} and returns a {@link JpaCteCriteria}, + * which can be used for querying. + * + * @see JpaCriteriaQuery#from(JpaCteCriteria) + * @see JpaFrom#join(JpaCteCriteria, SqmJoinType) + */ + JpaCteCriteria with(AbstractQuery criteria); + + /** + * Allows to register a recursive CTE. The base {@link CriteriaQuery} serves + * for the structure of the {@link JpaCteCriteria}, which is made available in the recursive criteria producer function, + * so that the recursive {@link CriteriaQuery} is able to refer to the CTE again. + * + * @see JpaCriteriaQuery#from(JpaCteCriteria) + * @see JpaFrom#join(JpaCteCriteria, SqmJoinType) + */ + JpaCteCriteria withRecursiveUnionAll(AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer); + + /** + * Allows to register a recursive CTE. The base {@link CriteriaQuery} serves + * for the structure of the {@link JpaCteCriteria}, which is made available in the recursive criteria producer function, + * so that the recursive {@link CriteriaQuery} is able to refer to the CTE again. + * + * @see JpaCriteriaQuery#from(JpaCteCriteria) + * @see JpaFrom#join(JpaCteCriteria, SqmJoinType) + */ + JpaCteCriteria withRecursiveUnionDistinct(AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer); + + /** + * Like {@link #with(AbstractQuery)} but assigns an explicit CTE name. + */ + JpaCteCriteria with(String name, AbstractQuery criteria); + + /** + * Like {@link #withRecursiveUnionAll(AbstractQuery, Function)} but assigns an explicit CTE name. + */ + JpaCteCriteria withRecursiveUnionAll(String name, AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer); + + /** + * Like {@link #withRecursiveUnionDistinct(AbstractQuery, Function)} but assigns an explicit CTE name. + */ + JpaCteCriteria withRecursiveUnionDistinct(String name, AbstractQuery baseCriteria, Function, AbstractQuery> recursiveCriteriaProducer); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteria.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteria.java new file mode 100644 index 0000000000..cd26919427 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteria.java @@ -0,0 +1,125 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import java.util.Arrays; +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; +import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind; + +/** + * A CTE (common table expression) criteria. + */ +@Incubating +public interface JpaCteCriteria extends JpaCriteriaNode { + + /** + * The name under which this CTE is registered. + */ + String getName(); + + /** + * The type of the CTE. + */ + JpaCteCriteriaType getType(); + + /** + * The definition of the CTE. + */ + JpaSelectCriteria getCteDefinition(); + + /** + * The container within this CTE is registered. + */ + JpaCteContainer getCteContainer(); + + /** + * The materialization hint for the CTE. + */ + CteMaterialization getMaterialization(); + void setMaterialization(CteMaterialization materialization); + + /** + * The kind of search (breadth-first or depth-first) that should be done for a recursive query. + * May be null if unspecified or if this is not a recursive query. + */ + CteSearchClauseKind getSearchClauseKind(); + /** + * The order by which should be searched. + */ + List getSearchBySpecifications(); + /** + * The attribute name by which one can order the final CTE result, to achieve the search order. + * Note that an implicit {@link JpaCteCriteriaAttribute} will be made available for this. + */ + String getSearchAttributeName(); + + default void search(CteSearchClauseKind kind, String searchAttributeName, JpaSearchOrder... searchOrders) { + search( kind, searchAttributeName, Arrays.asList( searchOrders ) ); + } + + void search(CteSearchClauseKind kind, String searchAttributeName, List searchOrders); + + /** + * The attributes to use for cycle detection. + */ + List getCycleAttributes(); + + /** + * The attribute name which is used to mark when a cycle has been detected. + * Note that an implicit {@link JpaCteCriteriaAttribute} will be made available for this. + */ + String getCycleMarkAttributeName(); + + /** + * The attribute name that represents the computation path, which is used for cycle detection. + * Note that an implicit {@link JpaCteCriteriaAttribute} will be made available for this. + */ + String getCyclePathAttributeName(); + + /** + * The value which is set for the cycle mark attribute when a cycle is detected. + */ + Object getCycleValue(); + + /** + * The default value for the cycle mark attribute when no cycle is detected. + */ + Object getNoCycleValue(); + + default void cycle(String cycleMarkAttributeName, JpaCteCriteriaAttribute... cycleColumns) { + cycleUsing( cycleMarkAttributeName, null, Arrays.asList( cycleColumns ) ); + } + + default void cycle(String cycleMarkAttributeName, List cycleColumns) { + cycleUsing( cycleMarkAttributeName, null, true, false, cycleColumns ); + } + + default void cycleUsing(String cycleMarkAttributeName, String cyclePathAttributeName, JpaCteCriteriaAttribute... cycleColumns) { + cycleUsing( cycleMarkAttributeName, cyclePathAttributeName, Arrays.asList( cycleColumns ) ); + } + + default void cycleUsing(String cycleMarkAttributeName, String cyclePathAttributeName, List cycleColumns) { + cycleUsing( cycleMarkAttributeName, cyclePathAttributeName, true, false, cycleColumns ); + } + + default void cycle(String cycleMarkAttributeName, X cycleValue, X noCycleValue, JpaCteCriteriaAttribute... cycleColumns) { + cycleUsing( cycleMarkAttributeName, null, cycleValue, noCycleValue, Arrays.asList( cycleColumns ) ); + } + + default void cycle(String cycleMarkAttributeName, X cycleValue, X noCycleValue, List cycleColumns) { + cycleUsing( cycleMarkAttributeName, null, cycleValue, noCycleValue, cycleColumns ); + } + + default void cycleUsing(String cycleMarkAttributeName, String cyclePathAttributeName, X cycleValue, X noCycleValue, JpaCteCriteriaAttribute... cycleColumns) { + cycleUsing( cycleMarkAttributeName, cyclePathAttributeName, cycleValue, noCycleValue, Arrays.asList( cycleColumns ) ); + } + + void cycleUsing(String cycleMarkAttributeName, String cyclePathAttributeName, X cycleValue, X noCycleValue, List cycleColumns); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteriaAttribute.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteriaAttribute.java new file mode 100644 index 0000000000..7dbf3e5971 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteriaAttribute.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * Describes the attribute of a {@link JpaCteCriteriaType}. + */ +@Incubating +public interface JpaCteCriteriaAttribute extends JpaCriteriaNode { + + /** + * The declaring type. + */ + JpaCteCriteriaType getDeclaringType(); + + /** + * The name of the attribute. + */ + String getName(); + + /** + * The java type of the attribute. + */ + Class getJavaType(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteriaType.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteriaType.java new file mode 100644 index 0000000000..a85d86c9cf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCteCriteriaType.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.model.domain.DomainType; + +/** + * A CTE (common table expression) criteria type. + */ +@Incubating +public interface JpaCteCriteriaType extends JpaCriteriaNode { + + /** + * The name under which this CTE is registered. + */ + String getName(); + + /** + * The domain type of the CTE. + */ + DomainType getType(); + + /** + * The attributes of the CTE type. + */ + List getAttributes(); + + /** + * Returns the found attribute or null. + */ + JpaCteCriteriaAttribute getAttribute(String name); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java index 8b81a36080..0c1a2eff86 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java @@ -9,12 +9,14 @@ package org.hibernate.query.criteria; import org.hibernate.Incubating; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + /** * @author Christian Beikov */ @Incubating -public interface JpaDerivedJoin extends JpaDerivedFrom, SqmQualifiedJoin, JpaJoinedFrom { - +public interface JpaDerivedJoin extends JpaDerivedFrom, JpaJoinedFrom { /** * Specifies whether the subquery part can access previous from node aliases. * Normally, subqueries in the from clause are unable to access other from nodes, @@ -23,4 +25,16 @@ public interface JpaDerivedJoin extends JpaDerivedFrom, SqmQualifiedJoin on(JpaExpression restriction); + + @Override + JpaDerivedJoin on(Expression restriction); + + @Override + JpaDerivedJoin on(JpaPredicate... restrictions); + + @Override + JpaDerivedJoin on(Predicate... restrictions); + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaEntityJoin.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaEntityJoin.java index 1338cfc1f7..ba8d67c361 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaEntityJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaEntityJoin.java @@ -8,10 +8,25 @@ package org.hibernate.query.criteria; import org.hibernate.metamodel.model.domain.EntityDomainType; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + /** * @author Steve Ebersole */ public interface JpaEntityJoin extends JpaJoinedFrom { @Override EntityDomainType getModel(); + + @Override + JpaEntityJoin on(JpaExpression restriction); + + @Override + JpaEntityJoin on(Expression restriction); + + @Override + JpaEntityJoin on(JpaPredicate... restrictions); + + @Override + JpaEntityJoin on(Predicate... restrictions); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java index 0a6fe8d6e1..a566b8c104 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java @@ -10,8 +10,19 @@ import org.hibernate.Incubating; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.sqm.tree.SqmJoinType; +import jakarta.persistence.criteria.CollectionJoin; import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.ListJoin; +import jakarta.persistence.criteria.MapJoin; +import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.criteria.Subquery; +import jakarta.persistence.metamodel.CollectionAttribute; +import jakarta.persistence.metamodel.ListAttribute; +import jakarta.persistence.metamodel.MapAttribute; +import jakarta.persistence.metamodel.SetAttribute; +import jakarta.persistence.metamodel.SingularAttribute; /** * API extension to the JPA {@link From} contract @@ -45,4 +56,71 @@ public interface JpaFrom extends JpaPath, JpaFetchParent, From @Incubating JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType, boolean lateral); + @Incubating + JpaJoinedFrom join(JpaCteCriteria cte); + + @Incubating + JpaJoinedFrom join(JpaCteCriteria cte, SqmJoinType joinType); + + // Covariant overrides + + @Override + JpaJoin join(SingularAttribute attribute); + + @Override + JpaJoin join(SingularAttribute attribute, JoinType jt); + + @Override + JpaCollectionJoin join(CollectionAttribute collection); + + @Override + JpaSetJoin join(SetAttribute set); + + @Override + JpaListJoin join(ListAttribute list); + + @Override + JpaMapJoin join(MapAttribute map); + + @Override + JpaCollectionJoin join(CollectionAttribute collection, JoinType jt); + + @Override + JpaSetJoin join(SetAttribute set, JoinType jt); + + @Override + JpaListJoin join(ListAttribute list, JoinType jt); + + @Override + JpaMapJoin join(MapAttribute map, JoinType jt); + + @Override + JpaJoin join(String attributeName); + + @Override + JpaCollectionJoin joinCollection(String attributeName); + + @Override + JpaSetJoin joinSet(String attributeName); + + @Override + JpaListJoin joinList(String attributeName); + + @Override + JpaMapJoin joinMap(String attributeName); + + @Override + JpaJoin join(String attributeName, JoinType jt); + + @Override + JpaCollectionJoin joinCollection(String attributeName, JoinType jt); + + @Override + JpaSetJoin joinSet(String attributeName, JoinType jt); + + @Override + JpaListJoin joinList(String attributeName, JoinType jt); + + @Override + JpaMapJoin joinMap(String attributeName, JoinType jt); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJoinedFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJoinedFrom.java index a2dfa8a932..b0a248f870 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJoinedFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJoinedFrom.java @@ -6,13 +6,29 @@ */ package org.hibernate.query.criteria; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + /** * Exists within the hierarchy mainly to support "entity joins". * * @see JpaEntityJoin * @see org.hibernate.query.sqm.tree.from.SqmEntityJoin + * @see JpaDerivedJoin + * @see org.hibernate.query.sqm.tree.from.SqmDerivedJoin * * @author Steve Ebersole */ public interface JpaJoinedFrom extends JpaFrom { + + JpaJoinedFrom on(JpaExpression restriction); + + JpaJoinedFrom on(Expression restriction); + + JpaJoinedFrom on(JpaPredicate... restrictions); + + JpaJoinedFrom on(Predicate... restrictions); + + JpaPredicate getOn(); + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaQueryableCriteria.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaQueryableCriteria.java index 8fafbfd259..6552169671 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaQueryableCriteria.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaQueryableCriteria.java @@ -22,5 +22,5 @@ import jakarta.persistence.criteria.CommonAbstractCriteria; * * @author Steve Ebersole */ -public interface JpaQueryableCriteria extends JpaCriteriaBase, JpaCriteriaNode { +public interface JpaQueryableCriteria extends JpaCriteriaBase, JpaCriteriaNode, JpaCteContainer { } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSearchOrder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSearchOrder.java new file mode 100644 index 0000000000..8ba7991888 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSearchOrder.java @@ -0,0 +1,51 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.NullPrecedence; +import org.hibernate.query.sqm.SortOrder; + +import jakarta.persistence.criteria.Order; + +/** + * Represents the search order for a recursive CTE (common table expression). + * + * @see JpaCteCriteria + */ +@Incubating +public interface JpaSearchOrder extends JpaCriteriaNode { + SortOrder getSortOrder(); + + /** + * Set the precedence for nulls for this search order element + */ + JpaSearchOrder nullPrecedence(NullPrecedence precedence); + + /** + * The precedence for nulls for this search order element + */ + NullPrecedence getNullPrecedence(); + + /** + * Whether ascending ordering is in effect. + * @return boolean indicating whether ordering is ascending + */ + boolean isAscending(); + + /** + * Switch the ordering. + * @return a new Order instance with the reversed ordering + */ + JpaSearchOrder reverse(); + + /** + * Return the CTE attribute that is used for ordering. + * @return CTE attribute used for ordering + */ + JpaCteCriteriaAttribute getAttribute(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java index 21d068e346..54d8e42ddf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java @@ -38,6 +38,15 @@ public interface JpaSelectCriteria extends AbstractQuery, JpaCriteriaBase */ JpaDerivedRoot from(Subquery subquery); + /** + * Create and add a query root corresponding to the given cte, + * forming a cartesian product with any existing roots. + * + * @param cte the cte criteria + * @return query root corresponding to the given cte + */ + JpaRoot from(JpaCteCriteria cte); + @Override JpaSelectCriteria distinct(boolean distinct); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java index f4800fcc70..8b19f5884d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java @@ -8,10 +8,17 @@ package org.hibernate.query.criteria; import java.util.List; import java.util.Set; + +import jakarta.persistence.criteria.CollectionJoin; import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.ListJoin; +import jakarta.persistence.criteria.MapJoin; import jakarta.persistence.criteria.Order; import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Selection; +import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.criteria.Subquery; import org.hibernate.query.sqm.FetchClauseType; @@ -22,7 +29,7 @@ import org.hibernate.query.sqm.tree.from.SqmJoin; /** * @author Steve Ebersole */ -public interface JpaSubQuery extends Subquery, JpaSelectCriteria, JpaExpression { +public interface JpaSubQuery extends Subquery, JpaSelectCriteria, JpaExpression, JpaCteContainer { JpaSubQuery multiselect(Selection... selections); @@ -93,4 +100,22 @@ public interface JpaSubQuery extends Subquery, JpaSelectCriteria, JpaEx @Override JpaSubQuery having(Predicate... restrictions); + + @Override + JpaRoot correlate(Root parentRoot); + + @Override + JpaJoin correlate(Join parentJoin); + + @Override + JpaCollectionJoin correlate(CollectionJoin parentCollection); + + @Override + JpaSetJoin correlate(SetJoin parentSet); + + @Override + JpaListJoin correlate(ListJoin parentList); + + @Override + JpaMapJoin correlate(MapJoin parentMap); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java index a26d09d464..21b068462d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java @@ -227,52 +227,40 @@ public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPar final SessionFactoryImplementor sessionFactory = creationContext.getSessionFactory(); final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() ); - final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); - final EntityPersister entityPersister = delegate.getEntityMappingType().getEntityPersister(); - final LazyTableGroup lazyTableGroup = new LazyTableGroup( - canUseInnerJoin, + final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, - fetched, - () -> createTableGroupInternal( - canUseInnerJoin, - navigablePath, - fetched, - null, - sqlAliasBase, - sqlExpressionResolver, - creationContext - ), - (np, tableExpression) -> { - if ( !tableExpression.isEmpty() && !entityPersister.containsTableReference( tableExpression ) ) { - return false; - } - if ( navigablePath.equals( np.getParent() ) ) { - return targetKeyPropertyNames.contains( np.getLocalName() ); - } - - final String relativePath = np.relativize( navigablePath ); - if ( relativePath == null ) { - return false; - } - - // Empty relative path means the navigable paths are equal, - // in which case we allow resolving the parent table group - return relativePath.isEmpty() || targetKeyPropertyNames.contains( relativePath ); - }, - this, + lhs, explicitSourceAlias, - sqlAliasBase, - creationContext.getSessionFactory(), - lhs + requestedJoinType, + fetched, + null, + aliasBaseGenerator, + sqlExpressionResolver, + fromClauseAccess, + creationContext ); final TableGroupJoin tableGroupJoin = new TableGroupJoin( - lazyTableGroup.getNavigablePath(), + navigablePath, joinType, lazyTableGroup, null ); + lazyTableGroup.setTableGroupInitializerCallback( + createTableGroupInitializerCallback( + lhs, + sqlExpressionResolver, + sessionFactory, + tableGroupJoin::applyPredicate + ) + ); + return tableGroupJoin; + } + private Consumer createTableGroupInitializerCallback( + TableGroup lhs, + SqlExpressionResolver sqlExpressionResolver, + SessionFactoryImplementor sessionFactory, + Consumer predicateConsumer) { // ----------------- // Collect the selectable mappings for the FK key side and target side // As we will "resolve" the derived column references for these mappings @@ -350,8 +338,7 @@ public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPar } ); } - lazyTableGroup.setTableGroupInitializerCallback( - tg -> { + Consumer tableGroupInitializerCallback = tg -> { this.identifierMapping.forEachSelectable( (i, selectableMapping) -> { final SelectableMapping targetMapping = targetMappings.get( i ); @@ -360,7 +347,7 @@ public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPar targetMapping.getContainingTableExpression(), false ); - tableGroupJoin.applyPredicate( + predicateConsumer.accept( new ComparisonPredicate( keyColumnReferences.get( i ), ComparisonOperator.EQUAL, @@ -373,9 +360,8 @@ public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPar ); } ); - } - ); - return tableGroupJoin; + }; + return tableGroupInitializerCallback; } public TableGroup createTableGroupInternal( @@ -415,7 +401,7 @@ public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPar } @Override - public TableGroup createRootTableGroupJoin( + public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, String explicitSourceAlias, @@ -426,18 +412,58 @@ public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPar SqlExpressionResolver sqlExpressionResolver, FromClauseAccess fromClauseAccess, SqlAstCreationContext creationContext) { - return ( (TableGroupJoinProducer) delegate ).createRootTableGroupJoin( + final SqlAliasBase sqlAliasBase = aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() ); + final boolean canUseInnerJoin = sqlAstJoinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); + final EntityPersister entityPersister = delegate.getEntityMappingType().getEntityPersister(); + final LazyTableGroup lazyTableGroup = new LazyTableGroup( + canUseInnerJoin, navigablePath, - lhs, - explicitSourceAlias, - sqlAstJoinType, fetched, - predicateConsumer, - aliasBaseGenerator, - sqlExpressionResolver, - fromClauseAccess, - creationContext + () -> createTableGroupInternal( + canUseInnerJoin, + navigablePath, + fetched, + null, + sqlAliasBase, + sqlExpressionResolver, + creationContext + ), + (np, tableExpression) -> { + if ( !tableExpression.isEmpty() && !entityPersister.containsTableReference( tableExpression ) ) { + return false; + } + if ( navigablePath.equals( np.getParent() ) ) { + return targetKeyPropertyNames.contains( np.getLocalName() ); + } + + final String relativePath = np.relativize( navigablePath ); + if ( relativePath == null ) { + return false; + } + + // Empty relative path means the navigable paths are equal, + // in which case we allow resolving the parent table group + return relativePath.isEmpty() || targetKeyPropertyNames.contains( relativePath ); + }, + this, + explicitSourceAlias, + sqlAliasBase, + creationContext.getSessionFactory(), + lhs ); + + if ( predicateConsumer != null ) { + lazyTableGroup.setTableGroupInitializerCallback( + createTableGroupInitializerCallback( + lhs, + sqlExpressionResolver, + creationContext.getSessionFactory(), + predicateConsumer + ) + ); + } + + return lazyTableGroup; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSource.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSource.java index 8a664c01bc..9353fe36df 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSource.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSource.java @@ -7,7 +7,9 @@ package org.hibernate.query.derived; import org.hibernate.Incubating; +import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource; @@ -19,6 +21,7 @@ import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.spi.NavigablePath; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; /** @@ -76,8 +79,8 @@ public class AnonymousTupleSqmPathSource implements SqmPathSource { else { navigablePath = lhs.getNavigablePath().append( intermediatePathSource.getPathName() ).append( getPathName() ); } - final SqmPathSource nodeType = path.getNodeType(); - if ( nodeType instanceof BasicSqmPathSource ) { + final DomainType domainType = path.getNodeType().getSqmPathType(); + if ( domainType instanceof BasicDomainType ) { return new SqmBasicValuedSimplePath<>( navigablePath, this, @@ -85,7 +88,7 @@ public class AnonymousTupleSqmPathSource implements SqmPathSource { lhs.nodeBuilder() ); } - else if ( nodeType instanceof EmbeddedSqmPathSource ) { + else if ( domainType instanceof EmbeddableDomainType ) { return new SqmEmbeddedValuedSimplePath<>( navigablePath, this, @@ -93,8 +96,7 @@ public class AnonymousTupleSqmPathSource implements SqmPathSource { lhs.nodeBuilder() ); } - else if ( nodeType instanceof EntitySqmPathSource || nodeType instanceof EntityDomainType - || nodeType instanceof PersistentAttribute && nodeType.getSqmPathType() instanceof EntityDomainType ) { + else if ( domainType instanceof EntityDomainType ) { return new SqmEntityValuedSimplePath<>( navigablePath, this, @@ -103,6 +105,6 @@ public class AnonymousTupleSqmPathSource implements SqmPathSource { ); } - throw new UnsupportedOperationException( "Unsupported path source: " + nodeType ); + throw new UnsupportedOperationException( "Unsupported path source: " + domainType ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java index 86bb0434cb..8280a6616c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.derived; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -29,6 +30,7 @@ import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.metamodel.model.domain.DomainType; @@ -43,6 +45,7 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.cte.CteColumn; import org.hibernate.sql.ast.tree.from.LazyTableGroup; import org.hibernate.sql.ast.tree.from.PluralTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -304,6 +307,10 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map } } + public Map getModelParts() { + return modelParts; + } + @Override public String getSqlAliasStem() { return aliasStem; @@ -314,6 +321,16 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map return javaTypeDescriptor; } + @Override + public int forEachSelectable(int offset, SelectableConsumer consumer) { + final int originalOffset = offset; + for ( ModelPart modelPart : modelParts.values() ) { + offset += modelPart.forEachSelectable( offset, consumer ); + } + + return offset - originalOffset; + } + //-------------------------------- // Support for using the anonymous tuple as table reference directly somewhere is not yet implemented //-------------------------------- @@ -371,5 +388,4 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map public int forEachJdbcType(int offset, IndexedConsumer action) { throw new UnsupportedOperationException( "Not yet implemented" ); } - } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java index eb489de4ee..7e45f620a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java @@ -16,6 +16,7 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SimpleDomainType; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; @@ -32,6 +33,8 @@ import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.ObjectArrayJavaType; +import jakarta.persistence.metamodel.Attribute; + /** * @author Christian Beikov @@ -88,6 +91,53 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur return new AnonymousTupleTableGroupProducer( this, aliasStem, sqlSelections, fromClauseAccess ); } + public List determineColumnNames() { + final int componentCount = componentCount(); + final List columnNames = new ArrayList<>( componentCount ); + for ( int i = 0; i < componentCount; i++ ) { + final SqmSelectableNode selectableNode = getSelectableNode( i ); + final String componentName = getComponentName( i ); + if ( selectableNode instanceof SqmPath ) { + addColumnNames( + columnNames, + ( (SqmPath) selectableNode ).getNodeType().getSqmPathType(), + componentName + ); + } + else { + columnNames.add( componentName ); + } + } + return columnNames; + } + + private static void addColumnNames(List columnNames, DomainType domainType, String componentName) { + if ( domainType instanceof EntityDomainType ) { + final EntityDomainType entityDomainType = (EntityDomainType) domainType; + final SingularPersistentAttribute idAttribute = entityDomainType.findIdAttribute(); + final String idPath; + if ( idAttribute == null ) { + idPath = componentName; + } + else { + idPath = componentName + "_" + idAttribute.getName(); + } + addColumnNames( columnNames, entityDomainType.getIdentifierDescriptor().getSqmPathType(), idPath ); + } + else if ( domainType instanceof ManagedDomainType ) { + for ( Attribute attribute : ( (ManagedDomainType) domainType ).getAttributes() ) { + if ( !( attribute instanceof SingularPersistentAttribute ) ) { + throw new IllegalArgumentException( "Only embeddables without collections are supported" ); + } + final DomainType attributeType = ( (SingularPersistentAttribute) attribute ).getType(); + addColumnNames( columnNames, attributeType, componentName + "_" + attribute.getName() ); + } + } + else { + columnNames.add( componentName ); + } + } + @Override public int componentCount() { return components.length; @@ -114,6 +164,10 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur return index == null ? null : components[index].getExpressible(); } + protected Integer getIndex(String componentName) { + return componentIndexMap.get( componentName ); + } + public SqmSelectableNode getSelectableNode(int index) { return components[index]; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java new file mode 100644 index 0000000000..82b3b331ed --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java @@ -0,0 +1,101 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.derived; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.cte.SqmCteTable; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.cte.CteColumn; +import org.hibernate.type.BasicType; + +/** + * The table group producer for a CTE tuple type. + * + * Exposes additional access to some special model parts for recursive CTE attributes. + * + * @author Christian Beikov + */ +@Incubating +public class CteTupleTableGroupProducer extends AnonymousTupleTableGroupProducer { + + private final AnonymousTupleBasicValuedModelPart searchModelPart; + private final AnonymousTupleBasicValuedModelPart cycleMarkModelPart; + private final AnonymousTupleBasicValuedModelPart cyclePathModelPart; + + public CteTupleTableGroupProducer( + SqmCteTable sqmCteTable, + String aliasStem, + List sqlSelections, + FromClauseAccess fromClauseAccess) { + super( sqmCteTable, aliasStem, sqlSelections, fromClauseAccess ); + final SqmCteStatement cteStatement = sqmCteTable.getCteStatement(); + final BasicType stringType = cteStatement.nodeBuilder() + .getTypeConfiguration() + .getBasicTypeForJavaType( String.class ); + this.searchModelPart = createModelPart( cteStatement.getSearchAttributeName(), stringType ); + this.cycleMarkModelPart = createModelPart( + cteStatement.getCycleMarkAttributeName(), + cteStatement.getCycleLiteral() == null + ? null + : (BasicType) cteStatement.getCycleLiteral().getNodeType() + ); + this.cyclePathModelPart = createModelPart( cteStatement.getCyclePathAttributeName(), stringType ); + } + + private static AnonymousTupleBasicValuedModelPart createModelPart(String attributeName, BasicType basicType) { + if ( attributeName != null ) { + return new AnonymousTupleBasicValuedModelPart( + attributeName, + attributeName, + basicType, + basicType + ); + } + return null; + } + + public List determineCteColumns() { + final List columns = new ArrayList<>( getModelParts().size() ); + forEachSelectable( + (selectionIndex, selectableMapping) -> { + columns.add( + new CteColumn( + selectableMapping.getSelectionExpression(), + selectableMapping.getJdbcMapping() + ) + ); + } + ); + return columns; + } + + @Override + public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { + final ModelPart subPart = super.findSubPart( name, treatTargetType ); + if ( subPart != null ) { + return subPart; + } + if ( searchModelPart != null && name.equals( searchModelPart.getPartName() ) ) { + return searchModelPart; + } + if ( cycleMarkModelPart != null && name.equals( cycleMarkModelPart.getPartName() ) ) { + return cycleMarkModelPart; + } + if ( cyclePathModelPart != null && name.equals( cyclePathModelPart.getPartName() ) ) { + return cyclePathModelPart; + } + return null; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index 17e3d054ef..e7e0665c85 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java @@ -17,7 +17,11 @@ import org.hibernate.query.sqm.SqmJoinable; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; +import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; @@ -270,7 +274,7 @@ public class QualifiedJoinPathConsumer implements DotIdentifierConsumer { private final StringBuilder path = new StringBuilder(); - private SqmEntityJoin join; + private SqmPath join; public ExpectingEntityJoinDelegate( String identifier, @@ -301,6 +305,12 @@ public class QualifiedJoinPathConsumer implements DotIdentifierConsumer { .getJpaMetamodel() .resolveHqlEntityReference( fullPath ); if ( joinedEntityType == null ) { + final SqmCteStatement cteStatement = creationState.findCteStatement( fullPath ); + if ( cteStatement != null ) { + join = new SqmCteJoin<>( cteStatement, alias, joinType, sqmRoot ); + creationState.getCurrentProcessingState().getPathRegistry().register( join ); + return; + } throw new SemanticException( "Could not resolve join path - " + fullPath ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java index a1ca350be8..f39572749a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java @@ -10,13 +10,22 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaSearchOrder; +import org.hibernate.query.sqm.tree.cte.SqmCteTableColumn; +import org.hibernate.query.sqm.tree.cte.SqmSearchClauseSpecification; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; import org.hibernate.spi.NavigablePath; import org.hibernate.query.hql.spi.SqmCreationOptions; import org.hibernate.query.hql.spi.SqmCreationProcessingState; @@ -81,6 +90,9 @@ import org.hibernate.query.sqm.tree.update.SqmSetClause; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.type.descriptor.java.JavaType; +import jakarta.persistence.criteria.AbstractQuery; +import jakarta.persistence.criteria.CriteriaQuery; + /** * Handles splitting queries containing unmapped polymorphic references. * @@ -215,17 +227,129 @@ public class QuerySplitter { public Object visitCteContainer(SqmCteContainer consumer) { final SqmCteContainer processingQuery = (SqmCteContainer) getProcessingStateStack().getCurrent() .getProcessingQuery(); - processingQuery.setWithRecursive( consumer.isWithRecursive() ); for ( SqmCteStatement cteStatement : consumer.getCteStatements() ) { - processingQuery.addCteStatement( visitCteStatement( cteStatement ) ); + visitCteStatement( cteStatement ); } return processingQuery; } @Override public SqmCteStatement visitCteStatement(SqmCteStatement sqmCteStatement) { - // No need to copy anything here - return sqmCteStatement; + final SqmCteContainer processingQuery = (SqmCteContainer) getProcessingStateStack().getCurrent() + .getProcessingQuery(); + final SqmSelectQuery cteDefinition = sqmCteStatement.getCteDefinition(); + final SqmQueryPart queryPart = cteDefinition.getQueryPart(); + JpaCteCriteria cteCriteria = null; + if ( cteDefinition.getCteStatements().isEmpty() + && queryPart instanceof SqmQueryGroup && queryPart.getSortSpecifications().isEmpty() + && queryPart.getFetchExpression() == null && queryPart.getOffsetExpression() == null ) { + final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; + boolean unionDistinct = false; + switch ( queryGroup.getSetOperator() ) { + case UNION: + unionDistinct = true; + case UNION_ALL: + if ( queryGroup.getQueryParts().size() == 2 ) { + final SqmSelectQuery nonRecursiveStatement = visitSelectQuery( + cteDefinition, + queryGroup.getQueryParts().get( 0 ) + ); + final Function, AbstractQuery> recursiveCriteriaProducer = jpaCteCriteria -> { + return visitSelectQuery( cteDefinition, queryGroup.getQueryParts().get( 1 ) ); + }; + if ( sqmCteStatement.getName() == null ) { + if ( unionDistinct ) { + cteCriteria = processingQuery.withRecursiveUnionDistinct( + nonRecursiveStatement, + recursiveCriteriaProducer + ); + } + else { + cteCriteria = processingQuery.withRecursiveUnionAll( + nonRecursiveStatement, + recursiveCriteriaProducer + ); + } + } + else { + if ( unionDistinct ) { + cteCriteria = processingQuery.withRecursiveUnionDistinct( + sqmCteStatement.getName(), + nonRecursiveStatement, + recursiveCriteriaProducer + ); + } + else { + cteCriteria = processingQuery.withRecursiveUnionAll( + sqmCteStatement.getName(), + nonRecursiveStatement, + recursiveCriteriaProducer + ); + } + } + + if ( sqmCteStatement.getSearchClauseKind() != null ) { + final List searchBySpecifications = sqmCteStatement.getSearchBySpecifications(); + final List newSearchBySpecifications = new ArrayList<>( searchBySpecifications.size() ); + for ( JpaSearchOrder searchBySpecification : searchBySpecifications ) { + newSearchBySpecifications.add( + new SqmSearchClauseSpecification( + (SqmCteTableColumn) cteCriteria.getType().getAttribute( searchBySpecification.getAttribute().getName() ), + searchBySpecification.getSortOrder(), + searchBySpecification.getNullPrecedence() + ) + ); + } + cteCriteria.search( + sqmCteStatement.getSearchClauseKind(), + sqmCteStatement.getSearchAttributeName(), + newSearchBySpecifications + ); + } + if ( sqmCteStatement.getCycleMarkAttributeName() != null ) { + final List cycleAttributes = sqmCteStatement.getCycleAttributes(); + final List newCycleAttributes = new ArrayList<>( cycleAttributes.size() ); + for ( JpaCteCriteriaAttribute cycleAttribute : cycleAttributes ) { + newCycleAttributes.add( + cteCriteria.getType().getAttribute( cycleAttribute.getName() ) + ); + } + cteCriteria.cycleUsing( + sqmCteStatement.getCycleMarkAttributeName(), + sqmCteStatement.getCyclePathAttributeName(), + sqmCteStatement.getCycleValue(), + sqmCteStatement.getNoCycleValue(), + newCycleAttributes + ); + } + } + } + } + if ( cteCriteria == null ) { + if ( sqmCteStatement.getName() == null ) { + cteCriteria = processingQuery.with( visitSelectQuery( cteDefinition ) ); + } + else { + cteCriteria = processingQuery.with( sqmCteStatement.getName(), visitSelectQuery( cteDefinition ) ); + } + } + if ( sqmCteStatement.getMaterialization() != null ) { + cteCriteria.setMaterialization( sqmCteStatement.getMaterialization() ); + } + return (SqmCteStatement) cteCriteria; + } + + @Override + public SqmCteStatement findCteStatement(String name) { + return processingStateStack.findCurrentFirst( + state -> { + if ( state.getProcessingQuery() instanceof SqmCteContainer ) { + final SqmCteContainer container = (SqmCteContainer) state.getProcessingQuery(); + return container.getCteStatement( name ); + } + return null; + } + ); } @Override @@ -285,6 +409,43 @@ public class QuerySplitter { return copy; } + @Override + protected SqmSelectQuery visitSelectQuery(SqmSelectQuery selectQuery) { + if ( selectQuery instanceof SqmSelectStatement ) { + return (SqmSelectQuery) visitSelectStatement( (SqmSelectStatement) selectQuery ); + } + else { + return visitSubQueryExpression( (SqmSubQuery) selectQuery ); + } + } + + protected SqmSelectQuery visitSelectQuery(SqmSelectQuery selectQuery, SqmQueryPart queryPart) { + if ( selectQuery instanceof SqmSelectStatement ) { + final SqmSelectStatement sqmSelectStatement = (SqmSelectStatement) selectQuery; + //noinspection rawtypes + return (SqmSelectQuery) visitSelectStatement( + new SqmSelectStatement( + queryPart, + sqmSelectStatement.getResultType(), + sqmSelectStatement.getQuerySource(), + sqmSelectStatement.nodeBuilder() + ) + ); + } + else { + final SqmSubQuery sqmSubQuery = (SqmSubQuery) selectQuery; + //noinspection rawtypes + return visitSubQueryExpression( + new SqmSubQuery( + processingStateStack.getCurrent().getProcessingQuery(), + queryPart, + sqmSubQuery.getResultType(), + sqmSubQuery.nodeBuilder() + ) + ); + } + } + @Override public SqmQueryPart visitQueryPart(SqmQueryPart queryPart) { return (SqmQueryPart) super.visitQueryPart( queryPart ); @@ -411,6 +572,25 @@ public class QuerySplitter { return copy; } + @Override + public SqmCteRoot visitRootCte(SqmCteRoot sqmRoot) { + SqmFrom sqmFrom = sqmFromCopyMap.get( sqmRoot ); + if ( sqmFrom != null ) { + return (SqmCteRoot) sqmFrom; + } + final SqmCteRoot copy = new SqmCteRoot<>( + (SqmCteStatement) sqmRoot.getCte().accept( this ), + sqmRoot.getExplicitAlias() + ); + getProcessingStateStack().getCurrent().getPathRegistry().register( copy ); + sqmFromCopyMap.put( sqmRoot, copy ); + sqmPathCopyMap.put( sqmRoot.getNavigablePath(), copy ); + if ( currentFromClauseCopy != null ) { + currentFromClauseCopy.addRoot( copy ); + } + return copy; + } + @Override public SqmCrossJoin visitCrossJoin(SqmCrossJoin join) { final SqmFrom sqmFrom = sqmFromCopyMap.get( join ); @@ -506,6 +686,26 @@ public class QuerySplitter { return copy; } + @Override + public SqmCteJoin visitQualifiedCteJoin(SqmCteJoin join) { + SqmFrom sqmFrom = sqmFromCopyMap.get( join ); + if ( sqmFrom != null ) { + return (SqmCteJoin) sqmFrom; + } + final SqmRoot sqmRoot = (SqmRoot) sqmFromCopyMap.get( join.findRoot() ); + final SqmCteJoin copy = new SqmCteJoin( + (SqmCteStatement) join.getCte().accept( this ), + join.getExplicitAlias(), + join.getSqmJoinType(), + sqmRoot + ); + getProcessingStateStack().getCurrent().getPathRegistry().register( copy ); + sqmFromCopyMap.put( join, copy ); + sqmPathCopyMap.put( join.getNavigablePath(), copy ); + sqmRoot.addSqmJoin( copy ); + return copy; + } + @Override public SqmBasicValuedSimplePath visitBasicValuedPath(SqmBasicValuedSimplePath path) { final SqmPathRegistry pathRegistry = getProcessingStateStack().getCurrent().getPathRegistry(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 939c74f1f1..88aa42662d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -61,6 +61,10 @@ import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; import org.hibernate.query.PathException; import org.hibernate.query.ReturnableType; import org.hibernate.query.SemanticException; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaCteCriteriaType; +import org.hibernate.query.criteria.JpaSearchOrder; import org.hibernate.query.hql.HqlLogging; import org.hibernate.query.hql.spi.DotIdentifierConsumer; import org.hibernate.query.hql.spi.SemanticPathPart; @@ -103,9 +107,12 @@ import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmQuery; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.cte.SqmCteContainer; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.AbstractSqmFrom; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -151,6 +158,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; @@ -195,6 +203,8 @@ import org.hibernate.query.sqm.tree.select.SqmSortSpecification; import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; +import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; @@ -301,6 +311,12 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem private ParameterCollector parameterCollector; private ParameterStyle parameterStyle; + private boolean isExtractingJdbcTemporalType; + // Provides access to the current CTE that is being processed, which is potentially recursive + // This is necessary, so that the recursive query part of a CTE can access its own structure. + // Note that the structure is based on the non-recursive query part, so there is no cycle + private JpaCteCriteria currentPotentialRecursiveCte; + public SemanticQueryBuilder( Class expectedResultType, SqmCreationOptions creationOptions, @@ -617,10 +633,284 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Query spec + @Override + public Object visitWithClause(HqlParser.WithClauseContext ctx) { + if ( creationOptions.useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + StrictJpaComplianceViolation.Type.CTES + ); + } + final List children = ctx.children; + for ( int i = 1; i < children.size(); i += 2 ) { + visitCte( (HqlParser.CteContext) children.get( i ) ); + } + return null; + } + + @Override + public Object visitCte(HqlParser.CteContext ctx) { + final SqmCteContainer cteContainer = (SqmCteContainer) processingStateStack.getCurrent().getProcessingQuery(); + final String name = visitIdentifier( (HqlParser.IdentifierContext) ctx.children.get( 0 ) ); + final TerminalNode thirdChild = (TerminalNode) ctx.getChild( 2 ); + final int queryExpressionIndex; + final CteMaterialization materialization; + switch ( thirdChild.getSymbol().getType() ) { + case HqlParser.NOT: + materialization = CteMaterialization.NOT_MATERIALIZED; + queryExpressionIndex = 5; + break; + case HqlParser.MATERIALIZED: + materialization = CteMaterialization.MATERIALIZED; + queryExpressionIndex = 4; + break; + default: + materialization = null; + queryExpressionIndex = 3; + break; + } + + final HqlParser.QueryExpressionContext queryExpressionContext = (HqlParser.QueryExpressionContext) ctx.getChild( queryExpressionIndex ); + final SqmSelectQuery cte; + if ( cteContainer instanceof SqmSubQuery ) { + cte = new SqmSubQuery<>( + processingStateStack.getCurrent().getProcessingQuery(), + creationContext.getNodeBuilder() + ); + } + else { + cte = new SqmSelectStatement<>( creationContext.getNodeBuilder() ); + } + processingStateStack.push( + new SqmQueryPartCreationProcessingStateStandardImpl( + processingStateStack.getCurrent(), + cte, + this + ) + ); + final JpaCteCriteria oldCte = currentPotentialRecursiveCte; + try { + currentPotentialRecursiveCte = null; + if ( queryExpressionContext instanceof HqlParser.SetQueryGroupContext ) { + final HqlParser.SetQueryGroupContext setContext = (HqlParser.SetQueryGroupContext) queryExpressionContext; + // A recursive query is only possible if the child count is lower than 5 e.g. `withClause? q1 op q2` + if ( setContext.getChildCount() < 5 ) { + final SetOperator setOperator = (SetOperator) setContext.getChild( setContext.getChildCount() - 2 ) + .accept( this ); + switch ( setOperator ) { + case UNION: + case UNION_ALL: + final HqlParser.OrderedQueryContext nonRecursiveQueryContext; + final HqlParser.OrderedQueryContext recursiveQueryContext; + // On count == 4, we have a withClause at index 0 + if ( setContext.getChildCount() == 4 ) { + nonRecursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 1 ); + recursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 3 ); + } + else { + nonRecursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 0 ); + recursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 2 ); + } + // First visit the non-recursive part + nonRecursiveQueryContext.accept( this ); + + // Visiting the possibly recursive part must happen within the call to SqmCteContainer.with, + // because in there, the SqmCteStatement/JpaCteCriteria is available for use in the recursive part. + // The structure (SqmCteTable) for the SqmCteStatement is based on the non-recursive part, + // which is necessary to have, so that the SqmCteRoot/SqmCteJoin can resolve sub-paths. + final SqmSelectStatement recursivePart = new SqmSelectStatement<>( creationContext.getNodeBuilder() ); + + processingStateStack.pop(); + processingStateStack.push( + new SqmQueryPartCreationProcessingStateStandardImpl( + processingStateStack.getCurrent(), + recursivePart, + this + ) + ); + final JpaCteCriteria cteDefinition; + if ( setOperator == SetOperator.UNION ) { + cteDefinition = cteContainer.withRecursiveUnionDistinct( + name, + cte, + cteCriteria -> { + currentPotentialRecursiveCte = cteCriteria; + recursiveQueryContext.accept( this ); + return recursivePart; + } + ); + } + else { + cteDefinition = cteContainer.withRecursiveUnionAll( + name, + cte, + cteCriteria -> { + currentPotentialRecursiveCte = cteCriteria; + recursiveQueryContext.accept( this ); + return recursivePart; + } + ); + } + if ( materialization != null ) { + cteDefinition.setMaterialization( materialization ); + } + final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 ); + final ParseTree potentialSearchClause; + if ( lastChild instanceof HqlParser.CycleClauseContext ) { + applyCycleClause( cteDefinition, (HqlParser.CycleClauseContext) lastChild ); + potentialSearchClause = ctx.getChild( ctx.getChildCount() - 2 ); + } + else { + potentialSearchClause = lastChild; + } + if ( potentialSearchClause instanceof HqlParser.SearchClauseContext ) { + applySearchClause( cteDefinition, (HqlParser.SearchClauseContext) potentialSearchClause ); + } + return null; + } + } + } + queryExpressionContext.accept( this ); + final JpaCteCriteria cteDefinition = cteContainer.with( name, cte ); + if ( materialization != null ) { + cteDefinition.setMaterialization( materialization ); + } + } + finally { + processingStateStack.pop(); + currentPotentialRecursiveCte = oldCte; + } + return null; + } + + private void applyCycleClause(JpaCteCriteria cteDefinition, HqlParser.CycleClauseContext ctx) { + final HqlParser.CteAttributesContext attributesContext = (HqlParser.CteAttributesContext) ctx.getChild( 1 ); + final String cycleMarkAttributeName = visitIdentifier( (HqlParser.IdentifierContext) ctx.getChild( 3 ) ); + final List cycleAttributes = new ArrayList<>( ( attributesContext.getChildCount() + 1 ) >> 1 ); + final List children = attributesContext.children; + final JpaCteCriteriaType type = cteDefinition.getType(); + for ( int i = 0; i < children.size(); i += 2 ) { + final String attributeName = visitIdentifier( (HqlParser.IdentifierContext) children.get( i ) ); + final JpaCteCriteriaAttribute attribute = type.getAttribute( attributeName ); + if ( attribute == null ) { + throw new SemanticException( + String.format( + "Cycle attribute '%s' not found in the CTE %s", + attributeName, + cteDefinition.getName() + ) + ); + } + cycleAttributes.add( attribute ); + } + + final String cyclePathAttributeName; + final Object cycleValue; + final Object noCycleValue; + if ( ctx.getChildCount() > 4 ) { + if ( ctx.getChildCount() > 6 ) { + final SqmLiteral cycleLiteral = (SqmLiteral) visitLiteral( (HqlParser.LiteralContext) ctx.getChild( 5 ) ); + final SqmLiteral noCycleLiteral = (SqmLiteral) visitLiteral( (HqlParser.LiteralContext) ctx.getChild( 7 ) ); + cycleValue = cycleLiteral.getLiteralValue(); + noCycleValue = noCycleLiteral.getLiteralValue(); + } + else { + cycleValue = Boolean.TRUE; + noCycleValue = Boolean.FALSE; + } + final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 ); + if ( lastChild instanceof HqlParser.IdentifierContext ) { + cyclePathAttributeName = visitIdentifier( (HqlParser.IdentifierContext) lastChild ); + } + else { + cyclePathAttributeName = null; + } + } + else { + cyclePathAttributeName = null; + cycleValue = Boolean.TRUE; + noCycleValue = Boolean.FALSE; + } + + cteDefinition.cycleUsing( cycleMarkAttributeName, cyclePathAttributeName, cycleValue, noCycleValue, cycleAttributes ); + } + + private void applySearchClause(JpaCteCriteria cteDefinition, HqlParser.SearchClauseContext ctx) { + final CteSearchClauseKind kind; + if ( ( (TerminalNode) ctx.getChild( 1 ) ).getSymbol().getType() == HqlParser.BREADTH ) { + kind = CteSearchClauseKind.BREADTH_FIRST; + } + else { + kind = CteSearchClauseKind.DEPTH_FIRST; + } + final String searchAttributeName = visitIdentifier( (HqlParser.IdentifierContext) ctx.getChild( ctx.getChildCount() - 1 ) ); + final HqlParser.SearchSpecificationsContext searchCtx = (HqlParser.SearchSpecificationsContext) ctx.getChild( 4 ); + final List searchOrders = new ArrayList<>( ( searchCtx.getChildCount() + 1 ) >> 1 );; + final List children = searchCtx.children; + final JpaCteCriteriaType type = cteDefinition.getType(); + for ( int i = 0; i < children.size(); i += 2 ) { + final HqlParser.SearchSpecificationContext specCtx = (HqlParser.SearchSpecificationContext) children.get( i ); + final String attributeName = visitIdentifier( (HqlParser.IdentifierContext) specCtx.getChild( 0 ) ); + final JpaCteCriteriaAttribute attribute = type.getAttribute( attributeName ); + if ( attribute == null ) { + throw new SemanticException( + String.format( + "Search attribute '%s' not found in the CTE %s", + attributeName, + cteDefinition.getName() + ) + ); + } + SortOrder sortOrder = SortOrder.ASCENDING; + NullPrecedence nullPrecedence = NullPrecedence.NONE; + int index = 1; + if ( index < specCtx.getChildCount() ) { + if ( specCtx.getChild( index ) instanceof HqlParser.SortDirectionContext ) { + final HqlParser.SortDirectionContext sortCtx = (HqlParser.SortDirectionContext) specCtx.getChild( index ); + switch ( ( (TerminalNode) sortCtx.getChild( 0 ) ).getSymbol().getType() ) { + case HqlParser.ASC: + sortOrder = SortOrder.ASCENDING; + break; + case HqlParser.DESC: + sortOrder = SortOrder.DESCENDING; + break; + default: + throw new SemanticException( "Unrecognized sort ordering: " + sortCtx.getText() ); + } + index++; + } + if ( index < specCtx.getChildCount() ) { + final HqlParser.NullsPrecedenceContext nullsPrecedenceContext = (HqlParser.NullsPrecedenceContext) specCtx.getChild( index ); + switch ( ( (TerminalNode) nullsPrecedenceContext.getChild( 1 ) ).getSymbol().getType() ) { + case HqlParser.FIRST: + nullPrecedence = NullPrecedence.FIRST; + break; + case HqlParser.LAST: + nullPrecedence = NullPrecedence.LAST; + break; + default: + throw new SemanticException( "Unrecognized null precedence: " + nullsPrecedenceContext.getText() ); + } + } + } + searchOrders.add( + creationContext.getNodeBuilder().search( + attribute, + sortOrder, + nullPrecedence + ) + ); + } + cteDefinition.search( kind, searchAttributeName, searchOrders ); + } + @Override public SqmQueryPart visitSimpleQueryGroup(HqlParser.SimpleQueryGroupContext ctx) { + final int lastChild = ctx.getChildCount() - 1; + if ( lastChild != 0 ) { + ctx.getChild( 0 ).accept( this ); + } //noinspection unchecked - return (SqmQueryPart) ctx.getChild( 0 ).accept( this ); + return (SqmQueryPart) ctx.getChild( lastChild ).accept( this ); } @Override @@ -654,14 +944,22 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem @Override public SqmQueryGroup visitSetQueryGroup(HqlParser.SetQueryGroupContext ctx) { + final List children = ctx.children; + final int firstIndex; + if ( children.get( 0 ) instanceof HqlParser.WithClauseContext ) { + children.get( 0 ).accept( this ); + firstIndex = 1; + } + else { + firstIndex = 0; + } if ( creationOptions.useStrictJpaCompliance() ) { throw new StrictJpaComplianceViolation( StrictJpaComplianceViolation.Type.SET_OPERATIONS ); } - final List children = ctx.children; //noinspection unchecked - final SqmQueryPart firstQueryPart = (SqmQueryPart) children.get( 0 ).accept( this ); + final SqmQueryPart firstQueryPart = (SqmQueryPart) children.get( firstIndex ).accept( this ); SqmQueryGroup queryGroup; if ( firstQueryPart instanceof SqmQueryGroup) { queryGroup = (SqmQueryGroup) firstQueryPart; @@ -672,7 +970,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem setCurrentQueryPart( queryGroup ); final int size = children.size(); final SqmCreationProcessingState firstProcessingState = processingStateStack.pop(); - for ( int i = 1; i < size; i += 2 ) { + for ( int i = firstIndex + 1; i < size; i += 2 ) { final SetOperator operator = visitSetOperator( (HqlParser.SetOperatorContext) children.get( i ) ); final HqlParser.OrderedQueryContext simpleQueryCtx = (HqlParser.OrderedQueryContext) children.get( i + 1 ); @@ -1625,6 +1923,12 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } throw new SemanticException( "Could not resolve entity or correlation path '" + name + "'" ); } + final SqmCteStatement cteStatement = findCteStatement( name ); + if ( cteStatement != null ) { + final SqmCteRoot root = new SqmCteRoot<>( cteStatement, alias ); + pathRegistry.register( root ); + return root; + } throw new UnknownEntityException( "Could not resolve root entity '" + name + "'", name ); } checkFQNEntityNameJpaComplianceViolationIfNeeded( name, entityDescriptor ); @@ -1652,6 +1956,22 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem return sqmRoot; } + @Override + public SqmCteStatement findCteStatement(String name) { + if ( currentPotentialRecursiveCte != null && name.equals( currentPotentialRecursiveCte.getName() ) ) { + return (SqmCteStatement) currentPotentialRecursiveCte; + } + return processingStateStack.findCurrentFirst( + state -> { + if ( state.getProcessingQuery() instanceof SqmCteContainer ) { + final SqmCteContainer container = (SqmCteContainer) state.getProcessingQuery(); + return container.getCteStatement( name ); + } + return null; + } + ); + } + @Override public SqmRoot visitRootSubquery(HqlParser.RootSubqueryContext ctx) { if ( getCreationOptions().useStrictJpaCompliance() ) { @@ -1867,7 +2187,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } final HqlParser.JoinRestrictionContext qualifiedJoinRestrictionContext = parserJoin.joinRestriction(); - if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin ) { + if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin || join instanceof SqmCteJoin ) { sqmRoot.addSqmJoin( join ); } else if ( join instanceof SqmAttributeJoin ) { @@ -2165,7 +2485,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem final Enum enumValue; if ( possibleEnumValues != null && ( enumValue = possibleEnumValues.get( enumType ) ) != null ) { DotIdentifierConsumer dotIdentifierConsumer = dotIdentifierConsumerStack.getCurrent(); - dotIdentifierConsumer.consumeIdentifier( enumValue.getClass().getCanonicalName(), true, false ); + dotIdentifierConsumer.consumeIdentifier( enumValue.getClass().getName(), true, false ); dotIdentifierConsumer.consumeIdentifier( enumValue.name(), false, true ); return (SqmExpression) dotIdentifierConsumerStack.getCurrent().getConsumedPart(); } @@ -3940,8 +4260,6 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } } - private boolean isExtractingJdbcTemporalType; - @Override public Object visitExtractFunction(HqlParser.ExtractFunctionContext ctx) { final SqmExpression expressionToExtract = (SqmExpression) ctx.getChild( ctx.getChildCount() - 2 ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationState.java b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationState.java index 0ef1d5a516..484fe7ed63 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationState.java @@ -9,6 +9,7 @@ package org.hibernate.query.hql.spi; import org.hibernate.Incubating; import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.sqm.spi.SqmCreationContext; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; /** * Models the state pertaining to the creation of a single SQM. @@ -39,4 +40,6 @@ public interface SqmCreationState { default SqmCreationProcessingState getCurrentProcessingState() { return getProcessingStateStack().getCurrent(); } + + SqmCteStatement findCteStatement(String name); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index 05e7b43d21..d01e6ad1f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -17,6 +17,7 @@ import org.hibernate.query.sqm.tree.domain.NonAggregatedCompositeSimplePath; import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -64,6 +65,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFromClause; @@ -134,6 +136,8 @@ public interface SemanticQueryWalker { T visitRootDerived(SqmDerivedRoot sqmRoot); + T visitRootCte(SqmCteRoot sqmRoot); + T visitCrossJoin(SqmCrossJoin joinedFromElement); T visitPluralPartJoin(SqmPluralPartJoin joinedFromElement); @@ -144,6 +148,8 @@ public interface SemanticQueryWalker { T visitQualifiedDerivedJoin(SqmDerivedJoin joinedFromElement); + T visitQualifiedCteJoin(SqmCteJoin joinedFromElement); + T visitBasicValuedPath(SqmBasicValuedSimplePath path); T visitEmbeddableValuedPath(SqmEmbeddedValuedSimplePath path); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java index 49bf6d9c80..08f715a3ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java @@ -29,6 +29,7 @@ public class StrictJpaComplianceViolation extends SemanticException { SUBQUERY_ORDER_BY( "use of ORDER BY clause in subquery" ), FROM_SUBQUERY( "use of subquery in FROM clause" ), SET_OPERATIONS( "use of set operations" ), + CTES( "use of CTEs (common table expressions)" ), LIMIT_OFFSET_CLAUSE( "use of LIMIT/OFFSET clause" ), IDENTIFICATION_VARIABLE_NOT_DECLARED_IN_FROM_CLAUSE( "use of an alias not declared in the FROM clause" ), FQN_ENTITY_NAME( "use of FQN for entity name" ), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 11dbbe9455..c7eac428f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -43,6 +44,9 @@ import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; import org.hibernate.query.ReturnableType; import org.hibernate.query.BindableType; import org.hibernate.query.SemanticException; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaSearchOrder; +import org.hibernate.query.criteria.JpaSubQuery; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.NullPrecedence; @@ -65,7 +69,11 @@ import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.query.sqm.spi.SqmCreationContext; +import org.hibernate.query.sqm.tree.SqmQuery; import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.cte.SqmCteTableColumn; +import org.hibernate.query.sqm.tree.cte.SqmSearchClauseSpecification; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.SqmBagJoin; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -304,6 +312,21 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, return setOperation( all ? SetOperator.EXCEPT_ALL : SetOperator.EXCEPT, query1, queries ); } + @Override + public JpaSubQuery union(boolean all, Subquery query1, Subquery... queries) { + return setOperation( all ? SetOperator.UNION_ALL : SetOperator.UNION, query1, queries ); + } + + @Override + public JpaSubQuery intersect(boolean all, Subquery query1, Subquery... queries) { + return setOperation( all ? SetOperator.INTERSECT_ALL : SetOperator.INTERSECT, query1, queries ); + } + + @Override + public JpaSubQuery except(boolean all, Subquery query1, Subquery... queries) { + return setOperation( all ? SetOperator.EXCEPT_ALL : SetOperator.EXCEPT, query1, queries ); + } + @SuppressWarnings("unchecked") private JpaCriteriaQuery setOperation( SetOperator operator, @@ -311,21 +334,68 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, CriteriaQuery... queries) { final Class resultType = (Class) query1.getResultType(); final List> queryParts = new ArrayList<>( queries.length + 1 ); - queryParts.add( ( (SqmSelectQuery) query1 ).getQueryPart() ); + final Map> cteStatements = new LinkedHashMap<>(); + final SqmSelectStatement selectStatement1 = (SqmSelectStatement) query1; + collectQueryPartsAndCtes( selectStatement1, queryParts, cteStatements ); for ( CriteriaQuery query : queries ) { if ( query.getResultType() != resultType ) { throw new IllegalArgumentException( "Result type of all operands must match" ); } - queryParts.add( ( (SqmSelectQuery) query ).getQueryPart() ); + collectQueryPartsAndCtes( (SqmSelectQuery) query, queryParts, cteStatements ); } return new SqmSelectStatement<>( new SqmQueryGroup<>( this, operator, queryParts ), resultType, - SqmQuerySource.CRITERIA, + cteStatements, + selectStatement1.getQuerySource(), this ); } + @SuppressWarnings("unchecked") + private JpaSubQuery setOperation( + SetOperator operator, + Subquery query1, + Subquery... queries) { + final Class resultType = (Class) query1.getResultType(); + final SqmQuery parent = (SqmQuery) query1.getParent(); + final List> queryParts = new ArrayList<>( queries.length + 1 ); + final Map> cteStatements = new LinkedHashMap<>(); + collectQueryPartsAndCtes( (SqmSelectQuery) query1, queryParts, cteStatements ); + for ( Subquery query : queries ) { + if ( query.getResultType() != resultType ) { + throw new IllegalArgumentException( "Result type of all operands must match" ); + } + if ( query.getParent() != parent ) { + throw new IllegalArgumentException( "Subquery parent of all operands must match" ); + } + collectQueryPartsAndCtes( (SqmSelectQuery) query, queryParts, cteStatements ); + } + return new SqmSubQuery<>( + parent, + new SqmQueryGroup<>( this, operator, queryParts ), + resultType, + cteStatements, + this + ); + } + + private void collectQueryPartsAndCtes( + SqmSelectQuery query, + List> queryParts, + Map> cteStatements) { + queryParts.add( query.getQueryPart() ); + for ( SqmCteStatement cteStatement : query.getCteStatements() ) { + final String name = cteStatement.getCteTable().getCteName(); + final SqmCteStatement old = cteStatements.put( name, cteStatement ); + if ( old != null && old != cteStatement ) { + throw new IllegalArgumentException( + String.format( "Different CTE with same name [%s] found in different set operands!", name ) + ); + } + } + } + @Override public SqmExpression cast(JpaExpression expression, Class castTargetJavaType) { final BasicDomainType type = getTypeConfiguration().standardBasicTypeForJavaType( castTargetJavaType ); @@ -451,6 +521,49 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, ); } + @Override + public JpaSearchOrder search(JpaCteCriteriaAttribute sortExpression, SortOrder sortOrder, NullPrecedence nullPrecedence) { + return new SqmSearchClauseSpecification( (SqmCteTableColumn) sortExpression, sortOrder, nullPrecedence ); + } + + @Override + public JpaSearchOrder search(JpaCteCriteriaAttribute sortExpression, SortOrder sortOrder) { + return new SqmSearchClauseSpecification( (SqmCteTableColumn) sortExpression, sortOrder, NullPrecedence.NONE ); + } + + @Override + public JpaSearchOrder search(JpaCteCriteriaAttribute sortExpression) { + return new SqmSearchClauseSpecification( (SqmCteTableColumn) sortExpression, SortOrder.ASCENDING, NullPrecedence.NONE ); + } + + @Override + public JpaSearchOrder asc(JpaCteCriteriaAttribute x) { + return new SqmSearchClauseSpecification( (SqmCteTableColumn) x, SortOrder.ASCENDING, NullPrecedence.NONE ); + } + + @Override + public JpaSearchOrder desc(JpaCteCriteriaAttribute x) { + return new SqmSearchClauseSpecification( (SqmCteTableColumn) x, SortOrder.DESCENDING, NullPrecedence.NONE ); + } + + @Override + public JpaSearchOrder asc(JpaCteCriteriaAttribute x, boolean nullsFirst) { + return new SqmSearchClauseSpecification( + (SqmCteTableColumn) x, + SortOrder.ASCENDING, + nullsFirst ? NullPrecedence.FIRST : NullPrecedence.LAST + ); + } + + @Override + public JpaSearchOrder desc(JpaCteCriteriaAttribute x, boolean nullsFirst) { + return new SqmSearchClauseSpecification( + (SqmCteTableColumn) x, + SortOrder.DESCENDING, + nullsFirst ? NullPrecedence.FIRST : NullPrecedence.LAST + ); + } + @Override public JpaCompoundSelection tuple(Selection[] selections) { //noinspection unchecked diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java index a646082b32..4ef90b213b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java @@ -21,6 +21,7 @@ import org.hibernate.query.sqm.tree.domain.NonAggregatedCompositeSimplePath; import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -68,6 +69,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; @@ -517,6 +519,18 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitRootCte(SqmCteRoot sqmRoot) { + processStanza( + "cte", + "`" + sqmRoot.getNavigablePath() + "`", + () -> { + processJoins( sqmRoot ); + } + ); + return null; + } + private void processJoins(SqmFrom sqmFrom) { if ( !sqmFrom.hasJoins() ) { return; @@ -620,6 +634,24 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitQualifiedCteJoin(SqmCteJoin joinedFromElement) { + if ( inJoinPredicate ) { + logWithIndentation( "-> [joined-path] - `%s`", joinedFromElement.getNavigablePath() ); + } + else { + processStanza( + "cte", + "`" + joinedFromElement.getNavigablePath() + "`", + () -> { + processJoinPredicate( joinedFromElement ); + processJoins( joinedFromElement ); + } + ); + } + return null; + } + @Override public Object visitBasicValuedPath(SqmBasicValuedSimplePath path) { logWithIndentation( "-> [basic-path] - `%s`", path.getNavigablePath() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java index 54f3df031a..1fdb6fc451 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java @@ -28,9 +28,7 @@ import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.mutation.internal.MatchingIdSelectionHelper; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.spi.AbstractMutationHandler; -import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; -import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmStar; @@ -39,6 +37,7 @@ import org.hibernate.sql.ast.tree.cte.CteColumn; import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.cte.CteTable; import org.hibernate.sql.ast.tree.cte.CteTableGroup; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; @@ -70,12 +69,12 @@ public abstract class AbstractCteMutationHandler extends AbstractMutationHandler public static final String CTE_TABLE_IDENTIFIER = "id"; - private final SqmCteTable cteTable; + private final CteTable cteTable; private final DomainParameterXref domainParameterXref; private final CteMutationStrategy strategy; public AbstractCteMutationHandler( - SqmCteTable cteTable, + CteTable cteTable, SqmDeleteOrUpdateStatement sqmStatement, DomainParameterXref domainParameterXref, CteMutationStrategy strategy, @@ -87,7 +86,7 @@ public abstract class AbstractCteMutationHandler extends AbstractMutationHandler this.strategy = strategy; } - public SqmCteTable getCteTable() { + public CteTable getCteTable() { return cteTable; } @@ -142,7 +141,7 @@ public abstract class AbstractCteMutationHandler extends AbstractMutationHandler sqmConverter.pruneTableGroupJoins(); final CteStatement idSelectCte = new CteStatement( - BaseSqmToSqlAstConverter.createCteTable( getCteTable(), factory ), + getCteTable(), MatchingIdSelectionHelper.generateMatchingIdSelectStatement( entityDescriptor, sqmMutationStatement, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteDeleteHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteDeleteHandler.java index f4f8c13f1d..e10950dc14 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteDeleteHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteDeleteHandler.java @@ -19,7 +19,6 @@ import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.DeleteHandler; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.sql.internal.SqlAstQueryPartProcessingStateImpl; -import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.sql.ast.tree.MutationStatement; @@ -45,7 +44,7 @@ public class CteDeleteHandler extends AbstractCteMutationHandler implements Dele private static final String DELETE_RESULT_TABLE_NAME_PREFIX = "delete_cte_"; protected CteDeleteHandler( - SqmCteTable cteTable, + CteTable cteTable, SqmDeleteStatement sqmDeleteStatement, DomainParameterXref domainParameterXref, CteMutationStrategy strategy, @@ -108,8 +107,7 @@ public class CteDeleteHandler extends AbstractCteMutationHandler implements Dele final String tableExpression = pluralAttribute.getSeparateCollectionTable(); final CteTable dmlResultCte = new CteTable( getCteTableName( pluralAttribute ), - idSelectCte.getCteTable().getCteColumns(), - factory + idSelectCte.getCteTable().getCteColumns() ); final NamedTableReference dmlTableReference = new NamedTableReference( tableExpression, @@ -153,8 +151,7 @@ public class CteDeleteHandler extends AbstractCteMutationHandler implements Dele } final CteTable dmlResultCte = new CteTable( cteTableName, - idSelectCte.getCteTable().getCteColumns(), - factory + idSelectCte.getCteTable().getCteColumns() ); final TableReference updatingTableReference = updatingTableGroup.getTableReference( updatingTableGroup.getNavigablePath(), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java index 40f3239584..bd3e15016f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java @@ -31,7 +31,6 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.SqlExpressible; import org.hibernate.persister.entity.AbstractEntityPersister; @@ -40,6 +39,7 @@ import org.hibernate.persister.entity.Joinable; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; +import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; import org.hibernate.spi.NavigablePath; import org.hibernate.query.SemanticException; import org.hibernate.query.sqm.SortOrder; @@ -53,8 +53,6 @@ import org.hibernate.query.sqm.mutation.internal.InsertHandler; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; -import org.hibernate.query.sqm.tree.cte.SqmCteTable; -import org.hibernate.query.sqm.tree.cte.SqmCteTableColumn; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmStar; @@ -117,11 +115,11 @@ public class CteInsertHandler implements InsertHandler { private final SessionFactoryImplementor sessionFactory; private final EntityMappingType entityDescriptor; - private final SqmCteTable cteTable; + private final CteTable cteTable; private final DomainParameterXref domainParameterXref; public CteInsertHandler( - SqmCteTable cteTable, + CteTable cteTable, SqmInsertStatement sqmStatement, DomainParameterXref domainParameterXref, SessionFactoryImplementor sessionFactory) { @@ -137,6 +135,16 @@ public class CteInsertHandler implements InsertHandler { this.domainParameterXref = domainParameterXref; } + public static CteTable createCteTable( + CteTable sqmCteTable, + List sqmCteColumns, + SessionFactoryImplementor factory) { + return new CteTable( + sqmCteTable.getTableExpression(), + sqmCteColumns + ); + } + public SqmInsertStatement getSqmStatement() { return sqmStatement; } @@ -145,7 +153,7 @@ public class CteInsertHandler implements InsertHandler { return entityDescriptor; } - public SqmCteTable getCteTable() { + public CteTable getCteTable() { return cteTable; } @@ -188,11 +196,11 @@ public class CteInsertHandler implements InsertHandler { // information about the target paths final int size = sqmStatement.getInsertionTargetPaths().size(); - final List> targetPathColumns = new ArrayList<>( size ); - final List targetPathSqmCteColumns = new ArrayList<>( size ); + final List, Assignment>> targetPathColumns = new ArrayList<>( size ); + final List targetPathCteColumns = new ArrayList<>( size ); final Map, MappingModelExpressible> paramTypeResolutions = new LinkedHashMap<>(); final NamedTableReference entityTableReference = new NamedTableReference( - cteTable.getCteName(), + cteTable.getTableExpression(), TemporaryTable.DEFAULT_ALIAS, true, sessionFactory @@ -201,24 +209,28 @@ public class CteInsertHandler implements InsertHandler { final BaseSqmToSqlAstConverter.AdditionalInsertValues additionalInsertValues = sqmConverter.visitInsertionTargetPaths( (assignable, columnReferences) -> { - // Find a matching cte table column and set that at the current index - for ( SqmCteTableColumn column : cteTable.getColumns() ) { - if ( column.getType() == ( (Expression) assignable ).getExpressionType() ) { - insertStatement.addTargetColumnReferences( columnReferences ); - targetPathSqmCteColumns.add( column ); - targetPathColumns.add( - new AbstractMap.SimpleEntry<>( - column, - new Assignment( - assignable, - (Expression) assignable - ) - ) - ); - return; - } + final SqmPathInterpretation pathInterpretation = (SqmPathInterpretation) assignable; + final int offset = CteTable.determineModelPartStartIndex( + entityDescriptor, + pathInterpretation.getExpressionType() + ); + if ( offset == -1 ) { + throw new IllegalStateException( "Couldn't find matching cte column for: " + ( (Expression) assignable ).getExpressionType() ); } - throw new IllegalStateException( "Couldn't find matching cte column for: " + ( (Expression) assignable ).getExpressionType() ); + final int end = offset + pathInterpretation.getExpressionType().getJdbcTypeCount(); + // Find a matching cte table column and set that at the current index + final List columns = cteTable.getCteColumns().subList( offset, end ); + insertStatement.addTargetColumnReferences( columnReferences ); + targetPathCteColumns.addAll( columns ); + targetPathColumns.add( + new AbstractMap.SimpleEntry<>( + columns, + new Assignment( + assignable, + (Expression) assignable + ) + ) + ); }, sqmInsertStatement, entityDescriptor, @@ -232,7 +244,7 @@ public class CteInsertHandler implements InsertHandler { } ); - final boolean assignsId = targetPathSqmCteColumns.contains( cteTable.getColumns().get( 0 ) ); + final boolean assignsId = targetPathCteColumns.contains( cteTable.getCteColumns().get( 0 ) ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Create the statement that represent the source for the entity cte @@ -247,23 +259,23 @@ public class CteInsertHandler implements InsertHandler { // This returns true if the insertion target uses a sequence with an optimizer // in which case we will fill the row_number column instead of the id column if ( additionalInsertValues.applySelections( querySpec, sessionFactory ) ) { - final SqmCteTableColumn rowNumberColumn = cteTable.getColumns() - .get( cteTable.getColumns().size() - 1 ); + final CteColumn rowNumberColumn = cteTable.getCteColumns() + .get( cteTable.getCteColumns().size() - 1 ); final ColumnReference columnReference = new ColumnReference( (String) null, - rowNumberColumn.getColumnName(), + rowNumberColumn.getColumnExpression(), false, null, null, - (JdbcMapping) rowNumberColumn.getType(), + rowNumberColumn.getJdbcMapping(), sessionFactory ); insertStatement.getTargetColumnReferences().set( insertStatement.getTargetColumnReferences().size() - 1, columnReference ); - targetPathSqmCteColumns.set( - targetPathSqmCteColumns.size() - 1, + targetPathCteColumns.set( + targetPathCteColumns.size() - 1, rowNumberColumn ); } @@ -295,7 +307,7 @@ public class CteInsertHandler implements InsertHandler { final NavigablePath navigablePath = new NavigablePath( entityDescriptor.getRootPathName() ); final List columnNames = new ArrayList<>( targetPathColumns.size() ); final String valuesAlias = insertingTableGroup.getPrimaryTableReference().getIdentificationVariable(); - for ( Map.Entry entry : targetPathColumns ) { + for ( Map.Entry, Assignment> entry : targetPathColumns ) { for ( ColumnReference columnReference : entry.getValue().getAssignable().getColumnReferences() ) { columnNames.add( columnReference.getColumnExpression() ); querySpec.getSelectClause().addSqlSelection( @@ -334,24 +346,24 @@ public class CteInsertHandler implements InsertHandler { if ( !assignsId && entityDescriptor.getIdentifierGenerator() instanceof PostInsertIdentifierGenerator ) { // Add the row number to the assignments - final SqmCteTableColumn rowNumberColumn = cteTable.getColumns() - .get( cteTable.getColumns().size() - 1 ); + final CteColumn rowNumberColumn = cteTable.getCteColumns() + .get( cteTable.getCteColumns().size() - 1 ); final ColumnReference columnReference = new ColumnReference( (String) null, - rowNumberColumn.getColumnName(), + rowNumberColumn.getColumnExpression(), false, null, null, - (JdbcMapping) rowNumberColumn.getType(), + rowNumberColumn.getJdbcMapping(), sessionFactory ); insertStatement.getTargetColumnReferences().add( columnReference ); - targetPathSqmCteColumns.add( rowNumberColumn ); + targetPathCteColumns.add( rowNumberColumn ); } - final CteTable entityCteTable = BaseSqmToSqlAstConverter.createCteTable( + final CteTable entityCteTable = createCteTable( getCteTable(), - targetPathSqmCteColumns, + targetPathCteColumns, factory ); @@ -362,10 +374,7 @@ public class CteInsertHandler implements InsertHandler { final CteStatement entityCte; if ( additionalInsertValues.requiresRowNumberIntermediate() ) { - final CteTable fullEntityCteTable = BaseSqmToSqlAstConverter.createCteTable( - getCteTable(), - factory - ); + final CteTable fullEntityCteTable = getCteTable(); final String baseTableName = "base_" + entityCteTable.getTableExpression(); final CteStatement baseEntityCte = new CteStatement( entityCteTable.withName( baseTableName ), @@ -452,8 +461,7 @@ public class CteInsertHandler implements InsertHandler { ); final CteTable rowsWithSequenceCteTable = new CteTable( ROW_NUMBERS_WITH_SEQUENCE_VALUE, - Arrays.asList( rowNumberColumn, idColumn ), - sessionFactory + List.of( rowNumberColumn, idColumn ) ); final SelectStatement rowsWithSequenceStatement = new SelectStatement( rowsWithSequenceQuery ); final CteStatement rowsWithSequenceCte = new CteStatement( @@ -548,14 +556,14 @@ public class CteInsertHandler implements InsertHandler { ) ); final CteTable finalEntityCteTable; - if ( targetPathSqmCteColumns.contains( getCteTable().getColumns().get( 0 ) ) ) { + if ( targetPathCteColumns.contains( getCteTable().getCteColumns().get( 0 ) ) ) { finalEntityCteTable = entityCteTable; } else { - targetPathSqmCteColumns.add( 0, getCteTable().getColumns().get( 0 ) ); - finalEntityCteTable = BaseSqmToSqlAstConverter.createCteTable( + targetPathCteColumns.add( 0, getCteTable().getCteColumns().get( 0 ) ); + finalEntityCteTable = createCteTable( getCteTable(), - targetPathSqmCteColumns, + targetPathCteColumns, factory ); } @@ -598,10 +606,10 @@ public class CteInsertHandler implements InsertHandler { CteMaterialization.MATERIALIZED ); statement.addCteStatement( baseEntityCte ); - targetPathSqmCteColumns.add( 0, cteTable.getColumns().get( 0 ) ); - final CteTable finalEntityCteTable = BaseSqmToSqlAstConverter.createCteTable( + targetPathCteColumns.add( 0, cteTable.getCteColumns().get( 0 ) ); + final CteTable finalEntityCteTable = createCteTable( getCteTable(), - targetPathSqmCteColumns, + targetPathCteColumns, factory ); final QuerySpec finalQuerySpec = new QuerySpec( true ); @@ -703,7 +711,7 @@ public class CteInsertHandler implements InsertHandler { protected String addDmlCtes( CteContainer statement, CteStatement queryCte, - List> assignments, + List, Assignment>> assignments, boolean assignsId, MultiTableSqmMutationConverter sqmConverter, Map, List>> parameterResolutions, @@ -734,12 +742,12 @@ public class CteInsertHandler implements InsertHandler { collectTableReference( updatingTableGroup.getTableReferenceJoins().get( i ), tableReferenceByAlias::put ); } - final Map>> assignmentsByTable = CollectionHelper.mapOfSize( + final Map, Assignment>>> assignmentsByTable = CollectionHelper.mapOfSize( updatingTableGroup.getTableReferenceJoins().size() + 1 ); for ( int i = 0; i < assignments.size(); i++ ) { - final Map.Entry entry = assignments.get( i ); + final Map.Entry, Assignment> entry = assignments.get( i ); final Assignment assignment = entry.getValue(); final List assignmentColumnRefs = assignment.getAssignable().getColumnReferences(); @@ -762,7 +770,7 @@ public class CteInsertHandler implements InsertHandler { } assert assignmentTableReference != null; - List> assignmentsForTable = assignmentsByTable.get( assignmentTableReference ); + List, Assignment>> assignmentsForTable = assignmentsByTable.get( assignmentTableReference ); if ( assignmentsForTable == null ) { assignmentsForTable = new ArrayList<>(); assignmentsByTable.put( assignmentTableReference, assignmentsForTable ); @@ -785,7 +793,7 @@ public class CteInsertHandler implements InsertHandler { ); final IdentifierGenerator identifierGenerator = entityDescriptor.getEntityPersister().getIdentifierGenerator(); - final List> tableAssignments = assignmentsByTable.get( rootTableReference ); + final List, Assignment>> tableAssignments = assignmentsByTable.get( rootTableReference ); if ( ( tableAssignments == null || tableAssignments.isEmpty() ) && !( identifierGenerator instanceof PostInsertIdentifierGenerator ) ) { throw new IllegalStateException( "There must be at least a single root table assignment" ); } @@ -801,7 +809,7 @@ public class CteInsertHandler implements InsertHandler { true, true ); - final List> assignmentList = assignmentsByTable.get( updatingTableReference ); + final List, Assignment>> assignmentList = assignmentsByTable.get( updatingTableReference ); final NamedTableReference dmlTableReference = resolveUnionTableReference( updatingTableReference, tableExpression @@ -851,12 +859,9 @@ public class CteInsertHandler implements InsertHandler { SortOrder.ASCENDING ) ); - final List returningColumns = new ArrayList<>( keyCteColumns.size() + 1 ); - returningColumns.addAll( keyCteColumns ); dmlResultCte = new CteTable( cteTableName, - returningColumns, - factory + keyCteColumns ); for ( int j = 0; j < keyColumns.length; j++ ) { returningColumnReferences.add( @@ -960,8 +965,7 @@ public class CteInsertHandler implements InsertHandler { finalReturningColumns.add( rowNumberColumn ); final CteTable finalResultCte = new CteTable( getCteTableName( tableExpression ), - finalReturningColumns, - factory + finalReturningColumns ); final QuerySpec finalResultQuery = new QuerySpec( true ); finalResultQuery.getFromClause().addRoot( @@ -1028,8 +1032,7 @@ public class CteInsertHandler implements InsertHandler { ); dmlResultCte = new CteTable( cteTableName, - keyCteColumns, - factory + keyCteColumns ); for ( int j = 0; j < keyColumns.length; j++ ) { returningColumnReferences.add( @@ -1068,7 +1071,7 @@ public class CteInsertHandler implements InsertHandler { ); dmlStatement.addTargetColumnReferences( insertColumnReferences ); if ( assignmentList != null ) { - for ( Map.Entry entry : assignmentList ) { + for ( Map.Entry, Assignment> entry : assignmentList ) { final Assignment assignment = entry.getValue(); // Skip the id mapping here as we handled that already if ( assignment.getAssignedValue().getExpressionType() instanceof EntityIdentifierMapping ) { @@ -1079,16 +1082,13 @@ public class CteInsertHandler implements InsertHandler { final int size = assignmentReferences.size(); for ( int j = 0; j < size; j++ ) { final ColumnReference columnReference = assignmentReferences.get( j ); - final String columnName = size > 1 - ? entry.getKey().getColumnName() + '_' + i - : entry.getKey().getColumnName(); insertSelectSpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( 1, 0, new ColumnReference( "e", - columnName, + entry.getKey().get( j ).getColumnExpression(), columnReference.isColumnExpressionFormula(), null, null, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java index 445e463ef6..b103ee78c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java @@ -17,8 +17,8 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; -import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.sql.ast.tree.cte.CteTable; /** * @asciidoc @@ -99,7 +99,7 @@ public class CteInsertStrategy implements SqmMultiTableInsertStrategy { private final EntityPersister rootDescriptor; private final SessionFactoryImplementor sessionFactory; - private final SqmCteTable entityCteTable; + private final CteTable entityCteTable; public CteInsertStrategy( EntityMappingType rootEntityType, @@ -148,7 +148,7 @@ public class CteInsertStrategy implements SqmMultiTableInsertStrategy { else { qualifiedTableName = name; } - this.entityCteTable = SqmCteTable.createEntityTable( qualifiedTableName, rootDescriptor ); + this.entityCteTable = CteTable.createEntityTable( qualifiedTableName, rootDescriptor ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java index 06c9e86091..35acb256bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java @@ -18,9 +18,9 @@ import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; -import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.sql.ast.tree.cte.CteTable; /** * @asciidoc @@ -56,7 +56,7 @@ public class CteMutationStrategy implements SqmMultiTableMutationStrategy { private final EntityPersister rootDescriptor; private final SessionFactoryImplementor sessionFactory; - private final SqmCteTable idCteTable; + private final CteTable idCteTable; public CteMutationStrategy( EntityMappingType rootEntityType, @@ -89,7 +89,7 @@ public class CteMutationStrategy implements SqmMultiTableMutationStrategy { ); } - this.idCteTable = SqmCteTable.createIdTable( ID_TABLE_NAME, rootDescriptor ); + this.idCteTable = CteTable.createIdTable( ID_TABLE_NAME, rootDescriptor ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java index cbd3af6266..87b19afe76 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java @@ -25,7 +25,6 @@ import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.UpdateHandler; -import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.update.SqmSetClause; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; @@ -62,7 +61,7 @@ public class CteUpdateHandler extends AbstractCteMutationHandler implements Upda private static final String INSERT_RESULT_TABLE_NAME_PREFIX = "insert_cte_"; public CteUpdateHandler( - SqmCteTable cteTable, + CteTable cteTable, SqmUpdateStatement sqmStatement, DomainParameterXref domainParameterXref, CteMutationStrategy strategy, @@ -174,8 +173,7 @@ public class CteUpdateHandler extends AbstractCteMutationHandler implements Upda } final CteTable dmlResultCte = new CteTable( insertCteTableName, - idSelectCte.getCteTable().getCteColumns(), - factory + idSelectCte.getCteTable().getCteColumns() ); final NamedTableReference dmlTableReference = resolveUnionTableReference( updatingTableReference, @@ -272,8 +270,7 @@ public class CteUpdateHandler extends AbstractCteMutationHandler implements Upda } final CteTable dmlResultCte = new CteTable( cteTableName, - idSelectCte.getCteTable().getCteColumns(), - factory + idSelectCte.getCteTable().getCteColumns() ); final TableReference updatingTableReference = updatingTableGroup.getTableReference( updatingTableGroup.getNavigablePath(), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java index 31d01d288a..d0790fd7d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java @@ -20,6 +20,7 @@ import org.hibernate.query.sqm.tree.domain.NonAggregatedCompositeSimplePath; import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -70,6 +71,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFromClause; @@ -99,6 +101,7 @@ import org.hibernate.query.sqm.tree.select.SqmQueryGroup; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectClause; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelection; @@ -196,10 +199,19 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmCteStatement) { - visitStatement( sqmCteStatement.getCteDefinition() ); + visitSelectQuery( sqmCteStatement.getCteDefinition() ); return sqmCteStatement; } + protected Object visitSelectQuery(SqmSelectQuery selectQuery) { + if ( selectQuery instanceof SqmSelectStatement ) { + return visitSelectStatement( (SqmSelectStatement) selectQuery ); + } + else { + return visitSubQueryExpression( (SqmSubQuery) selectQuery ); + } + } + @Override public Object visitCteContainer(SqmCteContainer consumer) { for ( SqmCteStatement cteStatement : consumer.getCteStatements() ) { @@ -258,6 +270,13 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmRoot) { + sqmRoot.visitReusablePaths( path -> path.accept( this ) ); + sqmRoot.visitSqmJoins( sqmJoin -> sqmJoin.accept( this ) ); + return sqmRoot; + } + @Override public Object visitCrossJoin(SqmCrossJoin joinedFromElement) { joinedFromElement.visitReusablePaths( path -> path.accept( this ) ); @@ -303,6 +322,16 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker joinedFromElement) { + joinedFromElement.visitReusablePaths( path -> path.accept( this ) ); + joinedFromElement.visitSqmJoins( sqmJoin -> sqmJoin.accept( this ) ); + if ( joinedFromElement.getJoinPredicate() != null ) { + joinedFromElement.getJoinPredicate().accept( this ); + } + return joinedFromElement; + } + @Override public Object visitBasicValuedPath(SqmBasicValuedSimplePath path) { return path; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 981be35e8c..245180d901 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -55,7 +55,6 @@ import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.loader.MultipleBagFetchException; import org.hibernate.metamodel.CollectionClassification; import org.hibernate.metamodel.MappingMetamodel; -import org.hibernate.metamodel.mapping.Association; import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; @@ -90,14 +89,13 @@ import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.metamodel.model.convert.internal.OrdinalEnumValueConverter; import org.hibernate.metamodel.model.convert.spi.BasicValueConverter; import org.hibernate.metamodel.model.domain.BasicDomainType; -import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; -import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; -import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPath; import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPathSource; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaSearchOrder; import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource; @@ -161,7 +159,6 @@ import org.hibernate.query.sqm.tree.cte.SqmCteContainer; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.query.sqm.tree.cte.SqmCteTableColumn; -import org.hibernate.query.sqm.tree.cte.SqmSearchClauseSpecification; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.AbstractSqmSpecificPluralPartPath; import org.hibernate.query.sqm.tree.domain.NonAggregatedCompositeSimplePath; @@ -169,6 +166,7 @@ import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedRootJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; @@ -224,6 +222,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; @@ -259,6 +258,7 @@ import org.hibernate.query.sqm.tree.select.SqmQueryGroup; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectClause; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelection; @@ -290,6 +290,7 @@ import org.hibernate.sql.ast.tree.cte.CteColumn; import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.cte.CteTable; +import org.hibernate.sql.ast.tree.cte.CteTableGroup; import org.hibernate.sql.ast.tree.cte.SearchClauseSpecification; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.Any; @@ -309,6 +310,7 @@ import org.hibernate.sql.ast.tree.expression.ExtractUnit; import org.hibernate.sql.ast.tree.expression.Format; import org.hibernate.sql.ast.tree.expression.JdbcLiteral; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; import org.hibernate.sql.ast.tree.expression.Over; import org.hibernate.sql.ast.tree.expression.Overflow; @@ -395,7 +397,6 @@ import org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType; import org.jboss.logging.Logger; import jakarta.persistence.TemporalType; -import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.Type; @@ -421,7 +422,6 @@ public abstract class BaseSqmToSqlAstConverter extends Base private final SqlAstCreationContext creationContext; private final boolean jpaQueryComplianceEnabled; private final SqmStatement statement; - private final CteContainer cteContainer = new GlobalCteContainer(); private final QueryOptions queryOptions; private final LoadQueryInfluencers loadQueryInfluencers; @@ -441,6 +441,12 @@ public abstract class BaseSqmToSqlAstConverter extends Base private boolean deduplicateSelectionItems; private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide; private SqmQueryPart currentSqmQueryPart; + private CteContainer cteContainer; + /** + * A map from {@link SqmCteTable#getCteName()} to the final SQL name. + * We use this global map as most databases don't support shadowing of names. + */ + private Map cteNameMapping; private boolean containsCollectionFetches; private boolean trackSelectionsForGroup; /* @@ -729,6 +735,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public UpdateStatement visitUpdateStatement(SqmUpdateStatement sqmStatement) { + final CteContainer oldCteContainer = cteContainer; final CteContainer cteContainer = this.visitCteContainer( sqmStatement ); final SqmRoot sqmTarget = sqmStatement.getTarget(); @@ -798,6 +805,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base } finally { popProcessingStateStack(); + this.cteContainer = oldCteContainer; } } @@ -966,6 +974,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public DeleteStatement visitDeleteStatement(SqmDeleteStatement statement) { + final CteContainer oldCteContainer = cteContainer; final CteContainer cteContainer = this.visitCteContainer( statement ); final String entityName = statement.getTarget().getEntityName(); @@ -1024,6 +1033,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base } finally { popProcessingStateStack(); + this.cteContainer = oldCteContainer; } } @@ -1032,6 +1042,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public InsertStatement visitInsertSelectStatement(SqmInsertSelectStatement sqmStatement) { + final CteContainer oldCteContainer = cteContainer; final CteContainer cteContainer = this.visitCteContainer( sqmStatement ); final String entityName = sqmStatement.getTarget().getEntityName(); @@ -1109,6 +1120,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base } ); + this.cteContainer = oldCteContainer; + return insertStatement; } @@ -1135,6 +1148,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public InsertStatement visitInsertValuesStatement(SqmInsertValuesStatement sqmStatement) { + final CteContainer oldCteContainer = cteContainer; final CteContainer cteContainer = this.visitCteContainer( sqmStatement ); final String entityName = sqmStatement.getTarget().getEntityName(); final EntityPersister entityDescriptor = creationContext.getSessionFactory() @@ -1198,6 +1212,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base } finally { popProcessingStateStack(); + this.cteContainer = oldCteContainer; } } @@ -1473,10 +1488,16 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public SelectStatement visitSelectStatement(SqmSelectStatement statement) { + final CteContainer oldCteContainer = cteContainer; final CteContainer cteContainer = this.visitCteContainer( statement ); final QueryPart queryPart = visitQueryPart( statement.getQueryPart() ); final List> domainResults = queryPart.isRoot() ? this.domainResults : Collections.emptyList(); - return new SelectStatement( cteContainer, queryPart, domainResults ); + try { + return new SelectStatement( cteContainer, queryPart, domainResults ); + } + finally { + this.cteContainer = oldCteContainer; + } } @Override @@ -1527,37 +1548,196 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public CteStatement visitCteStatement(SqmCteStatement sqmCteStatement) { - final CteTable cteTable = createCteTable( - sqmCteStatement.getCteTable(), - getCreationContext().getSessionFactory() + final SqmCteTable sqmCteTable = sqmCteStatement.getCteTable(); + final String cteName = getCteName( sqmCteTable ); + final SqmSelectQuery selectStatement = sqmCteStatement.getCteDefinition(); + final SqmQueryPart queryPart = selectStatement.getQueryPart(); + final Literal cycleLiteral = getLiteral( sqmCteStatement.getCycleLiteral() ); + final Literal noCycleLiteral = getLiteral( sqmCteStatement.getNoCycleLiteral() ); + final JdbcMapping cycleMarkType = cycleLiteral == null ? null : cycleLiteral.getJdbcMapping(); + final BasicType stringType = creationContext.getSessionFactory() + .getTypeConfiguration() + .getBasicTypeForJavaType( String.class ); + if ( queryPart instanceof SqmQueryGroup && queryPart.getSortSpecifications().isEmpty() + && queryPart.getFetchExpression() == null && queryPart.getOffsetExpression() == null ) { + final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; + switch ( queryGroup.getSetOperator() ) { + case UNION: + case UNION_ALL: + if ( queryGroup.getQueryParts().size() == 2 ) { + // This could potentially be a recursive CTE, + // for which we need to visit the non-recursive part first + // and register a CteStatement before visiting the recursive part. + // This is important, because the recursive part will refer to the CteStatement, + // hence we require that it is registered already + final CteContainer oldCteContainer = cteContainer; + final CteContainer subCteContainer = this.visitCteContainer( selectStatement ); + // Note that the following is a trimmed down version of what visitQueryGroup does + try { + final SqmQueryPart firstPart = queryGroup.getQueryParts().get( 0 ); + final SqmQueryPart secondPart = queryGroup.getQueryParts().get( 1 ); + final List newQueryParts = new ArrayList<>( 2 ); + final QueryGroup group = new QueryGroup( + getProcessingStateStack().isEmpty(), + queryGroup.getSetOperator(), + newQueryParts + ); + + final SqlAstQueryPartProcessingStateImpl processingState = new SqlAstQueryPartProcessingStateImpl( + group, + getCurrentProcessingState(), + this, + DelegatingSqmAliasedNodeCollector::new, + currentClauseStack::getCurrent, + deduplicateSelectionItems + ); + final DelegatingSqmAliasedNodeCollector collector = (DelegatingSqmAliasedNodeCollector) processingState + .getSqlExpressionResolver(); + final SqmQueryPart oldSqmQueryPart = currentSqmQueryPart; + currentSqmQueryPart = queryGroup; + pushProcessingState( processingState ); + + try { + newQueryParts.add( visitQueryPart( firstPart ) ); + + collector.setSqmAliasedNodeCollector( + (SqmAliasedNodeCollector) lastPoppedProcessingState.getSqlExpressionResolver() + ); + + // Before visiting the second query part, setup the CteStatement and register it + final CteTable cteTable = new CteTable( + cteName, + sqmCteTable.resolveTableGroupProducer( + cteName, + newQueryParts.get( 0 ) + .getFirstQuerySpec() + .getSelectClause() + .getSqlSelections(), + lastPoppedFromClauseIndex + ) + ); + + final CteStatement cteStatement = new CteStatement( + cteTable, + new SelectStatement( subCteContainer, group, Collections.emptyList() ), + sqmCteStatement.getMaterialization(), + sqmCteStatement.getSearchClauseKind(), + visitSearchBySpecifications( cteTable, sqmCteStatement.getSearchBySpecifications() ), + createCteColumn( sqmCteStatement.getSearchAttributeName(), stringType ), + visitCycleColumns( cteTable, sqmCteStatement.getCycleAttributes() ), + createCteColumn( sqmCteStatement.getCycleMarkAttributeName(), cycleMarkType ), + createCteColumn( sqmCteStatement.getCyclePathAttributeName(), stringType ), + cycleLiteral, + noCycleLiteral + ); + oldCteContainer.addCteStatement( cteStatement ); + + // Finally, visit the second part, which is potentially the recursive part + newQueryParts.add( visitQueryPart( secondPart ) ); + return cteStatement; + } + finally { + popProcessingStateStack(); + currentSqmQueryPart = oldSqmQueryPart; + } + } + finally { + this.cteContainer = oldCteContainer; + } + } + break; + } + } + final SelectStatement statement; + if ( selectStatement instanceof SqmSubQuery ) { + statement = visitSubQueryExpression( (SqmSubQuery) selectStatement ); + } + else { + statement = visitSelectStatement( (SqmSelectStatement) selectStatement ); + } + + final CteTable cteTable = new CteTable( + cteName, + sqmCteTable.resolveTableGroupProducer( + cteName, + statement.getQuerySpec().getSelectClause().getSqlSelections(), + lastPoppedFromClauseIndex + ) ); - return new CteStatement( + final CteStatement cteStatement = new CteStatement( cteTable, - visitStatement( sqmCteStatement.getCteDefinition() ), + statement, sqmCteStatement.getMaterialization(), sqmCteStatement.getSearchClauseKind(), visitSearchBySpecifications( cteTable, sqmCteStatement.getSearchBySpecifications() ), - visitCycleColumns( cteTable, sqmCteStatement.getCycleColumns() ), - findCteColumn( cteTable, sqmCteStatement.getCycleMarkColumn() ), - sqmCteStatement.getCycleValue(), - sqmCteStatement.getNoCycleValue() + createCteColumn( sqmCteStatement.getSearchAttributeName(), stringType ), + visitCycleColumns( cteTable, sqmCteStatement.getCycleAttributes() ), + createCteColumn( sqmCteStatement.getCycleMarkAttributeName(), cycleMarkType ), + createCteColumn( sqmCteStatement.getCyclePathAttributeName(), stringType ), + cycleLiteral, + noCycleLiteral ); + cteContainer.addCteStatement( cteStatement ); + return cteStatement; + } + + private String getCteName(SqmCteTable sqmCteTable) { + final String name = sqmCteTable.getName(); + if ( cteNameMapping == null ) { + cteNameMapping = new HashMap<>(); + } + final String key = sqmCteTable.getCteName(); + final String generatedCteName = cteNameMapping.get( key ); + if ( generatedCteName != null ) { + return generatedCteName; + } + final String cteName; + if ( name != null ) { + cteName = generateCteName( name ); + } + else { + cteName = generateCteName( "cte" + cteNameMapping.size() ); + } + cteNameMapping.put( key, cteName ); + return cteName; + } + + private String generateCteName(String baseName) { + String name = baseName; + int maxTries = 5; + for ( int i = 0; i < maxTries; i++ ) { + if ( !cteNameMapping.containsKey( name ) ) { + return name; + } + name = baseName + "_" + i; + } + throw new InterpretationException( + String.format( + "Couldn't generate CTE name for base name [%s] after %d tries", + baseName, + maxTries + ) + ); + } + + private Literal getLiteral(SqmLiteral value) { + return value == null ? null : (Literal) visitLiteral( value ); } protected List visitSearchBySpecifications( CteTable cteTable, - List searchBySpecifications) { + List searchBySpecifications) { if ( searchBySpecifications == null || searchBySpecifications.isEmpty() ) { return null; } final int size = searchBySpecifications.size(); final List searchClauseSpecifications = new ArrayList<>( size ); for ( int i = 0; i < size; i++ ) { - final SqmSearchClauseSpecification specification = searchBySpecifications.get( i ); + final JpaSearchOrder specification = searchBySpecifications.get( i ); forEachCteColumn( cteTable, - specification.getCteColumn(), + (SqmCteTableColumn) specification.getAttribute(), cteColumn -> searchClauseSpecifications.add( new SearchClauseSpecification( cteColumn, @@ -1571,25 +1751,11 @@ public abstract class BaseSqmToSqlAstConverter extends Base return searchClauseSpecifications; } - protected CteColumn findCteColumn(CteTable cteTable, SqmCteTableColumn cteColumn) { + protected CteColumn createCteColumn(String cteColumn, JdbcMapping jdbcMapping) { if ( cteColumn == null ) { return null; } - final List cteColumns = cteTable.getCteColumns(); - final int size = cteColumns.size(); - for ( int i = 0; i < size; i++ ) { - final CteColumn column = cteColumns.get( i ); - if ( cteColumn.getColumnName().equals( column.getColumnExpression() ) ) { - return column; - } - } - throw new IllegalArgumentException( - String.format( - "Couldn't find cte column %s in cte %s", - cteColumn.getColumnName(), - cteTable.getTableExpression() - ) - ); + return new CteColumn( cteColumn, jdbcMapping ); } protected void forEachCteColumn(CteTable cteTable, SqmCteTableColumn cteColumn, Consumer consumer) { @@ -1597,13 +1763,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base final int size = cteColumns.size(); for ( int i = 0; i < size; i++ ) { final CteColumn column = cteColumns.get( i ); - if ( cteColumn.getColumnName().equals( column.getColumnExpression() ) ) { + final String columnName = column.getColumnExpression(); + final String sqmName = cteColumn.getName(); + if ( columnName.regionMatches( 0, sqmName, 0, sqmName.length() ) + && ( columnName.length() == sqmName.length() + || columnName.charAt( sqmName.length() ) == '_' ) ) { consumer.accept( column ); } } } - protected List visitCycleColumns(CteTable cteTable, List cycleColumns) { + protected List visitCycleColumns(CteTable cteTable, List cycleColumns) { if ( cycleColumns == null || cycleColumns.isEmpty() ) { return null; } @@ -1612,64 +1782,26 @@ public abstract class BaseSqmToSqlAstConverter extends Base for ( int i = 0; i < size; i++ ) { forEachCteColumn( cteTable, - cycleColumns.get( i ), + (SqmCteTableColumn) cycleColumns.get( i ), columns::add ); } return columns; } - public static CteTable createCteTable(SqmCteTable sqmCteTable, SessionFactoryImplementor factory) { - return createCteTable( sqmCteTable, sqmCteTable.getColumns(), factory ); - } - - public static CteTable createCteTable( - SqmCteTable sqmCteTable, - List sqmCteColumns, - SessionFactoryImplementor factory) { - final List sqlCteColumns = new ArrayList<>( sqmCteColumns.size() ); - - for ( int i = 0; i < sqmCteColumns.size(); i++ ) { - final SqmCteTableColumn sqmCteTableColumn = sqmCteColumns.get( i ); - ValueMapping valueMapping = sqmCteTableColumn.getType(); - if ( valueMapping instanceof Association ) { - valueMapping = ( (Association) valueMapping ).getForeignKeyDescriptor(); - } - if ( valueMapping instanceof EmbeddableValuedModelPart ) { - valueMapping.forEachJdbcType( - (index, jdbcMapping) -> sqlCteColumns.add( - new CteColumn( - sqmCteTableColumn.getColumnName() + "_" + index, - jdbcMapping - ) - ) - ); - } - else { - sqlCteColumns.add( - new CteColumn( - sqmCteTableColumn.getColumnName(), - ( (BasicValuedMapping) valueMapping ).getJdbcMapping() - ) - ); - } - } - - return new CteTable( - sqmCteTable.getCteName(), - sqlCteColumns, - factory - ); - } - @Override public CteContainer visitCteContainer(SqmCteContainer consumer) { final Collection> sqmCteStatements = consumer.getCteStatements(); - if ( consumer.isWithRecursive() ) { - cteContainer.setWithRecursive( true ); - } - for ( SqmCteStatement sqmCteStatement : sqmCteStatements ) { - cteContainer.addCteStatement( visitCteStatement( sqmCteStatement ) ); + cteContainer = new CteContainerImpl( cteContainer ); + if ( !sqmCteStatements.isEmpty() ) { + currentClauseStack.push( Clause.WITH ); + for ( SqmCteStatement sqmCteStatement : sqmCteStatements ) { + visitCteStatement( sqmCteStatement ); + } + currentClauseStack.pop(); + // Avoid leaking the processing state from CTEs to upper levels + lastPoppedFromClauseIndex = null; + lastPoppedProcessingState = null; } return cteContainer; } @@ -2543,15 +2675,15 @@ public abstract class BaseSqmToSqlAstConverter extends Base final TableGroup tableGroup; if ( sqmRoot instanceof SqmDerivedRoot ) { final SqmDerivedRoot derivedRoot = (SqmDerivedRoot) sqmRoot; - final QueryPart queryPart = (QueryPart) derivedRoot.getQueryPart().accept( this ); + final SelectStatement statement = (SelectStatement) derivedRoot.getQueryPart().accept( this ); final AnonymousTupleType tupleType = (AnonymousTupleType) sqmRoot.getNodeType(); - final List sqlSelections = queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + final List sqlSelections = statement.getQueryPart().getFirstQuerySpec().getSelectClause().getSqlSelections(); final AnonymousTupleTableGroupProducer tableGroupProducer = tupleType.resolveTableGroupProducer( derivedRoot.getExplicitAlias(), sqlSelections, lastPoppedFromClauseIndex ); - final List columnNames = determineColumnNames( tupleType ); + final List columnNames = tupleType.determineColumnNames(); final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( derivedRoot.getExplicitAlias() == null ? "derived" : derivedRoot.getExplicitAlias() ); @@ -2559,7 +2691,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base tableGroup = new QueryPartTableGroup( derivedRoot.getNavigablePath(), tableGroupProducer, - queryPart, + statement, identifierVariable, columnNames, tableGroupProducer.getCompatibleTableExpressions(), @@ -2568,6 +2700,15 @@ public abstract class BaseSqmToSqlAstConverter extends Base creationContext.getSessionFactory() ); } + else if ( sqmRoot instanceof SqmCteRoot ) { + final SqmCteRoot cteRoot = (SqmCteRoot) sqmRoot; + tableGroup = createCteTableGroup( + getCteName( cteRoot.getCte().getCteTable() ), + cteRoot.getNavigablePath(), + cteRoot.getExplicitAlias(), + true + ); + } else { final EntityPersister entityDescriptor = resolveEntityPersister( sqmRoot.getModel() ); tableGroup = entityDescriptor.createRootTableGroup( @@ -2600,24 +2741,42 @@ public abstract class BaseSqmToSqlAstConverter extends Base consumeJoins( sqmRoot, fromClauseIndex, tableGroup ); } - private List determineColumnNames(AnonymousTupleType tupleType) { - final int componentCount = tupleType.componentCount(); - final List columnNames = new ArrayList<>( componentCount ); - for ( int i = 0; i < componentCount; i++ ) { - final SqmSelectableNode selectableNode = tupleType.getSelectableNode( i ); - final String componentName = tupleType.getComponentName( i ); - if ( selectableNode instanceof SqmPath ) { - addColumnNames( - columnNames, - ( (SqmPath) selectableNode ).getNodeType().getSqmPathType(), - componentName - ); - } - else { - columnNames.add( componentName ); - } + private TableGroup createCteTableGroup( + String cteName, + NavigablePath navigablePath, + String explicitAlias, + boolean canUseInnerJoins) { + final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( + explicitAlias == null ? cteName : explicitAlias + ); + final String identifierVariable = sqlAliasBase.generateNewAlias(); + final CteStatement cteStatement = cteContainer.getCteStatement( cteName ); + if ( cteStatement == null ) { + throw new InterpretationException( "Could not find CTE for name '" + cteName + "'!" ); } - return columnNames; + final QueryPart cteQueryPart = ( (SelectStatement) cteStatement.getCteDefinition() ).getQueryPart(); + // If the query part of the CTE is one which we are currently processing, then this is a recursive CTE + if ( cteQueryPart instanceof QueryGroup && Boolean.TRUE == processingStateStack.findCurrentFirst( + state -> { + if ( state instanceof SqlAstQueryPartProcessingState ) { + if ( ( (SqlAstQueryPartProcessingState) state ).getInflightQueryPart() == cteQueryPart ) { + return Boolean.TRUE; + } + } + return null; + } + ) ) { + cteStatement.setRecursive(); + } + final AnonymousTupleTableGroupProducer tableGroupProducer = cteStatement.getCteTable().getTableGroupProducer(); + return new CteTableGroup( + canUseInnerJoins, + navigablePath, + sqlAliasBase, + tableGroupProducer, + new NamedTableReference( cteName, identifierVariable, false, null ), + tableGroupProducer.getCompatibleTableExpressions() + ); } private void consumeJoins(SqmRoot sqmRoot, FromClauseIndex fromClauseIndex, TableGroup tableGroup) { @@ -2759,6 +2918,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base else if ( sqmJoin instanceof SqmDerivedJoin ) { return consumeDerivedJoin( ( (SqmDerivedJoin) sqmJoin ), lhsTableGroup, transitive ); } + else if ( sqmJoin instanceof SqmCteJoin ) { + return consumeCteJoin( ( (SqmCteJoin) sqmJoin ), lhsTableGroup, transitive ); + } else if ( sqmJoin instanceof SqmPluralPartJoin ) { return consumePluralPartJoin( ( (SqmPluralPartJoin) sqmJoin ), ownerTableGroup, transitive ); } @@ -2955,7 +3117,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base final SqlAstJoinType correspondingSqlJoinType = sqmJoin.getSqmJoinType().getCorrespondingSqlJoinType(); final TableGroup tableGroup = entityDescriptor.createRootTableGroup( - correspondingSqlJoinType == SqlAstJoinType.INNER || correspondingSqlJoinType == SqlAstJoinType.CROSS , + correspondingSqlJoinType == SqlAstJoinType.INNER || correspondingSqlJoinType == SqlAstJoinType.CROSS, sqmJoin.getNavigablePath(), sqmJoin.getExplicitAlias(), () -> predicate -> additionalRestrictions = SqlAstTreeHelper.combinePredicates( @@ -2992,15 +3154,15 @@ public abstract class BaseSqmToSqlAstConverter extends Base } private TableGroup consumeDerivedJoin(SqmDerivedJoin sqmJoin, TableGroup parentTableGroup, boolean transitive) { - final QueryPart queryPart = (QueryPart) sqmJoin.getQueryPart().accept( this ); + final SelectStatement statement = (SelectStatement) sqmJoin.getQueryPart().accept( this ); final AnonymousTupleType tupleType = (AnonymousTupleType) sqmJoin.getNodeType(); - final List sqlSelections = queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + final List sqlSelections = statement.getQueryPart().getFirstQuerySpec().getSelectClause().getSqlSelections(); final AnonymousTupleTableGroupProducer tableGroupProducer = tupleType.resolveTableGroupProducer( sqmJoin.getExplicitAlias(), sqlSelections, lastPoppedFromClauseIndex ); - final List columnNames = determineColumnNames( tupleType ); + final List columnNames = tupleType.determineColumnNames(); final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( sqmJoin.getExplicitAlias() == null ? "derived" : sqmJoin.getExplicitAlias() ); @@ -3008,7 +3170,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base final QueryPartTableGroup queryPartTableGroup = new QueryPartTableGroup( sqmJoin.getNavigablePath(), tableGroupProducer, - queryPart, + statement, identifierVariable, columnNames, tableGroupProducer.getCompatibleTableExpressions(), @@ -3042,31 +3204,38 @@ public abstract class BaseSqmToSqlAstConverter extends Base return queryPartTableGroup; } - private void addColumnNames(List columnNames, DomainType domainType, String componentName) { - if ( domainType instanceof EntityDomainType ) { - final EntityDomainType entityDomainType = (EntityDomainType) domainType; - final SingularPersistentAttribute idAttribute = entityDomainType.findIdAttribute(); - final String idPath; - if ( idAttribute == null ) { - idPath = componentName; - } - else { - idPath = componentName + "_" + idAttribute.getName(); - } - addColumnNames( columnNames, entityDomainType.getIdentifierDescriptor().getSqmPathType(), idPath ); + private TableGroup consumeCteJoin(SqmCteJoin sqmJoin, TableGroup parentTableGroup, boolean transitive) { + final SqlAstJoinType correspondingSqlJoinType = sqmJoin.getSqmJoinType().getCorrespondingSqlJoinType(); + final TableGroup tableGroup = createCteTableGroup( + getCteName( sqmJoin.getCte().getCteTable() ), + sqmJoin.getNavigablePath(), + sqmJoin.getExplicitAlias(), + correspondingSqlJoinType == SqlAstJoinType.INNER || correspondingSqlJoinType == SqlAstJoinType.CROSS + ); + getFromClauseIndex().register( sqmJoin, tableGroup ); + + final TableGroupJoin tableGroupJoin = new TableGroupJoin( + tableGroup.getNavigablePath(), + correspondingSqlJoinType, + tableGroup, + null + ); + + // add any additional join restrictions + if ( sqmJoin.getJoinPredicate() != null ) { + final SqmJoin oldJoin = currentlyProcessingJoin; + currentlyProcessingJoin = sqmJoin; + tableGroupJoin.applyPredicate( visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ) ); + currentlyProcessingJoin = oldJoin; } - else if ( domainType instanceof ManagedDomainType ) { - for ( Attribute attribute : ( (ManagedDomainType) domainType ).getAttributes() ) { - if ( !( attribute instanceof SingularPersistentAttribute ) ) { - throw new IllegalArgumentException( "Only embeddables without collections are supported" ); - } - final DomainType attributeType = ( (SingularPersistentAttribute) attribute ).getType(); - addColumnNames( columnNames, attributeType, componentName + "_" + attribute.getName() ); - } - } - else { - columnNames.add( componentName ); + + // Note that we add the entity join after processing the predicate because implicit joins needed in there + // can be just ordered right before the entity join without changing the semantics + parentTableGroup.addTableGroupJoin( tableGroupJoin ); + if ( transitive ) { + consumeExplicitJoins( sqmJoin, tableGroup ); } + return tableGroup; } private TableGroup consumePluralPartJoin(SqmPluralPartJoin sqmJoin, TableGroup lhsTableGroup, boolean transitive) { @@ -3266,7 +3435,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base final TableGroup tableGroup; if ( subPart instanceof TableGroupJoinProducer ) { final TableGroupJoinProducer joinProducer = (TableGroupJoinProducer) subPart; - if ( fromClauseIndex.findTableGroupOnCurrentFromClause( actualParentTableGroup.getNavigablePath() ) == null ) { + if ( fromClauseIndex.findTableGroupOnCurrentFromClause( actualParentTableGroup.getNavigablePath() ) == null + && !isRecursiveCte( actualParentTableGroup ) ) { final QuerySpec querySpec = currentQuerySpec(); // The parent table group is on a parent query, so we need a root table group tableGroup = joinProducer.createRootTableGroupJoin( @@ -3330,6 +3500,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base return tableGroup; } + private boolean isRecursiveCte(TableGroup tableGroup) { + if ( tableGroup instanceof CteTableGroup ) { + final CteTableGroup cteTableGroup = (CteTableGroup) tableGroup; + return cteContainer.getCteStatement( cteTableGroup.getPrimaryTableReference().getTableId() ).isRecursive(); + } + return false; + } + private TableGroup findCompatibleJoinedGroup( TableGroup parentTableGroup, TableGroupJoinProducer joinProducer, @@ -3408,6 +3586,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base throw new InterpretationException( "SqmDerivedRoot not yet resolved to TableGroup" ); } + @Override + public Object visitRootCte(SqmCteRoot sqmRoot) { + final TableGroup resolved = getFromClauseAccess().findTableGroup( sqmRoot.getNavigablePath() ); + if ( resolved != null ) { + log.tracef( "SqmCteRoot [%s] resolved to existing TableGroup [%s]", sqmRoot, resolved ); + return visitTableGroup( resolved, sqmRoot ); + } + + throw new InterpretationException( "SqmCteRoot not yet resolved to TableGroup" ); + } + @Override public Expression visitQualifiedAttributeJoin(SqmAttributeJoin sqmJoin) { final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); @@ -3430,6 +3619,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base throw new InterpretationException( "SqmDerivedJoin not yet resolved to TableGroup" ); } + @Override + public Object visitQualifiedCteJoin(SqmCteJoin sqmJoin) { + final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); + if ( existing != null ) { + log.tracef( "SqmCteJoin [%s] resolved to existing TableGroup [%s]", sqmJoin, existing ); + return visitTableGroup( existing, sqmJoin ); + } + + throw new InterpretationException( "SqmCteJoin not yet resolved to TableGroup" ); + } + @Override public Expression visitCrossJoin(SqmCrossJoin sqmJoin) { final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); @@ -4025,7 +4225,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base finally { popProcessingStateStack(); } - return subQuerySpec; + return new SelectStatement( subQuerySpec ); } @Override @@ -4211,7 +4411,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base finally { popProcessingStateStack(); } - return subQuerySpec; + return new SelectStatement( subQuerySpec ); } protected Expression createLateralJoinExpression( @@ -4406,7 +4606,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base lateralTableGroup = new QueryPartTableGroup( queryPath, null, - subQuerySpec, + new SelectStatement( subQuerySpec ), identifierVariable, columnNames, compatibleTableExpressions, @@ -4720,7 +4920,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base final MappingModelExpressible localExpressible = SqmMappingModelHelper.resolveMappingModelExpressible( literal, creationContext.getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(), - getFromClauseAccess()::findTableGroup + getFromClauseAccess() == null ? null : getFromClauseAccess()::findTableGroup ); if ( localExpressible == null ) { expressible = getElementExpressible( inferableExpressible ); @@ -6052,14 +6252,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base } @Override - public QueryPart visitSubQueryExpression(SqmSubQuery sqmSubQuery) { + public SelectStatement visitSubQueryExpression(SqmSubQuery sqmSubQuery) { // The only purpose for tracking the current join is to // Reset the current join for subqueries because in there, we won't add nested joins final SqmJoin oldJoin = currentlyProcessingJoin; + final CteContainer oldCteContainer = cteContainer; currentlyProcessingJoin = null; + final CteContainer cteContainer = this.visitCteContainer( sqmSubQuery ); final QueryPart queryPart = visitQueryPart( sqmSubQuery.getQueryPart() ); currentlyProcessingJoin = oldJoin; - return queryPart; + this.cteContainer = oldCteContainer; + return new SelectStatement( cteContainer, queryPart, Collections.emptyList() ); } @Override @@ -6496,7 +6699,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base return new InSubQueryPredicate( lhs, - subQuerySpec, + new SelectStatement( subQuerySpec ), predicate.isNegated(), getBooleanType() ); @@ -6864,7 +7067,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public Object visitExistsPredicate(SqmExistsPredicate predicate) { return new ExistsPredicate( - (QueryPart) predicate.getExpression().accept( this ), + (SelectStatement) predicate.getExpression().accept( this ), predicate.isNegated(), getBooleanType() ); @@ -7410,24 +7613,15 @@ public abstract class BaseSqmToSqlAstConverter extends Base return type1; } - private static class GlobalCteContainer implements CteContainer { + private static class CteContainerImpl implements CteContainer { + private final CteContainer parent; private final Map cteStatements; - private boolean recursive; - public GlobalCteContainer() { + public CteContainerImpl(CteContainer parent) { + this.parent = parent; this.cteStatements = new LinkedHashMap<>(); } - @Override - public boolean isWithRecursive() { - return recursive; - } - - @Override - public void setWithRecursive(boolean recursive) { - this.recursive = recursive; - } - @Override public Map getCteStatements() { return cteStatements; @@ -7435,7 +7629,11 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public CteStatement getCteStatement(String cteLabel) { - return cteStatements.get( cteLabel ); + final CteStatement cteStatement = cteStatements.get( cteLabel ); + if ( cteStatement == null && parent != null ) { + return parent.getCteStatement( cteLabel ); + } + return cteStatement; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java index 52a7392cef..9f9d842be7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java @@ -24,6 +24,7 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.internal.EntityCollectionPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.query.derived.AnonymousTupleEntityValuedModelPart; import org.hibernate.spi.NavigablePath; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -110,6 +111,17 @@ public class EntityValuedPathInterpretation extends AbstractSqmPathInterpreta } } } + else if ( pathMapping instanceof AnonymousTupleEntityValuedModelPart ) { + // AnonymousEntityValuedModelParts use the PK, which the inferred path will also use in this case, + // so we render this path as it is + return from( + sqmPath.getNavigablePath(), + tableGroup, + pathMapping, + inferredMapping, + sqlAstCreationState + ); + } else { // This is the case when the inferred mapping is an association, but the path mapping is not, // or the path mapping and the inferred mapping are for the same association, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java index 079949ff39..f183c02fc2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java @@ -10,14 +10,21 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import jakarta.persistence.criteria.AbstractQuery; +import jakarta.persistence.criteria.CriteriaQuery; + /** * @author Steve Ebersole */ @@ -25,7 +32,6 @@ public abstract class AbstractSqmDmlStatement extends AbstractSqmStatement implements SqmDmlStatement { private final Map> cteStatements; - private boolean withRecursiveCte; private SqmRoot target; public AbstractSqmDmlStatement(SqmQuerySource querySource, NodeBuilder nodeBuilder) { @@ -43,11 +49,9 @@ public abstract class AbstractSqmDmlStatement SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target) { super( builder, querySource, parameters ); this.cteStatements = cteStatements; - this.withRecursiveCte = withRecursiveCte; this.target = target; } @@ -59,16 +63,6 @@ public abstract class AbstractSqmDmlStatement return cteStatements; } - @Override - public boolean isWithRecursive() { - return withRecursiveCte; - } - - @Override - public void setWithRecursive(boolean withRecursiveCte) { - this.withRecursiveCte = withRecursiveCte; - } - @Override public Collection> getCteStatements() { return cteStatements.values(); @@ -80,10 +74,100 @@ public abstract class AbstractSqmDmlStatement } @Override - public void addCteStatement(SqmCteStatement cteStatement) { - if ( cteStatements.putIfAbsent( cteStatement.getCteTable().getCteName(), cteStatement ) != null ) { + public Collection> getCteCriterias() { + return cteStatements.values(); + } + + @Override + public JpaCteCriteria getCteCriteria(String cteName) { + return (JpaCteCriteria) cteStatements.get( cteName ); + } + + @Override + public JpaCteCriteria with(AbstractQuery criteria) { + return withInternal( Long.toString( System.nanoTime() ), criteria ); + } + + @Override + public JpaCteCriteria withRecursiveUnionAll( + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( Long.toString( System.nanoTime() ), baseCriteria, false, recursiveCriteriaProducer ); + } + + @Override + public JpaCteCriteria withRecursiveUnionDistinct( + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( Long.toString( System.nanoTime() ), baseCriteria, true, recursiveCriteriaProducer ); + } + + @Override + public JpaCteCriteria with(String name, AbstractQuery criteria) { + return withInternal( validateCteName( name ), criteria ); + } + + @Override + public JpaCteCriteria withRecursiveUnionAll( + String name, + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( validateCteName( name ), baseCriteria, false, recursiveCriteriaProducer ); + } + + @Override + public JpaCteCriteria withRecursiveUnionDistinct( + String name, + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( validateCteName( name ), baseCriteria, true, recursiveCriteriaProducer ); + } + + private String validateCteName(String name) { + if ( name == null || name.isBlank() ) { + throw new IllegalArgumentException( "Illegal empty CTE name" ); + } + if ( !Character.isAlphabetic( name.charAt( 0 ) ) ) { + throw new IllegalArgumentException( + String.format( + "Illegal CTE name [%s]. Names must start with an alphabetic character!", + name + ) + ); + } + return name; + } + + private JpaCteCriteria withInternal(String name, AbstractQuery criteria) { + final SqmCteStatement cteStatement = new SqmCteStatement<>( + name, + (SqmSelectQuery) criteria, + this, + nodeBuilder() + ); + if ( cteStatements.putIfAbsent( name, cteStatement ) != null ) { throw new IllegalArgumentException( "A CTE with the label " + cteStatement.getCteTable().getCteName() + " already exists" ); } + return cteStatement; + } + + private JpaCteCriteria withInternal( + String name, + AbstractQuery baseCriteria, + boolean unionDistinct, + Function, AbstractQuery> recursiveCriteriaProducer) { + final SqmCteStatement cteStatement = new SqmCteStatement<>( + name, + (SqmSelectQuery) baseCriteria, + unionDistinct, + recursiveCriteriaProducer, + this, + nodeBuilder() + ); + if ( cteStatements.putIfAbsent( name, cteStatement ) != null ) { + throw new IllegalArgumentException( "A CTE with the label " + cteStatement.getCteTable().getCteName() + " already exists" ); + } + return cteStatement; } @Override @@ -100,4 +184,15 @@ public abstract class AbstractSqmDmlStatement public SqmSubQuery subquery(Class type) { return new SqmSubQuery<>( this, type, nodeBuilder() ); } + + protected void appendHqlCteString(StringBuilder sb) { + if ( !cteStatements.isEmpty() ) { + sb.append( "with " ); + for ( SqmCteStatement value : cteStatements.values() ) { + value.appendHqlString( sb ); + sb.append( ", " ); + } + sb.setLength( sb.length() - 2 ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmRestrictedDmlStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmRestrictedDmlStatement.java index ccc12462ed..00950fde10 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmRestrictedDmlStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmRestrictedDmlStatement.java @@ -46,9 +46,8 @@ public abstract class AbstractSqmRestrictedDmlStatement extends AbstractSqmDm SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target) { - super( builder, querySource, parameters, cteStatements, withRecursiveCte, target ); + super( builder, querySource, parameters, cteStatements, target ); } protected SqmWhereClause copyWhereClause(SqmCopyContext context) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteContainer.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteContainer.java index 7aa630735f..5297a574d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteContainer.java @@ -8,20 +8,16 @@ package org.hibernate.query.sqm.tree.cte; import java.util.Collection; +import org.hibernate.query.criteria.JpaCteContainer; import org.hibernate.query.sqm.tree.SqmNode; /** * @author Christian Beikov */ -public interface SqmCteContainer extends SqmNode { - - boolean isWithRecursive(); - - void setWithRecursive(boolean recursive); +public interface SqmCteContainer extends SqmNode, JpaCteContainer { Collection> getCteStatements(); SqmCteStatement getCteStatement(String cteLabel); - void addCteStatement(SqmCteStatement cteStatement); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteStatement.java index e82dd3475d..f061ed6736 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteStatement.java @@ -6,10 +6,22 @@ */ package org.hibernate.query.sqm.tree.cte; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.function.Function; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaCteCriteriaType; +import org.hibernate.query.criteria.JpaSearchOrder; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; +import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.expression.SqmLiteral; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind; import org.hibernate.query.sqm.NodeBuilder; @@ -19,79 +31,111 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.SqmVisitableNode; +import jakarta.persistence.criteria.AbstractQuery; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Subquery; + /** * @author Steve Ebersole * @author Christian Beikov */ -public class SqmCteStatement extends AbstractSqmNode implements SqmVisitableNode { +public class SqmCteStatement extends AbstractSqmNode implements SqmVisitableNode, JpaCteCriteria { private final SqmCteContainer cteContainer; - private final SqmCteTable cteTable; - private final CteMaterialization materialization; - private final SqmStatement cteDefinition; - private final CteSearchClauseKind searchClauseKind; - private final List searchBySpecifications; - private final List cycleColumns; - private final SqmCteTableColumn cycleMarkColumn; - private final char cycleValue; - private final char noCycleValue; + private final SqmCteTable cteTable; + private final SqmSelectQuery cteDefinition; + private CteMaterialization materialization; + private CteSearchClauseKind searchClauseKind; + private List searchBySpecifications; + private String searchAttributeName; + private List cycleAttributes; + private String cycleMarkAttributeName; + private String cyclePathAttributeName; + private SqmLiteral cycleValue; + private SqmLiteral noCycleValue; public SqmCteStatement( - SqmCteTable cteTable, - SqmStatement cteDefinition, - CteMaterialization materialization, + String name, + SqmSelectQuery cteDefinition, + SqmCteContainer cteContainer, NodeBuilder nodeBuilder) { super( nodeBuilder ); - this.cteTable = cteTable; this.cteDefinition = cteDefinition; - this.materialization = materialization; - this.cteContainer = null; - this.searchClauseKind = null; - this.searchBySpecifications = null; - this.cycleColumns = null; - this.cycleMarkColumn = null; - this.cycleValue = '\0'; - this.noCycleValue = '\0'; + this.cteContainer = cteContainer; + this.materialization = CteMaterialization.UNDEFINED; + this.searchBySpecifications = Collections.emptyList(); + this.cycleAttributes = Collections.emptyList(); + this.cteTable = SqmCteTable.createStatementTable( name, this, cteDefinition ); } public SqmCteStatement( - SqmCteTable cteTable, - SqmStatement cteDefinition, - CteMaterialization materialization, - SqmCteContainer cteContainer) { - super( cteContainer.nodeBuilder() ); - this.cteTable = cteTable; - this.cteDefinition = cteDefinition; - this.materialization = materialization; + String name, + SqmSelectQuery nonRecursiveQueryPart, + boolean unionDistinct, + Function, AbstractQuery> finalCriteriaProducer, + SqmCteContainer cteContainer, + NodeBuilder nodeBuilder) { + super( nodeBuilder ); this.cteContainer = cteContainer; - this.searchClauseKind = null; - this.searchBySpecifications = null; - this.cycleColumns = null; - this.cycleMarkColumn = null; - this.cycleValue = '\0'; - this.noCycleValue = '\0'; + this.materialization = CteMaterialization.UNDEFINED; + this.searchBySpecifications = Collections.emptyList(); + this.cycleAttributes = Collections.emptyList(); + this.cteTable = SqmCteTable.createStatementTable( name, this, nonRecursiveQueryPart ); + final AbstractQuery recursiveQueryPart = finalCriteriaProducer.apply( this ); + if ( nonRecursiveQueryPart instanceof Subquery ) { + if ( unionDistinct ) { + this.cteDefinition = (SqmSelectQuery) nodeBuilder.union( + (SqmSubQuery) nonRecursiveQueryPart, + (SqmSubQuery) recursiveQueryPart + ); + } + else { + this.cteDefinition = (SqmSelectQuery) nodeBuilder.unionAll( + (SqmSubQuery) nonRecursiveQueryPart, + (SqmSubQuery) recursiveQueryPart + ); + } + } + else { + if ( unionDistinct ) { + this.cteDefinition = (SqmSelectQuery) nodeBuilder.union( + (SqmSelectStatement) nonRecursiveQueryPart, + (SqmSelectStatement) recursiveQueryPart + ); + } + else { + this.cteDefinition = (SqmSelectQuery) nodeBuilder.unionAll( + (SqmSelectStatement) nonRecursiveQueryPart, + (SqmSelectStatement) recursiveQueryPart + ); + } + } } private SqmCteStatement( NodeBuilder builder, SqmCteContainer cteContainer, - SqmCteTable cteTable, + SqmCteTable cteTable, + SqmSelectQuery cteDefinition, CteMaterialization materialization, - SqmStatement cteDefinition, CteSearchClauseKind searchClauseKind, - List searchBySpecifications, - List cycleColumns, - SqmCteTableColumn cycleMarkColumn, - char cycleValue, - char noCycleValue) { + List searchBySpecifications, + String searchAttributeName, + List cycleAttributes, + String cycleMarkAttributeName, + String cyclePathAttributeName, + SqmLiteral cycleValue, + SqmLiteral noCycleValue) { super( builder ); this.cteContainer = cteContainer; this.cteTable = cteTable; - this.materialization = materialization; this.cteDefinition = cteDefinition; + this.materialization = materialization; this.searchClauseKind = searchClauseKind; this.searchBySpecifications = searchBySpecifications; - this.cycleColumns = cycleColumns; - this.cycleMarkColumn = cycleMarkColumn; + this.searchAttributeName = searchAttributeName; + this.cycleAttributes = cycleAttributes; + this.cycleMarkAttributeName = cycleMarkAttributeName; + this.cyclePathAttributeName = cyclePathAttributeName; this.cycleValue = cycleValue; this.noCycleValue = noCycleValue; } @@ -108,64 +152,179 @@ public class SqmCteStatement extends AbstractSqmNode implements SqmVisitableN nodeBuilder(), cteContainer, cteTable, - materialization, cteDefinition.copy( context ), + materialization, searchClauseKind, searchBySpecifications, - cycleColumns, - cycleMarkColumn, - cycleValue, - noCycleValue + searchAttributeName, + cycleAttributes, + cycleMarkAttributeName, + cyclePathAttributeName, + cycleValue == null ? null : cycleValue.copy( context ), + noCycleValue == null ? null : noCycleValue.copy( context ) ) ); } - public SqmCteTable getCteTable() { + @Override + public String getName() { + return cteTable.getName(); + } + + public SqmCteTable getCteTable() { return cteTable; } - public SqmStatement getCteDefinition() { + @Override + public SqmSelectQuery getCteDefinition() { return cteDefinition; } + @Override public SqmCteContainer getCteContainer() { return cteContainer; } + @Override public CteMaterialization getMaterialization() { return materialization; } + @Override + public void setMaterialization(CteMaterialization materialization) { + this.materialization = materialization; + } + + @Override public CteSearchClauseKind getSearchClauseKind() { return searchClauseKind; } - public List getSearchBySpecifications() { + @Override + public List getSearchBySpecifications() { return searchBySpecifications; } - public List getCycleColumns() { - return cycleColumns; + @Override + public String getSearchAttributeName() { + return searchAttributeName; } - public SqmCteTableColumn getCycleMarkColumn() { - return cycleMarkColumn; + @Override + public List getCycleAttributes() { + return cycleAttributes; } - public char getCycleValue() { + @Override + public String getCycleMarkAttributeName() { + return cycleMarkAttributeName; + } + + @Override + public String getCyclePathAttributeName() { + return cyclePathAttributeName; + } + + @Override + public Object getCycleValue() { + return cycleValue == null ? null : cycleValue.getLiteralValue(); + } + + @Override + public Object getNoCycleValue() { + return noCycleValue == null ? null : noCycleValue.getLiteralValue(); + } + + public SqmLiteral getCycleLiteral() { return cycleValue; } - public char getNoCycleValue() { + public SqmLiteral getNoCycleLiteral() { return noCycleValue; } + @Override + public JpaCteCriteriaType getType() { + return cteTable; + } + + @Override + public void search(CteSearchClauseKind kind, String searchAttributeName, List searchOrders) { + if ( kind == null || searchAttributeName == null || searchOrders == null || searchOrders.isEmpty() ) { + this.searchClauseKind = null; + this.searchBySpecifications = Collections.emptyList(); + this.searchAttributeName = null; + } + else { + final List orders = new ArrayList<>( searchOrders.size() ); + for ( JpaSearchOrder order : searchOrders ) { + if ( !cteTable.getAttributes().contains( order.getAttribute() ) ) { + throw new IllegalArgumentException( + "Illegal search order attribute '" + + ( order.getAttribute() == null ? "null" : order.getAttribute().getName() ) + + "' passed, which is not part of the JpaCteCriteria!" + ); + } + orders.add( order ); + } + this.searchClauseKind = kind; + this.searchAttributeName = searchAttributeName; + this.searchBySpecifications = orders; + } + } + + @Override + public void cycleUsing( + String cycleMarkAttributeName, + String cyclePathAttributeName, + X cycleValue, + X noCycleValue, + List cycleAttributes) { + if ( cycleMarkAttributeName == null || cycleAttributes == null || cycleAttributes.isEmpty() ) { + this.cycleMarkAttributeName = null; + this.cyclePathAttributeName = null; + this.cycleValue = null; + this.noCycleValue = null; + this.cycleAttributes = Collections.emptyList(); + } + else { + if ( cycleValue == null || noCycleValue == null ) { + throw new IllegalArgumentException( "Null is an illegal value for cycle mark values!" ); + } + final SqmExpression cycleValueLiteral = nodeBuilder().literal( cycleValue ); + final SqmExpression noCycleValueLiteral = nodeBuilder().literal( noCycleValue ); + if ( cycleValueLiteral.getNodeType() != noCycleValueLiteral.getNodeType() ) { + throw new IllegalArgumentException( "Inconsistent types for cycle mark values: [" + cycleValueLiteral.getNodeType() + ", " + noCycleValueLiteral.getNodeType() + "]" ); + } + final List attributes = new ArrayList<>( cycleAttributes.size() ); + for ( JpaCteCriteriaAttribute cycleAttribute : cycleAttributes ) { + if ( !cteTable.getAttributes().contains( cycleAttribute ) ) { + throw new IllegalArgumentException( + "Illegal cycle attribute '" + + ( cycleAttribute == null ? "null" : cycleAttribute.getName() ) + + "' passed, which is not part of the JpaCteCriteria!" + ); + } + attributes.add( cycleAttribute ); + } + this.cycleMarkAttributeName = cycleMarkAttributeName; + this.cyclePathAttributeName = cyclePathAttributeName; + this.cycleValue = (SqmLiteral) cycleValueLiteral; + this.noCycleValue = (SqmLiteral) noCycleValueLiteral; + this.cycleAttributes = attributes; + } + } + @Override public X accept(SemanticQueryWalker walker) { return walker.visitCteStatement( this ); } + @Override public void appendHqlString(StringBuilder sb) { + if ( cteTable.getName() == null ) { + sb.append( "generated_" ); + } sb.append( cteTable.getCteName() ); sb.append( " (" ); final List columns = cteTable.getColumns(); @@ -180,10 +339,14 @@ public class SqmCteStatement extends AbstractSqmNode implements SqmVisitableN if ( getMaterialization() != CteMaterialization.UNDEFINED ) { sb.append( getMaterialization() ).append( ' ' ); } - sb.append( '(' ); - getCteDefinition().appendHqlString( sb ); - sb.append( ')' ); - + if ( getCteDefinition() instanceof SqmSubQuery ) { + ( (SqmSubQuery) getCteDefinition() ).appendHqlString( sb ); + } + else { + sb.append( '(' ); + ( (SqmSelectStatement) getCteDefinition() ).appendHqlString( sb ); + sb.append( ')' ); + } String separator; if ( getSearchClauseKind() != null ) { sb.append( " search " ); @@ -195,9 +358,9 @@ public class SqmCteStatement extends AbstractSqmNode implements SqmVisitableN } sb.append( " first by " ); separator = ""; - for ( SqmSearchClauseSpecification searchBySpecification : getSearchBySpecifications() ) { + for ( JpaSearchOrder searchBySpecification : getSearchBySpecifications() ) { sb.append( separator ); - sb.append( searchBySpecification.getCteColumn().getColumnName() ); + sb.append( searchBySpecification.getAttribute().getName() ); if ( searchBySpecification.getSortOrder() != null ) { if ( searchBySpecification.getSortOrder() == SortOrder.ASCENDING ) { sb.append( " asc" ); @@ -206,32 +369,39 @@ public class SqmCteStatement extends AbstractSqmNode implements SqmVisitableN sb.append( " desc" ); } if ( searchBySpecification.getNullPrecedence() != null ) { - if ( searchBySpecification.getNullPrecedence() == NullPrecedence.FIRST ) { - sb.append( " nulls first" ); - } - else { - sb.append( " nulls last" ); + switch ( searchBySpecification.getNullPrecedence() ) { + case FIRST: + sb.append( " nulls first" ); + break; + case LAST: + sb.append( " nulls last" ); + break; } } } separator = ", "; } + sb.append( " set " ); + sb.append( getSearchAttributeName() ); } - if ( getCycleMarkColumn() != null ) { + if ( getCycleMarkAttributeName() != null ) { sb.append( " cycle " ); separator = ""; - for ( SqmCteTableColumn cycleColumn : getCycleColumns() ) { + for ( JpaCteCriteriaAttribute cycleColumn : getCycleAttributes() ) { sb.append( separator ); - sb.append( cycleColumn.getColumnName() ); + sb.append( cycleColumn.getName() ); separator = ", "; } sb.append( " set " ); - sb.append( getCycleMarkColumn().getColumnName() ); - sb.append( " to '" ); - sb.append( getCycleValue() ); - sb.append( "' default '" ); - sb.append( getNoCycleValue() ); - sb.append( "'" ); + sb.append( getCycleMarkAttributeName() ); + sb.append( " to " ); + getCycleLiteral().appendHqlString( sb ); + sb.append( " default " ); + getNoCycleLiteral().appendHqlString( sb ); + if ( getCyclePathAttributeName() != null ) { + sb.append( " using " ); + sb.append( getCyclePathAttributeName() ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java index 5c30463fa0..246aa603ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java @@ -6,134 +6,153 @@ */ package org.hibernate.query.sqm.tree.cte; -import java.io.Serializable; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import org.hibernate.dialect.temptable.TemporaryTableColumn; -import org.hibernate.id.IdentifierGenerator; -import org.hibernate.id.PostInsertIdentifierGenerator; -import org.hibernate.mapping.Collection; -import org.hibernate.mapping.Column; -import org.hibernate.mapping.PersistentClass; -import org.hibernate.mapping.Selectable; -import org.hibernate.mapping.SimpleValue; -import org.hibernate.mapping.Value; -import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; -import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; -import org.hibernate.metamodel.mapping.EntityIdentifierMapping; -import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.ModelPart; -import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.internal.ExplicitColumnDiscriminatorMappingImpl; -import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; -import org.hibernate.metamodel.spi.RuntimeModelCreationContext; -import org.hibernate.sql.ast.tree.cte.CteColumn; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaCteCriteriaType; +import org.hibernate.query.derived.AnonymousTupleSimpleSqmPathSource; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.derived.CteTupleTableGroupProducer; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.type.BasicType; /** * @author Steve Ebersole * @author Christian Beikov */ -public class SqmCteTable implements Serializable { - private final String cteName; +public class SqmCteTable extends AnonymousTupleType implements JpaCteCriteriaType { + private final String name; + private final SqmCteStatement cteStatement; private final List columns; - private SqmCteTable(String cteName, Function> columnInitializer) { - this.cteName = cteName; - this.columns = columnInitializer.apply( this ); - } - - public SqmCteTable(String cteName, List columns) { - this.cteName = cteName; + private SqmCteTable( + String name, + SqmCteStatement cteStatement, + SqmSelectableNode[] sqmSelectableNodes) { + super( sqmSelectableNodes ); + this.name = name; + this.cteStatement = cteStatement; + final List columns = new ArrayList<>( componentCount() ); + for ( int i = 0; i < componentCount(); i++ ) { + columns.add( + new SqmCteTableColumn( + this, + getComponentName( i ), + get( i ) + ) + ); + } this.columns = columns; } - public static SqmCteTable createIdTable(String cteName, EntityMappingType entityDescriptor) { - return new SqmCteTable( - cteName, - sqmCteTable -> { - final int numberOfColumns = entityDescriptor.getIdentifierMapping().getJdbcTypeCount(); - final List columns = new ArrayList<>( numberOfColumns ); - final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); - final String idName; - if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { - idName = ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName(); - } - else { - idName = "id"; - } - columns.add( new SqmCteTableColumn( sqmCteTable, idName, identifierMapping ) ); - return columns; - } - ); + public static SqmCteTable createStatementTable( + String name, + SqmCteStatement cteStatement, + SqmSelectQuery selectStatement) { + final SqmSelectableNode[] sqmSelectableNodes = selectStatement.getQuerySpec() + .getSelectClause() + .getSelectionItems() + .toArray( SqmSelectableNode[]::new ); + return new SqmCteTable<>( name, cteStatement, sqmSelectableNodes ); } - public static SqmCteTable createEntityTable(String cteName, EntityMappingType entityDescriptor) { - return new SqmCteTable( - cteName, - sqmCteTable -> { - final int numberOfColumns = entityDescriptor.getIdentifierMapping().getJdbcTypeCount(); - final List columns = new ArrayList<>( numberOfColumns ); - final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); - final String idName; - if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { - idName = ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName(); - } - else { - idName = "id"; - } - columns.add( new SqmCteTableColumn( sqmCteTable, idName, identifierMapping ) ); - - final EntityDiscriminatorMapping discriminatorMapping = entityDescriptor.getDiscriminatorMapping(); - if ( discriminatorMapping != null && discriminatorMapping.isPhysical() && !discriminatorMapping.isFormula() ) { - columns.add( new SqmCteTableColumn( sqmCteTable, "class", discriminatorMapping ) ); - } - - // Collect all columns for all entity subtype attributes - entityDescriptor.visitSubTypeAttributeMappings( - attribute -> { - if ( !( attribute instanceof PluralAttributeMapping ) ) { - columns.add( - new SqmCteTableColumn( - sqmCteTable, - attribute.getAttributeName(), - attribute - ) - ); - } - } - ); - // We add a special row number column that we can use to identify and join rows - columns.add( - new SqmCteTableColumn( - sqmCteTable, - "rn_", - entityDescriptor.getEntityPersister() - .getFactory() - .getTypeConfiguration() - .getBasicTypeForJavaType( Integer.class ) - ) - ); - return columns; - } - ); + @Override + public CteTupleTableGroupProducer resolveTableGroupProducer( + String aliasStem, + List sqlSelections, + FromClauseAccess fromClauseAccess) { + return new CteTupleTableGroupProducer( this, aliasStem, sqlSelections, fromClauseAccess ); } public String getCteName() { - return cteName; + return name; + } + + public AnonymousTupleType getTupleType() { + return this; } public List getColumns() { return columns; } - public void visitColumns(Consumer columnConsumer) { - for ( int i = 0; i < columns.size(); i++ ) { - columnConsumer.accept( columns.get( i ) ); + public SqmCteStatement getCteStatement() { + return cteStatement; + } + + @Override + public String getName() { + if ( Character.isDigit( name.charAt( 0 ) ) ) { + // Created through JPA criteria without an explicit name + return null; } + return name; + } + + @Override + public DomainType getType() { + return this; + } + + @Override + public List getAttributes() { + //noinspection unchecked + return (List) (List) columns; + } + + @Override + public JpaCteCriteriaAttribute getAttribute(String name) { + final Integer index = getIndex( name ); + if ( index == null ) { + return null; + } + return columns.get( index ); + } + + @Override + public SqmExpressible get(String componentName) { + final SqmExpressible sqmExpressible = super.get( componentName ); + if ( sqmExpressible != null ) { + return sqmExpressible; + } + return determineRecursiveCteAttributeType( name ); + } + + @Override + public SqmPathSource findSubPathSource(String name) { + final SqmPathSource subPathSource = super.findSubPathSource( name ); + if ( subPathSource != null ) { + return subPathSource; + } + final BasicType type = determineRecursiveCteAttributeType( name ); + if ( type == null ) { + return null; + } + return new AnonymousTupleSimpleSqmPathSource<>( + name, + type, + BindableType.SINGULAR_ATTRIBUTE + ); + } + + private BasicType determineRecursiveCteAttributeType(String name) { + if ( name.equals( cteStatement.getSearchAttributeName() ) ) { + return cteStatement.nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( String.class ); + } + if ( name.equals( cteStatement.getCyclePathAttributeName() ) ) { + return cteStatement.nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( String.class ); + } + if ( name.equals( cteStatement.getCycleMarkAttributeName() ) ) { + return (BasicType) cteStatement.getCycleLiteral().getNodeType(); + } + return null; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTableColumn.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTableColumn.java index 1850e17529..e7d5f4945e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTableColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTableColumn.java @@ -6,29 +6,29 @@ */ package org.hibernate.query.sqm.tree.cte; -import java.io.Serializable; - -import org.hibernate.metamodel.mapping.ValueMapping; +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaCteCriteriaType; +import org.hibernate.query.sqm.SqmExpressible; /** * @author Steve Ebersole * @author Christian Beikov */ -public class SqmCteTableColumn implements Serializable { - private final SqmCteTable cteTable; +public class SqmCteTableColumn implements JpaCteCriteriaAttribute { + private final SqmCteTable cteTable; private final String columnName; - private final ValueMapping typeExpressible; + private final SqmExpressible typeExpressible; public SqmCteTableColumn( - SqmCteTable cteTable, + SqmCteTable cteTable, String columnName, - ValueMapping typeExpressible) { + SqmExpressible typeExpressible) { this.cteTable = cteTable; this.columnName = columnName; this.typeExpressible = typeExpressible; } - public SqmCteTable getCteTable() { + public SqmCteTable getCteTable() { return cteTable; } @@ -36,8 +36,22 @@ public class SqmCteTableColumn implements Serializable { return columnName; } - public ValueMapping getType() { + public SqmExpressible getType() { return typeExpressible; } + @Override + public JpaCteCriteriaType getDeclaringType() { + return cteTable; + } + + @Override + public String getName() { + return columnName; + } + + @Override + public Class getJavaType() { + return typeExpressible == null ? null : typeExpressible.getBindableJavaType(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmSearchClauseSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmSearchClauseSpecification.java index 9fdfe26fbe..71d6e4e578 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmSearchClauseSpecification.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmSearchClauseSpecification.java @@ -6,8 +6,8 @@ */ package org.hibernate.query.sqm.tree.cte; -import java.io.Serializable; - +import org.hibernate.query.criteria.JpaCteCriteriaAttribute; +import org.hibernate.query.criteria.JpaSearchOrder; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; import org.hibernate.query.sqm.tree.SqmCopyContext; @@ -15,12 +15,15 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; /** * @author Christian Beikov */ -public class SqmSearchClauseSpecification implements Serializable { +public class SqmSearchClauseSpecification implements JpaSearchOrder { private final SqmCteTableColumn cteColumn; private final SortOrder sortOrder; - private final NullPrecedence nullPrecedence; + private NullPrecedence nullPrecedence; public SqmSearchClauseSpecification(SqmCteTableColumn cteColumn, SortOrder sortOrder, NullPrecedence nullPrecedence) { + if ( cteColumn == null ) { + throw new IllegalArgumentException( "Null cte column" ); + } this.cteColumn = cteColumn; this.sortOrder = sortOrder; this.nullPrecedence = nullPrecedence; @@ -38,10 +41,34 @@ public class SqmSearchClauseSpecification implements Serializable { return cteColumn; } + @Override + public JpaSearchOrder nullPrecedence(NullPrecedence precedence) { + this.nullPrecedence = precedence; + return this; + } + + @Override + public boolean isAscending() { + return sortOrder == SortOrder.ASCENDING; + } + + @Override + public JpaSearchOrder reverse() { + SortOrder newSortOrder = this.sortOrder == null ? SortOrder.DESCENDING : sortOrder.reverse(); + return new SqmSearchClauseSpecification( cteColumn, newSortOrder, nullPrecedence ); + } + + @Override + public JpaCteCriteriaAttribute getAttribute() { + return cteColumn; + } + + @Override public SortOrder getSortOrder() { return sortOrder; } + @Override public NullPrecedence getNullPrecedence() { return nullPrecedence; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java index 5a14db0310..aaf90d5ee6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java @@ -53,9 +53,8 @@ public class SqmDeleteStatement SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target) { - super( builder, querySource, parameters, cteStatements, withRecursiveCte, target ); + super( builder, querySource, parameters, cteStatements, target ); } @Override @@ -71,7 +70,6 @@ public class SqmDeleteStatement getQuerySource(), copyParameters( context ), copyCteStatements( context ), - isWithRecursive(), getTarget().copy( context ) ) ); @@ -98,6 +96,7 @@ public class SqmDeleteStatement @Override public void appendHqlString(StringBuilder sb) { + appendHqlCteString( sb ); sb.append( "delete from " ); sb.append( getTarget().getEntityName() ); sb.append( ' ' ).append( getTarget().resolveAlias() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java index 947002e6e9..c1836b0227 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java @@ -34,14 +34,12 @@ import jakarta.persistence.criteria.Predicate; * @author Steve Ebersole */ public abstract class AbstractSqmAttributeJoin - extends AbstractSqmJoin + extends AbstractSqmQualifiedJoin implements SqmAttributeJoin { private static final Logger log = Logger.getLogger( AbstractSqmAttributeJoin.class ); private final boolean fetched; - private SqmPredicate onClausePredicate; - public AbstractSqmAttributeJoin( SqmFrom lhs, SqmJoinable joinedNavigable, @@ -80,11 +78,6 @@ public abstract class AbstractSqmAttributeJoin this.fetched = fetched; } - protected void copyTo(AbstractSqmAttributeJoin target, SqmCopyContext context) { - super.copyTo( target, context ); - target.onClausePredicate = onClausePredicate == null ? null : onClausePredicate.copy( context ); - } - @Override public SqmFrom getLhs() { //noinspection unchecked @@ -100,32 +93,6 @@ public abstract class AbstractSqmAttributeJoin return fetched; } - @Override - public SqmPredicate getJoinPredicate() { - return onClausePredicate; - } - - public void setJoinPredicate(SqmPredicate predicate) { - if ( log.isTraceEnabled() ) { - log.tracef( - "Setting join predicate [%s] (was [%s])", - predicate.toString(), - this.onClausePredicate == null ? "" : this.onClausePredicate.toString() - ); - } - - this.onClausePredicate = predicate; - } - - public void applyRestriction(SqmPredicate restriction) { - if ( this.onClausePredicate == null ) { - this.onClausePredicate = restriction; - } - else { - this.onClausePredicate = nodeBuilder().and( onClausePredicate, restriction ); - } - } - @Override public X accept(SemanticQueryWalker walker) { return walker.visitQualifiedAttributeJoin( this ); @@ -143,33 +110,28 @@ public abstract class AbstractSqmAttributeJoin @Override public SqmAttributeJoin on(JpaExpression restriction) { - applyRestriction( nodeBuilder().wrap( restriction ) ); + super.on( restriction ); return this; } @Override public SqmAttributeJoin on(Expression restriction) { - applyRestriction( nodeBuilder().wrap( restriction ) ); + super.on( restriction ); return this; } @Override public SqmAttributeJoin on(JpaPredicate... restrictions) { - applyRestriction( nodeBuilder().wrap( restrictions ) ); + super.on( restrictions ); return this; } @Override public SqmAttributeJoin on(Predicate... restrictions) { - applyRestriction( nodeBuilder().wrap( restrictions ) ); + super.on( restrictions ); return this; } - @Override - public Predicate getOn() { - return getJoinPredicate(); - } - @Override public SqmFrom getParent() { return getLhs(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 360d1801db..77519220fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -24,7 +24,11 @@ import org.hibernate.metamodel.model.domain.MapPersistentAttribute; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SetPersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.criteria.JpaJoinedFrom; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.spi.NavigablePath; @@ -592,6 +596,24 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements return join; } + @Override + public JpaJoinedFrom join(JpaCteCriteria cte) { + return join( cte, SqmJoinType.INNER, null ); + } + + @Override + public JpaJoinedFrom join(JpaCteCriteria cte, SqmJoinType joinType) { + return join( cte, joinType, null ); + } + + public JpaJoinedFrom join(JpaCteCriteria cte, SqmJoinType joinType, String alias) { + validateComplianceFromSubQuery(); + final JpaJoinedFrom join = new SqmCteJoin<>( ( SqmCteStatement ) cte, alias, joinType, findRoot() ); + //noinspection unchecked + addSqmJoin( (SqmJoin) join ); + return join; + } + private void validateComplianceFromSubQuery() { if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { throw new IllegalStateException( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmQualifiedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmQualifiedJoin.java new file mode 100644 index 0000000000..b2d63de4ac --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmQualifiedJoin.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaJoinedFrom; +import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.spi.NavigablePath; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + +/** + * @author Steve Ebersole + */ +public abstract class AbstractSqmQualifiedJoin extends AbstractSqmJoin implements SqmQualifiedJoin, JpaJoinedFrom { + + private SqmPredicate onClausePredicate; + + public AbstractSqmQualifiedJoin( + NavigablePath navigablePath, + SqmPathSource referencedNavigable, + SqmFrom lhs, String alias, SqmJoinType joinType, NodeBuilder nodeBuilder) { + super( navigablePath, referencedNavigable, lhs, alias, joinType, nodeBuilder ); + } + + protected void copyTo(AbstractSqmQualifiedJoin target, SqmCopyContext context) { + super.copyTo( target, context ); + target.onClausePredicate = onClausePredicate == null ? null : onClausePredicate.copy( context ); + } + + @Override + public JpaPredicate getOn() { + return onClausePredicate; + } + + @Override + public SqmPredicate getJoinPredicate() { + return onClausePredicate; + } + + @Override + public void setJoinPredicate(SqmPredicate predicate) { + if ( log.isTraceEnabled() ) { + log.tracef( + "Setting join predicate [%s] (was [%s])", + predicate.toString(), + this.onClausePredicate == null ? "" : this.onClausePredicate.toString() + ); + } + + this.onClausePredicate = predicate; + } + + public void applyRestriction(SqmPredicate restriction) { + if ( this.onClausePredicate == null ) { + this.onClausePredicate = restriction; + } + else { + this.onClausePredicate = nodeBuilder().and( onClausePredicate, restriction ); + } + } + + @Override + public JpaJoinedFrom on(JpaExpression restriction) { + applyRestriction( nodeBuilder().wrap( restriction ) ); + return this; + } + + @Override + public JpaJoinedFrom on(Expression restriction) { + applyRestriction( nodeBuilder().wrap( restriction ) ); + return this; + } + + @Override + public JpaJoinedFrom on(JpaPredicate... restrictions) { + applyRestriction( nodeBuilder().wrap( restrictions ) ); + return this; + } + + @Override + public JpaJoinedFrom on(Predicate... restrictions) { + applyRestriction( nodeBuilder().wrap( restrictions ) ); + return this; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java new file mode 100644 index 0000000000..49744622ca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java @@ -0,0 +1,120 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.Incubating; +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.PathException; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.spi.NavigablePath; + +/** + * @author Christian Beikov + */ +@Incubating +public class SqmCteRoot extends SqmRoot implements JpaRoot { + + private final SqmCteStatement cte; + + public SqmCteRoot( + SqmCteStatement cte, + String alias) { + this( + SqmCreationHelper.buildRootNavigablePath( "<>", alias ), + cte, + (SqmPathSource) cte.getCteTable().getTupleType(), + alias + ); + } + + protected SqmCteRoot( + NavigablePath navigablePath, + SqmCteStatement cte, + SqmPathSource pathSource, + String alias) { + super( + navigablePath, + pathSource, + alias, + true, + cte.nodeBuilder() + ); + this.cte = cte; + } + + @Override + public SqmCteRoot copy(SqmCopyContext context) { + final SqmCteRoot existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmCteRoot path = context.registerCopy( + this, + new SqmCteRoot<>( + getNavigablePath(), + getCte().copy( context ), + getReferencedPathSource(), + getExplicitAlias() + ) + ); + copyTo( path, context ); + return path; + } + + public SqmCteStatement getCte() { + return cte; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitRootCte( this ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public EntityDomainType getModel() { + // Or should we throw an exception instead? + return null; + } + + @Override + public SqmCorrelatedRoot createCorrelation() { + // todo: implement + throw new NotYetImplementedFor6Exception( getClass()); +// return new SqmCorrelatedRoot<>( this ); + } + + @Override + public SqmTreatedRoot treatAs(Class treatJavaType) throws PathException { + throw new UnsupportedOperationException( "CTE roots can not be treated" ); + } + + @Override + public SqmTreatedRoot treatAs(EntityDomainType treatTarget) throws PathException { + throw new UnsupportedOperationException( "CTE roots can not be treated" ); + } + + @Override + public SqmFrom treatAs(Class treatJavaType, String alias) { + throw new UnsupportedOperationException( "CTE roots can not be treated" ); + } + + @Override + public SqmFrom treatAs(EntityDomainType treatTarget, String alias) { + throw new UnsupportedOperationException( "CTE roots can not be treated" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java new file mode 100644 index 0000000000..fefb003aca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java @@ -0,0 +1,139 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.from; + +import org.hibernate.Incubating; +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.PathException; +import org.hibernate.query.criteria.JpaJoinedFrom; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.domain.AbstractSqmQualifiedJoin; +import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.spi.NavigablePath; + +/** + * @author Christian Beikov + */ +@Incubating +public class SqmCteJoin extends AbstractSqmQualifiedJoin implements JpaJoinedFrom { + private final SqmCteStatement cte; + + public SqmCteJoin( + SqmCteStatement cte, + String alias, + SqmJoinType joinType, + SqmRoot sqmRoot) { + this( + SqmCreationHelper.buildRootNavigablePath( "<>", alias ), + cte, + (SqmPathSource) cte.getCteTable().getTupleType(), + alias, + joinType, + sqmRoot + ); + } + + protected SqmCteJoin( + NavigablePath navigablePath, + SqmCteStatement cte, + SqmPathSource pathSource, + String alias, + SqmJoinType joinType, + SqmRoot sqmRoot) { + super( + navigablePath, + pathSource, + sqmRoot, + alias, + joinType, + sqmRoot.nodeBuilder() + ); + this.cte = cte; + } + + @Override + public SqmCteJoin copy(SqmCopyContext context) { + final SqmCteJoin existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmCteJoin path = context.registerCopy( + this, + new SqmCteJoin<>( + getNavigablePath(), + cte.copy( context ), + getReferencedPathSource(), + getExplicitAlias(), + getSqmJoinType(), + findRoot().copy( context ) + ) + ); + copyTo( path, context ); + return path; + } + + public SqmRoot getRoot() { + return (SqmRoot) super.getLhs(); + } + + @Override + public SqmRoot findRoot() { + return getRoot(); + } + + public SqmCteStatement getCte() { + return cte; + } + + @Override + public SqmPath getLhs() { + // A cte-join has no LHS + return null; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitQualifiedCteJoin( this ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public SqmCorrelatedEntityJoin createCorrelation() { + // todo: implement + throw new NotYetImplementedFor6Exception(getClass()); +// return new SqmCorrelatedEntityJoin<>( this ); + } + + @Override + public SqmFrom treatAs(Class treatJavaType) throws PathException { + throw new UnsupportedOperationException( "CTE joins can not be treated" ); + } + @Override + public SqmFrom treatAs(EntityDomainType treatTarget) throws PathException { + throw new UnsupportedOperationException( "CTE joins can not be treated" ); + } + + @Override + public SqmFrom treatAs(Class treatJavaType, String alias) { + throw new UnsupportedOperationException( "CTE joins can not be treated" ); + } + + @Override + public SqmFrom treatAs(EntityDomainType treatTarget, String alias) { + throw new UnsupportedOperationException( "CTE joins can not be treated" ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java index 836c1ab7d1..2f95ebc8d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java @@ -9,6 +9,9 @@ package org.hibernate.query.sqm.tree.from; import org.hibernate.Incubating; import org.hibernate.NotYetImplementedFor6Exception; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaJoinedFrom; +import org.hibernate.query.criteria.JpaPredicate; import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.PathException; import org.hibernate.query.criteria.JpaDerivedJoin; @@ -17,22 +20,22 @@ import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmJoinType; -import org.hibernate.query.sqm.tree.domain.AbstractSqmJoin; +import org.hibernate.query.sqm.tree.domain.AbstractSqmQualifiedJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.query.sqm.tree.domain.SqmTreatedEntityJoin; -import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.spi.NavigablePath; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + /** * @author Christian Beikov */ @Incubating -public class SqmDerivedJoin extends AbstractSqmJoin implements JpaDerivedJoin { +public class SqmDerivedJoin extends AbstractSqmQualifiedJoin implements JpaDerivedJoin { private final SqmSubQuery subQuery; private final boolean lateral; - private SqmPredicate joinPredicate; public SqmDerivedJoin( SqmSubQuery subQuery, @@ -106,11 +109,6 @@ public class SqmDerivedJoin extends AbstractSqmJoin implements JpaDeriv return path; } - protected void copyTo(SqmDerivedJoin target, SqmCopyContext context) { - super.copyTo( target, context ); - target.joinPredicate = joinPredicate == null ? null : joinPredicate.copy( context ); - } - public SqmRoot getRoot() { return (SqmRoot) super.getLhs(); } @@ -137,13 +135,23 @@ public class SqmDerivedJoin extends AbstractSqmJoin implements JpaDeriv } @Override - public SqmPredicate getJoinPredicate() { - return joinPredicate; + public SqmDerivedJoin on(JpaExpression restriction) { + return (SqmDerivedJoin) super.on( restriction ); } @Override - public void setJoinPredicate(SqmPredicate predicate) { - this.joinPredicate = predicate; + public SqmDerivedJoin on(Expression restriction) { + return (SqmDerivedJoin) super.on( restriction ); + } + + @Override + public SqmDerivedJoin on(JpaPredicate... restrictions) { + return (SqmDerivedJoin) super.on( restrictions ); + } + + @Override + public SqmDerivedJoin on(Predicate... restrictions) { + return (SqmDerivedJoin) super.on( restrictions ); } @Override @@ -162,11 +170,11 @@ public class SqmDerivedJoin extends AbstractSqmJoin implements JpaDeriv } @Override - public SqmTreatedEntityJoin treatAs(Class treatJavaType) throws PathException { + public SqmFrom treatAs(Class treatJavaType) throws PathException { throw new UnsupportedOperationException( "Derived joins can not be treated" ); } @Override - public SqmTreatedEntityJoin treatAs(EntityDomainType treatTarget) throws PathException { + public SqmFrom treatAs(EntityDomainType treatTarget) throws PathException { throw new UnsupportedOperationException( "Derived joins can not be treated" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java index 0536b0a26b..d64243e329 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmEntityJoin.java @@ -7,6 +7,10 @@ package org.hibernate.query.sqm.tree.from; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaJoinedFrom; +import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.tree.domain.AbstractSqmQualifiedJoin; import org.hibernate.spi.NavigablePath; import org.hibernate.query.PathException; import org.hibernate.query.criteria.JpaEntityJoin; @@ -17,19 +21,19 @@ import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmJoinType; -import org.hibernate.query.sqm.tree.domain.AbstractSqmJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmTreatedEntityJoin; import org.hibernate.query.sqm.tree.expression.SqmExpression; -import org.hibernate.query.sqm.tree.predicate.SqmPredicate; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; /** * @author Steve Ebersole */ -public class SqmEntityJoin extends AbstractSqmJoin implements SqmQualifiedJoin, JpaEntityJoin { +public class SqmEntityJoin extends AbstractSqmQualifiedJoin implements JpaEntityJoin { private final SqmRoot sqmRoot; - private SqmPredicate joinPredicate; public SqmEntityJoin( EntityDomainType joinedEntityDescriptor, @@ -75,11 +79,6 @@ public class SqmEntityJoin extends AbstractSqmJoin implements SqmQualif return path; } - protected void copyTo(SqmEntityJoin target, SqmCopyContext context) { - super.copyTo( target, context ); - target.joinPredicate = joinPredicate == null ? null : joinPredicate.copy( context ); - } - public SqmRoot getRoot() { return sqmRoot; } @@ -118,12 +117,23 @@ public class SqmEntityJoin extends AbstractSqmJoin implements SqmQualif } @Override - public SqmPredicate getJoinPredicate() { - return joinPredicate; + public SqmEntityJoin on(JpaExpression restriction) { + return (SqmEntityJoin) super.on( restriction ); } - public void setJoinPredicate(SqmPredicate predicate) { - this.joinPredicate = predicate; + @Override + public SqmEntityJoin on(Expression restriction) { + return (SqmEntityJoin) super.on( restriction ); + } + + @Override + public SqmEntityJoin on(JpaPredicate... restrictions) { + return (SqmEntityJoin) super.on( restrictions ); + } + + @Override + public SqmEntityJoin on(Predicate... restrictions) { + return (SqmEntityJoin) super.on( restrictions ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFrom.java index 865d1122e2..b5f68f939d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFrom.java @@ -128,25 +128,25 @@ public interface SqmFrom extends SqmVisitableNode, SqmPath, JpaFrom SqmAttributeJoin join(String attributeName, JoinType jt); @Override - CollectionJoin joinCollection(String attributeName); + SqmBagJoin joinCollection(String attributeName); @Override SqmBagJoin joinCollection(String attributeName, JoinType jt); @Override - SetJoin joinSet(String attributeName); + SqmSetJoin joinSet(String attributeName); @Override SqmSetJoin joinSet(String attributeName, JoinType jt); @Override - ListJoin joinList(String attributeName); + SqmListJoin joinList(String attributeName); @Override SqmListJoin joinList(String attributeName, JoinType jt); @Override - MapJoin joinMap(String attributeName); + SqmMapJoin joinMap(String attributeName); @Override SqmMapJoin joinMap(String attributeName, JoinType jt); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java index c281363484..77f83a2142 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java @@ -43,10 +43,9 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target, List> insertionTargetPaths) { - super( builder, querySource, parameters, cteStatements, withRecursiveCte, target ); + super( builder, querySource, parameters, cteStatements, target ); this.insertionTargetPaths = insertionTargetPaths; } @@ -90,6 +89,7 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem @Override public void appendHqlString(StringBuilder sb) { + appendHqlCteString( sb ); sb.append( "insert into " ); sb.append( getTarget().getEntityName() ); if ( insertionTargetPaths != null && !insertionTargetPaths.isEmpty() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java index b61975d9a5..d9b10a24f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java @@ -55,11 +55,10 @@ public class SqmInsertSelectStatement extends AbstractSqmInsertStatement i SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target, List> insertionTargetPaths, SqmQueryPart selectQueryPart) { - super( builder, querySource, parameters, cteStatements, withRecursiveCte, target, insertionTargetPaths ); + super( builder, querySource, parameters, cteStatements, target, insertionTargetPaths ); this.selectQueryPart = selectQueryPart; } @@ -76,7 +75,6 @@ public class SqmInsertSelectStatement extends AbstractSqmInsertStatement i getQuerySource(), copyParameters( context ), copyCteStatements( context ), - isWithRecursive(), getTarget().copy( context ), copyInsertionTargetPaths( context ), selectQueryPart.copy( context ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java index 86076c30e2..5adbf4d7ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java @@ -38,11 +38,10 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement { SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target, List> insertionTargetPaths, List valuesList) { - super( builder, querySource, parameters, cteStatements, withRecursiveCte, target, insertionTargetPaths ); + super( builder, querySource, parameters, cteStatements, target, insertionTargetPaths ); this.valuesList = valuesList; } @@ -63,7 +62,6 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement { getQuerySource(), copyParameters( context ), copyCteStatements( context ), - isWithRecursive(), getTarget().copy( context ), copyInsertionTargetPaths( context ), valuesList diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java index c13ae1a9b1..e020dd9941 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java @@ -12,18 +12,22 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.tree.AbstractSqmNode; import org.hibernate.query.sqm.tree.SqmCopyContext; -import org.hibernate.query.sqm.tree.cte.SqmCteContainer; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import jakarta.persistence.criteria.AbstractQuery; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -36,9 +40,8 @@ import jakarta.persistence.metamodel.EntityType; @SuppressWarnings("unchecked") public abstract class AbstractSqmSelectQuery extends AbstractSqmNode - implements SqmSelectQuery, SqmCteContainer { + implements SqmSelectQuery { private final Map> cteStatements; - private boolean withRecursive; private SqmQueryPart sqmQueryPart; private Class resultType; @@ -56,11 +59,9 @@ public abstract class AbstractSqmSelectQuery protected AbstractSqmSelectQuery( NodeBuilder builder, Map> cteStatements, - boolean withRecursive, Class resultType) { super( builder ); this.cteStatements = cteStatements; - this.withRecursive = withRecursive; this.resultType = resultType; } @@ -72,16 +73,6 @@ public abstract class AbstractSqmSelectQuery return cteStatements; } - @Override - public boolean isWithRecursive() { - return withRecursive; - } - - @Override - public void setWithRecursive(boolean withRecursive) { - this.withRecursive = withRecursive; - } - @Override public Collection> getCteStatements() { return cteStatements.values(); @@ -93,10 +84,100 @@ public abstract class AbstractSqmSelectQuery } @Override - public void addCteStatement(SqmCteStatement cteStatement) { - if ( cteStatements.putIfAbsent( cteStatement.getCteTable().getCteName(), cteStatement ) != null ) { + public Collection> getCteCriterias() { + return cteStatements.values(); + } + + @Override + public JpaCteCriteria getCteCriteria(String cteName) { + return (JpaCteCriteria) cteStatements.get( cteName ); + } + + @Override + public JpaCteCriteria with(AbstractQuery criteria) { + return withInternal( Long.toString( System.nanoTime() ), criteria ); + } + + @Override + public JpaCteCriteria withRecursiveUnionAll( + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( Long.toString( System.nanoTime() ), baseCriteria, false, recursiveCriteriaProducer ); + } + + @Override + public JpaCteCriteria withRecursiveUnionDistinct( + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( Long.toString( System.nanoTime() ), baseCriteria, true, recursiveCriteriaProducer ); + } + + @Override + public JpaCteCriteria with(String name, AbstractQuery criteria) { + return withInternal( validateCteName( name ), criteria ); + } + + @Override + public JpaCteCriteria withRecursiveUnionAll( + String name, + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( validateCteName( name ), baseCriteria, false, recursiveCriteriaProducer ); + } + + @Override + public JpaCteCriteria withRecursiveUnionDistinct( + String name, + AbstractQuery baseCriteria, + Function, AbstractQuery> recursiveCriteriaProducer) { + return withInternal( validateCteName( name ), baseCriteria, true, recursiveCriteriaProducer ); + } + + private String validateCteName(String name) { + if ( name == null || name.isBlank() ) { + throw new IllegalArgumentException( "Illegal empty CTE name" ); + } + if ( !Character.isAlphabetic( name.charAt( 0 ) ) ) { + throw new IllegalArgumentException( + String.format( + "Illegal CTE name [%s]. Names must start with an alphabetic character!", + name + ) + ); + } + return name; + } + + private JpaCteCriteria withInternal(String name, AbstractQuery criteria) { + final SqmCteStatement cteStatement = new SqmCteStatement<>( + name, + (SqmSelectQuery) criteria, + this, + nodeBuilder() + ); + if ( cteStatements.putIfAbsent( name, cteStatement ) != null ) { throw new IllegalArgumentException( "A CTE with the label " + cteStatement.getCteTable().getCteName() + " already exists" ); } + return cteStatement; + } + + private JpaCteCriteria withInternal( + String name, + AbstractQuery baseCriteria, + boolean unionDistinct, + Function, AbstractQuery> recursiveCriteriaProducer) { + final SqmCteStatement cteStatement = new SqmCteStatement<>( + name, + (SqmSelectQuery) baseCriteria, + unionDistinct, + recursiveCriteriaProducer, + this, + nodeBuilder() + ); + if ( cteStatements.putIfAbsent( name, cteStatement ) != null ) { + throw new IllegalArgumentException( "A CTE with the label " + cteStatement.getCteTable().getCteName() + " already exists" ); + } + return cteStatement; } @Override @@ -149,6 +230,12 @@ public abstract class AbstractSqmSelectQuery return root; } + public JpaRoot from(JpaCteCriteria cte) { + final SqmCteRoot root = new SqmCteRoot<>( ( SqmCteStatement ) cte, null ); + addRoot( root ); + return root; + } + private SqmRoot addRoot(SqmRoot root) { getQuerySpec().addRoot( root ); return root; @@ -278,9 +365,6 @@ public abstract class AbstractSqmSelectQuery public void appendHqlString(StringBuilder sb) { if ( !cteStatements.isEmpty() ) { sb.append( "with " ); - if ( withRecursive ) { - sb.append( "recursive " ); - } for ( SqmCteStatement value : cteStatements.values() ) { value.appendHqlString( sb ); sb.append( ", " ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectQuery.java index 59103163fa..94428ace84 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectQuery.java @@ -7,8 +7,10 @@ package org.hibernate.query.sqm.tree.select; import org.hibernate.query.criteria.JpaSelectCriteria; +import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmNode; import org.hibernate.query.sqm.tree.SqmQuery; +import org.hibernate.query.sqm.tree.cte.SqmCteContainer; /** * Common contract between a {@linkplain SqmSelectStatement root} and a @@ -16,7 +18,7 @@ import org.hibernate.query.sqm.tree.SqmQuery; * * @author Steve Ebersole */ -public interface SqmSelectQuery extends SqmQuery, JpaSelectCriteria, SqmNode { +public interface SqmSelectQuery extends SqmQuery, JpaSelectCriteria, SqmNode, SqmCteContainer { @Override SqmQuerySpec getQuerySpec(); @@ -24,4 +26,7 @@ public interface SqmSelectQuery extends SqmQuery, JpaSelectCriteria, Sq @Override SqmSelectQuery distinct(boolean distinct); + + @Override + SqmSelectQuery copy(SqmCopyContext context); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java index 9f88cb30be..8338ee30e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java @@ -68,6 +68,17 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements this.querySource = querySource; } + public SqmSelectStatement( + SqmQueryPart queryPart, + Class resultType, + Map> cteStatements, + SqmQuerySource querySource, + NodeBuilder builder) { + super( builder, cteStatements, resultType ); + this.querySource = querySource; + setQueryPart( queryPart ); + } + /** * @implNote This form is used from Hibernate's JPA criteria handling. */ @@ -83,11 +94,10 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements private SqmSelectStatement( NodeBuilder builder, Map> cteStatements, - boolean withRecursive, Class resultType, SqmQuerySource querySource, Set> parameters) { - super( builder, cteStatements, withRecursive, resultType ); + super( builder, cteStatements, resultType ); this.querySource = querySource; this.parameters = parameters; } @@ -113,7 +123,6 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements new SqmSelectStatement<>( nodeBuilder(), copyCteStatements( context ), - isWithRecursive(), getResultType(), getQuerySource(), parameters diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index 803392ce08..4d00d1a301 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.hibernate.query.criteria.JpaCteContainer; +import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaExpression; import org.hibernate.query.criteria.JpaOrder; import org.hibernate.query.criteria.JpaSelection; @@ -24,6 +26,7 @@ import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmQuery; +import org.hibernate.query.sqm.tree.cte.SqmCteContainer; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.domain.SqmBagJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedBagJoin; @@ -82,6 +85,17 @@ public class SqmSubQuery extends AbstractSqmSelectQuery implements SqmSele this.parent = parent; } + public SqmSubQuery( + SqmQuery parent, + SqmQueryPart queryPart, + Class resultType, + Map> cteStatements, + NodeBuilder builder) { + super( builder, cteStatements, resultType ); + this.parent = parent; + setQueryPart( queryPart ); + } + public SqmSubQuery( SqmQuery parent, Class resultType, @@ -100,12 +114,11 @@ public class SqmSubQuery extends AbstractSqmSelectQuery implements SqmSele private SqmSubQuery( NodeBuilder builder, Map> cteStatements, - boolean withRecursive, Class resultType, SqmQuery parent, SqmExpressible expressibleType, String alias) { - super( builder, cteStatements, withRecursive, resultType ); + super( builder, cteStatements, resultType ); this.parent = parent; this.expressibleType = expressibleType; this.alias = alias; @@ -122,7 +135,6 @@ public class SqmSubQuery extends AbstractSqmSelectQuery implements SqmSele new SqmSubQuery<>( nodeBuilder(), copyCteStatements( context ), - isWithRecursive(), getResultType(), parent.copy( context ), getExpressible(), @@ -133,6 +145,24 @@ public class SqmSubQuery extends AbstractSqmSelectQuery implements SqmSele return statement; } + @Override + public SqmCteStatement getCteStatement(String cteLabel) { + final SqmCteStatement cteCriteria = super.getCteStatement( cteLabel ); + if ( cteCriteria != null || !( parent instanceof SqmCteContainer ) ) { + return cteCriteria; + } + return ( (SqmCteContainer) parent ).getCteStatement( cteLabel ); + } + + @Override + public JpaCteCriteria getCteCriteria(String cteName) { + final JpaCteCriteria cteCriteria = super.getCteCriteria( cteName ); + if ( cteCriteria != null || !( parent instanceof JpaCteContainer ) ) { + return cteCriteria; + } + return ( (JpaCteContainer) parent ).getCteCriteria( cteName ); + } + @Override public SqmQuery getContainingQuery() { return parent; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java index 31e991868d..844a67fecf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java @@ -64,9 +64,8 @@ public class SqmUpdateStatement SqmQuerySource querySource, Set> parameters, Map> cteStatements, - boolean withRecursiveCte, SqmRoot target) { - super( builder, querySource, parameters, cteStatements, withRecursiveCte, target ); + super( builder, querySource, parameters, cteStatements, target ); } @Override @@ -82,7 +81,6 @@ public class SqmUpdateStatement getQuerySource(), copyParameters( context ), copyCteStatements( context ), - isWithRecursive(), getTarget().copy( context ) ) ); @@ -179,6 +177,7 @@ public class SqmUpdateStatement @Override public void appendHqlString(StringBuilder sb) { + appendHqlCteString( sb ); sb.append( "update " ); if ( versioned ) { sb.append( "versioned " ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java index 1d9f814357..9c9adc032f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java @@ -52,6 +52,10 @@ public enum Clause { FETCH, FOR_UPDATE, OVER, + /** + * The clause containing CTEs + */ + WITH, WITHIN_GROUP, PARTITION, CALL, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java index d31a58a8df..6c61770118 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java @@ -8,6 +8,7 @@ package org.hibernate.sql.ast; import java.util.Set; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.SqlAstNode; @@ -20,6 +21,8 @@ import org.hibernate.sql.exec.spi.JdbcParameterBindings; */ public interface SqlAstTranslator extends SqlAstWalker { + SessionFactoryImplementor getSessionFactory(); + /** * Renders the given SQL AST node with the given rendering mode. */ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlTreePrinter.java index a2a6b06bd9..c8d9734cbe 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlTreePrinter.java @@ -167,8 +167,8 @@ public class SqlTreePrinter { logNode( "PrimaryTableReference as " + tableGroup.getPrimaryTableReference().getIdentificationVariable(), () -> { - QueryPart queryPart = ( (QueryPartTableReference) tableGroup.getPrimaryTableReference() ).getQueryPart(); - visitQueryPart( queryPart ); + Statement statement = ( (QueryPartTableReference) tableGroup.getPrimaryTableReference() ).getStatement(); + visitStatement( statement ); } ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index bf439eefe5..39c8f7601c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -54,6 +54,7 @@ import org.hibernate.persister.entity.Loadable; import org.hibernate.persister.entity.Queryable; import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.ReturnableType; import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.BinaryArithmeticOperator; @@ -68,6 +69,9 @@ import org.hibernate.query.sqm.SortOrder; import org.hibernate.query.sqm.UnaryArithmeticOperator; import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; import org.hibernate.query.sqm.function.SelfRenderingAggregateFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.MultipatternSqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation; import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; import org.hibernate.query.sqm.tree.expression.Conversion; @@ -84,6 +88,7 @@ import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.cte.CteTableGroup; import org.hibernate.sql.ast.tree.cte.SearchClauseSpecification; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.Any; @@ -178,6 +183,7 @@ import org.hibernate.type.BasicType; import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; @@ -191,6 +197,52 @@ import static org.hibernate.sql.results.graph.DomainResultGraphPrinter.logDomain */ public abstract class AbstractSqlAstTranslator implements SqlAstTranslator, SqlAppender { + /** + * When emulating the recursive WITH clause subclauses SEARCH and CYCLE, + * we need to build a string path and some database like MySQL require that + * we cast the expression to a char with certain size. + * To estimate the size, we need to assume a certain max recursion depth. + */ + private static final int MAX_RECURSION_DEPTH_ESTIMATE = 1000; + /* The following are size estimates for various temporal types */ + private static final int DATE_CHAR_SIZE_ESTIMATE = + // year + 4 + + // separator + 1 + + // month + 2 + + // separator + 1 + + // day + 2; + private static final int TIME_CHAR_SIZE_ESTIMATE = + // hour + 2 + + // separator + 1 + + // minute + 2 + + // separator + 1 + + // second + 2; + private static final int TIMESTAMP_CHAR_SIZE_ESTIMATE = + DATE_CHAR_SIZE_ESTIMATE + + // separator + 1 + + TIME_CHAR_SIZE_ESTIMATE + + // separator + 1 + + // nanos + 9; + private static final int OFFSET_TIMESTAMP_CHAR_SIZE_ESTIMATE = + TIMESTAMP_CHAR_SIZE_ESTIMATE + + // separator + 1 + + // zone offset + 6; + // pre-req state private final SessionFactoryImplementor sessionFactory; @@ -204,11 +256,11 @@ public abstract class AbstractSqlAstTranslator implemen private final Stack clauseStack = new StandardStack<>(); private final Stack queryPartStack = new StandardStack<>(); + private final Stack statementStack = new StandardStack<>(); private final Dialect dialect; - private final Statement statement; private final Set affectedTableNames = new HashSet<>(); - private MutationStatement dmlStatement; + private CteStatement currentCteStatement; private boolean needsSelectAliases; // Column aliases that need to be injected private List columnAliases; @@ -221,9 +273,16 @@ public abstract class AbstractSqlAstTranslator implemen private int queryPartForRowNumberingClauseDepth = -1; private int queryPartForRowNumberingAliasCounter; private int queryGroupAliasCounter; + // This field is used to remember the index of the most recently rendered top level with clause element in the sqlBuffer. + // See #visitCteContainer for details about the usage. + private int topLevelWithClauseIndex; + // This field holds the index of where the "recursive" keyword should appear in the sqlBuffer. + // See #visitCteContainer for details about the usage. + private int withClauseRecursiveIndex = -1; private transient AbstractSqmSelfRenderingFunctionDescriptor castFunction; private transient LazySessionWrapperOptions lazySessionWrapperOptions; private transient BasicType integerType; + private transient BasicType stringType; private transient BasicType booleanType; private SqlAstNodeRenderingMode parameterRenderingMode = SqlAstNodeRenderingMode.DEFAULT; @@ -242,20 +301,18 @@ public abstract class AbstractSqlAstTranslator implemen protected AbstractSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { this.sessionFactory = sessionFactory; - this.statement = statement; this.dialect = sessionFactory.getJdbcServices().getDialect(); + this.statementStack.push( statement ); } + @Override public SessionFactoryImplementor getSessionFactory() { return sessionFactory; } protected AbstractSqmSelfRenderingFunctionDescriptor castFunction() { if ( castFunction == null ) { - castFunction = (AbstractSqmSelfRenderingFunctionDescriptor) sessionFactory - .getQueryEngine() - .getSqmFunctionRegistry() - .findFunctionDescriptor( "cast" ); + castFunction = findSelfRenderingFunction( "cast", 2 ); } return castFunction; } @@ -276,6 +333,15 @@ public abstract class AbstractSqlAstTranslator implemen return integerType; } + public BasicType getStringType() { + if ( stringType == null ) { + stringType = sessionFactory.getTypeConfiguration() + .getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ); + } + return stringType; + } + public BasicType getBooleanType() { if ( booleanType == null ) { booleanType = sessionFactory.getTypeConfiguration() @@ -378,15 +444,25 @@ public abstract class AbstractSqlAstTranslator implemen } protected String getDmlTargetTableAlias() { - return dmlStatement == null ? null : dmlStatement.getTargetTable().getIdentificationVariable(); + final MutationStatement currentDmlStatement = getCurrentDmlStatement(); + return currentDmlStatement == null + ? null + : currentDmlStatement.getTargetTable().getIdentificationVariable(); } protected Statement getStatement() { - return statement; + return statementStack.getRoot(); } public MutationStatement getCurrentDmlStatement() { - return dmlStatement; + return statementStack.findCurrentFirst( + stmt -> { + if ( stmt instanceof MutationStatement && !( stmt instanceof InsertStatement ) ) { + return (MutationStatement) stmt; + } + return null; + } + ); } protected SqlAstNodeRenderingMode getParameterRenderingMode() { @@ -459,7 +535,8 @@ public abstract class AbstractSqlAstTranslator implemen } else { final JdbcMapping bindType = binding.getBindType(); - final Object value = bindType.getJavaTypeDescriptor() + //noinspection unchecked + final Object value = ( (JavaType) bindType.getJdbcJavaType() ) .getMutabilityPlan() .deepCopy( binding.getBindValue() ); appliedParameterBindings.put( parameter, new JdbcParameterBindingImpl( bindType, value ) ); @@ -625,12 +702,28 @@ public abstract class AbstractSqlAstTranslator implemen return clauseStack; } + protected CteStatement getCurrentCteStatement() { + return currentCteStatement; + } + + protected CteStatement getCteStatement(String cteName) { + return statementStack.findCurrentFirst( + stmt -> { + if ( stmt instanceof CteContainer ) { + return ( (CteContainer) stmt ).getCteStatement( cteName ); + } + return null; + } + ); + } + @Override public T translate(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { try { this.jdbcParameterBindings = jdbcParameterBindings; this.lockOptions = queryOptions.getLockOptions().makeCopy(); this.limit = queryOptions.getLimit() == null ? null : queryOptions.getLimit().makeCopy(); + final Statement statement = statementStack.pop(); final JdbcOperation jdbcOperation; if ( statement instanceof DeleteStatement ) { jdbcOperation = translateDelete( (DeleteStatement) statement ); @@ -700,24 +793,24 @@ public abstract class AbstractSqlAstTranslator implemen ); } - protected JdbcSelect translateSelect(SelectStatement sqlAstSelect) { - logDomainResultGraph( sqlAstSelect.getDomainResultDescriptors() ); - logSqlAst( sqlAstSelect ); + protected JdbcSelect translateSelect(SelectStatement selectStatement) { + logDomainResultGraph( selectStatement.getDomainResultDescriptors() ); + logSqlAst( selectStatement ); - visitSelectStatement( sqlAstSelect ); + visitSelectStatement( selectStatement ); final int rowsToSkip; return new JdbcSelect( getSql(), getParameterBinders(), new JdbcValuesMappingProducerStandard( - sqlAstSelect.getQuerySpec().getSelectClause().getSqlSelections(), - sqlAstSelect.getDomainResultDescriptors() + selectStatement.getQuerySpec().getSelectClause().getSqlSelections(), + selectStatement.getDomainResultDescriptors() ), getAffectedTableNames(), getFilterJdbcParameters(), - rowsToSkip = getRowsToSkip( sqlAstSelect, getJdbcParameterBindings() ), - getMaxRows( sqlAstSelect, getJdbcParameterBindings(), rowsToSkip ), + rowsToSkip = getRowsToSkip( selectStatement, getJdbcParameterBindings() ), + getMaxRows( selectStatement, getJdbcParameterBindings(), rowsToSkip ), getAppliedParameterBindings(), getJdbcLockStrategy(), getOffsetParameter(), @@ -827,42 +920,44 @@ public abstract class AbstractSqlAstTranslator implemen @Override public void visitSelectStatement(SelectStatement statement) { - MutationStatement oldDmlStatement = dmlStatement; - dmlStatement = null; try { + statementStack.push( statement ); + final boolean needsParenthesis = !statement.getQueryPart().isRoot(); + if ( needsParenthesis ) { + appendSql( OPEN_PARENTHESIS ); + } visitCteContainer( statement ); statement.getQueryPart().accept( this ); + if ( needsParenthesis ) { + appendSql( CLOSE_PARENTHESIS ); + } } finally { - dmlStatement = oldDmlStatement; + statementStack.pop(); } } @Override public void visitDeleteStatement(DeleteStatement statement) { - MutationStatement oldDmlStatement = dmlStatement; - dmlStatement = null; try { + statementStack.push( statement ); visitCteContainer( statement ); - dmlStatement = statement; visitDeleteStatementOnly( statement ); } finally { - dmlStatement = oldDmlStatement; + statementStack.pop(); } } @Override public void visitUpdateStatement(UpdateStatement statement) { - MutationStatement oldDmlTargetTableAlias = dmlStatement; - dmlStatement = null; try { + statementStack.push( statement ); visitCteContainer( statement ); - dmlStatement = statement; visitUpdateStatementOnly( statement ); } finally { - dmlStatement = oldDmlTargetTableAlias; + statementStack.pop(); } } @@ -873,14 +968,13 @@ public abstract class AbstractSqlAstTranslator implemen @Override public void visitInsertStatement(InsertStatement statement) { - MutationStatement oldDmlStatement = dmlStatement; - dmlStatement = null; try { + statementStack.push( statement ); visitCteContainer( statement ); visitInsertStatementOnly( statement ); } finally { - dmlStatement = oldDmlStatement; + statementStack.pop(); } } @@ -1323,48 +1417,402 @@ public abstract class AbstractSqlAstTranslator implemen } public void visitCteContainer(CteContainer cteContainer) { - final Collection cteStatements = cteContainer.getCteStatements().values(); + final Collection originalCteStatements = cteContainer.getCteStatements().values(); + final Collection cteStatements; + // If CTE inlining is needed, collect all recursive CTEs, since these can't be inlined + if ( needsCteInlining() && !originalCteStatements.isEmpty() ) { + cteStatements = new ArrayList<>( originalCteStatements.size() ); + for ( CteStatement cteStatement : originalCteStatements ) { + if ( cteStatement.isRecursive() ) { + cteStatements.add( cteStatement ); + } + } + } + else { + cteStatements = originalCteStatements; + } if ( cteStatements.isEmpty() ) { return; } - appendSql( "with " ); + if ( !supportsWithClause() ) { + if ( isRecursive( cteStatements ) ) { + throw new UnsupportedOperationException( "Can't emulate recursive CTEs!" ); + } + // This should be unreachable, because #needsCteInlining() must return true if #supportsWithClause() returns false, + // and hence the cteStatements should either contain a recursive CTE or be empty + throw new IllegalStateException( "Non-recursive CTEs found that need inlining, but were collected: " + cteStatements ); + } + final boolean renderRecursiveKeyword = needsRecursiveKeywordInWithClause() && isRecursive( cteStatements ); + if ( renderRecursiveKeyword && !getDialect().supportsRecursiveCTE() ) { + throw new UnsupportedOperationException( "Can't emulate recursive CTEs!" ); + } + // Here we compute if the CTEs should be pushed to the top level WITH clause + final boolean isTopLevel = clauseStack.isEmpty(); + final boolean pushToTopLevel; + if ( isTopLevel ) { + pushToTopLevel = false; + } + else { + pushToTopLevel = !supportsNestedWithClause() + || !supportsWithClauseInSubquery() && isInSubquery(); + } + final boolean inNestedWithClause = clauseStack.findCurrentFirst( + clause -> { + if ( clause == Clause.WITH ) { + return Clause.WITH; + } + return null; + } + ) != null; + clauseStack.push( Clause.WITH ); + if ( !pushToTopLevel ) { + appendSql( "with " ); - if ( cteContainer.isWithRecursive() ) { - appendSql( "recursive " ); + withClauseRecursiveIndex = sqlBuffer.length(); + if ( renderRecursiveKeyword ) { + appendSql( "recursive " ); + } + } + // The following lines are a bit complicated because they alter the sqlBuffer contents instead of just appending. + // Like SQL, we support nested CTEs i.e. `with a as (with b as (...) select ..) select ...` and CTEs in subqueries. + // Some DBs don't support this though, but to detect the usage of nested CTEs or in subqueries, + // we'd have to *always* traverse the AST an additional time to collect them for the top-level statement. + // It's also tricky to pre-collect the CTEs because the processing context stacks would be different. + // To avoid these two issues, the following code will insert CTEs into the sqlBuffer at the appropriate location. + // To do that, we remember the end-index of the last recently rendered CTE in the sqlBuffer, the `topLevelWithClauseIndex` + // Nested CTEs need to be rendered before the current CTE. CTEs in subqueries get append to the top level. + String mainSeparator = ""; + if ( isTopLevel ) { + topLevelWithClauseIndex = sqlBuffer.length(); + for ( CteStatement cte : cteStatements ) { + appendSql( mainSeparator ); + visitCteStatement( cte ); + mainSeparator = COMA_SEPARATOR; + topLevelWithClauseIndex = sqlBuffer.length(); + } + appendSql( WHITESPACE ); + } + else if ( pushToTopLevel ) { + // We need to push the CTEs of this level to the top level WITH clause + // and to do that we must first ensure that the top level WITH clause is even setup correctly + if ( topLevelWithClauseIndex == 0 ) { + // When we get here, there is no top level WITH clause yet, so we must insert that + // The recursive keyword must be at index 5, which is the length of "with " + withClauseRecursiveIndex = 5; + if ( renderRecursiveKeyword ) { + sqlBuffer.insert( 0, "with recursive " ); + // The next CTE must be inserted at index 15, which is the length of "with recursive " + topLevelWithClauseIndex = 15; + } + else { + sqlBuffer.insert( 0, "with " ); + // The next CTE must be inserted at index 5, which is the length of "with " + topLevelWithClauseIndex = 5; + } + } + else if ( renderRecursiveKeyword ) { + // When we get here, we know that there is a top level WITH clause, + // and that at least one of the CTEs that need to be pushed to the top level needs the recursive keyword, + final String recursiveKeyword = "recursive "; + if ( !sqlBuffer.substring( withClauseRecursiveIndex, recursiveKeyword.length() ).equals( recursiveKeyword ) ) { + // If the buffer doesn't contain the keyword at the expected index, we have to add it + sqlBuffer.insert( withClauseRecursiveIndex, recursiveKeyword ); + // and also adjust the index at which CTEs have to be inserted + topLevelWithClauseIndex += recursiveKeyword.length(); + } + } + // At this point, we have to insert CTEs at topLevelWithClauseIndex, + // but constantly inserting would lead to many buffer copies, + // so instead we cut out the suffix, render via append as usual and re-add the suffix in the end + final String temporaryRest = sqlBuffer.substring( topLevelWithClauseIndex ); + sqlBuffer.setLength( topLevelWithClauseIndex ); + if ( sqlBuffer.charAt( topLevelWithClauseIndex - 1 ) == ')' ) { + // This is the case when there is an existing CTE, so we need a comma for the CTE that are about to render + mainSeparator = COMA_SEPARATOR; + } + for ( CteStatement cte : cteStatements ) { + appendSql( mainSeparator ); + visitCteStatement( cte ); + mainSeparator = COMA_SEPARATOR; + // Make that the topLevelWithClauseIndex is up-to-date + topLevelWithClauseIndex = sqlBuffer.length(); + } + if ( inNestedWithClause ) { + // If this is a nested CTE, we need a comma at the end because the parent CTE will append further + appendSql( mainSeparator ); + } + sqlBuffer.append( temporaryRest ); + } + else { + for ( CteStatement cte : cteStatements ) { + appendSql( mainSeparator ); + visitCteStatement( cte ); + mainSeparator = COMA_SEPARATOR; + } + appendSql( WHITESPACE ); + } + clauseStack.pop(); + } + + private void visitCteStatement(CteStatement cte) { + appendSql( cte.getCteTable().getTableExpression() ); + + appendSql( " (" ); + + renderCteColumns( cte ); + + appendSql( ") as " ); + + if ( cte.getMaterialization() != CteMaterialization.UNDEFINED ) { + renderMaterializationHint( cte.getMaterialization() ); } - String mainSeparator = ""; - for ( CteStatement cte : cteStatements ) { - appendSql( mainSeparator ); - appendSql( cte.getCteTable().getTableExpression() ); + final boolean needsParenthesis = !( cte.getCteDefinition() instanceof SelectStatement ) + || ( (SelectStatement) cte.getCteDefinition() ).getQueryPart().isRoot(); + if ( needsParenthesis ) { + appendSql( OPEN_PARENTHESIS ); + } + visitCteDefinition( cte ); + if ( needsParenthesis ) { + appendSql( CLOSE_PARENTHESIS ); + } - appendSql( " (" ); + renderSearchClause( cte ); + renderCycleClause( cte ); + } - String separator = ""; + private boolean isRecursive(Collection cteStatements) { + for ( CteStatement cteStatement : cteStatements ) { + if ( cteStatement.isRecursive() ) { + return true; + } + } + return false; + } + protected void renderCteColumns(CteStatement cte) { + String separator = ""; + if ( cte.getCteTable().getCteColumns() == null ) { + final List columnExpressions = new ArrayList<>(); + cte.getCteTable().getTableGroupProducer().visitSubParts( + modelPart -> { + modelPart.forEachSelectable( + 0, + (index, mapping) -> columnExpressions.add( mapping.getSelectionExpression() ) + ); + }, + null + ); + for ( String columnExpression : columnExpressions ) { + appendSql( separator ); + appendSql( columnExpression ); + separator = COMA_SEPARATOR; + } + } + else { for ( CteColumn cteColumn : cte.getCteTable().getCteColumns() ) { appendSql( separator ); appendSql( cteColumn.getColumnExpression() ); separator = COMA_SEPARATOR; } + } + if ( cte.isRecursive() ) { + if ( !supportsRecursiveSearchClause() ) { + if ( cte.getSearchColumn() != null ) { + appendSql( COMA_SEPARATOR ); + if ( cte.getSearchClauseKind() == CteSearchClauseKind.BREADTH_FIRST ) { + appendSql( determineDepthColumnName( cte ) ); + appendSql( COMA_SEPARATOR ); + } + appendSql( cte.getSearchColumn().getColumnExpression() ); + } + } + if ( !supportsRecursiveCycleClause() ) { + if ( cte.getCycleMarkColumn() != null ) { + appendSql( COMA_SEPARATOR ); + appendSql( cte.getCycleMarkColumn().getColumnExpression() ); + } + } + if ( cte.getCycleMarkColumn() != null && !supportsRecursiveCycleClause() + || cte.getCyclePathColumn() != null && !supportsRecursiveCycleUsingClause() ) { + appendSql( COMA_SEPARATOR ); + appendSql( determineCyclePathColumnName( cte ) ); + } + } + } - appendSql( ") as " ); - - if ( cte.getMaterialization() != CteMaterialization.UNDEFINED ) { - renderMaterializationHint( cte.getMaterialization() ); + private String determineDepthColumnName(CteStatement cte) { + String baseName = "depth"; + OUTER: for ( int tries = 0; tries < 5; tries++ ) { + final String name = tries == 0 ? baseName : (baseName + "_" + tries); + for ( CteColumn cteColumn : cte.getCteTable().getCteColumns() ) { + if ( name.equals( cteColumn.getColumnExpression() ) ) { + continue OUTER; + } + } + if ( cte.getSearchColumn() != null && name.equals( cte.getSearchColumn().getColumnExpression() ) ) { + continue; + } + if ( cte.getCycleMarkColumn() != null && name.equals( cte.getCycleMarkColumn().getColumnExpression() ) ) { + continue; + } + if ( cte.getCyclePathColumn() != null && name.equals( cte.getCyclePathColumn().getColumnExpression() ) ) { + continue; } - appendSql( OPEN_PARENTHESIS ); - cte.getCteDefinition().accept( this ); - - appendSql( ')' ); - - renderSearchClause( cte ); - renderCycleClause( cte ); - - mainSeparator = COMA_SEPARATOR; + return name; } - appendSql( WHITESPACE ); + throw new IllegalStateException( "Could not determine a depth column name after 5 tries!" ); + } + + protected String determineCyclePathColumnName(CteStatement cte) { + final CteColumn cyclePathColumn = cte.getCyclePathColumn(); + if ( cyclePathColumn != null ) { + return cyclePathColumn.getColumnExpression(); + } + String baseName = "path"; + OUTER: for ( int tries = 0; tries < 5; tries++ ) { + final String name = tries == 0 ? baseName : (baseName + "_" + tries); + for ( CteColumn cteColumn : cte.getCteTable().getCteColumns() ) { + if ( name.equals( cteColumn.getColumnExpression() ) ) { + continue OUTER; + } + } + if ( cte.getSearchColumn() != null && name.equals( cte.getSearchColumn().getColumnExpression() ) ) { + continue; + } + if ( cte.getCycleMarkColumn() != null && name.equals( cte.getCycleMarkColumn().getColumnExpression() ) ) { + continue; + } + + return name; + } + throw new IllegalStateException( "Could not determine a path column name after 5 tries!" ); + } + + protected boolean isInRecursiveQueryPart() { + return currentCteStatement != null && currentCteStatement.isRecursive() + && ( (QueryGroup) ( (SelectStatement) currentCteStatement.getCteDefinition() ).getQueryPart() ).getQueryParts() + .get( 1 ) == getCurrentQueryPart(); + } + + private boolean isInSubquery() { + return statementStack.depth() > 1 && statementStack.getCurrent() instanceof SelectStatement + && !( (SelectStatement) statementStack.getCurrent() ).getQueryPart().isRoot(); + } + + protected void visitCteDefinition(CteStatement cte) { + final CteStatement oldCteStatement = currentCteStatement; + currentCteStatement = cte; + cte.getCteDefinition().accept( this ); + currentCteStatement = oldCteStatement; + } + + /** + * Whether the SQL with clause is supported. + */ + protected boolean supportsWithClause() { + return true; + } + + /** + * Whether the SQL with clause is supported within a CTE. + */ + protected boolean supportsNestedWithClause() { + return supportsWithClauseInSubquery(); + } + + /** + * Whether the SQL with clause is supported within a subquery. + */ + protected boolean supportsWithClauseInSubquery() { + return supportsWithClause(); + } + + /** + * Whether CTEs should be inlined rather than rendered as CTEs. + */ + protected boolean needsCteInlining() { + return !supportsWithClause() || !supportsWithClauseInSubquery() && isInSubquery(); + } + + /** + * Whether CTEs should be inlined rather than rendered as CTEs. + */ + protected boolean shouldInlineCte(TableGroup tableGroup) { + if ( tableGroup instanceof CteTableGroup ) { + if (!supportsWithClause()) { + return true; + } + if ( !supportsWithClauseInSubquery() && isInSubquery() ) { + final String cteName = tableGroup.getPrimaryTableReference().getTableId(); + final CteContainer cteOwner = statementStack.findCurrentFirst( + stmt -> { + if ( stmt instanceof CteContainer ) { + final CteContainer cteContainer = (CteContainer) stmt; + if ( cteContainer.getCteStatement( cteName ) != null ) { + return cteContainer; + } + } + return null; + } + ); + // If the CTE is owned by the root statement, it will be rendered as CTE, so we can refer to it + return cteOwner != statementStack.getRoot() && !cteOwner.getCteStatement( cteName ).isRecursive(); + } + } + return false; + } + + /** + * Whether the SQL with clause requires the "recursive" keyword for recursive CTEs. + */ + protected boolean needsRecursiveKeywordInWithClause() { + return true; + } + + /** + * Whether the SQL search clause is supported, which can be used for recursive CTEs. + */ + protected boolean supportsRecursiveSearchClause() { + return false; + } + + /** + * Whether the SQL cycle clause is supported, which can be used for recursive CTEs. + */ + protected boolean supportsRecursiveCycleClause() { + return false; + } + + /** + * Whether the SQL cycle clause supports the using sub-clause. + */ + protected boolean supportsRecursiveCycleUsingClause() { + return false; + } + + /** + * Whether the recursive search and cycle clause emulations based on the array and row constructor is supported. + */ + protected boolean supportsRecursiveClauseArrayAndRowEmulation() { + return ( supportsRowConstructor() || currentCteStatement.getSearchClauseKind() == CteSearchClauseKind.DEPTH_FIRST + && currentCteStatement.getSearchBySpecifications().size() == 1 ) + && supportsArrayConstructor(); + } + + /** + * Whether the SQL row constructor is supported. + */ + protected boolean supportsRowConstructor() { + return false; + } + + /** + * Whether the SQL array constructor is supported. + */ + protected boolean supportsArrayConstructor() { + return false; } protected void renderMaterializationHint(CteMaterialization materialization) { @@ -1372,6 +1820,12 @@ public abstract class AbstractSqlAstTranslator implemen } protected void renderSearchClause(CteStatement cte) { + if ( supportsRecursiveSearchClause() ) { + renderStandardSearchClause( cte ); + } + } + + protected void renderStandardSearchClause(CteStatement cte) { String separator; if ( cte.getSearchClauseKind() != null ) { appendSql( " search " ); @@ -1386,14 +1840,21 @@ public abstract class AbstractSqlAstTranslator implemen for ( SearchClauseSpecification searchBySpecification : cte.getSearchBySpecifications() ) { appendSql( separator ); appendSql( searchBySpecification.getCteColumn().getColumnExpression() ); - if ( searchBySpecification.getSortOrder() != null ) { - if ( searchBySpecification.getSortOrder() == SortOrder.ASCENDING ) { - appendSql( " asc" ); + final SortOrder sortOrder = searchBySpecification.getSortOrder(); + if ( sortOrder != null ) { + NullPrecedence nullPrecedence = searchBySpecification.getNullPrecedence(); + if ( nullPrecedence == null || nullPrecedence == NullPrecedence.NONE ) { + nullPrecedence = sessionFactory.getSessionFactoryOptions().getDefaultNullPrecedence(); } - else { + final boolean renderNullPrecedence = nullPrecedence != null && + !nullPrecedence.isDefaultOrdering( sortOrder, getDialect().getNullOrdering() ); + if ( sortOrder == SortOrder.DESCENDING ) { appendSql( " desc" ); } - if ( searchBySpecification.getNullPrecedence() != null ) { + else if ( renderNullPrecedence ) { + appendSql( " asc" ); + } + if ( renderNullPrecedence ) { if ( searchBySpecification.getNullPrecedence() == NullPrecedence.FIRST ) { appendSql( " nulls first" ); } @@ -1404,10 +1865,18 @@ public abstract class AbstractSqlAstTranslator implemen } separator = COMA_SEPARATOR; } + appendSql( " set " ); + appendSql( cte.getSearchColumn().getColumnExpression() ); } } protected void renderCycleClause(CteStatement cte) { + if ( supportsRecursiveCycleClause() ) { + renderStandardCycleClause( cte ); + } + } + + protected void renderStandardCycleClause(CteStatement cte) { String separator; if ( cte.getCycleMarkColumn() != null ) { appendSql( " cycle " ); @@ -1419,14 +1888,930 @@ public abstract class AbstractSqlAstTranslator implemen } appendSql( " set " ); appendSql( cte.getCycleMarkColumn().getColumnExpression() ); - appendSql( " to '" ); - appendSql( cte.getCycleValue() ); - appendSql( "' default '" ); - appendSql( cte.getNoCycleValue() ); - appendSql( '\'' ); + appendSql( " to " ); + cte.getCycleValue().accept( this ); + appendSql( " default " ); + cte.getNoCycleValue().accept( this ); + if ( cte.getCyclePathColumn() != null && supportsRecursiveCycleUsingClause() ) { + appendSql( " using " ); + appendSql( cte.getCyclePathColumn().getColumnExpression() ); + } } } + protected void renderRecursiveCteVirtualSelections(SelectClause selectClause) { + if ( currentCteStatement != null && currentCteStatement.isRecursive() ) { + if ( currentCteStatement.getSearchColumn() != null && !supportsRecursiveSearchClause() ) { + appendSql( COMA_SEPARATOR ); + if ( supportsRecursiveClauseArrayAndRowEmulation() ) { + emulateSearchClauseOrderWithRowAndArray( selectClause ); + } + else { + emulateSearchClauseOrderWithString( selectClause ); + } + } + if ( !supportsRecursiveCycleClause() || currentCteStatement.getCyclePathColumn() != null && !supportsRecursiveCycleUsingClause() ) { + if ( currentCteStatement.getCycleMarkColumn() != null ) { + appendSql( COMA_SEPARATOR ); + if ( supportsRecursiveClauseArrayAndRowEmulation() ) { + emulateCycleClauseWithRowAndArray( selectClause ); + } + else { + emulateCycleClauseWithString( selectClause ); + } + if ( !supportsRecursiveCycleClause() && isInRecursiveQueryPart() ) { + final ColumnReference cycleColumnReference = new ColumnReference( + findTableReferenceByTableId( currentCteStatement.getCteTable().getTableExpression() ), + currentCteStatement.getCycleMarkColumn().getColumnExpression(), + false, + null, + null, + currentCteStatement.getCycleMarkColumn().getJdbcMapping(), + sessionFactory + ); + if ( currentCteStatement.getCycleValue().getJdbcMapping() == getBooleanType() + && currentCteStatement.getCycleValue().getLiteralValue() == Boolean.TRUE + && currentCteStatement.getNoCycleValue().getLiteralValue() == Boolean.FALSE ) { + addAdditionalWherePredicate( + new BooleanExpressionPredicate( + cycleColumnReference, + true, + cycleColumnReference.getExpressionType() + ) + ); + } + else { + addAdditionalWherePredicate( + new ComparisonPredicate( + cycleColumnReference, + ComparisonOperator.EQUAL, + currentCteStatement.getNoCycleValue() + ) + ); + } + } + } + } + } + } + + protected void emulateSearchClauseOrderWithRowAndArray(SelectClause selectClause) { + final BasicType integerType = getIntegerType(); + + if ( isInRecursiveQueryPart() ) { + final TableReference recursiveTableReference = findTableReferenceByTableId( + currentCteStatement.getCteTable().getTableExpression() + ); + if ( currentCteStatement.getSearchClauseKind() == CteSearchClauseKind.BREADTH_FIRST ) { + final String depthColumnName = determineDepthColumnName( currentCteStatement ); + final ColumnReference depthColumnReference = new ColumnReference( + recursiveTableReference, + depthColumnName, + false, + null, + null, + integerType, + sessionFactory + ); + visitColumnReference( depthColumnReference ); + appendSql( "+1" ); + appendSql( COMA_SEPARATOR ); + appendSql( "row(" ); + visitColumnReference( depthColumnReference ); + + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( COMA_SEPARATOR ); + sqlSelection.accept( this ); + } + appendSql( ')' ); + } + else { + visitColumnReference( + new ColumnReference( + recursiveTableReference, + currentCteStatement.getSearchColumn().getColumnExpression(), + false, + null, + null, + currentCteStatement.getSearchColumn().getJdbcMapping(), + sessionFactory + ) + ); + appendSql( "||" ); + appendSql( "array[" ); + if ( currentCteStatement.getSearchBySpecifications().size() > 1 ) { + appendSql( "row(" ); + } + String separator = NO_SEPARATOR; + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( separator ); + sqlSelection.accept( this ); + separator = COMA_SEPARATOR; + } + if ( currentCteStatement.getSearchBySpecifications().size() > 1 ) { + appendSql( CLOSE_PARENTHESIS ); + } + appendSql( ']' ); + } + } + else { + if ( currentCteStatement.getSearchClauseKind() == CteSearchClauseKind.BREADTH_FIRST ) { + appendSql( '1' ); + appendSql( COMA_SEPARATOR ); + appendSql( "row(0" ); + + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( COMA_SEPARATOR ); + sqlSelection.accept( this ); + } + appendSql( ')' ); + } + else { + appendSql( "array[" ); + if ( currentCteStatement.getSearchBySpecifications().size() > 1 ) { + appendSql( "row(" ); + } + String separator = NO_SEPARATOR; + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( separator ); + sqlSelection.accept( this ); + separator = COMA_SEPARATOR; + } + if ( currentCteStatement.getSearchBySpecifications().size() > 1 ) { + appendSql( CLOSE_PARENTHESIS ); + } + appendSql( ']' ); + } + } + } + + /** + * The following emulation is not 100% perfect, because it will serialize search clause attributes to a string, + * which might have a different sort order than the attributes in their original data types, + * but we try our best to avoid issues with that by formatting data in a certain format. + * To support multiple search clause attributes, we also depend on the fact that regular data columns + * will not contain the NULL character represented by '\0', which is used as separator for column values. + * + * We serialize attributes to a string by concatenating them with each other, separated by '\0'. + * The mappings are implemented in {@link #wrapRowComponentAsOrderPreservingConcatArgument(Expression)}. + */ + private void emulateSearchClauseOrderWithString(SelectClause selectClause) { + final AbstractSqmSelfRenderingFunctionDescriptor concat = findSelfRenderingFunction( "concat", 2 ); + final AbstractSqmSelfRenderingFunctionDescriptor coalesce = findSelfRenderingFunction( "coalesce", 2 ); + final BasicType stringType = getStringType(); + final BasicType integerType = getIntegerType(); + // Shift by 1 bit instead of multiplying by 2 + final List arguments = new ArrayList<>( currentCteStatement.getSearchBySpecifications().size() << 1 ); + final Expression nullSeparator = createNullSeparator(); + + if ( isInRecursiveQueryPart() ) { + final TableReference recursiveTableReference = findTableReferenceByTableId( + currentCteStatement.getCteTable().getTableExpression() + ); + if ( currentCteStatement.getSearchClauseKind() == CteSearchClauseKind.BREADTH_FIRST ) { + final String depthColumnName = determineDepthColumnName( currentCteStatement ); + final ColumnReference depthColumnReference = new ColumnReference( + recursiveTableReference, + depthColumnName, + false, + null, + null, + integerType, + sessionFactory + ); + visitColumnReference( depthColumnReference ); + appendSql( "+1" ); + appendSql( COMA_SEPARATOR ); + + arguments.add( lpad( castToString( depthColumnReference ), 10, "0" ) ); + arguments.add( nullSeparator ); + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final Expression selectionExpression = selectClause.getSqlSelections().get( selectionIndex ) + .getExpression(); + arguments.add( + new SelfRenderingFunctionSqlAstExpression( + "coalesce", + coalesce, + List.of( + wrapRowComponentAsOrderPreservingConcatArgument( selectionExpression ), + nullSeparator + ), + stringType, + stringType + ) + ); + arguments.add( nullSeparator ); + } + concat.render( this, arguments, this ); + } + else { + arguments.add( + new ColumnReference( + recursiveTableReference, + currentCteStatement.getSearchColumn().getColumnExpression(), + false, + null, + null, + currentCteStatement.getSearchColumn().getJdbcMapping(), + sessionFactory + ) + ); + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final Expression selectionExpression = selectClause.getSqlSelections().get( selectionIndex ) + .getExpression(); + arguments.add( + new SelfRenderingFunctionSqlAstExpression( + "coalesce", + coalesce, + List.of( + wrapRowComponentAsOrderPreservingConcatArgument( selectionExpression ), + nullSeparator + ), + stringType, + stringType + ) + ); + arguments.add( nullSeparator ); + } + arguments.add( nullSeparator ); + concat.render( this, arguments, this ); + } + } + else { + int columnSizeEstimate = 0; + if ( currentCteStatement.getSearchClauseKind() == CteSearchClauseKind.BREADTH_FIRST ) { + appendSql( '1' ); + appendSql( COMA_SEPARATOR ); + + arguments.add( new QueryLiteral<>( StringHelper.repeat( '0', 10 ), stringType ) ); + arguments.add( nullSeparator ); + columnSizeEstimate += 11; + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final Expression selectionExpression = selectClause.getSqlSelections().get( selectionIndex ) + .getExpression(); + arguments.add( + new SelfRenderingFunctionSqlAstExpression( + "coalesce", + coalesce, + List.of( + wrapRowComponentAsOrderPreservingConcatArgument( selectionExpression ), + nullSeparator + ), + stringType, + stringType + ) + ); + arguments.add( nullSeparator ); + columnSizeEstimate += wrapRowComponentAsOrderPreservingConcatArgumentSizeEstimate( selectionExpression ) + 1; + } + visitRecursivePath( + new SelfRenderingFunctionSqlAstExpression( + "concat", + concat, + arguments, + stringType, + stringType + ), + columnSizeEstimate + ); + } + else { + for ( SearchClauseSpecification searchBySpecification : currentCteStatement.getSearchBySpecifications() ) { + if ( searchBySpecification.getSortOrder() == SortOrder.DESCENDING ) { + throw new IllegalArgumentException( "Can't emulate search clause for descending search specifications" ); + } + if ( searchBySpecification.getNullPrecedence() != NullPrecedence.NONE ) { + throw new IllegalArgumentException( "Can't emulate search clause for search specifications with explicit null precedence" ); + } + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( searchBySpecification.getCteColumn() ); + final Expression selectionExpression = selectClause.getSqlSelections().get( selectionIndex ) + .getExpression(); + arguments.add( + new SelfRenderingFunctionSqlAstExpression( + "coalesce", + coalesce, + List.of( + wrapRowComponentAsEqualityPreservingConcatArgument( selectionExpression ), + nullSeparator + ), + stringType, + stringType + ) + ); + arguments.add( nullSeparator ); + columnSizeEstimate += wrapRowComponentAsOrderPreservingConcatArgumentSizeEstimate( selectionExpression ) + 1; + } + arguments.add( nullSeparator ); + columnSizeEstimate += 1; + visitRecursivePath( + new SelfRenderingFunctionSqlAstExpression( + "concat", + concat, + arguments, + stringType, + stringType + ), + columnSizeEstimate * MAX_RECURSION_DEPTH_ESTIMATE + ); + } + } + } + + /** + * Renders the recursive path, possibly wrapping a cast expression around it, + * to make sure a type with proper size is chosen. + */ + protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { + recursivePath.accept( this ); + } + + protected void emulateCycleClauseWithRowAndArray(SelectClause selectClause) { + if ( isInRecursiveQueryPart() ) { + final BasicType stringType = getStringType(); + final TableReference recursiveTableReference = findTableReferenceByTableId( + currentCteStatement.getCteTable().getTableExpression() + ); + final String cyclePathColumnName = determineCyclePathColumnName( currentCteStatement ); + final ColumnReference cyclePathColumnReference = new ColumnReference( + recursiveTableReference, + cyclePathColumnName, + false, + null, + null, + stringType, + sessionFactory + ); + + if ( !supportsRecursiveCycleClause() ) { + // Cycle mark + appendSql( "case when " ); + final String arrayContainsFunction = getArrayContainsFunction(); + if ( arrayContainsFunction != null ) { + appendSql( arrayContainsFunction ); + appendSql( OPEN_PARENTHESIS ); + visitColumnReference( cyclePathColumnReference ); + appendSql( COMA_SEPARATOR ); + } + if ( currentCteStatement.getCycleColumns().size() > 1 ) { + appendSql( "row(" ); + String separator = NO_SEPARATOR; + for ( CteColumn cycleColumn : currentCteStatement.getCycleColumns() ) { + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( cycleColumn ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( separator ); + sqlSelection.accept( this ); + separator = COMA_SEPARATOR; + } + appendSql( ')' ); + } + else { + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( currentCteStatement.getCycleColumns().get( 0 ) ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + sqlSelection.accept( this ); + } + if ( arrayContainsFunction == null ) { + appendSql( "=any(" ); + visitColumnReference( cyclePathColumnReference ); + } + appendSql( CLOSE_PARENTHESIS ); + + appendSql( " then " ); + currentCteStatement.getCycleValue().accept( this ); + appendSql( " else " ); + currentCteStatement.getNoCycleValue().accept( this ); + appendSql( " end" ); + appendSql( COMA_SEPARATOR ); + } + + // Cycle path + visitColumnReference( cyclePathColumnReference ); + appendSql( "||array[" ); + if ( currentCteStatement.getCycleColumns().size() > 1 ) { + appendSql( "row(" ); + } + String separator = NO_SEPARATOR; + for ( CteColumn cycleColumn : currentCteStatement.getCycleColumns() ) { + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( cycleColumn ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( separator ); + sqlSelection.accept( this ); + separator = COMA_SEPARATOR; + } + if ( currentCteStatement.getCycleColumns().size() > 1 ) { + appendSql( CLOSE_PARENTHESIS ); + } + appendSql( ']' ); + } + else { + if ( !supportsRecursiveCycleClause() ) { + // Cycle mark + currentCteStatement.getNoCycleValue().accept( this ); + appendSql( COMA_SEPARATOR ); + } + + // Cycle path + appendSql( "array[" ); + if ( currentCteStatement.getCycleColumns().size() > 1 ) { + appendSql( "row(" ); + } + String separator = NO_SEPARATOR; + for ( CteColumn cycleColumn : currentCteStatement.getCycleColumns() ) { + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( cycleColumn ); + final SqlSelection sqlSelection = selectClause.getSqlSelections().get( selectionIndex ); + appendSql( separator ); + sqlSelection.accept( this ); + separator = COMA_SEPARATOR; + } + if ( currentCteStatement.getCycleColumns().size() > 1 ) { + appendSql( CLOSE_PARENTHESIS ); + } + appendSql( ']' ); + } + } + + /** + * Returns the name of the array_contains(array, element) function, + * which is used for emulating the cycle clause. + */ + protected String getArrayContainsFunction() { + return null; + } + + private Expression createNullSeparator() { + final AbstractSqmSelfRenderingFunctionDescriptor chr = findSelfRenderingFunction( "chr", 1 ); + final BasicType stringType = getStringType(); + return new SelfRenderingFunctionSqlAstExpression( + "chr", + chr, + List.of( new QueryLiteral<>( 0, getIntegerType() ) ), + stringType, + stringType + ); + } + + /** + * The following emulation is not 100% perfect, because it will serialize cycle clause attributes to a string, + * which might have a different equality rules than the attributes in the original data types, + * but we try our best to avoid issues with that by formatting data in a certain format. + * To support multiple cycle clause attributes, we also depend on the fact that regular data columns + * will not contain the NULL character represented by '\0', which is used as separator for column values. + * + * We serialize attributes to a string by concatenating them with each other, separated by '\0'. + * The mappings are implemented in {@link #wrapRowComponentAsEqualityPreservingConcatArgument(Expression)}. + */ + private void emulateCycleClauseWithString(SelectClause selectClause) { + final AbstractSqmSelfRenderingFunctionDescriptor concat = findSelfRenderingFunction( "concat", 2 ); + final AbstractSqmSelfRenderingFunctionDescriptor coalesce = findSelfRenderingFunction( "coalesce", 2 ); + final BasicType stringType = getStringType(); + // Shift by 1 bit instead of multiplying by 2 + final List arguments = new ArrayList<>( currentCteStatement.getCycleColumns().size() << 1 ); + final Expression nullSeparator = createNullSeparator(); + + if ( isInRecursiveQueryPart() ) { + final TableReference recursiveTableReference = findTableReferenceByTableId( + currentCteStatement.getCteTable().getTableExpression() + ); + final String cyclePathColumnName = determineCyclePathColumnName( currentCteStatement ); + final ColumnReference cyclePathColumnReference = new ColumnReference( + recursiveTableReference, + cyclePathColumnName, + false, + null, + null, + stringType, + sessionFactory + ); + arguments.add( new QueryLiteral<>( "%", stringType ) ); + for ( CteColumn cycleColumn : currentCteStatement.getCycleColumns() ) { + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( cycleColumn ); + final Expression selectionExpression = selectClause.getSqlSelections().get( selectionIndex ) + .getExpression(); + arguments.add( + new SelfRenderingFunctionSqlAstExpression( + "coalesce", + coalesce, + List.of( + wrapRowComponentAsEqualityPreservingConcatArgument( selectionExpression ), + nullSeparator + ), + stringType, + stringType + ) + ); + arguments.add( nullSeparator ); + } + arguments.add( nullSeparator ); + arguments.add( new QueryLiteral<>( "%", stringType ) ); + + if ( !supportsRecursiveCycleClause() ) { + // Cycle mark + appendSql( "case when " ); + visitColumnReference( cyclePathColumnReference ); + appendSql( " like " ); + concat.render( this, arguments, this ); + appendSql( " then " ); + currentCteStatement.getCycleValue().accept( this ); + appendSql( " else " ); + currentCteStatement.getNoCycleValue().accept( this ); + appendSql( " end" ); + appendSql( COMA_SEPARATOR ); + } + + // Remove the wildcard literals + arguments.remove( arguments.size() - 1 ); + arguments.set( 0, cyclePathColumnReference ); + // Cycle path + concat.render( this, arguments, this ); + } + else { + if ( !supportsRecursiveCycleClause() ) { + // Cycle mark + currentCteStatement.getNoCycleValue().accept( this ); + appendSql( COMA_SEPARATOR ); + } + + // Cycle path + int columnSizeEstimate = 1; + for ( CteColumn cycleColumn : currentCteStatement.getCycleColumns() ) { + final int selectionIndex = currentCteStatement.getCteTable() + .getCteColumns() + .indexOf( cycleColumn ); + final Expression selectionExpression = selectClause.getSqlSelections().get( selectionIndex ) + .getExpression(); + arguments.add( + new SelfRenderingFunctionSqlAstExpression( + "coalesce", + coalesce, + List.of( + wrapRowComponentAsEqualityPreservingConcatArgument( selectionExpression ), + nullSeparator + ), + stringType, + stringType + ) + ); + arguments.add( nullSeparator ); + columnSizeEstimate += wrapRowComponentAsEqualityPreservingConcatArgumentSizeEstimate( selectionExpression ) + 1; + } + arguments.add( nullSeparator ); + visitRecursivePath( + new SelfRenderingFunctionSqlAstExpression( + "concat", + concat, + arguments, + stringType, + stringType + ), + columnSizeEstimate * MAX_RECURSION_DEPTH_ESTIMATE + ); + } + } + + /** + * Wraps the given expression so that it produces a string, which should have the same ordering as the original value. + * Here are the mappings for various data types: + * - Boolean types are casted to strings, which will produce `true`/`false` which is ordered correctly + * - Integral types are left padded by 0 to lengths 19, as that is the maximum number of digits in a 64 bit number + * - Numeric/Decimal types are left padded by 0 to the length of `precision`, and if that isn't available will fail + * + * Encounters of data types other than character types will result in an exception to be thrown. + * This is because the translation from the types to strings is not guaranteed to result in the same ordering. + */ + private SqlAstNode wrapRowComponentAsOrderPreservingConcatArgument(Expression expression) { + final JdbcMapping jdbcMapping = expression.getExpressionType().getJdbcMappings().get( 0 ); + switch ( jdbcMapping.getCastType() ) { + case STRING: + return expression; + case BOOLEAN: + case INTEGER_BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + return castToString( expression ); + case INTEGER: + case LONG: + return castNumberToString( expression, 19, 0 ); + case FIXED: + if ( expression.getExpressionType() instanceof SqlTypedMapping ) { + final SqlTypedMapping sqlTypedMapping = (SqlTypedMapping) expression.getExpressionType(); + if ( sqlTypedMapping.getPrecision() != null && sqlTypedMapping.getScale() != null ) { + return castNumberToString( + expression, + sqlTypedMapping.getPrecision(), + sqlTypedMapping.getScale() + ); + } + } + throw new IllegalArgumentException( + String.format( + "Can't emulate order preserving row constructor through string concatenation for numeric expression [%s] without precision or scale", + expression + ) + ); + } + throw new IllegalArgumentException( + String.format( + "Can't emulate order preserving row constructor through string concatenation for expression [%s] which is of type [%s]", + expression, + jdbcMapping.getCastType() + ) + ); + } + + private int wrapRowComponentAsOrderPreservingConcatArgumentSizeEstimate(Expression expression) { + final JdbcMapping jdbcMapping = expression.getExpressionType().getJdbcMappings().get( 0 ); + switch ( jdbcMapping.getCastType() ) { + case STRING: + if ( expression.getExpressionType() instanceof SqlTypedMapping ) { + final SqlTypedMapping sqlTypedMapping = (SqlTypedMapping) expression.getExpressionType(); + if ( sqlTypedMapping.getLength() != null ) { + return sqlTypedMapping.getLength().intValue(); + } + } + return Short.MAX_VALUE; + case BOOLEAN: + case INTEGER_BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + return 5; + case INTEGER: + case LONG: + return 20; + case FIXED: + if ( expression.getExpressionType() instanceof SqlTypedMapping ) { + final SqlTypedMapping sqlTypedMapping = (SqlTypedMapping) expression.getExpressionType(); + if ( sqlTypedMapping.getPrecision() != null && sqlTypedMapping.getScale() != null ) { + return sqlTypedMapping.getPrecision() + sqlTypedMapping.getScale() + 2; + } + } + } + return 1; + } + + /** + * Wraps the given expression so that it produces a string, but preserves equality with respect to what the original value was. + * + * The following data types are supported and simply concatenated, with optional casting: + * - String types + * - Boolean types + * - Integral types + * - Numeric/Decimal types + * - Temporal types + * + * Encounters of other data types will result in an exception to be thrown. + * This is because the translation from the types to strings is not guaranteed to preserve equality. + */ + private SqlAstNode wrapRowComponentAsEqualityPreservingConcatArgument(Expression expression) { + final JdbcMapping jdbcMapping = expression.getExpressionType().getJdbcMappings().get( 0 ); + switch ( jdbcMapping.getCastType() ) { + case STRING: + return expression; + case BOOLEAN: + case INTEGER_BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + case INTEGER: + case LONG: + case FIXED: + case DATE: + case TIME: + case TIMESTAMP: + case OFFSET_TIMESTAMP: + case ZONE_TIMESTAMP: + if ( dialect.requiresCastForConcatenatingNonStrings() ) { + return castToString( expression ); + } + // Should we maybe always cast instead? Not sure what is faster/better... + final BasicType stringType = getStringType(); + final AbstractSqmSelfRenderingFunctionDescriptor concat = findSelfRenderingFunction( "concat", 2 ); + return new SelfRenderingFunctionSqlAstExpression( + "concat", + concat, + List.of( + expression, + new QueryLiteral<>( "", stringType ) + ), + stringType, + stringType + ); + } + throw new IllegalArgumentException( + String.format( + "Can't emulate equality preserving row constructor through string concatenation for expression [%s] which is of type [%s]", + expression, + jdbcMapping.getCastType() + ) + ); + } + + private int wrapRowComponentAsEqualityPreservingConcatArgumentSizeEstimate(Expression expression) { + final JdbcMapping jdbcMapping = expression.getExpressionType().getJdbcMappings().get( 0 ); + switch ( jdbcMapping.getCastType() ) { + case STRING: + if ( expression.getExpressionType() instanceof SqlTypedMapping ) { + final SqlTypedMapping sqlTypedMapping = (SqlTypedMapping) expression.getExpressionType(); + if ( sqlTypedMapping.getLength() != null ) { + return sqlTypedMapping.getLength().intValue(); + } + } + return Short.MAX_VALUE; + case BOOLEAN: + case INTEGER_BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + return 5; + case INTEGER: + case LONG: + return 20; + case FIXED: + if ( expression.getExpressionType() instanceof SqlTypedMapping ) { + final SqlTypedMapping sqlTypedMapping = (SqlTypedMapping) expression.getExpressionType(); + if ( sqlTypedMapping.getPrecision() != null && sqlTypedMapping.getScale() != null ) { + return sqlTypedMapping.getPrecision() + sqlTypedMapping.getScale() + 2; + } + } + case DATE: + return DATE_CHAR_SIZE_ESTIMATE; + case TIME: + return TIME_CHAR_SIZE_ESTIMATE; + case TIMESTAMP: + return TIMESTAMP_CHAR_SIZE_ESTIMATE; + case OFFSET_TIMESTAMP: + case ZONE_TIMESTAMP: + return OFFSET_TIMESTAMP_CHAR_SIZE_ESTIMATE; + } + return 1; + } + + private Expression abs(Expression expression) { + final AbstractSqmSelfRenderingFunctionDescriptor abs = findSelfRenderingFunction( "abs", 2 ); + return new SelfRenderingFunctionSqlAstExpression( + "abs", + abs, + List.of( expression ), + (ReturnableType) expression.getExpressionType(), + expression.getExpressionType() + ); + } + + private Expression lpad(Expression expression, int stringLength, String padString) { + final BasicType stringType = getStringType(); + final AbstractSqmSelfRenderingFunctionDescriptor lpad = findSelfRenderingFunction( "lpad", 3 ); + return new SelfRenderingFunctionSqlAstExpression( + "lpad", + lpad, + List.of( + expression, + new QueryLiteral<>( stringLength, getIntegerType() ), + new QueryLiteral<>( padString, stringType ) + ), + stringType, + stringType + ); + } + + private AbstractSqmSelfRenderingFunctionDescriptor findSelfRenderingFunction(String functionName, int argumentCount) { + final SqmFunctionDescriptor functionDescriptor = sessionFactory.getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( functionName ); + if ( functionDescriptor instanceof MultipatternSqmFunctionDescriptor ) { + final MultipatternSqmFunctionDescriptor multiPatternFunction = (MultipatternSqmFunctionDescriptor) functionDescriptor; + return (AbstractSqmSelfRenderingFunctionDescriptor) multiPatternFunction.getFunction( argumentCount ); + } + return (AbstractSqmSelfRenderingFunctionDescriptor) functionDescriptor; + } + + /** + * Casts a number expression to a string with the given precision and scale. + */ + protected Expression castNumberToString(Expression expression, int precision, int scale) { + final BasicType stringType = getStringType(); + final AbstractSqmSelfRenderingFunctionDescriptor concat = findSelfRenderingFunction( "concat", 2 ); + + final CaseSearchedExpression signExpression = new CaseSearchedExpression( stringType ); + signExpression.when( + new ComparisonPredicate( + expression, + ComparisonOperator.LESS_THAN, + new QueryLiteral<>( 0, getIntegerType() ) + ), + new QueryLiteral<>( "-", stringType ) + ); + signExpression.otherwise( new QueryLiteral<>( "-", stringType ) ); + final int stringLength = precision + ( scale > 0 ? ( scale + 1 ) : 0 ); + return new SelfRenderingFunctionSqlAstExpression( + "concat", + concat, + List.of( + signExpression, + lpad( castToString( abs( expression ) ), stringLength, "0" ) + ), + stringType, + stringType + ); + } + + private Expression castToString(SqlAstNode node) { + final BasicType stringType = getStringType(); + return new SelfRenderingFunctionSqlAstExpression( + "cast", + castFunction(), + List.of( node, new CastTarget( stringType ) ), + stringType, + stringType + ); + } + + private TableReference findTableReferenceByTableId(String tableExpression) { + final QuerySpec currentQuerySpec = (QuerySpec) getCurrentQueryPart(); + return currentQuerySpec.getFromClause().queryTableReferences( + tableReference -> { + if ( tableExpression.equals( tableReference.getTableId() ) ) { + return tableReference; + } + return null; + } + ); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // QuerySpec @@ -1561,13 +2946,12 @@ public abstract class AbstractSqlAstTranslator implemen this.queryPartForRowNumbering = null; this.queryPartForRowNumberingClauseDepth = -1; } - String queryGroupAlias = ""; - final boolean needsParenthesis; + String queryGroupAlias = null; if ( currentQueryPart instanceof QueryGroup ) { // We always need query wrapping if we are in a query group and this query spec has a fetch clause // because of order by precedence in SQL - needsParenthesis = querySpec.hasOffsetOrFetchClause(); - if ( needsParenthesis ) { + if ( querySpec.hasOffsetOrFetchClause() ) { + queryGroupAlias = ""; // If the parent is a query group with a fetch clause, // or if the database does not support simple query grouping, we must use a select wrapper if ( !supportsSimpleQueryGrouping() || currentQueryPart.hasOffsetOrFetchClause() ) { @@ -1584,11 +2968,8 @@ public abstract class AbstractSqlAstTranslator implemen } } } - else { - needsParenthesis = !querySpec.isRoot(); - } queryPartStack.push( querySpec ); - if ( needsParenthesis ) { + if ( queryGroupAlias != null ) { appendSql( OPEN_PARENTHESIS ); } visitSelectClause( querySpec.getSelectClause() ); @@ -1603,7 +2984,7 @@ public abstract class AbstractSqlAstTranslator implemen visitForUpdateClause( querySpec ); } - if ( needsParenthesis ) { + if ( queryGroupAlias != null ) { appendSql( CLOSE_PARENTHESIS ); appendSql( queryGroupAlias ); } @@ -2279,12 +3660,12 @@ public abstract class AbstractSqlAstTranslator implemen sortExpression.accept( this ); } - if ( sortOrder == SortOrder.ASCENDING ) { - appendSql( " asc" ); - } - else if ( sortOrder == SortOrder.DESCENDING ) { + if ( sortOrder == SortOrder.DESCENDING ) { appendSql( " desc" ); } + else if ( sortOrder == SortOrder.ASCENDING && renderNullPrecedence && supportsNullPrecedence ) { + appendSql( " asc" ); + } if ( renderNullPrecedence && supportsNullPrecedence ) { appendSql( " nulls " ); @@ -2938,19 +4319,14 @@ public abstract class AbstractSqlAstTranslator implemen this.needsSelectAliases = true; final String alias = "r_" + queryPartForRowNumberingAliasCounter + '_'; queryPartForRowNumberingAliasCounter++; - final boolean needsParenthesis; - if ( queryPart instanceof QueryGroup ) { - // We always need query wrapping if we are in a query group and the query part has a fetch clause - needsParenthesis = queryPart.hasOffsetOrFetchClause(); - } - else { - needsParenthesis = !queryPart.isRoot(); - } - if ( needsParenthesis && !queryPart.isRoot() ) { + // We always need query wrapping if we are in a query group and the query part has a fetch clause + final boolean needsParenthesis = queryPart instanceof QueryGroup && queryPart.hasOffsetOrFetchClause() + && !queryPart.isRoot(); + if ( needsParenthesis ) { appendSql( OPEN_PARENTHESIS ); } appendSql( "select " ); - if ( getClauseStack().isEmpty() && !( statement instanceof InsertStatement ) ) { + if ( getClauseStack().isEmpty() && !( getStatement() instanceof InsertStatement ) ) { appendSql( '*' ); } else { @@ -2965,13 +4341,9 @@ public abstract class AbstractSqlAstTranslator implemen } } appendSql( " from " ); - if ( !needsParenthesis || queryPart.isRoot() ) { - appendSql( OPEN_PARENTHESIS ); - } + appendSql( OPEN_PARENTHESIS ); queryPart.accept( this ); - if ( !needsParenthesis || queryPart.isRoot() ) { - appendSql( CLOSE_PARENTHESIS ); - } + appendSql( CLOSE_PARENTHESIS ); appendSql( WHITESPACE ); appendSql( alias ); appendSql( " where " ); @@ -3081,7 +4453,7 @@ public abstract class AbstractSqlAstTranslator implemen finally { clauseStack.pop(); } - if ( needsParenthesis && !queryPart.isRoot() ) { + if ( needsParenthesis ) { appendSql( CLOSE_PARENTHESIS ); } } @@ -3123,6 +4495,7 @@ public abstract class AbstractSqlAstTranslator implemen appendSql( "distinct " ); } visitSqlSelections( selectClause ); + renderVirtualSelections( selectClause ); } finally { clauseStack.pop(); @@ -3145,7 +4518,7 @@ public abstract class AbstractSqlAstTranslator implemen } final SqlAstNodeRenderingMode original = parameterRenderingMode; final SqlAstNodeRenderingMode defaultRenderingMode; - if ( statement instanceof InsertStatement && clauseStack.depth() == 1 && queryPartStack.depth() == 1 ) { + if ( getStatement() instanceof InsertStatement && clauseStack.depth() == 1 && queryPartStack.depth() == 1 ) { // Databases support inferring parameter types for simple insert-select statements defaultRenderingMode = SqlAstNodeRenderingMode.DEFAULT; } @@ -3230,6 +4603,10 @@ public abstract class AbstractSqlAstTranslator implemen } } + protected void renderVirtualSelections(SelectClause selectClause) { + renderRecursiveCteVirtualSelections( selectClause ); + } + private BitSet getSelectItemsToInline() { final QuerySpec querySpec = (QuerySpec) getQueryPartStack().getCurrent(); final List sqlSelections = querySpec.getSelectClause().getSqlSelections(); @@ -3885,6 +5262,10 @@ public abstract class AbstractSqlAstTranslator implemen } protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { + if ( shouldInlineCte( tableGroup ) ) { + inlineCteTableGroup( tableGroup, lockMode ); + return false; + } final TableReference tableReference = tableGroup.getPrimaryTableReference(); if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); @@ -3892,12 +5273,13 @@ public abstract class AbstractSqlAstTranslator implemen final DerivedTableReference derivedTableReference = (DerivedTableReference) tableReference; if ( derivedTableReference.isLateral() ) { if ( getDialect().supportsLateral() ) { - appendSql( "lateral" ); + appendSql( "lateral " ); } else if ( tableReference instanceof QueryPartTableReference ) { final QueryPartTableReference queryPartTableReference = (QueryPartTableReference) tableReference; - final QueryPart queryPart = queryPartTableReference.getQueryPart(); - final QueryPart emulationQueryPart = stripToSelectClause( queryPart ); + final SelectStatement emulationStatement = stripToSelectClause( queryPartTableReference.getStatement() ); + final QueryPart queryPart = queryPartTableReference.getStatement().getQueryPart(); + final QueryPart emulationQueryPart = emulationStatement.getQueryPart(); final List columnNames; if ( queryPart instanceof QuerySpec && needsLateralSortExpressionVirtualSelections( (QuerySpec) queryPart ) ) { // One of our lateral emulations requires that sort expressions are present in the select clause @@ -3926,7 +5308,7 @@ public abstract class AbstractSqlAstTranslator implemen columnNames = queryPartTableReference.getColumnNames(); } final QueryPartTableReference emulationTableReference = new QueryPartTableReference( - emulationQueryPart, + emulationStatement, tableReference.getIdentificationVariable(), columnNames, false, @@ -3940,6 +5322,40 @@ public abstract class AbstractSqlAstTranslator implemen return false; } + protected void inlineCteTableGroup(TableGroup tableGroup, LockMode lockMode) { + // Emulate CTE with a query part table reference + final TableReference tableReference = tableGroup.getPrimaryTableReference(); + final CteStatement cteStatement = getCteStatement( tableReference.getTableId() ); + final List cteColumns = cteStatement.getCteTable().getCteColumns(); + final List columnNames = new ArrayList<>( cteColumns.size() ); + for ( CteColumn cteColumn : cteColumns ) { + columnNames.add( cteColumn.getColumnExpression() ); + } + final SelectStatement cteDefinition = (SelectStatement) cteStatement.getCteDefinition(); + final QueryPartTableGroup queryPartTableGroup = new QueryPartTableGroup( + tableGroup.getNavigablePath(), + cteStatement.getCteTable().getTableGroupProducer(), + cteDefinition, + tableReference.getIdentificationVariable(), + columnNames, + isCorrelated( cteStatement ), + true, + null + ); + statementStack.push( cteDefinition ); + renderPrimaryTableReference( queryPartTableGroup, lockMode ); + if ( queryPartTableGroup.isLateral() && !getDialect().supportsLateral() ) { + addAdditionalWherePredicate( determineLateralEmulationPredicate( queryPartTableGroup ) ); + } + statementStack.pop(); + } + + protected boolean isCorrelated(CteStatement cteStatement) { + // Assume that a CTE is correlated/lateral when the CTE is defined in a subquery + return statementStack.getCurrent() instanceof SelectStatement + && !( (SelectStatement) statementStack.getCurrent() ).getQueryPart().isRoot(); + } + protected boolean renderNamedTableReference(NamedTableReference tableReference, LockMode lockMode) { appendSql( tableReference.getTableExpression() ); registerAffectedTable( tableReference ); @@ -3960,7 +5376,14 @@ public abstract class AbstractSqlAstTranslator implemen @Override public void visitQueryPartTableReference(QueryPartTableReference tableReference) { - tableReference.getQueryPart().accept( this ); + if ( tableReference.getQueryPart().isRoot() ) { + appendSql( OPEN_PARENTHESIS ); + tableReference.getStatement().accept( this ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + tableReference.getStatement().accept( this ); + } renderDerivedTableReference( tableReference ); } @@ -3973,7 +5396,14 @@ public abstract class AbstractSqlAstTranslator implemen protected void emulateQueryPartTableReferenceColumnAliasing(QueryPartTableReference tableReference) { final List columnAliases = this.columnAliases; this.columnAliases = tableReference.getColumnNames(); - tableReference.getQueryPart().accept( this ); + if ( tableReference.getQueryPart().isRoot() ) { + appendSql( OPEN_PARENTHESIS ); + tableReference.getStatement().accept( this ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + tableReference.getStatement().accept( this ); + } this.columnAliases = columnAliases; renderTableReferenceIdentificationVariable( tableReference ); } @@ -4143,7 +5573,7 @@ public abstract class AbstractSqlAstTranslator implemen final QueryPartTableReference tableReference = (QueryPartTableReference) tableGroup.getPrimaryTableReference(); final List columnNames = tableReference.getColumnNames(); final List columnReferences = new ArrayList<>( columnNames.size() ); - final QueryPart queryPart = tableReference.getQueryPart(); + final SelectStatement statement = tableReference.getStatement(); for ( String columnName : columnNames ) { columnReferences.add( new ColumnReference( @@ -4163,11 +5593,11 @@ public abstract class AbstractSqlAstTranslator implemen && supportsDistinctFromPredicate() ) { // Special case for limit 1 sub-queries to avoid double nested sub-query // ... x(c) on x.c is not distinct from (... fetch first 1 rows only) - if ( isFetchFirstRowOnly( queryPart ) ) { + if ( isFetchFirstRowOnly( statement.getQueryPart() ) ) { return new ComparisonPredicate( new SqlTuple( columnReferences, tableGroup.getModelPart() ), ComparisonOperator.NOT_DISTINCT_FROM, - queryPart + statement ); } } @@ -4187,9 +5617,13 @@ public abstract class AbstractSqlAstTranslator implemen } final List queryParts = new ArrayList<>( 2 ); queryParts.add( lhsReferencesQuery ); - queryParts.add( queryPart ); + queryParts.add( statement.getQueryPart() ); return new ExistsPredicate( - new QueryGroup( false, SetOperator.INTERSECT, queryParts ), + new SelectStatement( + statement.getCteStatements(), + new QueryGroup( false, SetOperator.INTERSECT, queryParts ), + Collections.emptyList() + ), false, getBooleanType() ); @@ -4203,7 +5637,7 @@ public abstract class AbstractSqlAstTranslator implemen final QueryPartTableGroup subTableGroup = new QueryPartTableGroup( tableGroup.getNavigablePath(), (TableGroupProducer) tableGroup.getModelPart(), - queryPart, + new SelectStatement( statement.getQueryPart() ), "synth_", columnNames, false, @@ -4241,8 +5675,17 @@ public abstract class AbstractSqlAstTranslator implemen ) ); - return new ExistsPredicate( existsQuery, false, getBooleanType() ); + return new ExistsPredicate( + new SelectStatement( + statement.getCteStatements(), + existsQuery, + Collections.emptyList() + ), + false, + getBooleanType() + ); } + final QueryPart queryPart = statement.getQueryPart(); if ( queryPart instanceof QueryGroup ) { // We can't use double nesting, but we need to add filter conditions, so fail if this is a query group throw new UnsupportedOperationException( "Can't emulate lateral query group with limit/offset" ); @@ -4277,7 +5720,15 @@ public abstract class AbstractSqlAstTranslator implemen ) ); - final ExistsPredicate existsPredicate = new ExistsPredicate( existsQuery, false, getBooleanType() ); + final ExistsPredicate existsPredicate = new ExistsPredicate( + new SelectStatement( + statement.getCteStatements(), + existsQuery, + Collections.emptyList() + ), + false, + getBooleanType() + ); if ( !queryPart.hasOffsetOrFetchClause() ) { return existsPredicate; } @@ -4400,7 +5851,11 @@ public abstract class AbstractSqlAstTranslator implemen List.of( existsPredicate, new BetweenPredicate( - countQuery, + new SelectStatement( + statement.getCteStatements(), + countQuery, + Collections.emptyList() + ), countLower, countUpper, false, @@ -4463,6 +5918,14 @@ public abstract class AbstractSqlAstTranslator implemen .equals( ( (QueryLiteral) queryPart.getFetchClauseExpression() ).getLiteralValue() ); } + private SelectStatement stripToSelectClause(SelectStatement statement) { + return new SelectStatement( + statement.getCteStatements(), + stripToSelectClause( statement.getQueryPart() ), + Collections.emptyList() + ); + } + private QueryPart stripToSelectClause(QueryPart queryPart) { if ( queryPart instanceof QueryGroup ) { return stripToSelectClause( (QueryGroup) queryPart ); @@ -4705,6 +6168,19 @@ public abstract class AbstractSqlAstTranslator implemen } } + protected void withParameterRenderingMode(SqlAstNodeRenderingMode renderingMode, Runnable runnable) { + SqlAstNodeRenderingMode original = this.parameterRenderingMode; + if ( original != SqlAstNodeRenderingMode.INLINE_ALL_PARAMETERS ) { + this.parameterRenderingMode = renderingMode; + } + try { + runnable.run(); + } + finally { + this.parameterRenderingMode = original; + } + } + @Override public void visitTuple(SqlTuple tuple) { appendSql( OPEN_PARENTHESIS ); @@ -5326,10 +6802,11 @@ public abstract class AbstractSqlAstTranslator implemen protected void emulateSubQueryRelationalRestrictionPredicate( Predicate predicate, boolean negated, - QueryPart queryPart, + SelectStatement selectStatement, X lhsTuple, SubQueryRelationalRestrictionEmulationRenderer renderer, ComparisonOperator tupleComparisonOperator) { + final QueryPart queryPart = selectStatement.getQueryPart(); final QuerySpec subQuery; if ( queryPart instanceof QuerySpec && queryPart.getFetchClauseExpression() == null && queryPart.getOffsetClauseExpression() == null ) { @@ -5422,9 +6899,10 @@ public abstract class AbstractSqlAstTranslator implemen */ protected void emulateQuantifiedTupleSubQueryPredicate( Predicate predicate, - QueryPart queryPart, + SelectStatement selectStatement, SqlTuple lhsTuple, ComparisonOperator tupleComparisonOperator) { + final QueryPart queryPart = selectStatement.getQueryPart(); final QuerySpec subQuery; if ( queryPart instanceof QuerySpec && queryPart.getFetchClauseExpression() == null && queryPart.getOffsetClauseExpression() == null ) { subQuery = (QuerySpec) queryPart; @@ -5655,11 +7133,11 @@ public abstract class AbstractSqlAstTranslator implemen if ( ( lhsTuple = SqlTupleContainer.getSqlTuple( comparisonPredicate.getLeftHandExpression() ) ) != null ) { final Expression rhsExpression = comparisonPredicate.getRightHandExpression(); final boolean all; - final QueryPart subquery; + final SelectStatement subquery; // Handle emulation of quantified comparison - if ( rhsExpression instanceof QueryPart ) { - subquery = (QueryPart) rhsExpression; + if ( rhsExpression instanceof SelectStatement ) { + subquery = (SelectStatement) rhsExpression; all = true; } else if ( rhsExpression instanceof Every ) { @@ -5710,7 +7188,7 @@ public abstract class AbstractSqlAstTranslator implemen case DISTINCT_FROM: case NOT_DISTINCT_FROM: { // For this special case, we can rely on scalar subquery handling, given that the subquery fetches only one row - if ( isFetchFirstRowOnly( subquery ) ) { + if ( isFetchFirstRowOnly( subquery.getQueryPart() ) ) { renderComparison( lhsTuple, operator, subquery ); return; } @@ -5755,7 +7233,7 @@ public abstract class AbstractSqlAstTranslator implemen else if ( ( rhsTuple = SqlTupleContainer.getSqlTuple( comparisonPredicate.getRightHandExpression() ) ) != null ) { final Expression lhsExpression = comparisonPredicate.getLeftHandExpression(); - if ( lhsExpression instanceof QueryGroup ) { + if ( lhsExpression instanceof SelectStatement && ( (SelectStatement) lhsExpression ).getQueryPart() instanceof QueryGroup ) { if ( rhsTuple.getExpressions().size() == 1 ) { // Special case for tuples with arity 1 as any DBMS supports scalar IN predicates renderComparison( @@ -5775,7 +7253,7 @@ public abstract class AbstractSqlAstTranslator implemen emulateSubQueryRelationalRestrictionPredicate( comparisonPredicate, false, - (QueryGroup) lhsExpression, + (SelectStatement) lhsExpression, rhsTuple, this::renderSelectTupleComparison, // Since we switch the order of operands, we have to invert the operator diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java index bf09ec35d0..651f2bc29c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java @@ -503,7 +503,7 @@ public class AbstractSqlAstWalker implements SqlAstWalker { @Override public void visitQueryPartTableReference(QueryPartTableReference tableReference) { - tableReference.getQueryPart().accept( this ); + tableReference.getStatement().accept( this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java index f750595206..24d9708cca 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java @@ -315,7 +315,7 @@ public class ExpressionReplacementWalker implements SqlAstWalker { @Override public void visitInSubQueryPredicate(InSubQueryPredicate inSubQueryPredicate) { final Expression testExpression = replaceExpression( inSubQueryPredicate.getTestExpression() ); - final QueryPart subQuery = replaceExpression( inSubQueryPredicate.getSubQuery() ); + final SelectStatement subQuery = replaceExpression( inSubQueryPredicate.getSubQuery() ); if ( testExpression != inSubQueryPredicate.getTestExpression() || subQuery != inSubQueryPredicate.getSubQuery() ) { returnedNode = new InSubQueryPredicate( @@ -332,10 +332,10 @@ public class ExpressionReplacementWalker implements SqlAstWalker { @Override public void visitExistsPredicate(ExistsPredicate existsPredicate) { - final QueryPart queryPart = replaceExpression( existsPredicate.getExpression() ); - if ( queryPart != existsPredicate.getExpression() ) { + final SelectStatement selectStatement = replaceExpression( existsPredicate.getExpression() ); + if ( selectStatement != existsPredicate.getExpression() ) { returnedNode = new ExistsPredicate( - queryPart, + selectStatement, existsPredicate.isNegated(), existsPredicate.getExpressionType() ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/AbstractStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/AbstractStatement.java index 2cce522cc0..28f5a85ada 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/AbstractStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/AbstractStatement.java @@ -6,7 +6,6 @@ */ package org.hibernate.sql.ast.tree; -import java.util.Collection; import java.util.Map; import org.hibernate.sql.ast.tree.cte.CteContainer; @@ -18,22 +17,11 @@ import org.hibernate.sql.ast.tree.cte.CteStatement; public abstract class AbstractStatement implements Statement, CteContainer { private final Map cteStatements; - private boolean withRecursive; public AbstractStatement(Map cteStatements) { this.cteStatements = cteStatements; } - @Override - public boolean isWithRecursive() { - return withRecursive; - } - - @Override - public void setWithRecursive(boolean withRecursive) { - this.withRecursive = withRecursive; - } - @Override public Map getCteStatements() { return cteStatements; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteContainer.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteContainer.java index f425dceae4..592cf1f238 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteContainer.java @@ -17,10 +17,6 @@ import java.util.Map; */ public interface CteContainer { - boolean isWithRecursive(); - - void setWithRecursive(boolean recursive); - Map getCteStatements(); CteStatement getCteStatement(String cteLabel); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteStatement.java index 8e5499a21c..8344c25565 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteStatement.java @@ -9,6 +9,7 @@ package org.hibernate.sql.ast.tree.cte; import java.util.List; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.expression.Literal; /** * A statement using a CTE @@ -22,10 +23,13 @@ public class CteStatement { private final CteMaterialization materialization; private final CteSearchClauseKind searchClauseKind; private final List searchBySpecifications; + private final CteColumn searchColumn; private final List cycleColumns; private final CteColumn cycleMarkColumn; - private final char cycleValue; - private final char noCycleValue; + private final CteColumn cyclePathColumn; + private final Literal cycleValue; + private final Literal noCycleValue; + private boolean recursive; public CteStatement(CteTable cteTable, Statement cteDefinition) { this( cteTable, cteDefinition, CteMaterialization.UNDEFINED ); @@ -37,10 +41,12 @@ public class CteStatement { this.materialization = materialization; this.searchClauseKind = null; this.searchBySpecifications = null; + this.searchColumn = null; this.cycleColumns = null; this.cycleMarkColumn = null; - this.cycleValue = '\0'; - this.noCycleValue = '\0'; + this.cyclePathColumn = null; + this.cycleValue = null; + this.noCycleValue = null; } public CteStatement( @@ -49,17 +55,21 @@ public class CteStatement { CteMaterialization materialization, CteSearchClauseKind searchClauseKind, List searchBySpecifications, + CteColumn searchColumn, List cycleColumns, CteColumn cycleMarkColumn, - char cycleValue, - char noCycleValue) { + CteColumn cyclePathColumn, + Literal cycleValue, + Literal noCycleValue) { this.cteTable = cteTable; this.cteDefinition = cteDefinition; this.materialization = materialization; this.searchClauseKind = searchClauseKind; this.searchBySpecifications = searchBySpecifications; + this.searchColumn = searchColumn; this.cycleColumns = cycleColumns; this.cycleMarkColumn = cycleMarkColumn; + this.cyclePathColumn = cyclePathColumn; this.cycleValue = cycleValue; this.noCycleValue = noCycleValue; } @@ -84,6 +94,10 @@ public class CteStatement { return searchBySpecifications; } + public CteColumn getSearchColumn() { + return searchColumn; + } + public List getCycleColumns() { return cycleColumns; } @@ -92,11 +106,23 @@ public class CteStatement { return cycleMarkColumn; } - public char getCycleValue() { + public CteColumn getCyclePathColumn() { + return cyclePathColumn; + } + + public Literal getCycleValue() { return cycleValue; } - public char getNoCycleValue() { + public Literal getNoCycleValue() { return noCycleValue; } + + public boolean isRecursive() { + return recursive; + } + + public void setRecursive() { + this.recursive = true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTable.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTable.java index 33ac1b5979..df410d55fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTable.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTable.java @@ -8,9 +8,24 @@ package org.hibernate.sql.ast.tree.cte; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; -import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.Association; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.metamodel.mapping.DiscriminatedAssociationModelPart; +import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; +import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.derived.CteTupleTableGroupProducer; /** * Describes the table definition for the CTE - its name amd its columns @@ -18,39 +33,198 @@ import org.hibernate.metamodel.mapping.EntityMappingType; * @author Steve Ebersole */ public class CteTable { - private final SessionFactoryImplementor sessionFactory; private final String cteName; + private final AnonymousTupleTableGroupProducer tableGroupProducer; private final List cteColumns; - public CteTable(String cteName, EntityMappingType entityDescriptor) { - final int numberOfColumns = entityDescriptor.getIdentifierMapping().getJdbcTypeCount(); - final List columns = new ArrayList<>( numberOfColumns ); - entityDescriptor.getIdentifierMapping().forEachSelectable( - (columnIndex, selection) -> columns.add( - new CteColumn("cte_" + selection.getSelectionExpression(), selection.getJdbcMapping() ) - ) - ); - this.cteName = cteName; - this.cteColumns = columns; - this.sessionFactory = entityDescriptor.getEntityPersister().getFactory(); + public CteTable(String cteName, List cteColumns) { + this( cteName, null, cteColumns ); } - public CteTable(String cteName, List cteColumns, SessionFactoryImplementor sessionFactory) { + public CteTable(String cteName, CteTupleTableGroupProducer tableGroupProducer) { + this( cteName, tableGroupProducer, tableGroupProducer.determineCteColumns() ); + } + + private CteTable(String cteName, AnonymousTupleTableGroupProducer tableGroupProducer, List cteColumns) { + assert cteName != null; this.cteName = cteName; - this.cteColumns = cteColumns; - this.sessionFactory = sessionFactory; + this.tableGroupProducer = tableGroupProducer; + this.cteColumns = List.copyOf( cteColumns ); } public String getTableExpression() { return cteName; } + public AnonymousTupleTableGroupProducer getTableGroupProducer() { + return tableGroupProducer; + } + public List getCteColumns() { return cteColumns; } public CteTable withName(String name) { - return new CteTable( name, cteColumns, sessionFactory ); + return new CteTable( name, tableGroupProducer, cteColumns ); } + public static CteTable createIdTable(String cteName, EntityMappingType entityDescriptor) { + final int numberOfColumns = entityDescriptor.getIdentifierMapping().getJdbcTypeCount(); + final List columns = new ArrayList<>( numberOfColumns ); + final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); + final String idName; + if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { + idName = ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName(); + } + else { + idName = "id"; + } + forEachCteColumn( idName, identifierMapping, columns::add ); + return new CteTable( cteName, columns ); + } + + public static CteTable createEntityTable(String cteName, EntityMappingType entityDescriptor) { + final int numberOfColumns = entityDescriptor.getIdentifierMapping().getJdbcTypeCount(); + final List columns = new ArrayList<>( numberOfColumns ); + final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); + final String idName; + if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { + idName = ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName(); + } + else { + idName = "id"; + } + forEachCteColumn( idName, identifierMapping, columns::add ); + + final EntityDiscriminatorMapping discriminatorMapping = entityDescriptor.getDiscriminatorMapping(); + if ( discriminatorMapping != null && discriminatorMapping.isPhysical() && !discriminatorMapping.isFormula() ) { + forEachCteColumn( "class", discriminatorMapping, columns::add ); + } + + // Collect all columns for all entity subtype attributes + entityDescriptor.visitSubTypeAttributeMappings( + attribute -> { + if ( !( attribute instanceof PluralAttributeMapping ) ) { + forEachCteColumn( attribute.getAttributeName(), attribute, columns::add ); + } + } + ); + // We add a special row number column that we can use to identify and join rows + columns.add( + new CteColumn( + "rn_", + entityDescriptor.getEntityPersister() + .getFactory() + .getTypeConfiguration() + .getBasicTypeForJavaType( Integer.class ) + ) + ); + return new CteTable( cteName, columns ); + } + + public static void forEachCteColumn(String prefix, ModelPart modelPart, Consumer consumer) { + if ( modelPart instanceof BasicValuedMapping ) { + consumer.accept( new CteColumn( prefix, ( (BasicValuedMapping) modelPart ).getJdbcMapping() ) ); + } + else if ( modelPart instanceof EntityValuedModelPart ) { + final EntityValuedModelPart entityPart = ( EntityValuedModelPart ) modelPart; + final ModelPart targetPart; + if ( modelPart instanceof Association ) { + final Association association = (Association) modelPart; + if ( association.getForeignKeyDescriptor() == null ) { + // This is expected to happen when processing a + // PostInitCallbackEntry because the callbacks + // are not ordered. The exception is caught in + // MappingModelCreationProcess.executePostInitCallbacks() + // and the callback is re-queued. + throw new IllegalStateException( "ForeignKeyDescriptor not ready for [" + association.getPartName() + "] on entity: " + modelPart.findContainingEntityMapping().getEntityName() ); + } + if ( association.getSideNature() != ForeignKeyDescriptor.Nature.KEY ) { + // Inverse one-to-one receives no column + return; + } + targetPart = association.getForeignKeyDescriptor().getTargetPart(); + } + else { + targetPart = entityPart.getEntityMappingType().getIdentifierMapping(); + } + forEachCteColumn( prefix + "_" + entityPart.getPartName(), targetPart, consumer ); + } + else if ( modelPart instanceof DiscriminatedAssociationModelPart ) { + final DiscriminatedAssociationModelPart discriminatedPart = (DiscriminatedAssociationModelPart) modelPart; + final String newPrefix = prefix + "_" + discriminatedPart.getPartName() + "_"; + forEachCteColumn( + newPrefix + "discriminator", + discriminatedPart.getDiscriminatorPart(), + consumer + ); + forEachCteColumn( + newPrefix + "key", + discriminatedPart.getKeyPart(), + consumer + ); + } + else { + final EmbeddableValuedModelPart embeddablePart = ( EmbeddableValuedModelPart ) modelPart; + for ( AttributeMapping mapping : embeddablePart.getEmbeddableTypeDescriptor().getAttributeMappings() ) { + if ( !( mapping instanceof PluralAttributeMapping ) ) { + forEachCteColumn( prefix + "_" + mapping.getAttributeName(), mapping, consumer ); + } + } + } + } + + public static int determineModelPartStartIndex(EntityPersister entityDescriptor, ModelPart modelPart) { + int offset = 0; + final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); + if ( modelPart == identifierMapping ) { + return offset; + } + offset += identifierMapping.getJdbcTypeCount(); + final EntityDiscriminatorMapping discriminatorMapping = entityDescriptor.getDiscriminatorMapping(); + if ( discriminatorMapping != null ) { + if ( modelPart == discriminatorMapping ) { + return offset; + } + offset += discriminatorMapping.getJdbcTypeCount(); + } + for ( AttributeMapping attribute : entityDescriptor.getAttributeMappings() ) { + if ( !( attribute instanceof PluralAttributeMapping ) ) { + final int result = determineModelPartStartIndex( offset, attribute, modelPart ); + if ( result < 0 ) { + return -result; + } + offset = result; + } + } + return -1; + } + + private static int determineModelPartStartIndex(int offset, ModelPart modelPart, ModelPart modelPartToFind) { + if ( modelPart == modelPartToFind ) { + return -offset; + } + if ( modelPart instanceof EntityValuedModelPart ) { + final ModelPart keyPart; + if ( modelPart instanceof Association ) { + keyPart = ( (Association) modelPart ).getForeignKeyDescriptor(); + } + else { + keyPart = ( (EntityValuedModelPart) modelPart ).getEntityMappingType().getIdentifierMapping(); + } + return determineModelPartStartIndex( offset, keyPart, modelPartToFind ); + } + else if ( modelPart instanceof EmbeddableValuedModelPart ) { + final EmbeddableValuedModelPart embeddablePart = ( EmbeddableValuedModelPart ) modelPart; + for ( AttributeMapping mapping : embeddablePart.getEmbeddableTypeDescriptor().getAttributeMappings() ) { + final int result = determineModelPartStartIndex( offset, mapping, modelPartToFind ); + if ( result < 0 ) { + return result; + } + offset = result; + } + return offset; + } + return offset + modelPart.getJdbcTypeCount(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTableGroup.java index 8ccc6f2185..8bfcec4306 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/cte/CteTableGroup.java @@ -8,12 +8,16 @@ package org.hibernate.sql.ast.tree.cte; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Consumer; +import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.tree.from.AbstractTableGroup; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; @@ -25,21 +29,36 @@ import org.hibernate.sql.ast.tree.from.TableReferenceJoin; */ public class CteTableGroup extends AbstractTableGroup { private final NamedTableReference cteTableReference; + private final Set compatibleTableExpressions; public CteTableGroup(NamedTableReference cteTableReference) { - this( false, cteTableReference ); - } - - public CteTableGroup(boolean canUseInnerJoins, NamedTableReference cteTableReference) { - super( - canUseInnerJoins, + this( + false, new NavigablePath( cteTableReference.getTableExpression() ), null, - cteTableReference.getIdentificationVariable(), null, + cteTableReference, + Collections.emptySet() + ); + } + + public CteTableGroup( + boolean canUseInnerJoins, + NavigablePath navigablePath, + SqlAliasBase sqlAliasBase, + ModelPartContainer modelPartContainer, + NamedTableReference cteTableReference, + Set compatibleTableExpressions) { + super( + canUseInnerJoins, + navigablePath, + modelPartContainer, + cteTableReference.getIdentificationVariable(), + sqlAliasBase, null ); this.cteTableReference = cteTableReference; + this.compatibleTableExpressions = compatibleTableExpressions; } @Override @@ -47,6 +66,34 @@ public class CteTableGroup extends AbstractTableGroup { return cteTableReference.getIdentificationVariable(); } + @Override + protected TableReference getTableReferenceInternal( + NavigablePath navigablePath, + String tableExpression, + boolean allowFkOptimization, + boolean resolve) { + if ( compatibleTableExpressions.contains( tableExpression ) ) { + return getPrimaryTableReference(); + } + for ( TableGroupJoin tableGroupJoin : getNestedTableGroupJoins() ) { + final TableReference groupTableReference = tableGroupJoin.getJoinedGroup() + .getPrimaryTableReference() + .getTableReference( navigablePath, tableExpression, allowFkOptimization, resolve ); + if ( groupTableReference != null ) { + return groupTableReference; + } + } + for ( TableGroupJoin tableGroupJoin : getTableGroupJoins() ) { + final TableReference groupTableReference = tableGroupJoin.getJoinedGroup() + .getPrimaryTableReference() + .getTableReference( navigablePath, tableExpression, allowFkOptimization, resolve ); + if ( groupTableReference != null ) { + return groupTableReference; + } + } + return null; + } + @Override public void applyAffectedTableNames(Consumer nameCollector) { nameCollector.accept( cteTableReference.getTableExpression() ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/delete/DeleteStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/delete/DeleteStatement.java index 738240811f..3cddb5ae9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/delete/DeleteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/delete/DeleteStatement.java @@ -47,7 +47,6 @@ public class DeleteStatement extends AbstractMutationStatement { Predicate restriction, List returningColumns) { this( - cteContainer.isWithRecursive(), cteContainer.getCteStatements(), targetTable, restriction, @@ -56,14 +55,12 @@ public class DeleteStatement extends AbstractMutationStatement { } public DeleteStatement( - boolean withRecursive, Map cteStatements, NamedTableReference targetTable, Predicate restriction, List returningColumns) { super( cteStatements, targetTable, returningColumns ); this.restriction = restriction; - setWithRecursive( withRecursive ); } public Predicate getRestriction() { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Any.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Any.java index c099f5bf6f..4ffa9461f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Any.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Any.java @@ -13,6 +13,7 @@ import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.basic.BasicResult; @@ -23,15 +24,15 @@ import org.hibernate.type.descriptor.java.JavaType; */ public class Any implements Expression, DomainResultProducer { - private final QueryPart subquery; + private final SelectStatement subquery; private final MappingModelExpressible type; - public Any(QueryPart subquery, MappingModelExpressible type) { + public Any(SelectStatement subquery, MappingModelExpressible type) { this.subquery = subquery; this.type = type; } - public QueryPart getSubquery() { + public SelectStatement getSubquery() { return subquery; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Every.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Every.java index 2fdee6fa36..998ab5afa3 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Every.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Every.java @@ -13,6 +13,7 @@ import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.basic.BasicResult; @@ -23,15 +24,15 @@ import org.hibernate.type.descriptor.java.JavaType; */ public class Every implements Expression, DomainResultProducer { - private final QueryPart subquery; + private final SelectStatement subquery; private final MappingModelExpressible type; - public Every(QueryPart subquery, MappingModelExpressible type) { + public Every(SelectStatement subquery, MappingModelExpressible type) { this.subquery = subquery; this.type = type; } - public QueryPart getSubquery() { + public SelectStatement getSubquery() { return subquery; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ModifiedSubQueryExpression.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ModifiedSubQueryExpression.java index 183a12151d..bf928f7ef0 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ModifiedSubQueryExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ModifiedSubQueryExpression.java @@ -9,6 +9,7 @@ package org.hibernate.sql.ast.tree.expression; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; /** * @author Steve Ebersole @@ -30,15 +31,15 @@ public class ModifiedSubQueryExpression implements Expression { } } - private final QueryPart subQuery; + private final SelectStatement subQuery; private final Modifier modifier; - public ModifiedSubQueryExpression(QueryPart subQuery, Modifier modifier) { + public ModifiedSubQueryExpression(SelectStatement subQuery, Modifier modifier) { this.subQuery = subQuery; this.modifier = modifier; } - public QueryPart getSubQuery() { + public SelectStatement getSubQuery() { return subQuery; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java index 9903588108..6b6f141212 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java @@ -15,6 +15,7 @@ import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; /** * A special table group for a sub-queries. @@ -29,7 +30,7 @@ public class QueryPartTableGroup extends AbstractTableGroup { public QueryPartTableGroup( NavigablePath navigablePath, TableGroupProducer tableGroupProducer, - QueryPart queryPart, + SelectStatement selectStatement, String sourceAlias, List columnNames, boolean lateral, @@ -38,7 +39,7 @@ public class QueryPartTableGroup extends AbstractTableGroup { this( navigablePath, tableGroupProducer, - queryPart, + selectStatement, sourceAlias, columnNames, Collections.emptySet(), @@ -51,7 +52,7 @@ public class QueryPartTableGroup extends AbstractTableGroup { public QueryPartTableGroup( NavigablePath navigablePath, TableGroupProducer tableGroupProducer, - QueryPart queryPart, + SelectStatement selectStatement, String sourceAlias, List columnNames, Set compatibleTableExpressions, @@ -68,7 +69,7 @@ public class QueryPartTableGroup extends AbstractTableGroup { ); this.compatibleTableExpressions = compatibleTableExpressions; this.queryPartTableReference = new QueryPartTableReference( - queryPart, + selectStatement, sourceAlias, columnNames, lateral, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableReference.java index 1f254ee75e..9cd8e51baf 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableReference.java @@ -12,6 +12,7 @@ import java.util.function.Function; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; /** * A table reference for a query part. @@ -20,20 +21,24 @@ import org.hibernate.sql.ast.tree.select.QueryPart; */ public class QueryPartTableReference extends DerivedTableReference { - private final QueryPart queryPart; + private final SelectStatement selectStatement; public QueryPartTableReference( - QueryPart queryPart, + SelectStatement selectStatement, String identificationVariable, List columnNames, boolean lateral, SessionFactoryImplementor sessionFactory) { super( identificationVariable, columnNames, lateral, sessionFactory ); - this.queryPart = queryPart; + this.selectStatement = selectStatement; } public QueryPart getQueryPart() { - return queryPart; + return selectStatement.getQueryPart(); + } + + public SelectStatement getStatement() { + return selectStatement; } @Override @@ -45,7 +50,7 @@ public class QueryPartTableReference extends DerivedTableReference { public Boolean visitAffectedTableNames(Function nameCollector) { final Function tableReferenceBooleanFunction = tableReference -> tableReference.visitAffectedTableNames( nameCollector ); - return queryPart.queryQuerySpecs( + return selectStatement.getQueryPart().queryQuerySpecs( querySpec -> querySpec.getFromClause().queryTableReferences( tableReferenceBooleanFunction ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertStatement.java index 3b7c336244..c9e2436497 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertStatement.java @@ -42,16 +42,14 @@ public class InsertStatement extends AbstractMutationStatement { CteContainer cteContainer, NamedTableReference targetTable, List returningColumns) { - this( cteContainer.isWithRecursive(), cteContainer.getCteStatements(), targetTable, returningColumns ); + this( cteContainer.getCteStatements(), targetTable, returningColumns ); } public InsertStatement( - boolean withRecursive, Map cteStatements, NamedTableReference targetTable, List returningColumns) { super( cteStatements, targetTable, returningColumns ); - setWithRecursive( withRecursive ); } public List getTargetColumnReferences() { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/ExistsPredicate.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/ExistsPredicate.java index a567771db5..46de89337f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/ExistsPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/ExistsPredicate.java @@ -9,6 +9,7 @@ package org.hibernate.sql.ast.tree.predicate; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; /** * @author Gavin King @@ -16,16 +17,20 @@ import org.hibernate.sql.ast.tree.select.QueryPart; public class ExistsPredicate implements Predicate { private final boolean negated; - private final QueryPart expression; + private final SelectStatement expression; private final JdbcMappingContainer expressionType; public ExistsPredicate(QueryPart expression, boolean negated, JdbcMappingContainer expressionType) { + this( new SelectStatement( expression ), negated, expressionType ); + } + + public ExistsPredicate(SelectStatement expression, boolean negated, JdbcMappingContainer expressionType) { this.negated = negated; this.expression = expression; this.expressionType = expressionType; } - public QueryPart getExpression() { + public SelectStatement getExpression() { return expression; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/InSubQueryPredicate.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/InSubQueryPredicate.java index b9480f6780..5f7846eafc 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/InSubQueryPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/InSubQueryPredicate.java @@ -10,19 +10,20 @@ import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.SelectStatement; /** * @author Steve Ebersole */ public class InSubQueryPredicate extends AbstractPredicate { private final Expression testExpression; - private final QueryPart subQuery; + private final SelectStatement subQuery; public InSubQueryPredicate(Expression testExpression, QueryPart subQuery, boolean negated) { - this( testExpression, subQuery, negated, null ); + this( testExpression, new SelectStatement( subQuery ), negated, null ); } - public InSubQueryPredicate(Expression testExpression, QueryPart subQuery, boolean negated, JdbcMappingContainer expressionType) { + public InSubQueryPredicate(Expression testExpression, SelectStatement subQuery, boolean negated, JdbcMappingContainer expressionType) { super( expressionType, negated ); this.testExpression = testExpression; this.subQuery = subQuery; @@ -32,7 +33,7 @@ public class InSubQueryPredicate extends AbstractPredicate { return testExpression; } - public QueryPart getSubQuery() { + public SelectStatement getSubQuery() { return subQuery; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryGroup.java index 0f9abc303e..02e84d6420 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryGroup.java @@ -10,11 +10,8 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.SetOperator; import org.hibernate.sql.ast.SqlAstWalker; -import org.hibernate.sql.results.graph.DomainResult; -import org.hibernate.sql.results.graph.DomainResultCreationState; /** * @author Christian Beikov @@ -70,21 +67,4 @@ public class QueryGroup extends QueryPart { sqlTreeWalker.visitQueryGroup( this ); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Expression - - @Override - public JdbcMappingContainer getExpressionType() { - return queryParts.get( 0 ).getExpressionType(); - } - - @Override - public void applySqlSelections(DomainResultCreationState creationState) { - queryParts.get( 0 ).applySqlSelections( creationState ); - } - - @Override - public DomainResult createDomainResult(String resultVariable, DomainResultCreationState creationState) { - return queryParts.get( 0 ).createDomainResult( resultVariable, creationState ); - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java index 42c29c21ca..e006275a0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java @@ -12,7 +12,6 @@ import java.util.function.Consumer; import java.util.function.Function; import org.hibernate.query.sqm.FetchClauseType; -import org.hibernate.query.sqm.sql.internal.DomainResultProducer; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; @@ -20,7 +19,7 @@ import org.hibernate.sql.ast.tree.expression.Expression; /** * @author Christian Beikov */ -public abstract class QueryPart implements SqlAstNode, Expression, DomainResultProducer { +public abstract class QueryPart implements SqlAstNode { private final boolean isRoot; private boolean hasPositionalSortItem; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java index c814393a47..18c99dde13 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java @@ -11,28 +11,18 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.JdbcMappingContainer; -import org.hibernate.query.sqm.sql.internal.DomainResultProducer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlAstTreeHelper; -import org.hibernate.sql.ast.spi.SqlExpressionResolver; -import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.predicate.PredicateContainer; -import org.hibernate.sql.results.graph.DomainResult; -import org.hibernate.sql.results.graph.DomainResultCreationState; -import org.hibernate.sql.results.graph.basic.BasicResult; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.spi.TypeConfiguration; /** * @author Steve Ebersole */ -public class QuerySpec extends QueryPart implements SqlAstNode, PredicateContainer, Expression, DomainResultProducer { +public class QuerySpec extends QueryPart implements SqlAstNode, PredicateContainer { private final FromClause fromClause; private final SelectClause selectClause; @@ -129,67 +119,4 @@ public class QuerySpec extends QueryPart implements SqlAstNode, PredicateContain sqlTreeWalker.visitQuerySpec( this ); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Expression - - @Override - public JdbcMappingContainer getExpressionType() { - final List sqlSelections = selectClause.getSqlSelections(); - switch ( sqlSelections.size() ) { - case 1: - return sqlSelections.get( 0 ).getExpressionType(); - default: - // todo (6.0): At some point we should create an ArrayTupleType and return that - case 0: - return null; - } - } - - @Override - public void applySqlSelections(DomainResultCreationState creationState) { - TypeConfiguration typeConfiguration = creationState.getSqlAstCreationState().getCreationContext().getMappingMetamodel().getTypeConfiguration(); - for ( SqlSelection sqlSelection : selectClause.getSqlSelections() ) { - sqlSelection.getExpressionType().forEachJdbcType( - (index, jdbcMapping) -> { - creationState.getSqlAstCreationState().getSqlExpressionResolver().resolveSqlSelection( - this, - jdbcMapping.getJdbcJavaType(), - null, - typeConfiguration - ); - } - ); - } - } - - @Override - public DomainResult createDomainResult(String resultVariable, DomainResultCreationState creationState) { - final TypeConfiguration typeConfiguration = creationState.getSqlAstCreationState() - .getCreationContext() - .getMappingMetamodel() - .getTypeConfiguration(); - final SqlExpressionResolver sqlExpressionResolver = creationState.getSqlAstCreationState().getSqlExpressionResolver(); - if ( selectClause.getSqlSelections().size() == 1 ) { - final SqlSelection first = selectClause.getSqlSelections().get( 0 ); - final JdbcMapping jdbcMapping = first.getExpressionType() - .getJdbcMappings() - .get( 0 ); - - final SqlSelection sqlSelection = sqlExpressionResolver.resolveSqlSelection( - this, - jdbcMapping.getJdbcJavaType(), - null, - typeConfiguration - ); - - return new BasicResult<>( - sqlSelection.getValuesArrayPosition(), - resultVariable, - jdbcMapping - ); - } - else { - throw new UnsupportedOperationException("Domain result for non-scalar subquery shouldn't be created"); - } - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java index 3ea63a9dce..55244e02db 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SelectStatement.java @@ -11,16 +11,28 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.sqm.sql.internal.DomainResultProducer; import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.AbstractStatement; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.basic.BasicResult; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.spi.TypeConfiguration; /** * @author Steve Ebersole */ -public class SelectStatement extends AbstractStatement { +public class SelectStatement extends AbstractStatement implements SqlAstNode, Expression, DomainResultProducer { private final QueryPart queryPart; private final List> domainResults; @@ -29,25 +41,23 @@ public class SelectStatement extends AbstractStatement { } public SelectStatement(QueryPart queryPart, List> domainResults) { - this( false, new LinkedHashMap<>(), queryPart, domainResults ); + this( new LinkedHashMap<>(), queryPart, domainResults ); } public SelectStatement( CteContainer cteContainer, QueryPart queryPart, List> domainResults) { - this( cteContainer.isWithRecursive(), cteContainer.getCteStatements(), queryPart, domainResults ); + this( cteContainer.getCteStatements(), queryPart, domainResults ); } public SelectStatement( - boolean withRecursive, Map cteStatements, QueryPart queryPart, List> domainResults) { super( cteStatements ); this.queryPart = queryPart; this.domainResults = domainResults; - setWithRecursive( withRecursive ); } public QuerySpec getQuerySpec() { @@ -66,4 +76,71 @@ public class SelectStatement extends AbstractStatement { public void accept(SqlAstWalker walker) { walker.visitSelectStatement( this ); } + + @Override + public DomainResult createDomainResult(String resultVariable, DomainResultCreationState creationState) { + final SelectClause selectClause = queryPart.getFirstQuerySpec().getSelectClause(); + final TypeConfiguration typeConfiguration = creationState.getSqlAstCreationState() + .getCreationContext() + .getMappingMetamodel() + .getTypeConfiguration(); + final SqlExpressionResolver sqlExpressionResolver = creationState.getSqlAstCreationState().getSqlExpressionResolver(); + if ( selectClause.getSqlSelections().size() == 1 ) { + final SqlSelection first = selectClause.getSqlSelections().get( 0 ); + final JdbcMapping jdbcMapping = first.getExpressionType() + .getJdbcMappings() + .get( 0 ); + + final SqlSelection sqlSelection = sqlExpressionResolver.resolveSqlSelection( + this, + jdbcMapping.getJdbcJavaType(), + null, + typeConfiguration + ); + + return new BasicResult<>( + sqlSelection.getValuesArrayPosition(), + resultVariable, + jdbcMapping + ); + } + else { + throw new UnsupportedOperationException("Domain result for non-scalar subquery shouldn't be created"); + } + } + + @Override + public void applySqlSelections(DomainResultCreationState creationState) { + final SelectClause selectClause = queryPart.getFirstQuerySpec().getSelectClause(); + final TypeConfiguration typeConfiguration = creationState.getSqlAstCreationState() + .getCreationContext() + .getMappingMetamodel() + .getTypeConfiguration(); + for ( SqlSelection sqlSelection : selectClause.getSqlSelections() ) { + sqlSelection.getExpressionType().forEachJdbcType( + (index, jdbcMapping) -> { + creationState.getSqlAstCreationState().getSqlExpressionResolver().resolveSqlSelection( + this, + jdbcMapping.getJdbcJavaType(), + null, + typeConfiguration + ); + } + ); + } + } + + @Override + public JdbcMappingContainer getExpressionType() { + final SelectClause selectClause = queryPart.getFirstQuerySpec().getSelectClause(); + final List sqlSelections = selectClause.getSqlSelections(); + switch ( sqlSelections.size() ) { + case 1: + return sqlSelections.get( 0 ).getExpressionType(); + default: + // todo (6.0): At some point we should create an ArrayTupleType and return that + case 0: + return null; + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/update/UpdateStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/update/UpdateStatement.java index 973b05414d..90643f2e62 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/update/UpdateStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/update/UpdateStatement.java @@ -53,7 +53,6 @@ public class UpdateStatement extends AbstractMutationStatement { Predicate restriction, List returningColumns) { this( - cteContainer.isWithRecursive(), cteContainer.getCteStatements(), targetTable, assignments, @@ -63,7 +62,6 @@ public class UpdateStatement extends AbstractMutationStatement { } public UpdateStatement( - boolean withRecursive, Map cteStatements, NamedTableReference targetTable, List assignments, @@ -72,7 +70,6 @@ public class UpdateStatement extends AbstractMutationStatement { super( cteStatements, targetTable, returningColumns ); this.assignments = assignments; this.restriction = restriction; - setWithRecursive( withRecursive ); } public List getAssignments() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java new file mode 100644 index 0000000000..c628b9e595 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java @@ -0,0 +1,600 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import java.time.LocalDate; +import java.util.List; +import java.util.function.Consumer; + +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.dialect.TiDBDialect; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaEntityJoin; +import org.hibernate.query.criteria.JpaJoin; +import org.hibernate.query.criteria.JpaParameterExpression; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.criteria.JpaSubQuery; +import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; +import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind; + +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.contacts.Address; +import org.hibernate.testing.orm.domain.contacts.Contact; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.CONTACTS) +@SessionFactory +public class CteTests { + + @Test + public void testBasic(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cte = cb.createTupleQuery(); + final JpaRoot cteRoot = cte.from( Contact.class ); + cte.multiselect( cteRoot.get( "id" ).alias( "id" ), cteRoot.get( "name" ).alias( "name" ) ); + cte.where( cb.equal( cteRoot.get( "gender" ), Contact.Gender.FEMALE ) ); + + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaCteCriteria femaleContacts = cq.with( cte ); + + final JpaRoot root = cq.from( femaleContacts ); + + cq.multiselect( root.get( "id" ), root.get( "name" ) ); + cq.orderBy( cb.asc( root.get( "id" ) ) ); + + final QueryImplementor query = session.createQuery( + "with femaleContacts as (" + + "select c.id id, c.name name from Contact c where c.gender = FEMALE" + + ")" + + "select c.id, c.name from femaleContacts c order by c.id", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 2, list.size() ); + assertEquals( "Jane", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 1 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + public void testNested(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery allContactsQuery = cb.createTupleQuery(); + { + final JpaRoot allContactsRoot = allContactsQuery.from( Contact.class ); + allContactsQuery.multiselect( + allContactsRoot.get( "id" ).alias( "id" ), + allContactsRoot.get( "name" ).alias( "name" ), + allContactsRoot.get( "gender" ).alias( "gender" ) + ); + } + + final JpaCriteriaQuery femaleContactsQuery = cb.createTupleQuery(); + { + final JpaCteCriteria allContacts = femaleContactsQuery.with( allContactsQuery ); + final JpaRoot femaleContactsRoot = femaleContactsQuery.from( allContacts ); + femaleContactsQuery.multiselect( + femaleContactsRoot.get( "id" ).alias( "id" ), + femaleContactsRoot.get( "name" ).alias( "name" ) + ); + femaleContactsQuery.where( cb.equal( + femaleContactsRoot.get( "gender" ), + Contact.Gender.FEMALE + ) ); + } + + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaCteCriteria femaleContacts = cq.with( femaleContactsQuery ); + + final JpaRoot root = cq.from( femaleContacts ); + + cq.multiselect( root.get( "id" ), root.get( "name" ) ); + cq.orderBy( cb.asc( root.get( "id" ) ) ); + + final QueryImplementor query = session.createQuery( + "with femaleContacts as (" + + "with allContacts as (" + + "select c.id id, c.name name, c.gender gender from Contact c" + + ")" + + "select c.id id, c.name name from allContacts c where c.gender = FEMALE" + + ")" + + "select c.id, c.name from femaleContacts c order by c.id", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 2, list.size() ); + assertEquals( "Jane", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 1 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + @SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "The emulation of CTEs in subqueries results in correlation in nesting level 2, which is not possible with Sybase ASE") + @SkipForDialect(dialectClass = TiDBDialect.class, reason = "The TiDB version on CI seems to be buggy") + public void testSubquery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + cq.multiselect( root.get( "id" ), root.get( "name" ) ); + cq.orderBy( cb.asc( root.get( "id" ) ) ); + + final JpaSubQuery subquery = cq.subquery( String.class ); + + final JpaSubQuery addressesQuery = cq.subquery( Tuple.class ); + { + final JpaJoin addresses = addressesQuery.correlate( root ).join( "addresses" ); + addressesQuery.multiselect( + addresses.get( "line1" ).alias( "line" ) + ); + } + final JpaCteCriteria addresses = subquery.with( addressesQuery ); + final JpaRoot addressesRoot = subquery.from( addresses ); + subquery.select( addressesRoot.get( "line" ) ); + cq.where( cb.exists( subquery ) ); + + final QueryImplementor query = session.createQuery( + "select c.id, c.name from Contact c where exists (" + + "with addresses as (" + + "select a.line1 line from c.addresses a" + + ")" + + "select a.line from addresses a" + + ") order by c.id", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + public void testMaterialized(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cte = cb.createTupleQuery(); + final JpaRoot cteRoot = cte.from( Contact.class ); + cte.multiselect( cteRoot.get( "id" ).alias( "id" ), cteRoot.get( "name" ).alias( "name" ) ); + cte.where( cb.equal( cteRoot.get( "gender" ), Contact.Gender.FEMALE ) ); + + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaCteCriteria femaleContacts = cq.with( cte ); + femaleContacts.setMaterialization( CteMaterialization.MATERIALIZED ); + + final JpaRoot root = cq.from( femaleContacts ); + + cq.multiselect( root.get( "id" ), root.get( "name" ) ); + cq.orderBy( cb.asc( root.get( "id" ) ) ); + + final QueryImplementor query = session.createQuery( + "with femaleContacts as materialized (" + + "select c.id id, c.name name from Contact c where c.gender = FEMALE" + + ")" + + "select c.id, c.name from femaleContacts c order by c.id", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 2, list.size() ); + assertEquals( "Jane", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 1 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsRecursiveCtes.class) + public void testSimpleRecursive(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaParameterExpression param = cb.parameter( Integer.class ); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + + final JpaCriteriaQuery baseQuery = cb.createTupleQuery(); + final JpaRoot baseRoot = baseQuery.from( Contact.class ); + baseQuery.multiselect( baseRoot.get( "alternativeContact" ).alias( "alt" ) ); + baseQuery.where( cb.equal( baseRoot.get( "id" ), param ) ); + + final JpaCteCriteria alternativeContacts = cq.withRecursiveUnionAll( + baseQuery, + selfType -> { + final JpaCriteriaQuery recursiveQuery = cb.createTupleQuery(); + final JpaRoot recursiveRoot = recursiveQuery.from( selfType ); + recursiveQuery.multiselect( recursiveRoot.get( "alt" ).get( "alternativeContact" ).alias( "alt" ) ); + recursiveQuery.where( cb.notEqual( recursiveRoot.get( "alt" ).get( "alternativeContact" ).get( "id" ), param ) ); + return recursiveQuery; + } + ); + + final JpaRoot root = cq.from( alternativeContacts ); + final JpaJoin alt = root.join( "alt" ); + cq.multiselect( alt ); + cq.orderBy( cb.asc( alt.get( "id" ) ) ); + + final QueryImplementor query = session.createQuery( + "with alternativeContacts as (" + + "select c.alternativeContact alt from Contact c where c.id = :param " + + "union all " + + "select c.alt.alternativeContact alt from alternativeContacts c where c.alt.alternativeContact.id <> :param" + + ")" + + "select ac from alternativeContacts c join c.alt ac order by ac.id", + Tuple.class + ); + verifySame( + session.createQuery( cq ).setParameter( param, 1 ).getResultList(), + query.setParameter( "param", 1 ).getResultList(), + list -> { + assertEquals( 2, list.size() ); + assertEquals( "Jane", list.get( 0 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "Granny", list.get( 1 ).get( 0, Contact.class ).getName().getFirst() ); + } + ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsRecursiveCtes.class) + public void testRecursiveCycleClause(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaParameterExpression param = cb.parameter( Integer.class ); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + + final JpaCriteriaQuery baseQuery = cb.createTupleQuery(); + final JpaRoot baseRoot = baseQuery.from( Contact.class ); + baseQuery.multiselect( baseRoot.get( "alternativeContact" ).alias( "alt" ) ); + baseQuery.where( cb.equal( baseRoot.get( "id" ), param ) ); + + final JpaCteCriteria alternativeContacts = cq.withRecursiveUnionAll( + baseQuery, + selfType -> { + final JpaCriteriaQuery recursiveQuery = cb.createTupleQuery(); + final JpaRoot recursiveRoot = recursiveQuery.from( selfType ); + recursiveQuery.multiselect( recursiveRoot.get( "alt" ).get( "alternativeContact" ).alias( "alt" ) ); + return recursiveQuery; + } + ); + alternativeContacts.cycle( + "isCycle", + true, + false, + alternativeContacts.getType().getAttribute( "alt" ) + ); + + final JpaRoot root = cq.from( alternativeContacts ); + final JpaJoin alt = root.join( "alt" ); + cq.multiselect( alt, root.get( "isCycle" ) ); + cq.orderBy( cb.asc( alt.get( "id" ) ), cb.asc( root.get( "isCycle" ) ) ); + + final QueryImplementor query = session.createQuery( + "with alternativeContacts as (" + + "select c.alternativeContact alt from Contact c where c.id = :param " + + "union all " + + "select c.alt.alternativeContact alt from alternativeContacts c" + + ")" + + "cycle alt set isCycle to true default false " + + "select ac, c.isCycle from alternativeContacts c join c.alt ac order by ac.id, c.isCycle", + Tuple.class + ); + verifySame( + session.createQuery( cq ).setParameter( param, 1 ).getResultList(), + query.setParameter( "param", 1 ).getResultList(), + list -> { + assertEquals( 4, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "Jane", list.get( 1 ).get( 0, Contact.class ).getName().getFirst() ); + assertFalse( list.get( 1 ).get( 1, Boolean.class ) ); + assertEquals( "Jane", list.get( 2 ).get( 0, Contact.class ).getName().getFirst() ); + assertTrue( list.get( 2 ).get( 1, Boolean.class ) ); + assertEquals( "Granny", list.get( 3 ).get( 0, Contact.class ).getName().getFirst() ); + } + ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsRecursiveCtes.class) + public void testRecursiveCycleUsingClause(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaParameterExpression param = cb.parameter( Integer.class ); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + + final JpaCriteriaQuery baseQuery = cb.createTupleQuery(); + final JpaRoot baseRoot = baseQuery.from( Contact.class ); + baseQuery.multiselect( baseRoot.get( "alternativeContact" ).alias( "alt" ) ); + baseQuery.where( cb.equal( baseRoot.get( "id" ), param ) ); + + final JpaCteCriteria alternativeContacts = cq.withRecursiveUnionAll( + baseQuery, + selfType -> { + final JpaCriteriaQuery recursiveQuery = cb.createTupleQuery(); + final JpaRoot recursiveRoot = recursiveQuery.from( selfType ); + recursiveQuery.multiselect( recursiveRoot.get( "alt" ).get( "alternativeContact" ).alias( "alt" ) ); + return recursiveQuery; + } + ); + alternativeContacts.cycleUsing( + "isCycle", + "path", + true, + false, + alternativeContacts.getType().getAttribute( "alt" ) + ); + + final JpaRoot root = cq.from( alternativeContacts ); + final JpaJoin alt = root.join( "alt" ); + cq.multiselect( alt, root.get( "isCycle" ) ); + cq.orderBy( cb.asc( alt.get( "id" ) ), cb.asc( root.get( "isCycle" ) ) ); + + final QueryImplementor query = session.createQuery( + "with alternativeContacts as (" + + "select c.alternativeContact alt from Contact c where c.id = :param " + + "union all " + + "select c.alt.alternativeContact alt from alternativeContacts c" + + ")" + + "cycle alt set isCycle to true default false using path " + + "select ac, c.isCycle from alternativeContacts c join c.alt ac order by ac.id, c.isCycle", + Tuple.class + ); + verifySame( + session.createQuery( cq ).setParameter( param, 1 ).getResultList(), + query.setParameter( "param", 1 ).getResultList(), + list -> { + assertEquals( 4, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "Jane", list.get( 1 ).get( 0, Contact.class ).getName().getFirst() ); + assertFalse( list.get( 1 ).get( 1, Boolean.class ) ); + assertEquals( "Jane", list.get( 2 ).get( 0, Contact.class ).getName().getFirst() ); + assertTrue( list.get( 2 ).get( 1, Boolean.class ) ); + assertEquals( "Granny", list.get( 3 ).get( 0, Contact.class ).getName().getFirst() ); + } + ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsRecursiveCtes.class) + public void testRecursiveSearchClause(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + //noinspection unchecked + final JpaParameterExpression> param = cb.parameter( (Class>) (Class) List.class ); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + + final JpaCriteriaQuery baseQuery = cb.createTupleQuery(); + final JpaRoot baseRoot = baseQuery.from( Contact.class ); + baseQuery.multiselect( + baseRoot.get( "id" ).alias( "id" ), + baseRoot.get( "alternativeContact" ).get( "id" ).alias( "altId" ), + cb.literal( 1 ).alias( "depth" ) + ); + baseQuery.where( cb.in( baseRoot.get( "id" ), param ) ); + + final JpaCteCriteria alternativeContacts = cq.withRecursiveUnionAll( + baseQuery, + selfType -> { + final JpaCriteriaQuery recursiveQuery = cb.createTupleQuery(); + final JpaRoot recursiveRoot = recursiveQuery.from( selfType ); + final JpaEntityJoin contact = recursiveRoot.join( Contact.class ); + contact.on( cb.equal( recursiveRoot.get( "altId" ), contact.get( "id" ) ) ); + recursiveQuery.multiselect( + contact.get( "id" ).alias( "id" ), + contact.get( "alternativeContact" ).get( "id" ).alias( "altId" ), + cb.sum( recursiveRoot.get( "depth" ), cb.literal( 1 ) ).alias( "depth" ) + ); + return recursiveQuery; + } + ); + alternativeContacts.search( + CteSearchClauseKind.BREADTH_FIRST, + "orderAttr", + cb.search( alternativeContacts.getType().getAttribute( "id" ) ) + ); + + final JpaRoot root = cq.from( alternativeContacts ); + final JpaEntityJoin alt = root.join( Contact.class ); + alt.on( cb.equal( root.get( "id" ), alt.get( "id" ) ) ); + cq.multiselect( alt ); + cq.orderBy( cb.asc( root.get( "orderAttr" ) ) ); + cq.fetch( 4 ); + + final QueryImplementor breadthFirstQuery = session.createQuery( + "with alternativeContacts as (" + + "select c.id id, c.alternativeContact.id altId, 1 depth from Contact c where c.id in :param " + + "union all " + + "select c.id id, c.alternativeContact.id altId, ac.depth + 1 depth from alternativeContacts ac join Contact c on ac.altId = c.id" + + ") search breadth first by id set orderAttr " + + "select c from alternativeContacts ac join Contact c on ac.id = c.id order by ac.orderAttr limit 4", + Tuple.class + ); + verifySame( + session.createQuery( cq ).setParameter( param, List.of( 4, 7 ) ).getResultList(), + breadthFirstQuery.setParameter( "param", List.of( 4, 7 ) ).getResultList(), + list -> { + assertEquals( 4, list.size() ); + assertEquals( "C4", list.get( 0 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "C7", list.get( 1 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "C5", list.get( 2 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "C8", list.get( 3 ).get( 0, Contact.class ).getName().getFirst() ); + } + ); + + final QueryImplementor depthFirstQuery = session.createQuery( + "with alternativeContacts as (" + + "select c.id id, c.alternativeContact.id altId, 1 depth from Contact c where c.id in :param " + + "union all " + + "select c.id id, c.alternativeContact.id altId, ac.depth + 1 depth from alternativeContacts ac join Contact c on ac.altId = c.id" + + ") search depth first by id set orderAttr " + + "select ac from alternativeContacts c join Contact ac on c.id = ac.id order by c.orderAttr limit 4", + Tuple.class + ); + alternativeContacts.search( + CteSearchClauseKind.DEPTH_FIRST, + "orderAttr", + cb.search( alternativeContacts.getType().getAttribute( "id" ) ) + ); + verifySame( + session.createQuery( cq ).setParameter( param, List.of( 4, 7 ) ).getResultList(), + depthFirstQuery.setParameter( "param", List.of( 4, 7 ) ).getResultList(), + list -> { + assertEquals( 4, list.size() ); + assertEquals( "C4", list.get( 0 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "C5", list.get( 1 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "C6", list.get( 2 ).get( 0, Contact.class ).getName().getFirst() ); + assertEquals( "C7", list.get( 3 ).get( 0, Contact.class ).getName().getFirst() ); + } + ); + } + ); + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Contact contact = new Contact( + 1, + new Contact.Name( "John", "Doe" ), + Contact.Gender.MALE, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact alternativeContact = new Contact( + 2, + new Contact.Name( "Jane", "Doe" ), + Contact.Gender.FEMALE, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact alternativeContact2 = new Contact( + 3, + new Contact.Name( "Granny", "Doe" ), + Contact.Gender.FEMALE, + LocalDate.of( 1970, 1, 1 ) + ); + alternativeContact.setAlternativeContact( alternativeContact2 ); + contact.setAlternativeContact( alternativeContact ); + contact.setAddresses( + List.of( + new Address( "Street 1", 1234 ), + new Address( "Street 2", 5678 ) + ) + ); + session.persist( alternativeContact2 ); + session.persist( alternativeContact ); + session.persist( contact ); + alternativeContact2.setAlternativeContact( contact ); + + final Contact c4 = new Contact( + 4, + new Contact.Name( "C4", "Doe" ), + Contact.Gender.OTHER, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact c5 = new Contact( + 5, + new Contact.Name( "C5", "Doe" ), + Contact.Gender.OTHER, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact c6 = new Contact( + 6, + new Contact.Name( "C6", "Doe" ), + Contact.Gender.OTHER, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact c7 = new Contact( + 7, + new Contact.Name( "C7", "Doe" ), + Contact.Gender.OTHER, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact c8 = new Contact( + 8, + new Contact.Name( "C8", "Doe" ), + Contact.Gender.OTHER, + LocalDate.of( 1970, 1, 1 ) + ); + c4.setAlternativeContact( c5 ); + c5.setAlternativeContact( c6 ); + c7.setAlternativeContact( c8 ); + + session.persist( c6 ); + session.persist( c5 ); + session.persist( c4 ); + session.persist( c8 ); + session.persist( c7 ); + } ); + } + + private void verifySame(T criteriaResult, T hqlResult, Consumer verifier) { + verifier.accept( criteriaResult ); + verifier.accept( hqlResult ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createMutationQuery( "update Contact set alternativeContact = null" ).executeUpdate(); + session.createMutationQuery( "delete Contact" ).executeUpdate(); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java index 27fa11d744..334d953a95 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java @@ -118,7 +118,7 @@ public class SubQueryInFromEmbeddedIdTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java index 872c0a39b2..027f54e678 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java @@ -117,7 +117,7 @@ public class SubQueryInFromIdClassTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id1" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneEmbeddedIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneEmbeddedIdTests.java index 689488d073..fcb74b8a12 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneEmbeddedIdTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneEmbeddedIdTests.java @@ -117,7 +117,7 @@ public class SubQueryInFromInverseOneEmbeddedIdTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneIdClassTests.java index 9ce7f09b58..d8a23fe884 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneIdClassTests.java @@ -117,7 +117,7 @@ public class SubQueryInFromInverseOneIdClassTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id1" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneTests.java index 9b96102370..f754a2c606 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromInverseOneTests.java @@ -114,7 +114,7 @@ public class SubQueryInFromInverseOneTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyEmbeddedIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyEmbeddedIdTests.java index 97cf7db4d7..28045cf701 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyEmbeddedIdTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyEmbeddedIdTests.java @@ -119,7 +119,7 @@ public class SubQueryInFromManyToManyEmbeddedIdTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyIdClassTests.java index 9d8f77bf06..794f095d58 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyIdClassTests.java @@ -120,7 +120,7 @@ public class SubQueryInFromManyToManyIdClassTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id1" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyTests.java index a112c98762..115dce5574 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromManyToManyTests.java @@ -117,7 +117,7 @@ public class SubQueryInFromManyToManyTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyEmbeddedIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyEmbeddedIdTests.java index 332a6672e6..173da2ff9f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyEmbeddedIdTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyEmbeddedIdTests.java @@ -118,7 +118,7 @@ public class SubQueryInFromOneToManyEmbeddedIdTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyIdClassTests.java index 23db425c0d..448b044972 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyIdClassTests.java @@ -119,7 +119,7 @@ public class SubQueryInFromOneToManyIdClassTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id1" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyTests.java index 1b00182b76..3a1320840d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromOneToManyTests.java @@ -116,7 +116,7 @@ public class SubQueryInFromOneToManyTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.orderBy( cb.asc( root.get( "id" ) ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java index 6c98403062..ebe01e7d23 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java @@ -315,7 +315,7 @@ public class SubQueryInFromTests { subquery.fetch( 1 ); final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); - final SqmAttributeJoin alt = a.join( "contact" ); + final Join alt = a.join( "contact" ); cq.multiselect( root.get( "name" ), alt.get( "name" ) ); cq.where( cb.equal( root.get( "id" ), 1 ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/CountExpressionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/CountExpressionTest.java index f5597beac6..7a5d25c2b6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/CountExpressionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/CountExpressionTest.java @@ -33,7 +33,8 @@ public class CountExpressionTest extends BaseCoreFunctionalTestCase { protected Class[] getAnnotatedClasses() { return new Class[] { Document.class, - Person.class + Person.class, + CountDistinctTestEntity.class }; } @@ -82,6 +83,7 @@ public class CountExpressionTest extends BaseCoreFunctionalTestCase { assertEquals(1, results.size()); Object[] tuple = (Object[]) results.get( 0 ); assertEquals(1, tuple[0]); + assertEquals(2L, tuple[1]); } ); } @@ -102,9 +104,28 @@ public class CountExpressionTest extends BaseCoreFunctionalTestCase { assertEquals(1, results.size()); Object[] tuple = (Object[]) results.get( 0 ); assertEquals(1, tuple[0]); + assertEquals(4L, tuple[1]); } ); } + @Test + @TestForIssue(jiraKey = "HHH-11042") + public void testCountDistinctTupleSanity() { + doInHibernate( this::sessionFactory, session -> { + // A simple concatenation of tuple arguments would produce a distinct count of 1 in this case + // This test checks if the chr(0) count tuple distinct emulation works correctly + session.persist( new CountDistinctTestEntity("10", "1") ); + session.persist( new CountDistinctTestEntity("1", "01") ); + List results = session.createQuery("SELECT count(distinct (t.x,t.y)) FROM CountDistinctTestEntity t", Long.class) + .getResultList(); + + assertEquals(1, results.size()); + assertEquals( 2L, results.get( 0 ).longValue() ); + } ); + } + + + @Entity(name = "Document") public static class Document { @@ -160,4 +181,37 @@ public class CountExpressionTest extends BaseCoreFunctionalTestCase { } } + @Entity(name = "CountDistinctTestEntity") + public static class CountDistinctTestEntity { + + private String x; + private String y; + + public CountDistinctTestEntity() { + } + + public CountDistinctTestEntity(String x, String y) { + this.x = x; + this.y = y; + } + + @Id + public String getX() { + return x; + } + + public void setX(String x) { + this.x = x; + } + + @Id + public String getY() { + return y; + } + + public void setY(String y) { + this.y = y; + } + } + } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java index 7fec1082e7..4a1f870dc3 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java @@ -303,4 +303,10 @@ abstract public class DialectChecks { return !( dialect instanceof TiDBDialect ); } } + + public static class SupportsRecursiveCtes implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.supportsRecursiveCTE(); + } + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 8922e6de0b..a93fa29b00 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -456,4 +456,10 @@ abstract public class DialectFeatureChecks { || dialect instanceof MariaDBDialect; } } + + public static class SupportsRecursiveCtes implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect.supportsRecursiveCTE(); + } + } }