diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java index 51ed63d104..75945266b1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java @@ -321,6 +321,8 @@ public class FirebirdDialect extends Dialect { integerType ); } + + CommonFunctionFactory.listagg_list( "varchar", queryEngine ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java index 49c806f9df..679d93b4cf 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java @@ -299,6 +299,11 @@ public class IngresDialect extends Dialect { .setInvariantType( stringType ) .register(); + // No idea since when this is supported + CommonFunctionFactory.listagg( null, queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteDialect.java index 04a9d35160..91126e7d9a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLiteDialect.java @@ -316,6 +316,7 @@ public class SQLiteDialect extends Dialect { .setParameterTypes(NUMERIC) .register(); } + CommonFunctionFactory.listagg_groupConcat( queryEngine ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java index d12c20c28a..c2e8945507 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java @@ -15,11 +15,13 @@ import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.SybaseDialect; import org.hibernate.dialect.TimeZoneSupport; +import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.TopLimitHandler; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.spi.QueryEngine; import org.hibernate.sql.ForUpdateFragment; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -75,6 +77,12 @@ public class SybaseAnywhereDialect extends SybaseDialect { } } + @Override + public void initializeFunctionRegistry(QueryEngine queryEngine) { + super.initializeFunctionRegistry( queryEngine ); + CommonFunctionFactory.listagg_list( "varchar", queryEngine ); + } + @Override public int getMaxVarcharLength() { return 32_767; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java index d665e4d29a..abeae191ef 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java @@ -284,6 +284,9 @@ public class TeradataDialect extends Dialect { CommonFunctionFactory.varPopSamp( queryEngine ); } + // No idea since when this is supported + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); } /** 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 9e31b531e6..a98c590ad3 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 @@ -152,6 +152,7 @@ BOTH : [bB] [oO] [tT] [hH]; CASE : [cC] [aA] [sS] [eE]; CAST : [cC] [aA] [sS] [tT]; COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE]; +COUNT : [cC] [oO] [uU] [nN] [tT]; CROSS : [cC] [rR] [oO] [sS] [sS]; CUBE : [cC] [uU] [bB] [eE]; CURRENT : [cC] [uU] [rR] [rR] [eE] [nN] [tT]; @@ -171,6 +172,7 @@ ELSE : [eE] [lL] [sS] [eE]; EMPTY : [eE] [mM] [pP] [tT] [yY]; END : [eE] [nN] [dD]; ENTRY : [eE] [nN] [tT] [rR] [yY]; +ERROR : [eE] [rR] [rR] [oO] [rR]; ESCAPE : [eE] [sS] [cC] [aA] [pP] [eE]; EVERY : [eE] [vV] [eE] [rR] [yY]; EXCEPT : [eE] [xX] [cC] [eE] [pP] [tT]; @@ -205,6 +207,7 @@ LIKE : [lL] [iI] [kK] [eE]; ILIKE : [iI] [lL] [iI] [kK] [eE]; LIMIT : [lL] [iI] [mM] [iI] [tT]; LIST : [lL] [iI] [sS] [tT]; +LISTAGG : [lL] [iI] [sS] [tT] [aA] [gG] [gG]; LOCAL : [lL] [oO] [cC] [aA] [lL]; 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]; @@ -235,6 +238,7 @@ ONLY : [oO] [nN] [lL] [yY]; OR : [oO] [rR]; ORDER : [oO] [rR] [dD] [eE] [rR]; OUTER : [oO] [uU] [tT] [eE] [rR]; +OVERFLOW : [oO] [vV] [eE] [rR] [fF] [lL] [oO] [wW]; OVERLAY : [oO] [vV] [eE] [rR] [lL] [aA] [yY]; PAD : [pP] [aA] [dD]; PERCENT : [pP] [eE] [rR] [cC] [eE] [nN] [tT]; @@ -261,6 +265,7 @@ TIMEZONE_MINUTE : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [mM] [iI] [nN] [u TRAILING : [tT] [rR] [aA] [iI] [lL] [iI] [nN] [gG]; TREAT : [tT] [rR] [eE] [aA] [tT]; TRIM : [tT] [rR] [iI] [mM]; +TRUNCATE : [tT] [rR] [uU] [nN] [cC] [aA] [tT] [eE]; TYPE : [tT] [yY] [pP] [eE]; UNION : [uU] [nN] [iI] [oO] [nN]; UPDATE : [uU] [pP] [dD] [aA] [tT] [eE]; @@ -270,6 +275,8 @@ WEEK : [wW] [eE] [eE] [kK]; WHEN : [wW] [hH] [eE] [nN]; WHERE : [wW] [hH] [eE] [rR] [eE]; WITH : [wW] [iI] [tT] [hH]; +WITHIN : [wW] [iI] [tT] [hH] [iI] [nN]; +WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT]; YEAR : [yY] [eE] [aA] [rR]; // case-insensitive true, false and null recognition (split vote :) 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 8f527a31f1..71744c4c7f 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 @@ -975,7 +975,7 @@ jpaNonstandardFunctionName * The function name, followed by a parenthesized list of comma-separated expressions */ genericFunction - : genericFunctionName LEFT_PAREN (genericFunctionArguments | ASTERISK)? RIGHT_PAREN filterClause? + : genericFunctionName LEFT_PAREN (genericFunctionArguments | ASTERISK)? RIGHT_PAREN withinGroupClause? filterClause? ; /** @@ -1045,6 +1045,7 @@ collectionFunctionMisuse aggregateFunction : everyFunction | anyFunction + | listaggFunction ; /** @@ -1065,6 +1066,27 @@ anyFunction | (ANY|SOME) (ELEMENTS|INDICES) LEFT_PAREN simplePath RIGHT_PAREN ; +/** + * The `listagg()` ordered set-aggregate function + */ +listaggFunction + : LISTAGG LEFT_PAREN DISTINCT? expressionOrPredicate COMMA expressionOrPredicate onOverflowClause? RIGHT_PAREN withinGroupClause? filterClause? + ; + +/** + * A `on overflow` clause: what to do when the text data type used for `listagg` overflows + */ +onOverflowClause + : ON OVERFLOW (ERROR | (TRUNCATE expression? (WITH|WITHOUT) COUNT)) + ; + +/** + * A 'within group' clause: defines the order in which the ordered set-aggregate function should work + */ +withinGroupClause + : WITHIN GROUP LEFT_PAREN orderByClause RIGHT_PAREN + ; + /** * A 'filter' clause: a restriction applied to an aggregate function */ @@ -1387,6 +1409,7 @@ identifier | CASE | CAST | COLLATE + | COUNT | CROSS | CUBE | CURRENT @@ -1406,6 +1429,7 @@ identifier | EMPTY | END | ENTRY + | ERROR | ESCAPE | EVERY | EXCEPT @@ -1441,6 +1465,7 @@ identifier | LIKE | LIMIT | LIST + | LISTAGG | LOCAL | LOCAL_DATE | LOCAL_DATETIME @@ -1472,6 +1497,7 @@ identifier | OR | ORDER | OUTER + | OVERFLOW | OVERLAY | PAD | PERCENT @@ -1498,6 +1524,7 @@ identifier | TRAILING | TREAT | TRIM + | TRUNCATE | TYPE | UNION | UPDATE @@ -1509,6 +1536,8 @@ identifier | WHEN | WHERE | WITH + | WITHIN + | WITHOUT | YEAR) { logUseOfReservedWordAsIdentifier( getCurrentToken() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java index 91ee57706d..20bb15923f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -307,6 +307,10 @@ public abstract class AbstractHANADialect extends Dialect { CommonFunctionFactory.currentUtcdatetimetimestamp( queryEngine ); CommonFunctionFactory.everyAny_sumCaseCase( queryEngine ); CommonFunctionFactory.bitLength_pattern( queryEngine, "length(to_binary(?1))*8" ); + + CommonFunctionFactory.listagg_stringAgg( "varchar", queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); } @Override 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 8e570c5bec..4a541d6ac3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -234,6 +234,9 @@ public class CockroachDialect extends Dialect { .setArgumentsValidator( CommonFunctionFactory.formatValidator() ) .setArgumentListSignature("(TEMPORAL datetime as STRING pattern)") .register(); + CommonFunctionFactory.listagg_stringAgg( "string", queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); } @Override 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 6d81e0a37c..1a7de86837 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -261,6 +261,14 @@ public class DB2Dialect extends Dialect { .setParameterTypes(FunctionParameterType.STRING, FunctionParameterType.STRING) .setArgumentListSignature("(STRING string, STRING pattern)") .register(); + + if ( getDB2Version().isSameOrAfter( 9, 5 ) ) { + CommonFunctionFactory.listagg( null, queryEngine ); + if ( getDB2Version().isSameOrAfter( 11, 1 ) ) { + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + } + } } @Override 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 6656bb428e..57da4447d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java @@ -6,6 +6,7 @@ */ package org.hibernate.dialect; +import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.DB2390IdentityColumnSupport; import org.hibernate.dialect.identity.DB2IdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupport; @@ -19,6 +20,7 @@ import org.hibernate.dialect.unique.DefaultUniqueDelegate; import org.hibernate.dialect.unique.UniqueDelegate; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.spi.QueryEngine; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; @@ -40,6 +42,16 @@ public class DB2iDialect extends DB2Dialect { registerKeywords( info ); } + @Override + public void initializeFunctionRegistry(QueryEngine queryEngine) { + super.initializeFunctionRegistry( queryEngine ); + if ( getVersion().isSameOrAfter( 7, 2 ) ) { + CommonFunctionFactory.listagg( null, queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + } + } + public DB2iDialect() { this( DatabaseVersion.make(7) ); } 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 83688c5dae..150c9fc539 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java @@ -9,6 +9,7 @@ package org.hibernate.dialect; import jakarta.persistence.TemporalType; +import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.DB2390IdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.pagination.FetchLimitHandler; @@ -19,6 +20,7 @@ import org.hibernate.dialect.sequence.NoSequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.sql.ast.SqlAstTranslator; @@ -51,6 +53,16 @@ public class DB2zDialect extends DB2Dialect { super(version); } + @Override + public void initializeFunctionRegistry(QueryEngine queryEngine) { + super.initializeFunctionRegistry( queryEngine ); + if ( getVersion().isSameOrAfter( 12 ) ) { + CommonFunctionFactory.listagg( null, queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + } + } + @Override protected String columnType(int jdbcTypeCode) { // See https://www.ibm.com/support/knowledgecenter/SSEPEK_10.0.0/wnew/src/tpc/db2z_10_timestamptimezone.html 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 17efa5ae55..8dd11b497c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -278,6 +278,16 @@ public class H2Dialect extends Dialect { CommonFunctionFactory.format_formatdatetime( queryEngine ); } CommonFunctionFactory.rownum( queryEngine ); + if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { + CommonFunctionFactory.listagg( null, queryEngine ); + if ( getVersion().isSameOrAfter( 2 ) ) { + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + } + } + else { + CommonFunctionFactory.listagg_groupConcat( queryEngine ); + } } @Override 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 5a7ad2094b..f82d839d1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -224,6 +224,7 @@ public class HSQLDialect extends Dialect { if ( getVersion().isSameOrAfter( 2, 2 ) ) { CommonFunctionFactory.rownum( queryEngine ); } + CommonFunctionFactory.listagg_groupConcat( queryEngine ); } @Override 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 f163775bfe..da95a2390b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -446,6 +446,7 @@ public class MySQLDialect extends Dialect { } queryEngine.getSqmFunctionRegistry().register( "field", new FieldFunction( queryEngine.getTypeConfiguration() ) ); + CommonFunctionFactory.listagg_groupConcat( queryEngine ); } @Override 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 d0587e4c27..2d71b44023 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -22,6 +22,7 @@ import org.hibernate.QueryTimeoutException; import org.hibernate.boot.model.TypeContributions; import org.hibernate.cfg.Environment; import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.ModeStatsModeEmulation; import org.hibernate.dialect.function.NvlCoalesceEmulation; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.identity.Oracle12cIdentityColumnSupport; @@ -188,6 +189,20 @@ public class OracleDialect extends Dialect { "instr(?2,?1,?3)", FunctionParameterType.STRING, FunctionParameterType.STRING, FunctionParameterType.INTEGER ).setArgumentListSignature("(pattern, string[, start])"); + // The within group clause became optional in 18 + if ( getVersion().isSameOrAfter( 18 ) ) { + CommonFunctionFactory.listagg( null, queryEngine ); + } + else { + CommonFunctionFactory.listagg( "within group (order by rownum)", queryEngine ); + } + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + // Oracle has a regular aggregate function named stats_mode + queryEngine.getSqmFunctionRegistry().register( + "mode", + new ModeStatsModeEmulation( queryEngine.getTypeConfiguration() ) + ); } @Override 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 a40394f203..bd86a5732b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -448,9 +448,13 @@ public class PostgreSQLDialect extends Dialect { CommonFunctionFactory.soundex( queryEngine ); //was introduced in Postgres 9 apparently CommonFunctionFactory.locate_positionSubstring( queryEngine ); + CommonFunctionFactory.listagg_stringAgg( "varchar", queryEngine ); if ( getVersion().isSameOrAfter( 9, 4 ) ) { CommonFunctionFactory.makeDateTimeTimestamp( queryEngine ); + // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); } } 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 f2272b3c27..c0167a6780 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -265,6 +265,11 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { .setParameterTypes(INTEGER) .register(); } + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); + if ( getVersion().isSameOrAfter( 14 ) ) { + CommonFunctionFactory.listagg_stringAggWithinGroup( "varchar(max)", queryEngine ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java index 6ae867a261..64e6d059c3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java @@ -427,6 +427,9 @@ public class SpannerDialect extends Dialect { .setArgumentsValidator( CommonFunctionFactory.formatValidator() ) .setArgumentListSignature("(TIMESTAMP datetime as STRING pattern)") .register(); + CommonFunctionFactory.listagg_stringAgg( "string", queryEngine ); + CommonFunctionFactory.inverseDistributionOrderedSetAggregates( queryEngine ); + CommonFunctionFactory.hypotheticalOrderedSetAggregates( queryEngine ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 6114ae91ed..8c9e97d115 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -1920,6 +1920,75 @@ public class CommonFunctionFactory { ); } + public static void listagg(String emptyWithinReplacement, QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + ListaggFunction.FUNCTION_NAME, + new ListaggFunction( emptyWithinReplacement, queryEngine.getTypeConfiguration() ) + ); + } + + public static void listagg_groupConcat(QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + ListaggGroupConcatEmulation.FUNCTION_NAME, + new ListaggGroupConcatEmulation( queryEngine.getTypeConfiguration() ) + ); + } + + public static void listagg_list(String stringType, QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + ListaggStringAggEmulation.FUNCTION_NAME, + new ListaggStringAggEmulation( "list", stringType, false, queryEngine.getTypeConfiguration() ) + ); + } + + public static void listagg_stringAgg(String stringType, QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + ListaggStringAggEmulation.FUNCTION_NAME, + new ListaggStringAggEmulation( "string_agg", stringType, false, queryEngine.getTypeConfiguration() ) + ); + } + + public static void listagg_stringAggWithinGroup(String stringType, QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + ListaggStringAggEmulation.FUNCTION_NAME, + new ListaggStringAggEmulation( "string_agg", stringType, true, queryEngine.getTypeConfiguration() ) + ); + } + + public static void inverseDistributionOrderedSetAggregates(QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + "mode", + new InverseDistributionFunction( "mode", null, queryEngine.getTypeConfiguration() ) + ); + queryEngine.getSqmFunctionRegistry().register( + "percentile_cont", + new InverseDistributionFunction( "percentile_cont", NUMERIC, queryEngine.getTypeConfiguration() ) + ); + queryEngine.getSqmFunctionRegistry().register( + "percentile_disc", + new InverseDistributionFunction( "percentile_disc", NUMERIC, queryEngine.getTypeConfiguration() ) + ); + } + + public static void hypotheticalOrderedSetAggregates(QueryEngine queryEngine) { + queryEngine.getSqmFunctionRegistry().register( + "rank", + new HypotheticalSetFunction( "rank", StandardBasicTypes.LONG, queryEngine.getTypeConfiguration() ) + ); + queryEngine.getSqmFunctionRegistry().register( + "dense_rank", + new HypotheticalSetFunction( "dense_rank", StandardBasicTypes.LONG, queryEngine.getTypeConfiguration() ) + ); + queryEngine.getSqmFunctionRegistry().register( + "percent_rank", + new HypotheticalSetFunction( "percent_rank", StandardBasicTypes.DOUBLE, queryEngine.getTypeConfiguration() ) + ); + queryEngine.getSqmFunctionRegistry().register( + "cume_dist", + new HypotheticalSetFunction( "cume_dist", StandardBasicTypes.DOUBLE, queryEngine.getTypeConfiguration() ) + ); + } + public static void math(QueryEngine queryEngine) { final BasicType integerType = queryEngine.getTypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.INTEGER ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetFunction.java new file mode 100644 index 0000000000..6622682a2e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetFunction.java @@ -0,0 +1,102 @@ +/* + * 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.Collections; +import java.util.List; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * @author Christian Beikov + */ +public class HypotheticalSetFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public HypotheticalSetFunction(String name, BasicTypeReference returnType, TypeConfiguration typeConfiguration) { + super( + name, + FunctionKind.ORDERED_SET_AGGREGATE, + null, + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( returnType ) + ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( getName() ); + sqlAppender.appendSql( '(' ); + if ( !sqlAstArguments.isEmpty() ) { + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + sqlAstArguments.get( 0 ).accept( translator ); + sqlAppender.appendSql( " else null end" ); + } + else { + sqlAstArguments.get( 0 ).accept( translator ); + } + for ( int i = 1; i < sqlAstArguments.size(); i++ ) { + sqlAppender.appendSql( ',' ); + sqlAstArguments.get( i ).accept( translator ); + } + } + else if ( caseWrapper ) { + throw new IllegalArgumentException( "Can't emulate filter clause for function [" + getName() + "] without arguments!" ); + } + sqlAppender.appendSql( ')' ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + } + if ( filter != null ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionFunction.java new file mode 100644 index 0000000000..df78a7ce22 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionFunction.java @@ -0,0 +1,177 @@ +/* + * 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.Collections; +import java.util.List; + +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SelfRenderingSqmAggregateFunction; +import org.hibernate.query.sqm.function.SelfRenderingSqmOrderedSetAggregateFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +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.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * @author Christian Beikov + */ +public class InverseDistributionFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public InverseDistributionFunction(String name, FunctionParameterType parameterType, TypeConfiguration typeConfiguration) { + super( + name, + FunctionKind.ORDERED_SET_AGGREGATE, + parameterType == null + ? StandardArgumentsValidators.exactly( 0 ) + : new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 1 ), parameterType ), + null + ); + } + + @Override + public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + return new SelfRenderingSqmOrderedSetAggregateFunction<>( + this, + this, + arguments, + filter, + withinGroupClause, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ) { + + @Override + protected ReturnableType resolveResultType(TypeConfiguration typeConfiguration) { + return (ReturnableType) withinGroupClause.getSortSpecifications().get( 0 ).getSortExpression() + .getExpressible(); + } + + @Override + protected MappingModelExpressible getMappingModelExpressible( + SqmToSqlAstConverter walker, + ReturnableType resultType) { + MappingModelExpressible mapping; + if ( resultType instanceof MappingModelExpressible) { + // here we have a BasicType, which can be cast + // directly to BasicValuedMapping + mapping = (MappingModelExpressible) resultType; + } + else { + // here we have something that is not a BasicType, + // and we have no way to get a BasicValuedMapping + // from it directly + final Expression expression = (Expression) withinGroupClause.getSortSpecifications().get( 0 ) + .getSortExpression() + .accept( walker ); + if ( expression.getExpressionType() instanceof BasicValuedMapping ) { + return (BasicValuedMapping) expression.getExpressionType(); + } + try { + final MappingMetamodelImplementor domainModel = walker.getCreationContext() + .getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + return domainModel.resolveMappingExpressible( + getNodeType(), + walker.getFromClauseAccess()::getTableGroup + ); + } + catch (Exception e) { + return null; // this works at least approximately + } + } + return mapping; + } + }; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( getName() ); + sqlAppender.appendSql( '(' ); + if ( !sqlAstArguments.isEmpty() ) { + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + sqlAstArguments.get( 0 ).accept( translator ); + sqlAppender.appendSql( " else null end" ); + } + else { + sqlAstArguments.get( 0 ).accept( translator ); + } + } + else if ( caseWrapper ) { + throw new IllegalArgumentException( "Can't emulate filter clause for function [" + getName() + "] without arguments!" ); + } + sqlAppender.appendSql( ')' ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + } + if ( filter != null ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggFunction.java new file mode 100644 index 0000000000..b47ddab9f9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggFunction.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.dialect.function; + +import java.util.Collections; +import java.util.List; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * @author Christian Beikov + */ +public class ListaggFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public static final String FUNCTION_NAME = "listagg"; + + private final String emptyWithinReplacement; + + public ListaggFunction(String emptyWithinReplacement, TypeConfiguration typeConfiguration) { + super( + FUNCTION_NAME, + FunctionKind.ORDERED_SET_AGGREGATE, + new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), STRING, STRING ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) + ) + ); + this.emptyWithinReplacement = emptyWithinReplacement; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "listagg(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + arg.accept( translator ); + sqlAppender.appendSql( " else null end" ); + } + else { + arg.accept( translator ); + } + if ( sqlAstArguments.size() != 1 ) { + sqlAppender.appendSql( ',' ); + sqlAstArguments.get( 1 ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + } + else if ( emptyWithinReplacement != null ) { + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( emptyWithinReplacement ); + } + if ( !caseWrapper && filter != null ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggGroupConcatEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggGroupConcatEmulation.java new file mode 100644 index 0000000000..8b9fd608a7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggGroupConcatEmulation.java @@ -0,0 +1,118 @@ +/* + * 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.Collections; +import java.util.List; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Overflow; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * @author Christian Beikov + */ +public class ListaggGroupConcatEmulation extends AbstractSqmSelfRenderingFunctionDescriptor { + + public static final String FUNCTION_NAME = "listagg"; + + public ListaggGroupConcatEmulation(TypeConfiguration typeConfiguration) { + super( + FUNCTION_NAME, + FunctionKind.ORDERED_SET_AGGREGATE, + new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), STRING, STRING ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) + ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "group_concat(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + arg.accept( translator ); + sqlAppender.appendSql( " else null end" ); + } + else { + arg.accept( translator ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + } + if ( sqlAstArguments.size() != 1 ) { + SqlAstNode separator = sqlAstArguments.get( 1 ); + // group_concat doesn't support the overflow clause, so we just omit it + if ( separator instanceof Overflow ) { + separator = ( (Overflow) separator ).getSeparatorExpression(); + } + sqlAppender.appendSql( " separator " ); + separator.accept( translator ); + } + sqlAppender.appendSql( ')' ); + if ( !caseWrapper && filter != null ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggStringAggEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggStringAggEmulation.java new file mode 100644 index 0000000000..98d3a554f8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/ListaggStringAggEmulation.java @@ -0,0 +1,156 @@ +/* + * 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.Collections; +import java.util.List; + +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.sqm.CastType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Overflow; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * @author Christian Beikov + */ +public class ListaggStringAggEmulation extends AbstractSqmSelfRenderingFunctionDescriptor { + + public static final String FUNCTION_NAME = "listagg"; + + private final String functionName; + private final String stringType; + private final boolean withinGroupClause; + + public ListaggStringAggEmulation( + String functionName, + String stringType, + boolean withinGroupClause, + TypeConfiguration typeConfiguration) { + super( + FUNCTION_NAME, + FunctionKind.ORDERED_SET_AGGREGATE, + new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), STRING, STRING ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) + ) + ); + this.functionName = functionName; + this.stringType = stringType; + this.withinGroupClause = withinGroupClause; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( functionName ); + sqlAppender.appendSql( '(' ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + renderAsString( sqlAppender, translator, arg ); + sqlAppender.appendSql( " else null end" ); + } + else { + renderAsString( sqlAppender, translator, arg ); + } + if ( sqlAstArguments.size() != 1 ) { + SqlAstNode separator = sqlAstArguments.get( 1 ); + // string_agg doesn't support the overflow clause, so we just omit it + if ( separator instanceof Overflow ) { + separator = ( (Overflow) separator ).getSeparatorExpression(); + } + sqlAppender.appendSql( ',' ); + separator.accept( translator ); + if ( !withinGroupClause && withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + } + } + sqlAppender.appendSql( ')' ); + if ( withinGroupClause && withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + } + if ( !caseWrapper && filter != null ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + + private void renderAsString(SqlAppender sqlAppender, SqlAstTranslator translator, Expression expression) { + final JdbcMapping sourceMapping = expression.getExpressionType().getJdbcMappings().get( 0 ); + // No need to cast if we already have a string + if ( sourceMapping.getCastType() == CastType.STRING ) { + expression.accept( translator ); + } + else { + sqlAppender.appendSql( "cast(" ); + expression.accept( translator ); + sqlAppender.appendSql( " as " ); + sqlAppender.appendSql( stringType ); + sqlAppender.appendSql( ')' ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/ModeStatsModeEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/ModeStatsModeEmulation.java new file mode 100644 index 0000000000..be6451b63d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/ModeStatsModeEmulation.java @@ -0,0 +1,63 @@ +/* + * 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 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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * @author Christian Beikov + */ +public class ModeStatsModeEmulation extends InverseDistributionFunction { + + public static final String FUNCTION_NAME = "mode"; + + public ModeStatsModeEmulation(TypeConfiguration typeConfiguration) { + super( + FUNCTION_NAME, + null, + typeConfiguration + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "stats_mode(" ); + if ( withinGroup == null || withinGroup.size() != 1 ) { + throw new IllegalArgumentException( "MODE function requires a WITHIN GROUP clause with exactly one order by item!" ); + } + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + withinGroup.get( 0 ).accept( translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + withinGroup.get( 0 ).accept( translator ); + sqlAppender.appendSql( ')' ); + if ( filter != null ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + } + +} 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 03167e2e2f..486afff35b 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 @@ -32,7 +32,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import jakarta.persistence.metamodel.SingularAttribute; import org.hibernate.NotYetImplementedFor6Exception; import org.hibernate.QueryException; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; @@ -46,7 +45,6 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.metamodel.CollectionClassification; import org.hibernate.metamodel.mapping.CollectionPart; -import org.hibernate.query.ReturnableType; import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; @@ -55,17 +53,9 @@ import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; -import org.hibernate.query.sqm.BinaryArithmeticOperator; -import org.hibernate.query.sqm.ComparisonOperator; -import org.hibernate.query.sqm.FetchClauseType; -import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.PathException; +import org.hibernate.query.ReturnableType; import org.hibernate.query.SemanticException; -import org.hibernate.query.sqm.SetOperator; -import org.hibernate.query.sqm.SortOrder; -import org.hibernate.query.sqm.TemporalUnit; -import org.hibernate.query.sqm.TrimSpec; -import org.hibernate.query.sqm.UnaryArithmeticOperator; import org.hibernate.query.hql.HqlLogging; import org.hibernate.query.hql.spi.DotIdentifierConsumer; import org.hibernate.query.hql.spi.SemanticPathPart; @@ -73,14 +63,23 @@ import org.hibernate.query.hql.spi.SqmCreationOptions; import org.hibernate.query.hql.spi.SqmCreationProcessingState; import org.hibernate.query.hql.spi.SqmCreationState; import org.hibernate.query.hql.spi.SqmPathRegistry; +import org.hibernate.query.sqm.BinaryArithmeticOperator; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.LiteralNumberFormatException; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.ParsingException; +import org.hibernate.query.sqm.SetOperator; +import org.hibernate.query.sqm.SortOrder; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.SqmTreeCreationLogger; import org.hibernate.query.sqm.StrictJpaComplianceViolation; +import org.hibernate.query.sqm.TemporalUnit; +import org.hibernate.query.sqm.TrimSpec; +import org.hibernate.query.sqm.UnaryArithmeticOperator; import org.hibernate.query.sqm.UnknownEntityException; import org.hibernate.query.sqm.function.FunctionKind; import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; @@ -99,10 +98,10 @@ import org.hibernate.query.sqm.tree.SqmTypedNode; 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.SqmMapEntryReference; -import org.hibernate.query.sqm.tree.domain.SqmMapJoin; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction; +import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference; +import org.hibernate.query.sqm.tree.domain.SqmMapJoin; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; @@ -124,6 +123,7 @@ import org.hibernate.query.sqm.tree.expression.SqmFormat; import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; @@ -185,6 +185,7 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.jboss.logging.Logger; +import jakarta.persistence.metamodel.SingularAttribute; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.ParseTree; @@ -195,7 +196,15 @@ import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import static org.hibernate.grammars.hql.HqlParser.*; +import static org.hibernate.grammars.hql.HqlParser.ELEMENTS; +import static org.hibernate.grammars.hql.HqlParser.EXCEPT; +import static org.hibernate.grammars.hql.HqlParser.IDENTIFIER; +import static org.hibernate.grammars.hql.HqlParser.INDICES; +import static org.hibernate.grammars.hql.HqlParser.INTERSECT; +import static org.hibernate.grammars.hql.HqlParser.ListaggFunctionContext; +import static org.hibernate.grammars.hql.HqlParser.OnOverflowClauseContext; +import static org.hibernate.grammars.hql.HqlParser.PLUS; +import static org.hibernate.grammars.hql.HqlParser.UNION; import static org.hibernate.query.sqm.TemporalUnit.DATE; import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_MONTH; import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_WEEK; @@ -3219,9 +3228,17 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem functionArguments = emptyList(); } + final SqmOrderByClause withinGroup = getWithinGroup( ctx ); final SqmPredicate filterExpression = getFilterExpression( ctx ); SqmFunctionDescriptor functionTemplate = getFunctionDescriptor( functionName ); if ( functionTemplate == null ) { + FunctionKind functionKind = FunctionKind.NORMAL; + if ( withinGroup != null ) { + functionKind = FunctionKind.ORDERED_SET_AGGREGATE; + } + else if ( filterExpression != null ) { + functionKind = FunctionKind.AGGREGATE; + } functionTemplate = new NamedSqmFunctionDescriptor( functionName, true, @@ -3230,32 +3247,118 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem resolveExpressibleTypeBasic( Object.class ) ), functionName, - filterExpression != null ? FunctionKind.AGGREGATE : FunctionKind.NORMAL, + functionKind, null, SqlAstNodeRenderingMode.DEFAULT ); } - if ( functionTemplate.getFunctionKind() == FunctionKind.AGGREGATE ) { - return functionTemplate.generateAggregateSqmExpression( - functionArguments, - filterExpression, - null, - creationContext.getQueryEngine(), - creationContext.getJpaMetamodel().getTypeConfiguration() + switch ( functionTemplate.getFunctionKind() ) { + case ORDERED_SET_AGGREGATE: + return functionTemplate.generateOrderedSetAggregateSqmExpression( + functionArguments, + filterExpression, + withinGroup, + null, + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ); + case AGGREGATE: + return functionTemplate.generateAggregateSqmExpression( + functionArguments, + filterExpression, + null, + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ); + default: + if ( filterExpression != null ) { + throw new ParsingException( "Illegal use of a FILTER clause for non-aggregate function: " + originalFunctionName ); + } + return functionTemplate.generateSqmExpression( + functionArguments, + null, + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ); + } + } + + @Override + public Object visitListaggFunction(ListaggFunctionContext ctx) { + if ( creationOptions.useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + "Encountered non-compliant non-standard function call [listagg], but strict JPA " + + "compliance was requested; use JPA's FUNCTION(functionName[,...]) " + + "syntax name instead", + StrictJpaComplianceViolation.Type.FUNCTION_CALL ); } + final SqmFunctionDescriptor functionTemplate = getFunctionDescriptor( "listagg" ); + if ( functionTemplate == null ) { + throw new SemanticException( + "The listagg function was not registered for the dialect!" + ); + } + final int argumentStartIndex; + final ParseTree thirdChild = ctx.getChild( 2 ); + final boolean distinct; + if ( thirdChild instanceof TerminalNode ) { + distinct = true; + argumentStartIndex = 3; + } else { - if ( filterExpression != null ) { - throw new ParsingException( "Illegal use of a FILTER clause for non-aggregate function: " + originalFunctionName ); - } - return functionTemplate.generateSqmExpression( - functionArguments, - null, - creationContext.getQueryEngine(), - creationContext.getJpaMetamodel().getTypeConfiguration() - ); + distinct = false; + argumentStartIndex = 2; } + final SqmExpression firstArgument = (SqmExpression) ctx.getChild( argumentStartIndex ).accept( this ); + final SqmExpression secondArgument = (SqmExpression) ctx.getChild( argumentStartIndex + 2 ).accept( this ); + final ParseTree overflowCtx = ctx.getChild( argumentStartIndex + 3 ); + final List> functionArguments = new ArrayList<>( 3 ); + if ( distinct ) { + functionArguments.add( new SqmDistinct<>( firstArgument, creationContext.getNodeBuilder() ) ); + } + else { + functionArguments.add( firstArgument ); + } + if ( overflowCtx instanceof OnOverflowClauseContext ) { + if ( overflowCtx.getChildCount() > 3 ) { + // ON OVERFLOW TRUNCATE + final TerminalNode countNode = (TerminalNode) overflowCtx.getChild( overflowCtx.getChildCount() - 2 ); + final boolean withCount = countNode.getSymbol().getType() == HqlParser.WITH; + final SqmExpression fillerExpression; + if ( overflowCtx.getChildCount() == 6 ) { + fillerExpression = (SqmExpression) overflowCtx.getChild( 3 ).accept( this ); + } + else { + // The SQL standard says the default is three periods `...` + fillerExpression = new SqmLiteral<>( + "...", + secondArgument.getNodeType(), + secondArgument.nodeBuilder() + ); + } + //noinspection unchecked,rawtypes + functionArguments.add( new SqmOverflow( secondArgument, fillerExpression, withCount ) ); + } + else { + // ON OVERFLOW ERROR + functionArguments.add( new SqmOverflow<>( secondArgument, null, false ) ); + } + } + else { + functionArguments.add( secondArgument ); + } + final SqmOrderByClause withinGroup = getWithinGroup( ctx ); + final SqmPredicate filterExpression = getFilterExpression( ctx ); + return functionTemplate.generateOrderedSetAggregateSqmExpression( + functionArguments, + filterExpression, + withinGroup, + null, + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ); } @Override @@ -3700,6 +3803,21 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem return (SqmSubQuery) subQuery; } + private SqmOrderByClause getWithinGroup(ParseTree functionCtx) { + HqlParser.WithinGroupClauseContext ctx = null; + for ( int i = functionCtx.getChildCount() - 2; i < functionCtx.getChildCount(); i++ ) { + final ParseTree child = functionCtx.getChild( i ); + if ( child instanceof HqlParser.WithinGroupClauseContext ) { + ctx = (HqlParser.WithinGroupClauseContext) child; + break; + } + } + if ( ctx != null ) { + return visitOrderByClause( (HqlParser.OrderByClauseContext) ctx.getChild( 3 ) ); + } + return null; + } + private SqmPredicate getFilterExpression(ParseTree functionCtx) { final ParseTree lastChild = functionCtx.getChild( functionCtx.getChildCount() - 1 ); if ( lastChild instanceof HqlParser.FilterClauseContext ) { 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 bfe04338da..6169794399 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 @@ -48,6 +48,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; import org.hibernate.query.sqm.tree.expression.SqmStar; @@ -235,6 +236,8 @@ public interface SemanticQueryWalker { T visitStar(SqmStar sqmStar); + T visitOverflow(SqmOverflow sqmOverflow); + T visitCoalesce(SqmCoalesce sqmCoalesce); T visitToDuration(SqmToDuration toDuration); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmFunctionDescriptor.java index a4e459802a..aa64f2445b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmFunctionDescriptor.java @@ -16,6 +16,7 @@ import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.query.sqm.tree.SqmTypedNode; import org.hibernate.query.sqm.tree.SqmVisitableNode; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.type.spi.TypeConfiguration; @@ -131,6 +132,26 @@ public abstract class AbstractSqmFunctionDescriptor implements SqmFunctionDescri ); } + @Override + public final SelfRenderingSqmFunction generateOrderedSetAggregateSqmExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + argumentsValidator.validate( arguments, getName(), queryEngine ); + + return generateSqmOrderedSetAggregateFunctionExpression( + arguments, + filter, + withinGroupClause, + impliedResultType, + queryEngine, + typeConfiguration + ); + } + /** * Return an SQM node or subtree representing an invocation of this function * with the given arguments. This method may be overridden in the case of @@ -165,5 +186,28 @@ public abstract class AbstractSqmFunctionDescriptor implements SqmFunctionDescri typeConfiguration ); } + + /** + * Return an SQM node or subtree representing an invocation of this ordered set-aggregate function + * with the given arguments. This method may be overridden in the case of + * function descriptors that wish to customize creation of the node. + * + * @param arguments the arguments of the function invocation + * @param impliedResultType the function return type as inferred from its usage + */ + protected SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + return (SelfRenderingSqmAggregateFunction) generateSqmExpression( + arguments, + impliedResultType, + queryEngine, + typeConfiguration + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingFunctionDescriptor.java index 02a2121f02..eeb855663c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingFunctionDescriptor.java @@ -12,10 +12,12 @@ import org.hibernate.query.sqm.produce.function.ArgumentsValidator; import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; import org.hibernate.query.sqm.tree.SqmTypedNode; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; 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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.type.spi.TypeConfiguration; import java.util.List; @@ -56,19 +58,36 @@ public abstract class AbstractSqmSelfRenderingFunctionDescriptor ReturnableType impliedResultType, QueryEngine queryEngine, TypeConfiguration typeConfiguration) { - if ( functionKind == FunctionKind.AGGREGATE ) { - return generateAggregateSqmExpression( arguments, null, impliedResultType, queryEngine, typeConfiguration ); - } - return new SelfRenderingSqmFunction<>( - this, - (sqlAppender, sqlAstArguments, walker) -> render(sqlAppender, sqlAstArguments, walker), - arguments, - impliedResultType, - getArgumentsValidator(), - getReturnTypeResolver(), - queryEngine.getCriteriaBuilder(), - getName() - ); + switch ( functionKind ) { + case ORDERED_SET_AGGREGATE: + return generateOrderedSetAggregateSqmExpression( + arguments, + null, + null, + impliedResultType, + queryEngine, + typeConfiguration + ); + case AGGREGATE: + return generateAggregateSqmExpression( + arguments, + null, + impliedResultType, + queryEngine, + typeConfiguration + ); + default: + return new SelfRenderingSqmFunction<>( + this, + (sqlAppender, sqlAstArguments, walker) -> render(sqlAppender, sqlAstArguments, walker), + arguments, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } } @Override @@ -94,6 +113,31 @@ public abstract class AbstractSqmSelfRenderingFunctionDescriptor ); } + @Override + public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + if ( functionKind != FunctionKind.ORDERED_SET_AGGREGATE ) { + throw new UnsupportedOperationException( "The function " + getName() + " is not an ordered set-aggregate function!" ); + } + return new SelfRenderingSqmOrderedSetAggregateFunction<>( + this, + this, + arguments, + filter, + withinGroupClause, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + /** * Must be overridden by subclasses */ @@ -109,4 +153,13 @@ public abstract class AbstractSqmSelfRenderingFunctionDescriptor SqlAstTranslator walker) { render( sqlAppender, sqlAstArguments, walker ); } + + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, walker ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java index 0ed02609a9..0e2d31f0bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java @@ -13,5 +13,6 @@ package org.hibernate.query.sqm.function; */ public enum FunctionKind { NORMAL, - AGGREGATE; + AGGREGATE, + ORDERED_SET_AGGREGATE; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionRenderingSupport.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionRenderingSupport.java index d5c7b636b3..f0049e7de4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionRenderingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionRenderingSupport.java @@ -10,6 +10,7 @@ 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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; import java.util.List; @@ -38,4 +39,14 @@ public interface FunctionRenderingSupport { // Ignore the filter by default. Subclasses will override this render( sqlAppender, sqlAstArguments, walker ); } + + default void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator walker) { + // Ignore the filter by default. Subclasses will override this + render( sqlAppender, sqlAstArguments, walker ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java index 6d55fe19ee..7106e3524b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java @@ -15,7 +15,9 @@ import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Distinct; import org.hibernate.sql.ast.tree.expression.Star; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import java.util.Collections; import java.util.List; import java.util.Locale; @@ -93,7 +95,15 @@ public class NamedSqmFunctionDescriptor SqlAppender sqlAppender, List sqlAstArguments, SqlAstTranslator translator) { - render( sqlAppender, sqlAstArguments, null, translator ); + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), translator ); + } + + public void render( + SqlAppender sqlAppender, + List args, + Predicate filter, + SqlAstTranslator translator) { + render( sqlAppender, args, filter, Collections.emptyList(), translator ); } @Override @@ -101,6 +111,7 @@ public class NamedSqmFunctionDescriptor SqlAppender sqlAppender, List sqlAstArguments, Predicate filter, + List withinGroup, SqlAstTranslator translator) { final boolean useParens = useParenthesesWhenNoArgs || !sqlAstArguments.isEmpty(); final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); @@ -137,6 +148,16 @@ public class NamedSqmFunctionDescriptor sqlAppender.appendSql( ")" ); } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " within group (order by" ); + translator.render( withinGroup.get( 0 ), argumentRenderingMode ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( SqlAppender.COMA_SEPARATOR_CHAR ); + translator.render( withinGroup.get( 0 ), argumentRenderingMode ); + } + sqlAppender.appendSql( ')' ); + } + if ( filter != null && !caseWrapper ) { sqlAppender.appendSql( " filter (where " ); filter.accept( translator ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/PatternBasedSqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/PatternBasedSqmFunctionDescriptor.java index 3a8970b8a2..d6f9741639 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/PatternBasedSqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/PatternBasedSqmFunctionDescriptor.java @@ -14,6 +14,7 @@ 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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; import java.util.List; @@ -77,10 +78,24 @@ public class PatternBasedSqmFunctionDescriptor } @Override - public void render(SqlAppender sqlAppender, List sqlAstArguments, Predicate filter, SqlAstTranslator walker) { + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator walker) { renderer.render( sqlAppender, sqlAstArguments, filter, walker ); } + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + SqlAstTranslator walker) { + renderer.render( sqlAppender, sqlAstArguments, filter, withinGroup, walker ); + } + @Override public String getArgumentListSignature() { return argumentListSignature == null ? super.getArgumentListSignature() : argumentListSignature; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingOrderedSetAggregateFunctionSqlAstExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingOrderedSetAggregateFunctionSqlAstExpression.java new file mode 100644 index 0000000000..bf80b999b4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingOrderedSetAggregateFunctionSqlAstExpression.java @@ -0,0 +1,56 @@ +/* + * 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.function; + +import java.util.List; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +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.OrderedSetAggregateFunctionExpression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; + +/** + * Representation of an aggregate function call in the SQL AST for impls that know how to + * render themselves. + * + * @author Christian Beikov + */ +public class SelfRenderingOrderedSetAggregateFunctionSqlAstExpression extends SelfRenderingAggregateFunctionSqlAstExpression + implements OrderedSetAggregateFunctionExpression { + + private final List withinGroup; + + public SelfRenderingOrderedSetAggregateFunctionSqlAstExpression( + String functionName, + FunctionRenderingSupport renderer, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType type, + JdbcMappingContainer expressible) { + super( functionName, renderer, sqlAstArguments, filter, type, expressible ); + this.withinGroup = withinGroup; + } + + @Override + public List getWithinGroup() { + return withinGroup; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + getRenderer().render( sqlAppender, getArguments(), getFilter(), withinGroup, walker ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java new file mode 100644 index 0000000000..6d71435524 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmOrderedSetAggregateFunction.java @@ -0,0 +1,175 @@ +/* + * 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.function; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmDistinct; +import org.hibernate.query.sqm.tree.expression.SqmOrderedSetAggregateFunction; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; +import org.hibernate.query.sqm.tree.select.SqmSortSpecification; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; + +/** + * @author Christian Beikov + */ +public class SelfRenderingSqmOrderedSetAggregateFunction extends SelfRenderingSqmAggregateFunction + implements SqmOrderedSetAggregateFunction { + + private final SqmOrderByClause withinGroup; + + public SelfRenderingSqmOrderedSetAggregateFunction( + SqmFunctionDescriptor descriptor, + FunctionRenderingSupport renderingSupport, + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroup, + ReturnableType impliedResultType, + ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderingSupport, + arguments, + filter, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + this.withinGroup = withinGroup; + } + + @Override + public SelfRenderingSqmOrderedSetAggregateFunction copy(SqmCopyContext context) { + final SelfRenderingSqmOrderedSetAggregateFunction existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + final SelfRenderingSqmOrderedSetAggregateFunction expression = context.registerCopy( + this, + new SelfRenderingSqmOrderedSetAggregateFunction<>( + getFunctionDescriptor(), + getRenderingSupport(), + arguments, + getFilter() == null ? null : getFilter().copy( context ), + withinGroup == null ? null : withinGroup.copy( context ), + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName() + ) + ); + copyTo( expression, context ); + return expression; + } + + @Override + public SelfRenderingFunctionSqlAstExpression convertToSqlAst(SqmToSqlAstConverter walker) { + final ReturnableType resultType = resolveResultType( + walker.getCreationContext().getMappingMetamodel().getTypeConfiguration() + ); + + List arguments = resolveSqlAstArguments( getArguments(), walker ); + ArgumentsValidator argumentsValidator = getArgumentsValidator(); + if ( argumentsValidator != null ) { + argumentsValidator.validateSqlTypes( arguments, getFunctionName() ); + } + List withinGroup; + if ( this.withinGroup == null ) { + withinGroup = Collections.emptyList(); + } + else { + walker.getCurrentClauseStack().push( Clause.ORDER ); + try { + final List sortSpecifications = this.withinGroup.getSortSpecifications(); + withinGroup = new ArrayList<>( sortSpecifications.size() ); + for ( SqmSortSpecification sortSpecification : sortSpecifications ) { + final SortSpecification specification = (SortSpecification) walker.visitSortSpecification( sortSpecification ); + if ( specification != null ) { + withinGroup.add( specification ); + } + } + } + finally { + walker.getCurrentClauseStack().pop(); + } + } + return new SelfRenderingOrderedSetAggregateFunctionSqlAstExpression( + getFunctionName(), + getRenderingSupport(), + resolveSqlAstArguments( getArguments(), walker ), + getFilter() == null ? null : (Predicate) getFilter().accept( walker ), + withinGroup, + resultType, + getMappingModelExpressible( walker, resultType ) + ); + } + + @Override + public SqmOrderByClause getWithinGroup() { + return withinGroup; + } + + @Override + public void appendHqlString(StringBuilder sb) { + final List> arguments = getArguments(); + sb.append( getFunctionName() ); + sb.append( '(' ); + int i = 1; + if ( arguments.get( 0 ) instanceof SqmDistinct ) { + ( (SqmSelectableNode) arguments.get( 0 ) ).appendHqlString( sb ); + sb.append( ' ' ); + ( (SqmSelectableNode) arguments.get( 1 ) ).appendHqlString( sb ); + i = 2; + } + for ( ; i < arguments.size(); i++ ) { + sb.append(", "); + ( (SqmSelectableNode) arguments.get( i ) ).appendHqlString( sb ); + } + + sb.append( ')' ); + if ( withinGroup != null ) { + sb.append( " within group (order by " ); + final List sortSpecifications = withinGroup.getSortSpecifications(); + sortSpecifications.get( 0 ).appendHqlString( sb ); + for ( int j = 1; j < sortSpecifications.size(); j++ ) { + sb.append( ", " ); + sortSpecifications.get( j ).appendHqlString( sb ); + } + sb.append( ')' ); + } + + if ( getFilter() != null ) { + sb.append( " filter (where " ); + getFilter().appendHqlString( sb ); + sb.append( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java index 274861e5c0..f6ce70f991 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java @@ -11,6 +11,7 @@ import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.produce.function.ArgumentsValidator; import org.hibernate.query.sqm.tree.SqmTypedNode; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; import org.hibernate.type.spi.TypeConfiguration; import java.util.List; @@ -60,6 +61,20 @@ public interface SqmFunctionDescriptor { throw new UnsupportedOperationException( "Not an aggregate function!" ); } + /** + * Like {@link #generateSqmExpression(List, ReturnableType, QueryEngine, TypeConfiguration)} + * but also accepts a filter predicate. This method is intended for ordered set-aggregate functions. + */ + default SelfRenderingSqmFunction generateOrderedSetAggregateSqmExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + throw new UnsupportedOperationException( "Not an ordered set-aggregate function!" ); + } + /** * Convenience for single argument */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java index 99135e43cd..fe17be4721 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java @@ -175,6 +175,19 @@ public class SqmFunctionRegistry { return namedAggregateDescriptorBuilder( name, name ); } + /** + * Get a builder for creating and registering a name-based ordered set-aggregate function descriptor + * using the passed name as both the registration key and underlying SQL + * function name + * + * @param name The function name (and registration key) + * + * @return The builder + */ + public NamedFunctionDescriptorBuilder namedOrderedSetAggregateDescriptorBuilder(String name) { + return namedOrderedSetAggregateDescriptorBuilder( name, name ); + } + /** * Get a builder for creating and registering a name-based function descriptor. * @@ -199,6 +212,18 @@ public class SqmFunctionRegistry { return new NamedFunctionDescriptorBuilder( this, registrationKey, FunctionKind.AGGREGATE, name ); } + /** + * Get a builder for creating and registering a name-based ordered set-aggregate function descriptor. + * + * @param registrationKey The name under which the descriptor will get registered + * @param name The underlying SQL function name to use + * + * @return The builder + */ + public NamedFunctionDescriptorBuilder namedOrderedSetAggregateDescriptorBuilder(String registrationKey, String name) { + return new NamedFunctionDescriptorBuilder( this, registrationKey, FunctionKind.ORDERED_SET_AGGREGATE, name ); + } + public NamedFunctionDescriptorBuilder noArgsBuilder(String name) { return noArgsBuilder( name, name ); } 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 b041545e8d..fb247756f5 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 @@ -53,6 +53,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; import org.hibernate.query.sqm.tree.expression.SqmStar; @@ -751,6 +752,11 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitOverflow(SqmOverflow sqmOverflow) { + return null; + } + @Override public Object visitDurationUnit(SqmDurationUnit durationUnit) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/PatternRenderer.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/PatternRenderer.java index 91011aa2d7..177b224dbc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/PatternRenderer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/PatternRenderer.java @@ -15,8 +15,10 @@ import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Distinct; import org.hibernate.sql.ast.tree.expression.Star; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -136,7 +138,7 @@ public class PatternRenderer { SqlAppender sqlAppender, List args, SqlAstTranslator translator) { - render( sqlAppender, args, null, translator ); + render( sqlAppender, args, null, Collections.emptyList(), translator ); } public void render( @@ -144,6 +146,15 @@ public class PatternRenderer { List args, Predicate filter, SqlAstTranslator translator) { + render( sqlAppender, args, filter, Collections.emptyList(), translator ); + } + + public void render( + SqlAppender sqlAppender, + List args, + Predicate filter, + List withinGroup, + SqlAstTranslator translator) { final int numberOfArguments = args.size(); final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); if ( numberOfArguments < maxParamIndex ) { @@ -193,6 +204,16 @@ public class PatternRenderer { } } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + sqlAppender.appendSql( " within group (order by" ); + translator.render( withinGroup.get( 0 ), argumentRenderingMode ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( SqlAppender.COMA_SEPARATOR_CHAR ); + translator.render( withinGroup.get( 0 ), argumentRenderingMode ); + } + sqlAppender.appendSql( ')' ); + } + if ( filter != null && !caseWrapper ) { sqlAppender.appendSql( " filter (where " ); filter.accept( translator ); 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 0da64a6398..7a244342c8 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 @@ -53,6 +53,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; import org.hibernate.query.sqm.tree.expression.SqmStar; @@ -625,6 +626,7 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker distinct) { + distinct.getExpression().accept( this ); return distinct; } @@ -633,7 +635,15 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmOverflow) { + sqmOverflow.getSeparatorExpression().accept( this ); + if ( sqmOverflow.getFillerExpression() != null ) { + sqmOverflow.getFillerExpression().accept( this ); + } + return sqmOverflow; + } +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // expressions 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 eb774af400..e44fc2108b 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 @@ -188,6 +188,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; @@ -283,6 +284,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcLiteral; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; @@ -4574,6 +4576,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base return new Distinct( (Expression) sqmDistinct.getExpression().accept( this ) ); } + @Override + public Object visitOverflow(SqmOverflow sqmOverflow) { + return new Overflow( + (Expression) sqmOverflow.getSeparatorExpression().accept( this ), + sqmOverflow.getFillerExpression() == null + ? null + : (Expression) sqmOverflow.getFillerExpression().accept( this ), + sqmOverflow.isWithCount() + ); + } + @Override public Object visitTrimSpecification(SqmTrimSpecification specification) { return new TrimSpecification( specification.getSpecification() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstProcessingStateImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstProcessingStateImpl.java index 50fbba8461..6e958b998f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstProcessingStateImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/SqlAstProcessingStateImpl.java @@ -108,8 +108,7 @@ public class SqlAstProcessingStateImpl protected Expression normalize(Expression expression) { final Clause currentClause = currentClauseAccess.get(); - if ( currentClause == Clause.ORDER - || currentClause == Clause.GROUP ) { + if ( sqlSelectionMap() != null && ( currentClause == Clause.ORDER || currentClause == Clause.GROUP ) ) { // see if this (Sql)Expression is used as a selection, and if so // wrap the (Sql)Expression in a special wrapper with access to both // the (Sql)Expression and the SqlSelection. diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmDistinct.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmDistinct.java index 1f3c3b2906..03a63630e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmDistinct.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmDistinct.java @@ -57,6 +57,7 @@ public class SqmDistinct extends AbstractSqmNode implements SqmTypedNode, @Override public void appendHqlString(StringBuilder sb) { - sb.append( "distinct" ); + sb.append( "distinct " ); + expression.appendHqlString( sb ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOrderedSetAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOrderedSetAggregateFunction.java new file mode 100644 index 0000000000..e3a2d4e89a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOrderedSetAggregateFunction.java @@ -0,0 +1,21 @@ +/* + * 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.expression; + +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; + +/** + * A SQM ordered set-aggregate function. + * + * @param The Java type of the expression + * + * @author Christian Beikov + */ +public interface SqmOrderedSetAggregateFunction extends SqmAggregateFunction { + + SqmOrderByClause getWithinGroup(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOverflow.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOverflow.java new file mode 100644 index 0000000000..99e25ef350 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOverflow.java @@ -0,0 +1,82 @@ +/* + * 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.expression; + +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.tree.SqmCopyContext; + +/** + * @author Christian Beikov + */ +public class SqmOverflow extends AbstractSqmExpression { + + private final SqmExpression separatorExpression; + private final SqmExpression fillerExpression; + private final boolean withCount; + + public SqmOverflow(SqmExpression separatorExpression, SqmExpression fillerExpression, boolean withCount) { + super( separatorExpression.getNodeType(), separatorExpression.nodeBuilder() ); + this.separatorExpression = separatorExpression; + this.fillerExpression = fillerExpression; + this.withCount = withCount; + } + + @Override + public SqmOverflow copy(SqmCopyContext context) { + final SqmOverflow existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmOverflow expression = context.registerCopy( + this, + new SqmOverflow<>( + separatorExpression.copy( context ), + fillerExpression == null ? null : fillerExpression.copy( context ), + withCount + ) + ); + copyTo( expression, context ); + return expression; + } + + public SqmExpression getSeparatorExpression() { + return separatorExpression; + } + + public SqmExpression getFillerExpression() { + return fillerExpression; + } + + public boolean isWithCount() { + return withCount; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitOverflow( this ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + separatorExpression.appendHqlString( sb ); + sb.append( " on overflow " ); + if ( fillerExpression == null ) { + sb.append( "error" ); + } + else { + sb.append( "truncate " ); + fillerExpression.appendHqlString( sb ); + if ( withCount ) { + sb.append( " with count" ); + } + else { + sb.append( " without count" ); + } + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java index 8ee6c19a7b..e842fcb233 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java @@ -30,6 +30,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcLiteral; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; @@ -122,6 +123,8 @@ public interface SqlAstWalker { void visitDistinct(Distinct distinct); + void visitOverflow(Overflow distinct); + void visitStar(Star star); void visitTrimSpecification(TrimSpecification trimSpecification); 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 05ec8e116b..fa66a90e43 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 @@ -102,6 +102,7 @@ import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.LiteralAsParameter; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; @@ -4225,6 +4226,25 @@ public abstract class AbstractSqlAstTranslator implemen distinct.getExpression().accept( this ); } + @Override + public void visitOverflow(Overflow overflow) { + overflow.getSeparatorExpression().accept( this ); + appendSql( " on overflow " ); + if ( overflow.getFillerExpression() == null ) { + appendSql( "error" ); + } + else { + appendSql( " truncate " ); + overflow.getFillerExpression().accept( this ); + if ( overflow.isWithCount() ) { + appendSql( " with count" ); + } + else { + appendSql( " without count" ); + } + } + } + @Override public void visitParameter(JdbcParameter jdbcParameter) { switch ( parameterRenderingMode ) { 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 ad092f9dbd..1539f9714b 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 @@ -35,6 +35,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcLiteral; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; @@ -325,6 +326,14 @@ public class AbstractSqlAstWalker implements SqlAstWalker { distinct.getExpression().accept( this ); } + @Override + public void visitOverflow(Overflow overflow) { + overflow.getSeparatorExpression().accept( this ); + if ( overflow.getFillerExpression() != null ) { + overflow.getFillerExpression().accept( this ); + } + } + @Override public void visitOffsetFetchClause(QueryPart querySpec) { if ( querySpec.getSortSpecifications() != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java index e3966e1fa8..e52ee28ddf 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java @@ -27,6 +27,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcLiteral; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; @@ -164,6 +165,10 @@ public class AggregateFunctionChecker extends AbstractSqlAstWalker { public void visitDistinct(Distinct distinct) { } + @Override + public void visitOverflow(Overflow overflow) { + } + @Override public void visitStar(Star star) { } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/OrderedSetAggregateFunctionExpression.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/OrderedSetAggregateFunctionExpression.java new file mode 100644 index 0000000000..c533808d9c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/OrderedSetAggregateFunctionExpression.java @@ -0,0 +1,21 @@ +/* + * 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.sql.ast.tree.expression; + +import java.util.List; + +import org.hibernate.sql.ast.tree.select.SortSpecification; + +/** + * Models an ordered set-aggregate function expression at the SQL AST level. + * + * @author Christian Beikov + */ +public interface OrderedSetAggregateFunctionExpression extends AggregateFunctionExpression { + + List getWithinGroup(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Overflow.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Overflow.java new file mode 100644 index 0000000000..58b29c35bc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Overflow.java @@ -0,0 +1,62 @@ +/* + * 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.sql.ast.tree.expression; + +import org.hibernate.mapping.IndexedConsumer; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.SqlExpressible; +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * @author Christian Beikov + */ +public class Overflow implements Expression, SqlExpressible, SqlAstNode { + private final Expression separatorExpression; + private final Expression fillerExpression; + private final boolean withCount; + + public Overflow(Expression separatorExpression, Expression fillerExpression, boolean withCount) { + this.separatorExpression = separatorExpression; + this.fillerExpression = fillerExpression; + this.withCount = withCount; + } + + public Expression getSeparatorExpression() { + return separatorExpression; + } + + public Expression getFillerExpression() { + return fillerExpression; + } + + public boolean isWithCount() { + return withCount; + } + + @Override + public JdbcMapping getJdbcMapping() { + return ( (SqlExpressible) separatorExpression ).getJdbcMapping(); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return separatorExpression.getExpressionType(); + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + sqlTreeWalker.visitOverflow( this ); + } + + @Override + public int forEachJdbcType(int offset, IndexedConsumer action) { + action.accept( offset, getJdbcMapping() ); + return getJdbcTypeCount(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java new file mode 100644 index 0000000000..7d9d4974e7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java @@ -0,0 +1,166 @@ +/* + * 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.hql; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.hibernate.dialect.AbstractHANADialect; +import org.hibernate.dialect.SQLServerDialect; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +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.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.TypedQuery; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Christian Beikov + */ +@ServiceRegistry +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +public class OrderedSetAggregateTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + Date now = new Date(); + + EntityOfBasics entity1 = new EntityOfBasics(); + entity1.setId( 1 ); + entity1.setTheString( "5" ); + entity1.setTheInt( 5 ); + entity1.setTheInteger( -1 ); + entity1.setTheDouble( 5.0 ); + entity1.setTheDate( now ); + entity1.setTheBoolean( true ); + em.persist( entity1 ); + + EntityOfBasics entity2 = new EntityOfBasics(); + entity2.setId( 2 ); + entity2.setTheString( "6" ); + entity2.setTheInt( 6 ); + entity2.setTheInteger( -2 ); + entity2.setTheDouble( 6.0 ); + entity2.setTheBoolean( true ); + em.persist( entity2 ); + + EntityOfBasics entity3 = new EntityOfBasics(); + entity3.setId( 3 ); + entity3.setTheString( "7" ); + entity3.setTheInt( 7 ); + entity3.setTheInteger( 3 ); + entity3.setTheDouble( 7.0 ); + entity3.setTheBoolean( false ); + entity3.setTheDate( new Date(now.getTime() + 200000L) ); + em.persist( entity3 ); + + EntityOfBasics entity4 = new EntityOfBasics(); + entity4.setId( 4 ); + entity4.setTheString( "13" ); + entity4.setTheInt( 13 ); + entity4.setTheInteger( 4 ); + entity4.setTheDouble( 13.0 ); + entity4.setTheBoolean( false ); + entity4.setTheDate( new Date(now.getTime() + 300000L) ); + em.persist( entity4 ); + + EntityOfBasics entity5 = new EntityOfBasics(); + entity5.setId( 5 ); + entity5.setTheString( "5" ); + entity5.setTheInt( 5 ); + entity5.setTheInteger( 5 ); + entity5.setTheDouble( 9.0 ); + entity5.setTheBoolean( false ); + em.persist( entity5 ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> session.createQuery( "delete from EntityOfBasics" ).executeUpdate() + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class) + public void testListaggWithoutOrder(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select listagg(eob.theString, ',') from EntityOfBasics eob", String.class ); + List elements = Arrays.asList( q.getSingleResult().split( "," ) ); + List expectedElements = List.of( "13", "5", "5", "6", "7" ); + elements.sort( String.CASE_INSENSITIVE_ORDER ); + + assertEquals( expectedElements, elements ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class) + public void testSimpleListagg(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select listagg(eob.theString, ',') within group (order by eob.id desc) from EntityOfBasics eob", String.class ); + assertEquals( "5,13,7,6,5", q.getSingleResult() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class) + public void testListaggWithFilter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select listagg(eob.theString, ',') within group (order by eob.id desc) filter (where eob.theInt < 10) from EntityOfBasics eob", String.class ); + assertEquals( "5,7,6,5", q.getSingleResult() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsInverseDistributionFunctions.class) + @SkipForDialect(dialectClass = SQLServerDialect.class, reason = "The function is only supported as window function and needs emulation") + public void testInverseDistribution(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select percentile_disc(0.5) within group (order by eob.theInt asc) from EntityOfBasics eob", Integer.class ); + assertEquals( 6, q.getSingleResult() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class) + @SkipForDialect(dialectClass = SQLServerDialect.class, reason = "The function is only supported as window function and needs emulation") + @SkipForDialect(dialectClass = AbstractHANADialect.class, matchSubTypes = true, reason = "The function is only supported as window function and needs emulation") + public void testHypotheticalSet(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select percent_rank(5) within group (order by eob.theInt asc) from EntityOfBasics eob", Double.class ); + assertEquals( 0.0D, q.getSingleResult() ); + } + ); + } +} 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 d37eb066af..0151dca486 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 @@ -11,12 +11,15 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SpannerDialect; import org.hibernate.dialect.TimeZoneSupport; import org.hibernate.dialect.TiDBDialect; import org.hibernate.query.sqm.FetchClauseType; @@ -380,4 +383,45 @@ abstract public class DialectFeatureChecks { return dialect.forceLobAsLastValue(); } } + + public static class SupportsStringAggregation implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect instanceof H2Dialect + || dialect instanceof HSQLDialect + || dialect instanceof MySQLDialect + || dialect instanceof PostgreSQLDialect + || dialect instanceof AbstractHANADialect + || dialect instanceof CockroachDialect + || dialect instanceof DB2Dialect + || dialect instanceof OracleDialect + || dialect instanceof SpannerDialect + || dialect instanceof SQLServerDialect; + } + } + + public static class SupportsInverseDistributionFunctions implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect instanceof H2Dialect && dialect.getVersion().isSameOrAfter( 2 ) + || dialect instanceof PostgreSQLDialect + || dialect instanceof AbstractHANADialect + || dialect instanceof CockroachDialect + || dialect instanceof DB2Dialect + || dialect instanceof OracleDialect + || dialect instanceof SpannerDialect + || dialect instanceof SQLServerDialect; + } + } + + public static class SupportsHypotheticalSetFunctions implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect instanceof H2Dialect && dialect.getVersion().isSameOrAfter( 2 ) + || dialect instanceof PostgreSQLDialect + || dialect instanceof AbstractHANADialect + || dialect instanceof CockroachDialect + || dialect instanceof DB2Dialect + || dialect instanceof OracleDialect + || dialect instanceof SpannerDialect + || dialect instanceof SQLServerDialect; + } + } }