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 8df4b098a157..22286f5d8e7d 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 @@ -308,19 +308,22 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { doubleType ); - if ( getVersion().isSameOrAfter( 4, 0 ) ) { - Arrays.asList( "md5", "sha1", "sha256", "sha512" ) - .forEach( hash -> functionRegistry.registerPattern( - hash, - "crypt_hash(?1 using " + hash + ")", - byteArrayType - ) ); - functionRegistry.registerAlternateKey( "sha", "sha1" ); - functionRegistry.registerPattern( - "crc32", - "hash(?1 using crc32)", - integerType - ); + if ( getVersion().isSameOrAfter( 3 ) ) { + functionFactory.windowFunctions(); + if ( getVersion().isSameOrAfter( 4, 0 ) ) { + Arrays.asList( "md5", "sha1", "sha256", "sha512" ) + .forEach( hash -> functionRegistry.registerPattern( + hash, + "crypt_hash(?1 using " + hash + ")", + byteArrayType + ) ); + functionRegistry.registerAlternateKey( "sha", "sha1" ); + functionRegistry.registerPattern( + "crc32", + "hash(?1 using crc32)", + integerType + ); + } } functionFactory.listagg_list( "varchar" ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java index 95fcc841ad3f..2a322cbf04b9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java @@ -185,6 +185,9 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { queryEngine.getSqmFunctionRegistry().register( "least", new CaseLeastGreatestEmulation( true ) ); queryEngine.getSqmFunctionRegistry().register( "greatest", new CaseLeastGreatestEmulation( false ) ); + if ( supportsWindowFunctions() ) { + functionFactory.windowFunctions(); + } } @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 94114721dac4..ed368470046a 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 @@ -301,6 +301,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { .register(); // No idea since when this is supported + functionFactory.windowFunctions(); functionFactory.listagg( null ); functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.hypotheticalOrderedSetAggregates(); 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 b849b4475438..52dde5cd769d 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 @@ -317,6 +317,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { .setParameterTypes(NUMERIC) .register(); } + functionFactory.windowFunctions(); functionFactory.listagg_groupConcat(); } 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 1c6de8700276..05c17745c12f 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 @@ -80,7 +80,11 @@ protected String columnType(int jdbcTypeCode) { @Override public void initializeFunctionRegistry(QueryEngine queryEngine) { super.initializeFunctionRegistry( queryEngine ); - new CommonFunctionFactory( queryEngine ).listagg_list( "varchar" ); + final CommonFunctionFactory functionFactory = new CommonFunctionFactory( queryEngine ); + functionFactory.listagg_list( "varchar" ); + if ( getVersion().isSameOrAfter( 12 ) ) { + functionFactory.windowFunctions(); + } } @Override 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 87c338e657e1..fd267c599c08 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 @@ -286,6 +286,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { } // No idea since when this is supported + functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.hypotheticalOrderedSetAggregates(); } 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 a98c590ad3e2..ba9b3ce48b6c 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 @@ -146,9 +146,9 @@ ANY : [aA] [nN] [yY]; AS : [aA] [sS]; ASC : [aA] [sS] [cC]; AVG : [aA] [vV] [gG]; -BY : [bB] [yY]; BETWEEN : [bB] [eE] [tT] [wW] [eE] [eE] [nN]; BOTH : [bB] [oO] [tT] [hH]; +BY : [bB] [yY]; CASE : [cC] [aA] [sS] [eE]; CAST : [cC] [aA] [sS] [tT]; COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE]; @@ -176,19 +176,24 @@ 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]; +EXCLUDE : [eE] [xX] [cC] [lL] [uU] [dD] [eE]; EXISTS : [eE] [xX] [iI] [sS] [tT] [sS]; EXTRACT : [eE] [xX] [tT] [rR] [aA] [cC] [tT]; FETCH : [fF] [eE] [tT] [cC] [hH]; FILTER : [fF] [iI] [lL] [tT] [eE] [rR]; FIRST : [fF] [iI] [rR] [sS] [tT]; -FROM : [fF] [rR] [oO] [mM]; +FOLLOWING : [fF] [oO] [lL] [lL] [oO] [wW] [iI] [nN] [gG]; FOR : [fF] [oO] [rR]; FORMAT : [fF] [oO] [rR] [mM] [aA] [tT]; +FROM : [fF] [rR] [oO] [mM]; FULL : [fF] [uU] [lL] [lL]; FUNCTION : [fF] [uU] [nN] [cC] [tT] [iI] [oO] [nN]; GROUP : [gG] [rR] [oO] [uU] [pP]; +GROUPS : [gG] [rR] [oO] [uU] [pP] [sS]; HAVING : [hH] [aA] [vV] [iI] [nN] [gG]; HOUR : [hH] [oO] [uU] [rR]; +IGNORE : [iI] [gG] [nN] [oO] [rR] [eE]; +ILIKE : [iI] [lL] [iI] [kK] [eE]; IN : [iI] [nN]; INDEX : [iI] [nN] [dD] [eE] [xX]; INDICES : [iI] [nN] [dD] [iI] [cC] [eE] [sS]; @@ -204,7 +209,6 @@ LAST : [lL] [aA] [sS] [tT]; LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG]; LEFT : [lL] [eE] [fF] [tT]; 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]; @@ -215,18 +219,19 @@ LOCAL_TIME : [lL] [oO] [cC] [aA] [lL] '_' [tT] [iI] [mM] [eE]; MAP : [mM] [aA] [pP]; MAX : [mM] [aA] [xX]; MAXELEMENT : [mM] [aA] [xX] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; -MIN : [mM] [iI] [nN]; MAXINDEX : [mM] [aA] [xX] [iI] [nN] [dD] [eE] [xX]; MEMBER : [mM] [eE] [mM] [bB] [eE] [rR]; MICROSECOND : [mM] [iI] [cC] [rR] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; MILLISECOND : [mM] [iI] [lL] [lL] [iI] [sS] [eE] [cC] [oO] [nN] [dD]; +MIN : [mM] [iI] [nN]; MINELEMENT : [mM] [iI] [nN] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX]; MINUTE : [mM] [iI] [nN] [uU] [tT] [eE]; MONTH : [mM] [oO] [nN] [tT] [hH]; NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; -NEXT : [nN] [eE] [xX] [tT]; NEW : [nN] [eE] [wW]; +NEXT : [nN] [eE] [xX] [tT]; +NO : [nN] [oO]; NOT : [nN] [oO] [tT]; NULLS : [nN] [uU] [lL] [lL] [sS]; OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT]; @@ -237,18 +242,24 @@ ON : [oO] [nN]; ONLY : [oO] [nN] [lL] [yY]; OR : [oO] [rR]; ORDER : [oO] [rR] [dD] [eE] [rR]; +OTHERS : [oO] [tT] [hH] [eE] [rR] [sS]; OUTER : [oO] [uU] [tT] [eE] [rR]; +OVER : [oO] [vV] [eE] [rR]; OVERFLOW : [oO] [vV] [eE] [rR] [fF] [lL] [oO] [wW]; OVERLAY : [oO] [vV] [eE] [rR] [lL] [aA] [yY]; PAD : [pP] [aA] [dD]; +PARTITION : [pP] [aA] [rR] [tT] [iI] [tT] [iI] [oO] [nN]; PERCENT : [pP] [eE] [rR] [cC] [eE] [nN] [tT]; PLACING : [pP] [lL] [aA] [cC] [iI] [nN] [gG]; POSITION : [pP] [oO] [sS] [iI] [tT] [iI] [oO] [nN]; +PRECEDING : [pP] [rR] [eE] [cC] [eE] [dD] [iI] [nN] [gG]; QUARTER : [qQ] [uU] [aA] [rR] [tT] [eE] [rR]; +RANGE : [rR] [aA] [nN] [gG] [eE]; +RESPECT : [rR] [eE] [sS] [pP] [eE] [cC] [tT]; RIGHT : [rR] [iI] [gG] [hH] [tT]; ROLLUP : [rR] [oO] [lL] [lL] [uU] [pP]; -ROWS : [rR] [oO] [wW] [sS]; ROW : [rR] [oO] [wW]; +ROWS : [rR] [oO] [wW] [sS]; SECOND : [sS] [eE] [cC] [oO] [nN] [dD]; SELECT : [sS] [eE] [lL] [eE] [cC] [tT]; SET : [sS] [eE] [tT]; @@ -267,6 +278,7 @@ 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]; +UNBOUNDED : [uU] [nN] [bB] [oO] [uU] [nN] [dD] [eE] [dD]; UNION : [uU] [nN] [iI] [oO] [nN]; UPDATE : [uU] [pP] [dD] [aA] [tT] [eE]; VALUE : [vV] [aA] [lL] [uU] [eE]; 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 9c322c2799e4..5cba51acdc91 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 withinGroupClause? filterClause? + : genericFunctionName LEFT_PAREN (genericFunctionArguments | ASTERISK)? RIGHT_PAREN nthSideClause? nullsClause? withinGroupClause? filterClause? overClause? ; /** @@ -1052,7 +1052,7 @@ aggregateFunction * The functions 'every()' and 'all()' are synonyms */ everyFunction - : (EVERY|ALL) LEFT_PAREN predicate RIGHT_PAREN filterClause? + : (EVERY|ALL) LEFT_PAREN predicate RIGHT_PAREN filterClause? overClause? | (EVERY|ALL) LEFT_PAREN subquery RIGHT_PAREN | (EVERY|ALL) (ELEMENTS|INDICES) LEFT_PAREN simplePath RIGHT_PAREN ; @@ -1061,7 +1061,7 @@ everyFunction * The functions 'any()' and 'some()' are synonyms */ anyFunction - : (ANY|SOME) LEFT_PAREN predicate RIGHT_PAREN filterClause? + : (ANY|SOME) LEFT_PAREN predicate RIGHT_PAREN filterClause? overClause? | (ANY|SOME) LEFT_PAREN subquery RIGHT_PAREN | (ANY|SOME) (ELEMENTS|INDICES) LEFT_PAREN simplePath RIGHT_PAREN ; @@ -1070,7 +1070,7 @@ anyFunction * The 'listagg()' ordered set-aggregate function */ listaggFunction - : LISTAGG LEFT_PAREN DISTINCT? expressionOrPredicate COMMA expressionOrPredicate onOverflowClause? RIGHT_PAREN withinGroupClause? filterClause? + : LISTAGG LEFT_PAREN DISTINCT? expressionOrPredicate COMMA expressionOrPredicate onOverflowClause? RIGHT_PAREN withinGroupClause? filterClause? overClause? ; /** @@ -1094,6 +1094,74 @@ filterClause : FILTER LEFT_PAREN whereClause RIGHT_PAREN ; +/** + * A `nulls` clause: what should a value access window function do when encountering a `null` + */ +nullsClause + : RESPECT NULLS + | IGNORE NULLS + ; + +/** + * A `nulls` clause: what should a value access window function do when encountering a `null` + */ +nthSideClause + : FROM FIRST + | FROM LAST + ; + +/** + * A 'over' clause: the specification of a window within which the function should act + */ +overClause + : OVER LEFT_PAREN partitionClause? orderByClause? frameClause? RIGHT_PAREN + ; + +/** + * A 'partition' clause: the specification the group within which a function should act in a window + */ +partitionClause + : PARTITION BY expression (COMMA expression)* + ; + +/** + * A 'frame' clause: the specification the content of the window + */ +frameClause + : (RANGE|ROWS|GROUPS) frameStart frameExclusion? + | (RANGE|ROWS|GROUPS) BETWEEN frameStart AND frameEnd frameExclusion? + ; + +/** + * The start of the window content + */ +frameStart + : UNBOUNDED PRECEDING + | expression PRECEDING + | CURRENT ROW + | expression FOLLOWING + ; + +/** + * The end of the window content + */ +frameEnd + : expression PRECEDING + | CURRENT ROW + | expression FOLLOWING + | UNBOUNDED FOLLOWING + ; + +/** + * A 'exclusion' clause: the specification what to exclude from the window content + */ +frameExclusion + : EXCLUDE CURRENT ROW + | EXCLUDE GROUP + | EXCLUDE TIES + | EXCLUDE NO OTHERS + ; + /** * Any function with an irregular syntax for the argument list * @@ -1418,8 +1486,8 @@ identifier | CURRENT_TIME | CURRENT_TIMESTAMP | DATE - | DAY | DATETIME + | DAY | DELETE | DESC | DISTINCT @@ -1433,20 +1501,24 @@ identifier | ESCAPE | EVERY | EXCEPT + | EXCLUDE | EXISTS | EXTRACT | FETCH | FILTER | FIRST + | FOLLOWING | FOR | FORMAT | FROM | FULL | FUNCTION | GROUP + | GROUPS | HAVING | HOUR | ID + | IGNORE | ILIKE | IN | INDEX @@ -1486,6 +1558,7 @@ identifier | NATURALID | NEW | NEXT + | NO | NOT | NULLS | OBJECT @@ -1496,14 +1569,20 @@ identifier | ONLY | OR | ORDER + | OTHERS | OUTER + | OVER | OVERFLOW | OVERLAY | PAD + | PARTITION | PERCENT | PLACING | POSITION + | PRECEDING | QUARTER + | RANGE + | RESPECT | RIGHT | ROLLUP | ROW @@ -1526,6 +1605,7 @@ identifier | TRIM | TRUNCATE | TYPE + | UNBOUNDED | UNION | UPDATE | VALUE 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 8e6c2815b49f..915a2057a28f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -311,6 +311,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { functionFactory.everyAny_minMaxCase(); functionFactory.bitLength_pattern( "length(to_binary(?1))*8" ); + functionFactory.windowFunctions(); functionFactory.listagg_stringAgg( "varchar" ); functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); 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 9098337d24af..c23594b86b9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -235,6 +235,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { .setArgumentsValidator( CommonFunctionFactory.formatValidator() ) .setArgumentListSignature("(TEMPORAL datetime as STRING pattern)") .register(); + functionFactory.windowFunctions(); functionFactory.listagg_stringAgg( "string" ); functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.hypotheticalOrderedSetAggregates(); 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 412ce0515677..ce4c8d3ac492 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -263,6 +263,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { .setArgumentListSignature("(STRING string, STRING pattern)") .register(); + functionFactory.windowFunctions(); if ( getDB2Version().isSameOrAfter( 9, 5 ) ) { functionFactory.listagg( null ); if ( getDB2Version().isSameOrAfter( 11, 1 ) ) { 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 ffe510f1182c..c631cdfb5f01 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -281,6 +281,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { } functionFactory.rownum(); if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { + functionFactory.windowFunctions(); functionFactory.listagg( null ); if ( getVersion().isSameOrAfter( 2 ) ) { functionFactory.inverseDistributionOrderedSetAggregates(); @@ -580,6 +581,11 @@ public boolean supportsOffsetInSubquery() { return true; } + @Override + public boolean supportsWindowFunctions() { + return getVersion().isSameOrAfter( 1, 4, 200 ); + } + @Override public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 1, 4, 198 ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index d35d17f2c8bb..eb800240d8d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -9,6 +9,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; +import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.sequence.MariaDBSequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; @@ -65,12 +66,18 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { super.initializeFunctionRegistry(queryEngine); if ( getVersion().isSameOrAfter( 10, 2 ) ) { + CommonFunctionFactory commonFunctionFactory = new CommonFunctionFactory( queryEngine ); + commonFunctionFactory.windowFunctions(); + commonFunctionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); queryEngine.getSqmFunctionRegistry().registerNamed( "json_valid", queryEngine.getTypeConfiguration() .getBasicTypeRegistry() .resolve( StandardBasicTypes.BOOLEAN ) ); + if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { + commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); + } } } 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 e4e2e541a82e..027032ef1edc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -444,6 +444,12 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { // MySQL timestamp type defaults to precision 0 (seconds) but // we want the standard default precision of 6 (microseconds) functionFactory.sysdateExplicitMicros(); + if ( getMySQLVersion().isSameOrAfter( 8, 2 ) ) { + functionFactory.windowFunctions(); + if ( getMySQLVersion().isSameOrAfter( 8, 11 ) ) { + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); + } + } } functionFactory.listagg_groupConcat(); 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 de634ff6610a..4b6345e107a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -197,6 +197,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { else { functionFactory.listagg( "within group (order by rownum)" ); } + functionFactory.windowFunctions(); functionFactory.hypotheticalOrderedSetAggregates(); functionFactory.inverseDistributionOrderedSetAggregates(); // Oracle has a regular aggregate function named stats_mode diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java index 4ce4828013de..cd9127cee1f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -14,6 +14,8 @@ import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.sqm.FrameExclusion; +import org.hibernate.query.sqm.FrameKind; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; @@ -334,9 +336,9 @@ public void visitOver(Over over) { final Expression expression = over.getExpression(); if ( expression instanceof FunctionExpression && "row_number".equals( ( (FunctionExpression) expression ).getFunctionName() ) ) { if ( over.getPartitions().isEmpty() && over.getOrderList().isEmpty() - && over.getStartKind() == Over.FrameKind.UNBOUNDED_PRECEDING - && over.getEndKind() == Over.FrameKind.CURRENT_ROW - && over.getExclusion() == Over.FrameExclusion.NO_OTHERS ) { + && over.getStartKind() == FrameKind.UNBOUNDED_PRECEDING + && over.getEndKind() == FrameKind.CURRENT_ROW + && over.getExclusion() == FrameExclusion.NO_OTHERS ) { // Oracle doesn't allow an empty over clause for the row_number() function append( "rownum" ); return; 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 4b54c7e155d8..64cf4b7718b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -451,6 +451,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { functionFactory.soundex(); //was introduced in Postgres 9 apparently functionFactory.locate_positionSubstring(); + functionFactory.windowFunctions(); functionFactory.listagg_stringAgg( "varchar" ); if ( getVersion().isSameOrAfter( 9, 4 ) ) { 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 1b425f2adfed..e04b03a00ac7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -267,6 +267,7 @@ public void initializeFunctionRegistry(QueryEngine queryEngine) { .setParameterTypes(INTEGER) .register(); } + functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 14 ) ) { 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 d9cbb804b30a..615653761cb5 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 @@ -1827,6 +1827,38 @@ public void hypotheticalOrderedSetAggregates_windowEmulation() { ); } + public void windowFunctions() { + functionRegistry.namedWindowDescriptorBuilder( "row_number" ) + .setExactArgumentCount( 0 ) + .setInvariantType( longType ) + .register(); + functionRegistry.namedWindowDescriptorBuilder( "lag" ) + .setArgumentCountBetween( 1, 3 ) + .setParameterTypes( ANY, INTEGER, ANY ) + .setArgumentListSignature( "ANY value[, INTEGER offset[, ANY default]]" ) + .register(); + functionRegistry.namedWindowDescriptorBuilder( "lead" ) + .setArgumentCountBetween( 1, 3 ) + .setParameterTypes( ANY, INTEGER, ANY ) + .setArgumentListSignature( "ANY value[, INTEGER offset[, ANY default]]" ) + .register(); + functionRegistry.namedWindowDescriptorBuilder( "first_value" ) + .setExactArgumentCount( 1 ) + .setParameterTypes( ANY ) + .setArgumentListSignature( "ANY value" ) + .register(); + functionRegistry.namedWindowDescriptorBuilder( "last_value" ) + .setExactArgumentCount( 1 ) + .setParameterTypes( ANY ) + .setArgumentListSignature( "ANY value" ) + .register(); + functionRegistry.namedWindowDescriptorBuilder( "nth_value" ) + .setExactArgumentCount( 2 ) + .setParameterTypes( ANY, INTEGER ) + .setArgumentListSignature( "ANY value, INTEGER nth" ) + .register(); + } + public void math() { functionRegistry.namedDescriptorBuilder( "round" ) // To avoid truncating to a specific data type, we default to using the argument type 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 index 6622682a2ec3..a1cb82add9dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetFunction.java @@ -60,28 +60,18 @@ public void render( Predicate filter, List withinGroup, SqlAstTranslator translator) { - final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + if ( filter != null && !translator.supportsFilterClause() ) { + throw new IllegalArgumentException( "Can't emulate filter clause for inverse distribution function [" + getName() + "]!" ); + } 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 ); - } + sqlAstArguments.get( 0 ).accept( translator ); for ( int i = 1; i < sqlAstArguments.size(); i++ ) { - sqlAppender.appendSql( ',' ); + sqlAppender.append( ',' ); 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 " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetWindowEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetWindowEmulation.java index beb14a2cca7d..e3c891d84706 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetWindowEmulation.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetWindowEmulation.java @@ -45,7 +45,7 @@ public HypotheticalSetWindowEmulation(String name, BasicTypeReference returnT } @Override - public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + public SelfRenderingSqmOrderedSetAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( List> arguments, SqmPredicate filter, SqmOrderByClause withinGroupClause, 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 index 981e003ab6d6..f64c39e00f21 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionFunction.java @@ -16,7 +16,6 @@ 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; @@ -50,7 +49,7 @@ public InverseDistributionFunction(String name, FunctionParameterType parameterT } @Override - public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + public SelfRenderingSqmOrderedSetAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( List> arguments, SqmPredicate filter, SqmOrderByClause withinGroupClause, @@ -90,24 +89,18 @@ public void render( Predicate filter, List withinGroup, SqlAstTranslator translator) { - final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + if ( filter != null && !translator.supportsFilterClause() ) { + throw new IllegalArgumentException( "Can't emulate filter clause for inverse distribution function [" + getName() + "]!" ); + } 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 ); + sqlAstArguments.get( 0 ).accept( translator ); + for ( int i = 1; i < sqlAstArguments.size(); i++ ) { + sqlAppender.append( ',' ); + 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 " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionWindowEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionWindowEmulation.java index 128879f3040b..8feecc7d4a07 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionWindowEmulation.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionWindowEmulation.java @@ -15,6 +15,7 @@ import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; import org.hibernate.query.sqm.function.SelfRenderingSqmAggregateFunction; +import org.hibernate.query.sqm.function.SelfRenderingSqmOrderedSetAggregateFunction; import org.hibernate.query.sqm.produce.function.ArgumentsValidator; import org.hibernate.query.sqm.produce.function.FunctionParameterType; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; @@ -44,7 +45,7 @@ public InverseDistributionWindowEmulation(String name, FunctionParameterType par } @Override - public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + public SelfRenderingSqmOrderedSetAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( List> arguments, SqmPredicate filter, SqmOrderByClause withinGroupClause, 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 c40236c9ffb1..9c7822921172 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 @@ -66,6 +66,9 @@ import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.query.sqm.FrameExclusion; +import org.hibernate.query.sqm.FrameKind; +import org.hibernate.query.sqm.FrameMode; import org.hibernate.query.sqm.LiteralNumberFormatException; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.NullPrecedence; @@ -83,6 +86,7 @@ import org.hibernate.query.sqm.UnknownEntityException; import org.hibernate.query.sqm.function.FunctionKind; import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.internal.ParameterCollector; import org.hibernate.query.sqm.internal.SqmCreationProcessingStateImpl; @@ -120,9 +124,11 @@ import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmExtractUnit; import org.hibernate.query.sqm.tree.expression.SqmFormat; +import org.hibernate.query.sqm.tree.expression.SqmFunction; 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.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; @@ -3228,14 +3234,20 @@ else if ( "*".equals( argumentChild.getText() ) ) { functionArguments = emptyList(); } + final Boolean fromFirst = getFromFirst( ctx ); + final Boolean respectNulls = getRespectNullsClause( ctx ); final SqmOrderByClause withinGroup = getWithinGroup( ctx ); final SqmPredicate filterExpression = getFilterExpression( ctx ); + final boolean hasOverClause = ctx.getChild( ctx.getChildCount() - 1 ) instanceof HqlParser.OverClauseContext; SqmFunctionDescriptor functionTemplate = getFunctionDescriptor( functionName ); if ( functionTemplate == null ) { FunctionKind functionKind = FunctionKind.NORMAL; if ( withinGroup != null ) { functionKind = FunctionKind.ORDERED_SET_AGGREGATE; } + else if ( hasOverClause ) { + functionKind = FunctionKind.WINDOW; + } else if ( filterExpression != null ) { functionKind = FunctionKind.AGGREGATE; } @@ -3252,10 +3264,34 @@ else if ( filterExpression != null ) { SqlAstNodeRenderingMode.DEFAULT ); } + else { + if ( hasOverClause && functionTemplate.getFunctionKind() == FunctionKind.NORMAL ) { + throw new SemanticException( "OVER clause is illegal for normal function: " + functionName ); + } + else if ( !hasOverClause && functionTemplate.getFunctionKind() == FunctionKind.WINDOW ) { + throw new SemanticException( "OVER clause is mandatory for window-only function: " + functionName ); + } + if ( respectNulls != null ) { + switch ( functionName ) { + case "lag": + case "lead": + case "first_value": + case "last_value": + case "nth_value": + break; + default: + throw new SemanticException( "RESPECT/IGNORE NULLS is illegal for function: " + functionName ); + } + } + if ( fromFirst != null && !"nth_value".equals( functionName ) ) { + throw new SemanticException( "FROM FIRST/LAST is illegal for function: " + functionName ); + } + } + final SqmFunction function; switch ( functionTemplate.getFunctionKind() ) { case ORDERED_SET_AGGREGATE: - return functionTemplate.generateOrderedSetAggregateSqmExpression( + function = functionTemplate.generateOrderedSetAggregateSqmExpression( functionArguments, filterExpression, withinGroup, @@ -3263,25 +3299,40 @@ else if ( filterExpression != null ) { creationContext.getQueryEngine(), creationContext.getJpaMetamodel().getTypeConfiguration() ); + break; case AGGREGATE: - return functionTemplate.generateAggregateSqmExpression( + function = functionTemplate.generateAggregateSqmExpression( functionArguments, filterExpression, null, creationContext.getQueryEngine(), creationContext.getJpaMetamodel().getTypeConfiguration() ); + break; + case WINDOW: + function = functionTemplate.generateWindowSqmExpression( + functionArguments, + filterExpression, + null, + null, + null, + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ); + break; default: if ( filterExpression != null ) { throw new ParsingException( "Illegal use of a FILTER clause for non-aggregate function: " + originalFunctionName ); } - return functionTemplate.generateSqmExpression( + function = functionTemplate.generateSqmExpression( functionArguments, null, creationContext.getQueryEngine(), creationContext.getJpaMetamodel().getTypeConfiguration() ); + break; } + return applyOverClause( ctx, function ); } @Override @@ -3351,13 +3402,16 @@ public Object visitListaggFunction(ListaggFunctionContext ctx) { } final SqmOrderByClause withinGroup = getWithinGroup( ctx ); final SqmPredicate filterExpression = getFilterExpression( ctx ); - return functionTemplate.generateOrderedSetAggregateSqmExpression( - functionArguments, - filterExpression, - withinGroup, - null, - creationContext.getQueryEngine(), - creationContext.getJpaMetamodel().getTypeConfiguration() + return applyOverClause( + ctx, + functionTemplate.generateOrderedSetAggregateSqmExpression( + functionArguments, + filterExpression, + withinGroup, + null, + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ) ); } @@ -3693,12 +3747,15 @@ else if ( argumentChild instanceof HqlParser.PredicateContext ) { } final SqmExpression argument = (SqmExpression) argumentChild.accept( this ); - return getFunctionDescriptor( "every" ).generateAggregateSqmExpression( - singletonList( argument ), - filterExpression, - resolveExpressibleTypeBasic( Boolean.class ), - creationContext.getQueryEngine(), - creationContext.getJpaMetamodel().getTypeConfiguration() + return applyOverClause( + ctx, + getFunctionDescriptor( "every" ).generateAggregateSqmExpression( + singletonList( argument ), + filterExpression, + resolveExpressibleTypeBasic( Boolean.class ), + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ) ); } else { @@ -3729,12 +3786,15 @@ else if ( argumentChild instanceof HqlParser.PredicateContext ) { } final SqmExpression argument = (SqmExpression) argumentChild.accept( this ); - return getFunctionDescriptor( "any" ).generateAggregateSqmExpression( - singletonList( argument ), - filterExpression, - resolveExpressibleTypeBasic( Boolean.class ), - creationContext.getQueryEngine(), - creationContext.getJpaMetamodel().getTypeConfiguration() + return applyOverClause( + ctx, + getFunctionDescriptor( "any" ).generateAggregateSqmExpression( + singletonList( argument ), + filterExpression, + resolveExpressibleTypeBasic( Boolean.class ), + creationContext.getQueryEngine(), + creationContext.getJpaMetamodel().getTypeConfiguration() + ) ); } else { @@ -3805,7 +3865,7 @@ private SqmSubQuery createCollectionReferenceSubQuery( private SqmOrderByClause getWithinGroup(ParseTree functionCtx) { HqlParser.WithinGroupClauseContext ctx = null; - for ( int i = functionCtx.getChildCount() - 2; i < functionCtx.getChildCount(); i++ ) { + for ( int i = functionCtx.getChildCount() - 3; i < functionCtx.getChildCount(); i++ ) { final ParseTree child = functionCtx.getChild( i ); if ( child instanceof HqlParser.WithinGroupClauseContext ) { ctx = (HqlParser.WithinGroupClauseContext) child; @@ -3818,14 +3878,174 @@ private SqmOrderByClause getWithinGroup(ParseTree functionCtx) { return null; } + private Boolean getFromFirst(ParseTree functionCtx) { + // The clause is either on index 3 or 4 is where the + final int end = Math.min( functionCtx.getChildCount(), 5 ); + for ( int i = 3; i < end; i++ ) { + final ParseTree child = functionCtx.getChild( i ); + if ( child instanceof HqlParser.NthSideClauseContext ) { + final HqlParser.NthSideClauseContext subCtx = (HqlParser.NthSideClauseContext) child.getChild( 6 ); + return ( (TerminalNode) subCtx.getChild( 1 ) ).getSymbol().getType() == HqlParser.FIRST; + } + } + return null; + } + + private Boolean getRespectNullsClause(ParseTree functionCtx) { + for ( int i = functionCtx.getChildCount() - 3; i < functionCtx.getChildCount(); i++ ) { + final ParseTree child = functionCtx.getChild( i ); + if ( child instanceof HqlParser.NullsClauseContext ) { + return ( (TerminalNode) child.getChild( 0 ) ).getSymbol().getType() == HqlParser.RESPECT; + } + } + return null; + } + private SqmPredicate getFilterExpression(ParseTree functionCtx) { - final ParseTree lastChild = functionCtx.getChild( functionCtx.getChildCount() - 1 ); - if ( lastChild instanceof HqlParser.FilterClauseContext ) { - return (SqmPredicate) lastChild.getChild( 2 ).getChild( 1 ).accept( this ); + for ( int i = functionCtx.getChildCount() - 2; i < functionCtx.getChildCount(); i++ ) { + final ParseTree child = functionCtx.getChild( i ); + if ( child instanceof HqlParser.FilterClauseContext ) { + return (SqmPredicate) child.getChild( 2 ).getChild( 1 ).accept( this ); + } } return null; } + private SqmExpression applyOverClause(ParseTree functionCtx, SqmFunction function) { + final ParseTree lastChild = functionCtx.getChild( functionCtx.getChildCount() - 1 ); + if ( lastChild instanceof HqlParser.OverClauseContext ) { + return applyOverClause( (HqlParser.OverClauseContext) lastChild, function ); + } + return function; + } + + private SqmExpression applyOverClause(HqlParser.OverClauseContext ctx, SqmFunction function) { + final List> partitions; + final List orderList; + final FrameMode mode; + final FrameKind startKind; + final SqmExpression startExpression; + final FrameKind endKind; + final SqmExpression endExpression; + final FrameExclusion exclusion; + int index = 2; + if ( ctx.getChild( index ) instanceof HqlParser.PartitionClauseContext ) { + final ParseTree subCtx = ctx.getChild( index ); + partitions = new ArrayList<>( ( subCtx.getChildCount() >> 1 ) - 1 ); + for ( int i = 2; i < subCtx.getChildCount(); i += 2 ) { + partitions.add( (SqmExpression) subCtx.getChild( i ).accept( this ) ); + } + index++; + } + else { + partitions = Collections.emptyList(); + } + if ( index < ctx.getChildCount() && ctx.getChild( index ) instanceof HqlParser.OrderByClauseContext ) { + orderList = visitOrderByClause( (HqlParser.OrderByClauseContext) ctx.getChild( index ) ).getSortSpecifications(); + index++; + } + else { + orderList = Collections.emptyList(); + } + if ( index < ctx.getChildCount() && ctx.getChild( index ) instanceof HqlParser.FrameClauseContext ) { + final ParseTree frameCtx = ctx.getChild( index ); + switch ( ( (TerminalNode) frameCtx.getChild( 0 ) ).getSymbol().getType() ) { + case HqlParser.RANGE: + mode = FrameMode.RANGE; + break; + case HqlParser.ROWS: + mode = FrameMode.ROWS; + break; + case HqlParser.GROUPS: + mode = FrameMode.GROUPS; + break; + default: + throw new IllegalArgumentException( "Unexpected frame mode: " + frameCtx.getChild( 0 ) ); + } + final int frameStartIndex; + if ( frameCtx.getChild( 1 ) instanceof TerminalNode ) { + frameStartIndex = 2; + endKind = getFrameKind( frameCtx.getChild( 4 ) ); + endExpression = endKind == FrameKind.OFFSET_FOLLOWING || endKind == FrameKind.OFFSET_PRECEDING + ? (SqmExpression) frameCtx.getChild( 4 ).getChild( 0 ).accept( this ) + : null; + } + else { + frameStartIndex = 1; + endKind = FrameKind.CURRENT_ROW; + endExpression = null; + } + startKind = getFrameKind( frameCtx.getChild( frameStartIndex ) ); + startExpression = startKind == FrameKind.OFFSET_FOLLOWING || startKind == FrameKind.OFFSET_PRECEDING + ? (SqmExpression) frameCtx.getChild( frameStartIndex ).getChild( 0 ).accept( this ) + : null; + final ParseTree lastChild = frameCtx.getChild( frameCtx.getChildCount() - 1 ); + if ( lastChild instanceof HqlParser.FrameExclusionContext ) { + switch ( ( (TerminalNode) lastChild.getChild( 1 ) ).getSymbol().getType() ) { + case HqlParser.CURRENT: + exclusion = FrameExclusion.CURRENT_ROW; + break; + case HqlParser.GROUP: + exclusion = FrameExclusion.GROUP; + break; + case HqlParser.TIES: + exclusion = FrameExclusion.TIES; + break; + case HqlParser.NO: + exclusion = FrameExclusion.NO_OTHERS; + break; + default: + throw new IllegalArgumentException( "Unexpected frame exclusion: " + lastChild ); + } + } + else { + exclusion = FrameExclusion.NO_OTHERS; + } + } + else { + mode = FrameMode.ROWS; + startKind = FrameKind.UNBOUNDED_PRECEDING; + startExpression = null; + endKind = FrameKind.CURRENT_ROW; + endExpression = null; + exclusion = FrameExclusion.NO_OTHERS; + } + return new SqmOver<>( + function, + partitions, + orderList, + mode, + startKind, + startExpression, + endKind, + endExpression, + exclusion + ); + } + + private FrameKind getFrameKind(ParseTree child) { + switch ( ( (TerminalNode) child.getChild( 1 ) ).getSymbol().getType() ) { + case HqlParser.PRECEDING: + if ( child.getChild( 0 ) instanceof TerminalNode ) { + return FrameKind.UNBOUNDED_PRECEDING; + } + else { + return FrameKind.OFFSET_PRECEDING; + } + case HqlParser.FOLLOWING: + if ( child.getChild( 0 ) instanceof TerminalNode ) { + return FrameKind.UNBOUNDED_FOLLOWING; + } + else { + return FrameKind.OFFSET_FOLLOWING; + } + case HqlParser.ROW: + return FrameKind.CURRENT_ROW; + default: + throw new IllegalArgumentException( "Illegal frame kind: " + child ); + } + } + @Override public SqmExpression visitCube(HqlParser.CubeContext ctx) { return new SqmSummarization<>( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameExclusion.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameExclusion.java new file mode 100644 index 000000000000..4df33105faa9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameExclusion.java @@ -0,0 +1,17 @@ +/* + * 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; + +/** + * @author Christian Beikov + */ +public enum FrameExclusion { + CURRENT_ROW, + GROUP, + TIES, + NO_OTHERS +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameKind.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameKind.java new file mode 100644 index 000000000000..2f31269629bb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameKind.java @@ -0,0 +1,18 @@ +/* + * 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; + +/** + * @author Christian Beikov + */ +public enum FrameKind { + UNBOUNDED_PRECEDING, + OFFSET_PRECEDING, + CURRENT_ROW, + OFFSET_FOLLOWING, + UNBOUNDED_FOLLOWING +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameMode.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameMode.java new file mode 100644 index 000000000000..9048aeaeaac7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/FrameMode.java @@ -0,0 +1,16 @@ +/* + * 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; + +/** + * @author Christian Beikov + */ +public enum FrameMode { + ROWS, + RANGE, + GROUPS +} 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 6169794399aa..b614c4927d33 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.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; @@ -236,6 +237,8 @@ public interface SemanticQueryWalker { T visitStar(SqmStar sqmStar); + T visitOver(SqmOver over); + T visitOverflow(SqmOverflow sqmOverflow); T visitCoalesce(SqmCoalesce sqmCoalesce); 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 8385ea210a1c..d18e01bffb88 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 @@ -152,6 +152,28 @@ public final SelfRenderingSqmFunction generateOrderedSetAggregateSqmExpre ); } + @Override + public final SelfRenderingSqmFunction generateWindowSqmExpression( + List> arguments, + SqmPredicate filter, + Boolean respectNulls, + Boolean fromFirst, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + argumentsValidator.validate( arguments, getName(), queryEngine ); + + return generateSqmWindowFunctionExpression( + arguments, + filter, + respectNulls, + fromFirst, + 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 @@ -209,5 +231,30 @@ protected SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregat typeConfiguration ); } + + /** + * Return an SQM node or subtree representing an invocation of this window 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 respectNulls + * @param fromFirst + * @param impliedResultType the function return type as inferred from its usage + */ + protected SelfRenderingSqmWindowFunction generateSqmWindowFunctionExpression( + List> arguments, + SqmPredicate filter, + Boolean respectNulls, + Boolean fromFirst, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + return (SelfRenderingSqmWindowFunction) 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 eeb855663cdb..663608b389eb 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 @@ -76,6 +76,16 @@ protected SelfRenderingSqmFunction generateSqmFunctionExpression( queryEngine, typeConfiguration ); + case WINDOW: + return generateWindowSqmExpression( + arguments, + null, + null, + null, + impliedResultType, + queryEngine, + typeConfiguration + ); default: return new SelfRenderingSqmFunction<>( this, @@ -114,7 +124,7 @@ public SelfRenderingSqmAggregateFunction generateSqmAggregateFunctionExpr } @Override - public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + public SelfRenderingSqmOrderedSetAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( List> arguments, SqmPredicate filter, SqmOrderByClause withinGroupClause, @@ -138,28 +148,31 @@ public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFu ); } - /** - * Must be overridden by subclasses - */ - public abstract void render( - SqlAppender sqlAppender, - List sqlAstArguments, - SqlAstTranslator walker); - - public void render( - SqlAppender sqlAppender, - List sqlAstArguments, - Predicate filter, - SqlAstTranslator walker) { - render( sqlAppender, sqlAstArguments, walker ); + @Override + protected SelfRenderingSqmWindowFunction generateSqmWindowFunctionExpression( + List> arguments, + SqmPredicate filter, + Boolean respectNulls, + Boolean fromFirst, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + if ( functionKind != FunctionKind.WINDOW ) { + throw new UnsupportedOperationException( "The function " + getName() + " is not a window function!" ); + } + return new SelfRenderingSqmWindowFunction<>( + this, + this, + arguments, + filter, + respectNulls, + fromFirst, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); } - 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 0e2d31f0bf86..fe1c586793ca 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 @@ -14,5 +14,6 @@ public enum FunctionKind { NORMAL, AGGREGATE, - ORDERED_SET_AGGREGATE; + ORDERED_SET_AGGREGATE, + WINDOW; } 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 f0049e7de482..e73b3663edcf 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 @@ -49,4 +49,15 @@ default void render( // Ignore the filter by default. Subclasses will override this render( sqlAppender, sqlAstArguments, walker ); } + + default void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + Boolean respectNulls, + Boolean fromFirst, + 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 7106e3524bd2..4554aa37601e 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 @@ -95,15 +95,16 @@ public void render( SqlAppender sqlAppender, List sqlAstArguments, SqlAstTranslator translator) { - render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), translator ); + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), null, null, translator ); } + @Override public void render( SqlAppender sqlAppender, - List args, + List sqlAstArguments, Predicate filter, SqlAstTranslator translator) { - render( sqlAppender, args, filter, Collections.emptyList(), translator ); + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), null, null, translator ); } @Override @@ -113,6 +114,28 @@ public void render( Predicate filter, List withinGroup, SqlAstTranslator translator) { + render( sqlAppender, sqlAstArguments, filter, withinGroup, null, null, translator ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + Boolean respectNulls, + Boolean fromFirst, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), respectNulls, fromFirst, walker ); + } + + private void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + Boolean respectNulls, + Boolean fromFirst, + SqlAstTranslator translator) { final boolean useParens = useParenthesesWhenNoArgs || !sqlAstArguments.isEmpty(); final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); @@ -158,6 +181,23 @@ public void render( sqlAppender.appendSql( ')' ); } + if ( fromFirst != null ) { + if ( fromFirst ) { + sqlAppender.appendSql( " from first" ); + } + else { + sqlAppender.appendSql( " from last" ); + } + } + if ( respectNulls != null ) { + if ( respectNulls ) { + sqlAppender.appendSql( " respect nulls" ); + } + else { + sqlAppender.appendSql( " ignore nulls" ); + } + } + 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 d6f9741639b1..79796e165213 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 @@ -96,6 +96,17 @@ public void render( renderer.render( sqlAppender, sqlAstArguments, filter, withinGroup, walker ); } + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + Boolean respectNulls, + Boolean fromFirst, + SqlAstTranslator walker) { + renderer.render( sqlAppender, sqlAstArguments, filter, respectNulls, fromFirst, walker ); + } + @Override public String getArgumentListSignature() { return argumentListSignature == null ? super.getArgumentListSignature() : argumentListSignature; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java index c1e88bb31a79..38737d1c22f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmAggregateFunction.java @@ -88,7 +88,7 @@ public Expression convertToSqlAst(SqmToSqlAstConverter walker) { return new SelfRenderingAggregateFunctionSqlAstExpression( getFunctionName(), getRenderingSupport(), - resolveSqlAstArguments( getArguments(), walker ), + arguments, filter == null ? null : walker.visitNestedTopLevelPredicate( filter ), resultType, getMappingModelExpressible( walker, resultType ) 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 index d7822892d363..1104b2276429 100644 --- 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 @@ -125,8 +125,8 @@ public Expression convertToSqlAst(SqmToSqlAstConverter walker) { return new SelfRenderingOrderedSetAggregateFunctionSqlAstExpression( getFunctionName(), getRenderingSupport(), - resolveSqlAstArguments( getArguments(), walker ), - getFilter() == null ? null : (Predicate) getFilter().accept( walker ), + arguments, + getFilter() == null ? null : walker.visitNestedTopLevelPredicate( getFilter() ), withinGroup, resultType, getMappingModelExpressible( walker, resultType ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java new file mode 100644 index 000000000000..6d7148b97353 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java @@ -0,0 +1,163 @@ +/* + * 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.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.SqmWindowFunction; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; + +/** + * @author Christian Beikov + */ +public class SelfRenderingSqmWindowFunction extends SelfRenderingSqmFunction + implements SqmWindowFunction { + + private final SqmPredicate filter; + private final Boolean respectNulls; + private final Boolean fromFirst; + + public SelfRenderingSqmWindowFunction( + SqmFunctionDescriptor descriptor, + FunctionRenderingSupport renderingSupport, + List> arguments, + SqmPredicate filter, + Boolean respectNulls, + Boolean fromFirst, + ReturnableType impliedResultType, + ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( descriptor, renderingSupport, arguments, impliedResultType, argumentsValidator, returnTypeResolver, nodeBuilder, name ); + this.filter = filter; + this.respectNulls = respectNulls; + this.fromFirst = fromFirst; + } + + @Override + public SelfRenderingSqmWindowFunction copy(SqmCopyContext context) { + final SelfRenderingSqmWindowFunction 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 SelfRenderingSqmWindowFunction expression = context.registerCopy( + this, + new SelfRenderingSqmWindowFunction<>( + getFunctionDescriptor(), + getRenderingSupport(), + arguments, + filter == null ? null : filter.copy( context ), + respectNulls, + fromFirst, + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName() + ) + ); + copyTo( expression, context ); + return expression; + } + + @Override + public Expression 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() ); + } + return new SelfRenderingWindowFunctionSqlAstExpression( + getFunctionName(), + getRenderingSupport(), + arguments, + filter == null ? null : walker.visitNestedTopLevelPredicate( filter ), + respectNulls, + fromFirst, + resultType, + getMappingModelExpressible( walker, resultType ) + ); + } + + @Override + public SqmPredicate getFilter() { + return filter; + } + + @Override + public Boolean getRespectNulls() { + return respectNulls; + } + + @Override + public Boolean getFromFirst() { + return fromFirst; + } + + @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 ( fromFirst != null ) { + if ( fromFirst ) { + sb.append( " from first" ); + } + else { + sb.append( " from last" ); + } + } + if ( respectNulls != null ) { + if ( respectNulls ) { + sb.append( " respect nulls" ); + } + else { + sb.append( " ignore nulls" ); + } + } + if ( filter != null ) { + sb.append( " filter (where " ); + filter.appendHqlString( sb ); + sb.append( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingWindowFunctionSqlAstExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingWindowFunctionSqlAstExpression.java new file mode 100644 index 000000000000..e7990f5d03aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingWindowFunctionSqlAstExpression.java @@ -0,0 +1,70 @@ +/* + * 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.WindowFunctionExpression; +import org.hibernate.sql.ast.tree.predicate.Predicate; + +/** + * Representation of a window function call in the SQL AST for impls that know how to + * render themselves. + * + * @author Christian Beikov + */ +public class SelfRenderingWindowFunctionSqlAstExpression extends SelfRenderingFunctionSqlAstExpression + implements WindowFunctionExpression { + + private final Predicate filter; + private final Boolean respectNulls; + private final Boolean fromFirst; + + public SelfRenderingWindowFunctionSqlAstExpression( + String functionName, + FunctionRenderingSupport renderer, + List sqlAstArguments, + Predicate filter, + Boolean respectNulls, + Boolean fromFirst, + ReturnableType type, + JdbcMappingContainer expressible) { + super( functionName, renderer, sqlAstArguments, type, expressible ); + this.filter = filter; + this.respectNulls = respectNulls; + this.fromFirst = fromFirst; + } + + @Override + public Predicate getFilter() { + return filter; + } + + @Override + public Boolean getRespectNulls() { + return respectNulls; + } + + @Override + public Boolean getFromFirst() { + return fromFirst; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + getRenderer().render( sqlAppender, getArguments(), filter, respectNulls, fromFirst, walker ); + } +} 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 f6ce70f99144..c10811712462 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 @@ -75,6 +75,21 @@ default SelfRenderingSqmFunction generateOrderedSetAggregateSqmExpression throw new UnsupportedOperationException( "Not an ordered set-aggregate function!" ); } + /** + * Like {@link #generateSqmExpression(List, ReturnableType, QueryEngine, TypeConfiguration)} + * but also accepts a filter predicate. This method is intended for aggregate functions. + */ + default SelfRenderingSqmFunction generateWindowSqmExpression( + List> arguments, + SqmPredicate filter, + Boolean respectNulls, + Boolean fromFirst, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + throw new UnsupportedOperationException( "Not an 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 fe17be472121..7c0af63fdf1f 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 @@ -188,6 +188,19 @@ public NamedFunctionDescriptorBuilder namedOrderedSetAggregateDescriptorBuilder( return namedOrderedSetAggregateDescriptorBuilder( name, name ); } + /** + * Get a builder for creating and registering a name-based window 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 namedWindowDescriptorBuilder(String name) { + return namedWindowDescriptorBuilder( name, name ); + } + /** * Get a builder for creating and registering a name-based function descriptor. * @@ -224,6 +237,18 @@ public NamedFunctionDescriptorBuilder namedOrderedSetAggregateDescriptorBuilder( return new NamedFunctionDescriptorBuilder( this, registrationKey, FunctionKind.ORDERED_SET_AGGREGATE, name ); } + /** + * Get a builder for creating and registering a name-based window 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 namedWindowDescriptorBuilder(String registrationKey, String name) { + return new NamedFunctionDescriptorBuilder( this, registrationKey, FunctionKind.WINDOW, 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 fb247756f537..d856be2acd20 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.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; +import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; @@ -767,6 +768,11 @@ public Object visitStar(SqmStar sqmStar) { return null; } + @Override + public Object visitOver(SqmOver over) { + return null; + } + @Override public Object visitWhereClause(SqmWhereClause whereClause) { if ( whereClause != null && whereClause.getPredicate() != 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 177b224dbc7e..dd0e9d5cb611 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 @@ -146,7 +146,7 @@ public void render( List args, Predicate filter, SqlAstTranslator translator) { - render( sqlAppender, args, filter, Collections.emptyList(), translator ); + render( sqlAppender, args, filter, Collections.emptyList(), null, null, translator ); } public void render( @@ -155,6 +155,27 @@ public void render( Predicate filter, List withinGroup, SqlAstTranslator translator) { + render( sqlAppender, args, filter, withinGroup, null, null, translator ); + } + + public void render( + SqlAppender sqlAppender, + List args, + Predicate filter, + Boolean respectNulls, + Boolean fromFirst, + SqlAstTranslator translator) { + render( sqlAppender, args, filter, Collections.emptyList(), respectNulls, fromFirst, translator ); + } + + private void render( + SqlAppender sqlAppender, + List args, + Predicate filter, + List withinGroup, + Boolean respectNulls, + Boolean fromFirst, + SqlAstTranslator translator) { final int numberOfArguments = args.size(); final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); if ( numberOfArguments < maxParamIndex ) { @@ -214,6 +235,23 @@ else if ( i < paramIndexes.length ) { sqlAppender.appendSql( ')' ); } + if ( fromFirst != null ) { + if ( fromFirst ) { + sqlAppender.appendSql( " from first" ); + } + else { + sqlAppender.appendSql( " from last" ); + } + } + if ( respectNulls != null ) { + if ( respectNulls ) { + sqlAppender.appendSql( " respect nulls" ); + } + else { + sqlAppender.appendSql( " ignore nulls" ); + } + } + 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 038283b2ac7c..19cfc093ceac 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 @@ -54,6 +54,7 @@ import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmOrderedSetAggregateFunction; +import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; @@ -639,6 +640,24 @@ public Object visitStar(SqmStar sqmStar) { return sqmStar; } + @Override + public Object visitOver(SqmOver over) { + over.getExpression().accept( this ); + for ( SqmExpression partition : over.getPartitions() ) { + partition.accept( this ); + } + for ( SqmSortSpecification sqmSortSpecification : over.getOrderList() ) { + visitSortSpecification( sqmSortSpecification ); + } + if ( over.getStartExpression() != null ) { + over.getStartExpression().accept( this ); + } + if ( over.getEndExpression() != null ) { + over.getEndExpression().accept( this ); + } + return over; + } + @Override public Object visitOverflow(SqmOverflow sqmOverflow) { sqmOverflow.getSeparatorExpression().accept( this ); 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 7585c1ed2692..7452570f4678 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 @@ -189,6 +189,7 @@ 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.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; @@ -4760,6 +4761,33 @@ public Star visitStar(SqmStar sqmStar) { return new Star(); } + @Override + public Object visitOver(SqmOver over) { + currentClauseStack.push( Clause.OVER ); + final Expression expression = (Expression) over.getExpression().accept( this ); + final List partitions = new ArrayList<>(over.getPartitions().size()); + for ( SqmExpression partition : over.getPartitions() ) { + partitions.add( (Expression) partition.accept( this ) ); + } + final List orderList = new ArrayList<>( over.getOrderList().size() ); + for ( SqmSortSpecification sortSpecification : over.getOrderList() ) { + orderList.add( visitSortSpecification( sortSpecification ) ); + } + final Over overExpression = new Over<>( + expression, + partitions, + orderList, + over.getMode(), + over.getStartKind(), + over.getStartExpression() == null ? null : (Expression) over.getStartExpression().accept( this ), + over.getEndKind(), + over.getEndExpression() == null ? null : (Expression) over.getEndExpression().accept( this ), + over.getExclusion() + ); + currentClauseStack.pop(); + return overExpression; + } + @Override public Object visitDistinct(SqmDistinct sqmDistinct) { return new Distinct( (Expression) sqmDistinct.getExpression().accept( this ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java new file mode 100644 index 000000000000..124af839a1aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java @@ -0,0 +1,226 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import org.hibernate.query.sqm.FrameExclusion; +import org.hibernate.query.sqm.FrameKind; +import org.hibernate.query.sqm.FrameMode; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.select.SqmSortSpecification; + +/** + * @author Christian Beikov + */ +public class SqmOver extends AbstractSqmExpression { + + private final SqmExpression expression; + private final List> partitions; + private final List orderList; + private final FrameMode mode; + private final FrameKind startKind; + private final SqmExpression startExpression; + private final FrameKind endKind; + private final SqmExpression endExpression; + private final FrameExclusion exclusion; + + public SqmOver( + SqmExpression expression, + List> partitions, + List orderList, + FrameMode mode, + FrameKind startKind, + SqmExpression startExpression, + FrameKind endKind, + SqmExpression endExpression, + FrameExclusion exclusion) { + super( expression.getNodeType(), expression.nodeBuilder() ); + this.expression = expression; + this.partitions = partitions; + this.orderList = orderList; + this.mode = mode; + this.startKind = startKind; + this.startExpression = startExpression; + this.endKind = endKind; + this.endExpression = endExpression; + this.exclusion = exclusion; + } + + @Override + public SqmOver copy(SqmCopyContext context) { + final SqmOver existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> partitions = new ArrayList<>(this.partitions.size()); + for ( SqmExpression partition : this.partitions ) { + partitions.add( partition.copy( context ) ); + } + final List orderList = new ArrayList<>(this.orderList.size()); + for ( SqmSortSpecification sortSpecification : this.orderList ) { + orderList.add( sortSpecification.copy( context ) ); + } + final SqmOver over = context.registerCopy( + this, + new SqmOver<>( + expression.copy( context ), + partitions, + orderList, + mode, + startKind, + startExpression == null ? null : startExpression.copy( context ), + endKind, + endExpression == null ? null : endExpression.copy( context ), + exclusion + ) + ); + copyTo( over, context ); + return over; + } + + public SqmExpression getExpression() { + return expression; + } + + public List> getPartitions() { + return partitions; + } + + public List getOrderList() { + return orderList; + } + + public SqmExpression getStartExpression() { + return startExpression; + } + + public SqmExpression getEndExpression() { + return endExpression; + } + + public FrameMode getMode() { + return mode; + } + + public FrameKind getStartKind() { + return startKind; + } + + public FrameKind getEndKind() { + return endKind; + } + + public FrameExclusion getExclusion() { + return exclusion; + } + + @Override + public SqmExpressible getNodeType() { + return expression.getNodeType(); + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitOver( this ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + expression.appendHqlString( sb ); + sb.append( " over (" ); + boolean needsWhitespace = false; + if (!partitions.isEmpty()) { + needsWhitespace = true; + sb.append( "partition by " ); + partitions.get( 0 ).appendHqlString( sb ); + for ( int i = 1; i < partitions.size(); i++ ) { + sb.append( ',' ); + partitions.get( i ).appendHqlString( sb ); + } + } + if (!orderList.isEmpty()) { + if ( needsWhitespace ) { + sb.append( ' ' ); + } + needsWhitespace = true; + sb.append( "order by " ); + orderList.get( 0 ).appendHqlString( sb ); + for ( int i = 1; i < orderList.size(); i++ ) { + sb.append( ',' ); + orderList.get( i ).appendHqlString( sb ); + } + } + if ( mode == FrameMode.ROWS && startKind == FrameKind.UNBOUNDED_PRECEDING && endKind == FrameKind.CURRENT_ROW && exclusion == FrameExclusion.NO_OTHERS ) { + // This is the default, so we don't need to render anything + } + else { + if ( needsWhitespace ) { + sb.append( ' ' ); + } + switch ( mode ) { + case GROUPS: + sb.append( "groups " ); + break; + case RANGE: + sb.append( "range " ); + break; + case ROWS: + sb.append( "rows " ); + break; + } + if ( endKind == FrameKind.CURRENT_ROW ) { + renderFrameKind( sb, startKind, startExpression ); + } + else { + sb.append( "between " ); + renderFrameKind( sb, startKind, startExpression ); + sb.append( " and " ); + renderFrameKind( sb, endKind, endExpression ); + } + switch ( exclusion ) { + case TIES: + sb.append( " exclude ties" ); + break; + case CURRENT_ROW: + sb.append( " exclude current row" ); + break; + case GROUP: + sb.append( " exclude group" ); + break; + } + } + sb.append( ')' ); + } + + private static void renderFrameKind(StringBuilder sb, FrameKind kind, SqmExpression expression) { + switch ( kind ) { + case CURRENT_ROW: + sb.append( "current row" ); + break; + case UNBOUNDED_PRECEDING: + sb.append( "unbounded preceding" ); + break; + case UNBOUNDED_FOLLOWING: + sb.append( "unbounded following" ); + break; + case OFFSET_PRECEDING: + expression.appendHqlString( sb ); + sb.append( " preceding" ); + break; + case OFFSET_FOLLOWING: + expression.appendHqlString( sb ); + sb.append( " following" ); + break; + default: + throw new UnsupportedOperationException( "Unsupported frame kind: " + kind ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindowFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindowFunction.java new file mode 100644 index 000000000000..c351dbcc522a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindowFunction.java @@ -0,0 +1,26 @@ +/* + * 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.criteria.JpaFunction; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; + +/** + * A SQM window function. + * + * @param The Java type of the expression + * + * @author Christian Beikov + */ +public interface SqmWindowFunction extends JpaFunction, SqmExpression { + + SqmPredicate getFilter(); + + Boolean getRespectNulls(); + + Boolean getFromFirst(); +} 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 ff81369dc2a3..8a9e2394522b 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 @@ -35,6 +35,9 @@ import org.hibernate.persister.entity.Queryable; import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.sqm.FrameExclusion; +import org.hibernate.query.sqm.FrameKind; +import org.hibernate.query.sqm.FrameMode; import org.hibernate.query.sqm.SetOperator; import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; import org.hibernate.sql.ast.tree.cte.CteMaterialization; @@ -3273,12 +3276,12 @@ protected final void visitOverClause( visitOverClause( partitionExpressions, sortSpecifications, - Over.FrameMode.ROWS, - Over.FrameKind.UNBOUNDED_PRECEDING, + FrameMode.ROWS, + FrameKind.UNBOUNDED_PRECEDING, null, - Over.FrameKind.CURRENT_ROW, + FrameKind.CURRENT_ROW, null, - Over.FrameExclusion.NO_OTHERS, + FrameExclusion.NO_OTHERS, false ); } @@ -3286,12 +3289,12 @@ protected final void visitOverClause( protected void visitOverClause( List partitionExpressions, List sortSpecifications, - Over.FrameMode mode, - Over.FrameKind startKind, + FrameMode mode, + FrameKind startKind, Expression startExpression, - Over.FrameKind endKind, + FrameKind endKind, Expression endExpression, - Over.FrameExclusion exclusion, + FrameExclusion exclusion, boolean orderedSetAggregate) { try { clauseStack.push( Clause.OVER ); @@ -3300,7 +3303,7 @@ protected void visitOverClause( if ( !orderedSetAggregate ) { renderOrderBy( !partitionExpressions.isEmpty(), sortSpecifications ); } - if ( mode == Over.FrameMode.ROWS && startKind == Over.FrameKind.UNBOUNDED_PRECEDING && endKind == Over.FrameKind.CURRENT_ROW && exclusion == Over.FrameExclusion.NO_OTHERS ) { + if ( mode == FrameMode.ROWS && startKind == FrameKind.UNBOUNDED_PRECEDING && endKind == FrameKind.CURRENT_ROW && exclusion == FrameExclusion.NO_OTHERS ) { // This is the default, so we don't need to render anything } else { @@ -3318,7 +3321,7 @@ protected void visitOverClause( append( "rows " ); break; } - if ( endKind == Over.FrameKind.CURRENT_ROW ) { + if ( endKind == FrameKind.CURRENT_ROW ) { renderFrameKind( startKind, startExpression ); } else { @@ -3346,7 +3349,7 @@ protected void visitOverClause( } } - private void renderFrameKind(Over.FrameKind kind, Expression expression) { + private void renderFrameKind(FrameKind kind, Expression expression) { switch ( kind ) { case CURRENT_ROW: append( "current row" ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Over.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Over.java index eefdbc8f3c49..67040de44c63 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Over.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/Over.java @@ -9,6 +9,9 @@ import java.util.List; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.sqm.FrameExclusion; +import org.hibernate.query.sqm.FrameKind; +import org.hibernate.query.sqm.FrameMode; import org.hibernate.query.sqm.sql.internal.DomainResultProducer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlAstCreationState; @@ -138,24 +141,4 @@ private SqlSelection createSelection(SqlAstCreationState creationState) { ); } - public static enum FrameMode { - ROWS, - RANGE, - GROUPS - } - - public static enum FrameKind { - UNBOUNDED_PRECEDING, - OFFSET_PRECEDING, - CURRENT_ROW, - OFFSET_FOLLOWING, - UNBOUNDED_FOLLOWING - } - - public static enum FrameExclusion { - CURRENT_ROW, - GROUP, - TIES, - NO_OTHERS - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/WindowFunctionExpression.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/WindowFunctionExpression.java new file mode 100644 index 000000000000..41ff8549e024 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/WindowFunctionExpression.java @@ -0,0 +1,23 @@ +/* + * 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.sql.ast.tree.predicate.Predicate; + +/** + * Models a window function expression at the SQL AST level. + * + * @author Christian Beikov + */ +public interface WindowFunctionExpression extends FunctionExpression { + + Predicate getFilter(); + + Boolean getRespectNulls(); + + Boolean getFromFirst(); +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/WindowFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/WindowFunctionTest.java new file mode 100644 index 000000000000..6509601277d1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/WindowFunctionTest.java @@ -0,0 +1,168 @@ +/* + * 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.Date; +import java.util.List; + +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.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; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Christian Beikov + */ +@ServiceRegistry +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +public class WindowFunctionTest { + + @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.SupportsWindowFunctions.class) + public void testRowNumberWithoutOrder(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select row_number() over() from EntityOfBasics eob order by 1", Long.class ); + List resultList = q.getResultList(); + assertEquals( 5, resultList.size() ); + assertEquals( 1L, resultList.get( 0 ) ); + assertEquals( 2L, resultList.get( 1 ) ); + assertEquals( 3L, resultList.get( 2 ) ); + assertEquals( 4L, resultList.get( 3 ) ); + assertEquals( 5L, resultList.get( 4 ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsWindowFunctions.class) + public void testRank(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select rank() over(partition by eob.theInt order by eob.id) from EntityOfBasics eob order by 1", Long.class ); + List resultList = q.getResultList(); + assertEquals( 5, resultList.size() ); + assertEquals( 1L, resultList.get( 0 ) ); + assertEquals( 1L, resultList.get( 1 ) ); + assertEquals( 1L, resultList.get( 2 ) ); + assertEquals( 1L, resultList.get( 3 ) ); + assertEquals( 2L, resultList.get( 4 ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsWindowFunctions.class) + public void testSumWithFilterAsWindowFunction(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select sum(eob.theInt) filter (where eob.theInt > 5) over (order by eob.theInt) from EntityOfBasics eob order by eob.theInt", Long.class ); + List resultList = q.getResultList(); + assertEquals( 5L, resultList.size() ); + assertNull( resultList.get( 0 ) ); + assertNull( resultList.get( 1 ) ); + assertEquals( 6L, resultList.get( 2 ) ); + assertEquals( 13L, resultList.get( 3 ) ); + assertEquals( 26L, resultList.get( 4 ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsWindowFunctions.class) + public void testFrame(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select first_value(eob.theInt) over (order by eob.id rows between 2 preceding and current row) from EntityOfBasics eob order by eob.id", Integer.class ); + List resultList = q.getResultList(); + assertEquals( 5, resultList.size() ); + assertEquals( 5, resultList.get( 0 ) ); + assertEquals( 5, resultList.get( 1 ) ); + assertEquals( 5, resultList.get( 2 ) ); + assertEquals( 6, resultList.get( 3 ) ); + assertEquals( 7, resultList.get( 4 ) ); + } + ); + } +} 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 0151dca48643..c3d2085eedfb 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 @@ -424,4 +424,18 @@ public boolean apply(Dialect dialect) { || dialect instanceof SQLServerDialect; } } + + public static class SupportsWindowFunctions implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + // Derby doesn't really support window functions, only row_number() + return dialect.supportsWindowFunctions() && !( dialect instanceof DerbyDialect ); + } + } + + public static class SupportsFilterClause implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + // Derby doesn't really support window functions, only row_number() + return dialect instanceof PostgreSQLDialect; + } + } }