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 2e186361df..8e6c2815b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -313,7 +313,7 @@ public abstract class AbstractHANADialect extends Dialect { functionFactory.listagg_stringAgg( "varchar" ); functionFactory.inverseDistributionOrderedSetAggregates(); - functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); queryEngine.getSqmFunctionRegistry().register( "timestampadd", new IntegralTimestampaddFunction( this, queryEngine.getTypeConfiguration() ) ); 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 ab1b9205d5..412ce05156 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -267,7 +267,7 @@ public class DB2Dialect extends Dialect { functionFactory.listagg( null ); if ( getDB2Version().isSameOrAfter( 11, 1 ) ) { functionFactory.inverseDistributionOrderedSetAggregates(); - functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java index d3ce94f055..4c7568c157 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java @@ -49,7 +49,7 @@ public class DB2iDialect extends DB2Dialect { CommonFunctionFactory functionFactory = new CommonFunctionFactory(queryEngine); functionFactory.listagg( null ); functionFactory.inverseDistributionOrderedSetAggregates(); - functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java index fcfcf84097..1f3c650770 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java @@ -60,7 +60,7 @@ public class DB2zDialect extends DB2Dialect { CommonFunctionFactory functionFactory = new CommonFunctionFactory(queryEngine); functionFactory.listagg( null ); functionFactory.inverseDistributionOrderedSetAggregates(); - functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); } } 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 63d5d79d4b..4ce4828013 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -330,7 +330,7 @@ public class OracleSqlAstTranslator extends AbstractSql } @Override - public void visitOver(Over over) { + 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() 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 114d3566cb..1b425f2adf 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -267,8 +267,8 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { .setParameterTypes(INTEGER) .register(); } - functionFactory.inverseDistributionOrderedSetAggregates(); - functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java new file mode 100644 index 0000000000..18e530e251 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/AggregateWindowEmulationQueryTransformer.java @@ -0,0 +1,440 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.NavigablePath; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.function.SelfRenderingAggregateFunctionSqlAstExpression; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; +import org.hibernate.sql.ast.spi.ExpressionReplacementWalker; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.cte.CteContainer; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.QueryTransformer; +import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.QueryPartTableGroup; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.sql.results.internal.ResolvedSqlSelection; +import org.hibernate.type.BasicType; + +/** + * Since the query spec will render a hypothetical set window function instead of an aggregate, + * the following query transformer will wrap the query spec and apply aggregation in the outer query. + * We can do this because these functions can only be used in the select clause. + * A hypothetical set aggregate function like e.g. "rank" returns the rank of a passed value within + * the ordered set as defined through the WITHIN GROUP clause. + * When used as window function, the function provides the rank of the current row within the ordered set + * as defined for the window frame through the OVER clause, but does not do aggregation. + * The aggregation effect can be achieved by: + * 1. Selecting the elements by which the ordered set is sorted + * 2. In the outer query, add a comparison predicate `function_args`=`sort_expressions` + * 3. Use an arbitrary row produced by the inner query by using e.g. the "min" function + * + * The following query + * + * + * select rank(5) within group (order by e.num) + * from (values (1), (2), (5)) e(num) + * + * + * can be rewritten to + * + * + * select min(t.c1) from ( + * select rank() over (order by e.num), e.num + * from (values (1), (2), (5)) e(num) + * ) t(c1,c2) + * where t.c2 = 5 + * + * + * @author Christian Beikov + */ +public class AggregateWindowEmulationQueryTransformer implements QueryTransformer { + private final Over windowFunction; + private final List withinGroup; + private final List arguments; + + public AggregateWindowEmulationQueryTransformer( + Over windowFunction, + List withinGroup, + List arguments) { + this.windowFunction = windowFunction; + this.withinGroup = withinGroup; + this.arguments = arguments; + } + + @Override + public QuerySpec transform( + CteContainer cteContainer, + QuerySpec querySpec, + SqmToSqlAstConverter converter) { + final SessionFactoryImplementor factory = converter.getCreationContext() + .getSessionFactory(); + final QuerySpec outerQuerySpec = new QuerySpec( querySpec.isRoot() ); + final String identifierVariable = "hhh_"; + final NavigablePath navigablePath = new NavigablePath( + identifierVariable, + identifierVariable + ); + final SelectClause selectClause = outerQuerySpec.getSelectClause(); + final QuerySpec subQuerySpec = querySpec.asSubQuery(); + final SelectClause subSelectClause = subQuerySpec.getSelectClause(); + final List subSelections = subSelectClause.getSqlSelections(); + final List columnNames = new ArrayList<>( subSelections.size() ); + // A map to find the select item position for an expression + // which is needed to decide if we need to introduce synthetic select items + // for group by items, since these group by items are migrated to the outer query + final Map selectionMapping = new HashMap<>( subSelections.size() ); + // Create the expressions/selections for the outer query and the columnNames list + // for the QueryPartTableGroup within which the sub query spec is embedded + for ( int i = 0; i < subSelections.size(); i++ ) { + final BasicValuedMapping mapping = (BasicValuedMapping) subSelections.get( i ) + .getExpressionType(); + final String columnName = "col" + i; + final ColumnReference columnReference = new ColumnReference( + identifierVariable, + columnName, + false, + null, + null, + mapping.getJdbcMapping(), + factory + ); + final Expression expression = subSelections.get( i ).getExpression(); + final Expression finalExpression; + if ( expression == windowFunction ) { + finalExpression = new SelfRenderingAggregateFunctionSqlAstExpression( + "min", + (sqlAppender, sqlAstArguments, walker1) -> { + sqlAppender.appendSql( "min(" ); + sqlAstArguments.get( 0 ).accept( walker1 ); + sqlAppender.append( ')' ); + }, + Collections.singletonList( columnReference ), + null, + (ReturnableType) mapping.getMappedType(), + expression.getExpressionType() + ); + } + else { + finalExpression = columnReference; + selectionMapping.put( expression, i ); + } + columnNames.add( columnName ); + selectClause.addSqlSelection( + new ResolvedSqlSelection( + i + 1, + i, + finalExpression, + (BasicType) mapping.getJdbcMapping() + ) + ); + } + + // Migrate the group by clause to the outer query + // and push group by expressions into the partition by clause of the window function + final List groupByExpressions = new ArrayList<>( + subQuerySpec.getGroupByClauseExpressions().size() + ); + for ( Expression groupByClauseExpression : subQuerySpec.getGroupByClauseExpressions() ) { + final Expression realExpression; + final Expression outerGroupByExpression; + if ( groupByClauseExpression instanceof SqlSelectionExpression ) { + final SqlSelection selection = ( (SqlSelectionExpression) groupByClauseExpression ).getSelection(); + outerGroupByExpression = new SqlSelectionExpression( + selectClause.getSqlSelections().get( selection.getValuesArrayPosition() ) + ); + realExpression = selection.getExpression(); + } + else { + if ( groupByClauseExpression instanceof SqmPathInterpretation ) { + realExpression = ( (SqmPathInterpretation) groupByClauseExpression ).getSqlExpression(); + } + else { + realExpression = groupByClauseExpression; + } + final Integer position = selectionMapping.get( realExpression ); + if ( position == null ) { + // Group by something that has no corresponding selection item, + // so we need to introduce an intermediate selection item + final int valuesPosition = selectClause.getSqlSelections().size(); + final String columnName = "col" + valuesPosition; + final JdbcMapping jdbcMapping = realExpression.getExpressionType() + .getJdbcMappings() + .get( 0 ); + final ColumnReference columnReference = new ColumnReference( + identifierVariable, + columnName, + false, + null, + null, + jdbcMapping, + factory + ); + final int subValuesPosition = subSelectClause.getSqlSelections().size(); + final SqlSelection subSelection = new ResolvedSqlSelection( + subValuesPosition + 1, + subValuesPosition, + realExpression, + (BasicType) jdbcMapping + ); + columnNames.add( columnName ); + subSelectClause.addSqlSelection( subSelection ); + outerGroupByExpression = columnReference; + selectionMapping.put( realExpression, subValuesPosition ); + } + else { + outerGroupByExpression = new SqlSelectionExpression( + selectClause.getSqlSelections().get( position ) + ); + } + } + windowFunction.getPartitions().add( realExpression ); + groupByExpressions.add( outerGroupByExpression ); + } + outerQuerySpec.setGroupByClauseExpressions( groupByExpressions ); + subQuerySpec.setGroupByClauseExpressions( null ); + + // Migrate the having clause to the outer query + if ( subQuerySpec.getHavingClauseRestrictions() != null ) { + final Predicate predicate = new ExpressionReplacementWalker() { + @Override + protected X replaceExpression(X expression) { + if ( expression instanceof Literal || expression instanceof JdbcParameter ) { + return expression; + } + final Expression outerExpression; + if ( expression instanceof SqlSelectionExpression ) { + final SqlSelection selection = ( (SqlSelectionExpression) expression ).getSelection(); + outerExpression = selectClause.getSqlSelections() + .get( selection.getValuesArrayPosition() ) + .getExpression(); + } + else { + final Expression realExpression; + if ( expression instanceof SqmPathInterpretation ) { + realExpression = ( (SqmPathInterpretation) expression ).getSqlExpression(); + } + else { + realExpression = (Expression) expression; + } + final Integer position = selectionMapping.get( realExpression ); + if ( position == null ) { + // An expression that has no corresponding selection item, + // so we need to introduce an intermediate selection item + final int valuesPosition = selectClause.getSqlSelections().size(); + final String columnName = "col" + valuesPosition; + final JdbcMapping jdbcMapping = realExpression.getExpressionType() + .getJdbcMappings() + .get( 0 ); + final ColumnReference columnReference = new ColumnReference( + identifierVariable, + columnName, + false, + null, + null, + jdbcMapping, + factory + ); + final int subValuesPosition = subSelectClause.getSqlSelections().size(); + final SqlSelection subSelection = new ResolvedSqlSelection( + subValuesPosition + 1, + subValuesPosition, + realExpression, + (BasicType) jdbcMapping + ); + columnNames.add( columnName ); + subSelectClause.addSqlSelection( subSelection ); + outerExpression = columnReference; + selectionMapping.put( realExpression, subValuesPosition ); + } + else { + outerExpression = selectClause.getSqlSelections().get( position ) + .getExpression(); + } + } + return (X) outerExpression; + } + }.replaceExpressions( subQuerySpec.getHavingClauseRestrictions() ); + outerQuerySpec.setHavingClauseRestrictions( predicate ); + subQuerySpec.setHavingClauseRestrictions( null ); + } + + // Migrate the order by clause to the outer query + if ( subQuerySpec.hasSortSpecifications() ) { + for ( SortSpecification sortSpecification : subQuerySpec.getSortSpecifications() ) { + final Expression sortExpression = sortSpecification.getSortExpression(); + final Expression outerSortExpression; + if ( sortExpression instanceof SqlSelectionExpression ) { + final SqlSelection selection = ( (SqlSelectionExpression) sortExpression ).getSelection(); + outerSortExpression = new SqlSelectionExpression( + selectClause.getSqlSelections() + .get( selection.getValuesArrayPosition() ) + ); + } + else { + final Expression realExpression; + if ( sortExpression instanceof SqmPathInterpretation ) { + realExpression = ( (SqmPathInterpretation) sortExpression ).getSqlExpression(); + } + else { + realExpression = sortExpression; + } + final Integer position = selectionMapping.get( realExpression ); + if ( position == null ) { + // Group by something that has no corresponding selection item, + // so we need to introduce an intermediate selection item + final int valuesPosition = selectClause.getSqlSelections().size(); + final String columnName = "col" + valuesPosition; + final JdbcMapping jdbcMapping = realExpression.getExpressionType() + .getJdbcMappings() + .get( 0 ); + final ColumnReference columnReference = new ColumnReference( + identifierVariable, + columnName, + false, + null, + null, + jdbcMapping, + factory + ); + final int subValuesPosition = subSelectClause.getSqlSelections().size(); + final SqlSelection subSelection = new ResolvedSqlSelection( + subValuesPosition + 1, + subValuesPosition, + realExpression, + (BasicType) jdbcMapping + ); + columnNames.add( columnName ); + subSelectClause.addSqlSelection( subSelection ); + outerSortExpression = columnReference; + selectionMapping.put( realExpression, subValuesPosition ); + } + else { + outerSortExpression = new SqlSelectionExpression( + selectClause.getSqlSelections().get( position ) + ); + } + } + outerQuerySpec.addSortSpecification( + new SortSpecification( + outerSortExpression, + sortSpecification.getSortOrder(), + sortSpecification.getNullPrecedence() + ) + ); + } + subQuerySpec.getSortSpecifications().clear(); + } + + // We need to add selection items for the expressions we order by to the sub query spec. + final int selectionOffset = columnNames.size(); + // Collect the sorting column references so we can apply the filter later + final List sortingColumns = new ArrayList<>( withinGroup.size() ); + for ( int i = 0; i < withinGroup.size(); i++ ) { + final int valueIndex = selectionOffset + i; + final Expression sortExpression = withinGroup.get( i ).getSortExpression(); + final BasicValuedMapping mapping = (BasicValuedMapping) sortExpression.getExpressionType(); + final String columnName = "col" + valueIndex; + final int oldValueIndex = subSelectClause.getSqlSelections().size(); + columnNames.add( columnName ); + subSelectClause.addSqlSelection( + new ResolvedSqlSelection( + oldValueIndex + 1, + oldValueIndex, + sortExpression, + (BasicType) mapping.getJdbcMapping() + ) + ); + sortingColumns.add( + new ColumnReference( + identifierVariable, + columnName, + false, + null, + null, + mapping.getJdbcMapping(), + factory + ) + ); + } + + if ( arguments != null ) { + // Hypothetical set aggregate functions usually provide some rank based on a value + // i.e. which rank does the value 5 have when ordering by a column ascending + // So we add a filter to the outer query so we can extract the rank + switch ( arguments.size() ) { + case 0: + break; + case 1: + outerQuerySpec.applyPredicate( + new ComparisonPredicate( + sortingColumns.get( 0 ), + ComparisonOperator.EQUAL, + (Expression) arguments.get( 0 ) + ) + ); + break; + default: + outerQuerySpec.applyPredicate( + new ComparisonPredicate( + new SqlTuple( sortingColumns, null ), + ComparisonOperator.EQUAL, + new SqlTuple( + (List) (List) arguments, + null + ) + ) + ); + } + } + + final QueryPartTableGroup queryPartTableGroup = new QueryPartTableGroup( + navigablePath, + null, + subQuerySpec, + identifierVariable, + columnNames, + false, + true, + factory + ); + outerQuerySpec.getFromClause().addRoot( queryPartTableGroup ); + + // Migrate the offset/fetch clause + outerQuerySpec.setOffsetClauseExpression( subQuerySpec.getOffsetClauseExpression() ); + outerQuerySpec.setFetchClauseExpression( + subQuerySpec.getFetchClauseExpression(), + subQuerySpec.getFetchClauseType() + ); + subQuerySpec.setOffsetClauseExpression( null ); + subQuerySpec.setFetchClauseExpression( null, null ); + + return outerQuerySpec; + } +} 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 8b54a1134b..d9cbb804b3 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 @@ -43,14 +43,14 @@ public class CommonFunctionFactory { private final BasicType dateType; private final BasicType timeType; private final BasicType timestampType; - + private final SqmFunctionRegistry functionRegistry; private final TypeConfiguration typeConfiguration; public CommonFunctionFactory(QueryEngine queryEngine) { functionRegistry = queryEngine.getSqmFunctionRegistry(); typeConfiguration = queryEngine.getTypeConfiguration(); - + BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); dateType = basicTypeRegistry.resolve(StandardBasicTypes.DATE); timeType = basicTypeRegistry.resolve(StandardBasicTypes.TIME); @@ -1778,6 +1778,17 @@ public class CommonFunctionFactory { ); } + public void inverseDistributionOrderedSetAggregates_windowEmulation() { + functionRegistry.register( + "percentile_cont", + new InverseDistributionWindowEmulation( "percentile_cont", NUMERIC, typeConfiguration ) + ); + functionRegistry.register( + "percentile_disc", + new InverseDistributionWindowEmulation( "percentile_disc", NUMERIC, typeConfiguration ) + ); + } + public void hypotheticalOrderedSetAggregates() { functionRegistry.register( "rank", @@ -1797,6 +1808,25 @@ public class CommonFunctionFactory { ); } + public void hypotheticalOrderedSetAggregates_windowEmulation() { + functionRegistry.register( + "rank", + new HypotheticalSetWindowEmulation( "rank", StandardBasicTypes.LONG, typeConfiguration ) + ); + functionRegistry.register( + "dense_rank", + new HypotheticalSetWindowEmulation( "dense_rank", StandardBasicTypes.LONG, typeConfiguration ) + ); + functionRegistry.register( + "percent_rank", + new HypotheticalSetWindowEmulation( "percent_rank", StandardBasicTypes.DOUBLE, typeConfiguration ) + ); + functionRegistry.register( + "cume_dist", + new HypotheticalSetWindowEmulation( "cume_dist", StandardBasicTypes.DOUBLE, typeConfiguration ) + ); + } + 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/HypotheticalSetWindowEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetWindowEmulation.java new file mode 100644 index 0000000000..beb14a2cca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/HypotheticalSetWindowEmulation.java @@ -0,0 +1,123 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +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.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.query.sqm.tree.select.SqmSortSpecification; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * @author Christian Beikov + */ +public class HypotheticalSetWindowEmulation extends HypotheticalSetFunction { + + public HypotheticalSetWindowEmulation(String name, BasicTypeReference returnType, TypeConfiguration typeConfiguration) { + super( + name, + returnType, + typeConfiguration + ); + } + + @Override + public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + return new SelfRenderingSqmOrderedSetAggregateFunction<>( + this, + this, + arguments, + filter, + withinGroupClause, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ) { + + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + final Clause currentClause = walker.getCurrentClauseStack().getCurrent(); + if ( currentClause == Clause.OVER ) { + return super.convertToSqlAst( walker ); + } + else if ( currentClause != Clause.SELECT ) { + throw new IllegalArgumentException( "Can't emulate [" + getName() + "] in clause " + currentClause + ". Only the SELECT clause is supported!" ); + } + final ReturnableType resultType = resolveResultType( + walker.getCreationContext().getMappingMetamodel().getTypeConfiguration() + ); + + List arguments = resolveSqlAstArguments( getArguments(), walker ); + ArgumentsValidator argumentsValidator = getArgumentsValidator(); + if ( argumentsValidator != null ) { + argumentsValidator.validateSqlTypes( arguments, getFunctionName() ); + } + List withinGroup; + if ( this.getWithinGroup() == null ) { + withinGroup = Collections.emptyList(); + } + else { + walker.getCurrentClauseStack().push( Clause.ORDER ); + try { + final List sortSpecifications = this.getWithinGroup().getSortSpecifications(); + withinGroup = new ArrayList<>( sortSpecifications.size() ); + for ( SqmSortSpecification sortSpecification : sortSpecifications ) { + final SortSpecification specification = (SortSpecification) walker.visitSortSpecification( sortSpecification ); + if ( specification != null ) { + withinGroup.add( specification ); + } + } + } + finally { + walker.getCurrentClauseStack().pop(); + } + } + final SelfRenderingFunctionSqlAstExpression function = new SelfRenderingOrderedSetAggregateFunctionSqlAstExpression( + getFunctionName(), + getRenderingSupport(), + Collections.emptyList(), + getFilter() == null ? null : (Predicate) getFilter().accept( walker ), + Collections.emptyList(), + resultType, + getMappingModelExpressible( walker, resultType ) + ); + final Over windowFunction = new Over<>( function, new ArrayList<>(), withinGroup ); + walker.registerQueryTransformer( + new AggregateWindowEmulationQueryTransformer( windowFunction, withinGroup, arguments ) + ); + return windowFunction; + } + }; + } +} 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 df78a7ce22..981e003ab6 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 @@ -57,62 +57,13 @@ public class InverseDistributionFunction extends AbstractSqmSelfRenderingFunctio ReturnableType impliedResultType, QueryEngine queryEngine, TypeConfiguration typeConfiguration) { - return new SelfRenderingSqmOrderedSetAggregateFunction<>( - this, - this, + return new SelfRenderingInverseDistributionFunction<>( arguments, filter, withinGroupClause, impliedResultType, - getArgumentsValidator(), - getReturnTypeResolver(), - queryEngine.getCriteriaBuilder(), - getName() - ) { - - @Override - protected ReturnableType resolveResultType(TypeConfiguration typeConfiguration) { - return (ReturnableType) withinGroupClause.getSortSpecifications().get( 0 ).getSortExpression() - .getExpressible(); - } - - @Override - protected MappingModelExpressible getMappingModelExpressible( - SqmToSqlAstConverter walker, - ReturnableType resultType) { - MappingModelExpressible mapping; - if ( resultType instanceof MappingModelExpressible) { - // here we have a BasicType, which can be cast - // directly to BasicValuedMapping - mapping = (MappingModelExpressible) resultType; - } - else { - // here we have something that is not a BasicType, - // and we have no way to get a BasicValuedMapping - // from it directly - final Expression expression = (Expression) withinGroupClause.getSortSpecifications().get( 0 ) - .getSortExpression() - .accept( walker ); - if ( expression.getExpressionType() instanceof BasicValuedMapping ) { - return (BasicValuedMapping) expression.getExpressionType(); - } - try { - final MappingMetamodelImplementor domainModel = walker.getCreationContext() - .getSessionFactory() - .getRuntimeMetamodels() - .getMappingMetamodel(); - return domainModel.resolveMappingExpressible( - getNodeType(), - walker.getFromClauseAccess()::getTableGroup - ); - } - catch (Exception e) { - return null; // this works at least approximately - } - } - return mapping; - } - }; + queryEngine + ); } @Override @@ -174,4 +125,71 @@ public class InverseDistributionFunction extends AbstractSqmSelfRenderingFunctio } } + protected class SelfRenderingInverseDistributionFunction extends SelfRenderingSqmOrderedSetAggregateFunction { + + private final SqmOrderByClause withinGroupClause; + + public SelfRenderingInverseDistributionFunction( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, QueryEngine queryEngine) { + super( + InverseDistributionFunction.this, + InverseDistributionFunction.this, + arguments, + filter, + withinGroupClause, + impliedResultType, + InverseDistributionFunction.this.getArgumentsValidator(), + InverseDistributionFunction.this.getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + InverseDistributionFunction.this.getName() + ); + this.withinGroupClause = withinGroupClause; + } + + @Override + protected ReturnableType resolveResultType(TypeConfiguration typeConfiguration) { + return (ReturnableType) withinGroupClause.getSortSpecifications().get( 0 ).getSortExpression() + .getExpressible(); + } + + @Override + protected MappingModelExpressible getMappingModelExpressible( + SqmToSqlAstConverter walker, + ReturnableType resultType) { + MappingModelExpressible mapping; + if ( resultType instanceof MappingModelExpressible) { + // here we have a BasicType, which can be cast + // directly to BasicValuedMapping + mapping = (MappingModelExpressible) resultType; + } + else { + // here we have something that is not a BasicType, + // and we have no way to get a BasicValuedMapping + // from it directly + final Expression expression = (Expression) withinGroupClause.getSortSpecifications().get( 0 ) + .getSortExpression() + .accept( walker ); + if ( expression.getExpressionType() instanceof BasicValuedMapping ) { + return (BasicValuedMapping) expression.getExpressionType(); + } + try { + final MappingMetamodelImplementor domainModel = walker.getCreationContext() + .getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel(); + return domainModel.resolveMappingExpressible( + getNodeType(), + walker.getFromClauseAccess()::getTableGroup + ); + } + catch (Exception e) { + return null; // this works at least approximately + } + } + return mapping; + } + } } 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 new file mode 100644 index 0000000000..128879f304 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/InverseDistributionWindowEmulation.java @@ -0,0 +1,118 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +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.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.query.sqm.tree.select.SqmSortSpecification; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * @author Christian Beikov + */ +public class InverseDistributionWindowEmulation extends InverseDistributionFunction { + + public InverseDistributionWindowEmulation(String name, FunctionParameterType parameterType, TypeConfiguration typeConfiguration) { + super( + name, + parameterType, + typeConfiguration + ); + } + + @Override + public SelfRenderingSqmAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine, + TypeConfiguration typeConfiguration) { + return new SelfRenderingInverseDistributionFunction<>( + arguments, + filter, + withinGroupClause, + impliedResultType, + queryEngine + ) { + + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + final Clause currentClause = walker.getCurrentClauseStack().getCurrent(); + if ( currentClause == Clause.OVER ) { + return super.convertToSqlAst( walker ); + } + else if ( currentClause != Clause.SELECT ) { + throw new IllegalArgumentException( "Can't emulate [" + getName() + "] in clause " + currentClause + ". Only the SELECT clause is supported!" ); + } + final ReturnableType resultType = resolveResultType( + walker.getCreationContext().getMappingMetamodel().getTypeConfiguration() + ); + + List arguments = resolveSqlAstArguments( getArguments(), walker ); + ArgumentsValidator argumentsValidator = getArgumentsValidator(); + if ( argumentsValidator != null ) { + argumentsValidator.validateSqlTypes( arguments, getFunctionName() ); + } + List withinGroup; + if ( this.getWithinGroup() == null ) { + withinGroup = Collections.emptyList(); + } + else { + walker.getCurrentClauseStack().push( Clause.ORDER ); + try { + final List sortSpecifications = this.getWithinGroup().getSortSpecifications(); + withinGroup = new ArrayList<>( sortSpecifications.size() ); + for ( SqmSortSpecification sortSpecification : sortSpecifications ) { + final SortSpecification specification = (SortSpecification) walker.visitSortSpecification( sortSpecification ); + if ( specification != null ) { + withinGroup.add( specification ); + } + } + } + finally { + walker.getCurrentClauseStack().pop(); + } + } + final SelfRenderingFunctionSqlAstExpression function = new SelfRenderingOrderedSetAggregateFunctionSqlAstExpression( + getFunctionName(), + getRenderingSupport(), + arguments, + getFilter() == null ? null : (Predicate) getFilter().accept( walker ), + withinGroup, + resultType, + getMappingModelExpressible( walker, resultType ) + ); + final Over windowFunction = new Over<>( function, new ArrayList<>(), Collections.emptyList() ); + walker.registerQueryTransformer( + new AggregateWindowEmulationQueryTransformer( windowFunction, withinGroup, null ) + ); + return windowFunction; + } + }; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java index a166737a7b..30ac0c382c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java @@ -272,7 +272,7 @@ public class CteInsertHandler implements InsertHandler { new SqlSelectionImpl( 1, 0, - new Over( + new Over<>( new SelfRenderingFunctionSqlAstExpression( "row_number", (appender, args, walker) -> appender.appendSql( @@ -985,7 +985,7 @@ public class CteInsertHandler implements InsertHandler { new SqlSelectionImpl( 1, 0, - new Over( + new Over<>( new SelfRenderingFunctionSqlAstExpression( "row_number", (appender, args, walker) -> appender.appendSql( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java index 0dec64b438..028c1ff1a3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java @@ -222,7 +222,7 @@ public class TableBasedInsertHandler implements InsertHandler { new SqlSelectionImpl( 1, 0, - new Over( + new Over<>( new SelfRenderingFunctionSqlAstExpression( "row_number", (appender, args, walker) -> appender.appendSql( 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 16950ebbfc..7585c1ed26 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 @@ -1296,7 +1296,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base identifierSelection = new SqlSelectionImpl( 1, 0, - new Over( + new Over<>( new SelfRenderingFunctionSqlAstExpression( "row_number", (appender, args, walker) -> appender.appendSql( "row_number()" ), diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java index e842fcb233..62a08e10e3 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstWalker.java @@ -123,7 +123,7 @@ public interface SqlAstWalker { void visitDistinct(Distinct distinct); - void visitOverflow(Overflow distinct); + void visitOverflow(Overflow overflow); void visitStar(Star star); @@ -143,7 +143,7 @@ public interface SqlAstWalker { void visitSummarization(Summarization every); - void visitOver(Over over); + void visitOver(Over over); void visitSelfRenderingExpression(SelfRenderingExpression expression); 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 30a4bd50e1..0c5f23758e 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 @@ -101,6 +101,7 @@ import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.LiteralAsParameter; import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; +import org.hibernate.sql.ast.tree.expression.OrderedSetAggregateFunctionExpression; import org.hibernate.sql.ast.tree.expression.Over; import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; @@ -3242,8 +3243,17 @@ public abstract class AbstractSqlAstTranslator implemen } @Override - public void visitOver(Over over) { - over.getExpression().accept( this ); + public void visitOver(Over over) { + final Expression overExpression = over.getExpression(); + overExpression.accept( this ); + final boolean orderedSetAggregate; + if ( overExpression instanceof OrderedSetAggregateFunctionExpression ) { + final OrderedSetAggregateFunctionExpression expression = (OrderedSetAggregateFunctionExpression) overExpression; + orderedSetAggregate = expression.getWithinGroup() != null && !expression.getWithinGroup().isEmpty(); + } + else { + orderedSetAggregate = false; + } visitOverClause( over.getPartitions(), over.getOrderList(), @@ -3252,7 +3262,8 @@ public abstract class AbstractSqlAstTranslator implemen over.getStartExpression(), over.getEndKind(), over.getEndExpression(), - over.getExclusion() + over.getExclusion(), + orderedSetAggregate ); } @@ -3267,7 +3278,8 @@ public abstract class AbstractSqlAstTranslator implemen null, Over.FrameKind.CURRENT_ROW, null, - Over.FrameExclusion.NO_OTHERS + Over.FrameExclusion.NO_OTHERS, + false ); } @@ -3279,12 +3291,15 @@ public abstract class AbstractSqlAstTranslator implemen Expression startExpression, Over.FrameKind endKind, Expression endExpression, - Over.FrameExclusion exclusion) { + Over.FrameExclusion exclusion, + boolean orderedSetAggregate) { try { clauseStack.push( Clause.OVER ); appendSql( " over(" ); visitPartitionByClause( partitionExpressions ); - renderOrderBy( !partitionExpressions.isEmpty(), sortSpecifications ); + 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 ) { // This is the default, so we don't need to render anything } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java index 616fce50ee..df2be83f47 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java @@ -232,7 +232,7 @@ public class AbstractSqlAstWalker implements SqlAstWalker { } @Override - public void visitOver(Over over) { + public void visitOver(Over over) { over.getExpression().accept( this ); for ( Expression partition : over.getPartitions() ) { partition.accept( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java index e52ee28ddf..a5e8cb9d9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AggregateFunctionChecker.java @@ -100,7 +100,7 @@ public class AggregateFunctionChecker extends AbstractSqlAstWalker { } @Override - public void visitOver(Over over) { + public void visitOver(Over over) { // Only need to visit the expression over which the window is created as the window definition can't have aggregates // If the expression is an aggregate function, this means the aggregate is used as window function, which is fine // We only care about actually aggregating functions, which might be an argument of this function though diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java new file mode 100644 index 0000000000..11110dc076 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ExpressionReplacementWalker.java @@ -0,0 +1,566 @@ +/* + * 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.spi; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.query.sqm.tree.expression.Conversion; +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.Any; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; +import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Collation; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.ConvertedQueryLiteral; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Duration; +import org.hibernate.sql.ast.tree.expression.DurationUnit; +import org.hibernate.sql.ast.tree.expression.EntityTypeLiteral; +import org.hibernate.sql.ast.tree.expression.Every; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.ExtractUnit; +import org.hibernate.sql.ast.tree.expression.Format; +import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; +import org.hibernate.sql.ast.tree.expression.Over; +import org.hibernate.sql.ast.tree.expression.Overflow; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.Star; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.expression.TrimSpecification; +import org.hibernate.sql.ast.tree.expression.UnaryOperation; +import org.hibernate.sql.ast.tree.from.FromClause; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.InsertStatement; +import org.hibernate.sql.ast.tree.predicate.BetweenPredicate; +import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.ExistsPredicate; +import org.hibernate.sql.ast.tree.predicate.FilterPredicate; +import org.hibernate.sql.ast.tree.predicate.GroupedPredicate; +import org.hibernate.sql.ast.tree.predicate.InListPredicate; +import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; +import org.hibernate.sql.ast.tree.predicate.NegatedPredicate; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.predicate.SelfRenderingPredicate; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; + +/** + * A walker that allows to replace expressions. + * + * @author Christian Beikov + */ +public class ExpressionReplacementWalker implements SqlAstWalker { + + /** + * To avoid introducing a walker/visitor that returns an object, + * we use a heap variable to transfer the return value. + */ + private SqlAstNode returnedNode; + + public final X replaceExpressions(X expression) { + expression.accept( this ); + //noinspection unchecked + return (X) returnedNode; + } + + protected X replaceExpression(X expression) { + return expression; + } + + private void doReplaceExpression(SqlAstNode expression) { + returnedNode = replaceExpression( expression ); + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + doReplaceExpression( columnReference ); + } + + @Override + public void visitExtractUnit(ExtractUnit extractUnit) { + doReplaceExpression( extractUnit ); + } + + @Override + public void visitFormat(Format format) { + doReplaceExpression( format ); + } + + @Override + public void visitDistinct(Distinct distinct) { + doReplaceExpression( distinct ); + } + + @Override + public void visitOverflow(Overflow overflow) { + doReplaceExpression( overflow ); + } + + @Override + public void visitStar(Star star) { + doReplaceExpression( star ); + } + + @Override + public void visitTrimSpecification(TrimSpecification trimSpecification) { + doReplaceExpression( trimSpecification ); + } + + @Override + public void visitCastTarget(CastTarget castTarget) { + doReplaceExpression( castTarget ); + } + + @Override + public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) { + doReplaceExpression( arithmeticExpression ); + } + + @Override + public void visitCaseSearchedExpression(CaseSearchedExpression caseSearchedExpression) { + doReplaceExpression( caseSearchedExpression ); + } + + @Override + public void visitCaseSimpleExpression(CaseSimpleExpression caseSimpleExpression) { + doReplaceExpression( caseSimpleExpression ); + } + + @Override + public void visitAny(Any any) { + doReplaceExpression( any ); + } + + @Override + public void visitEvery(Every every) { + doReplaceExpression( every ); + } + + @Override + public void visitSummarization(Summarization every) { + doReplaceExpression( every ); + } + + @Override + public void visitOver(Over over) { + doReplaceExpression( over ); + } + + @Override + public void visitSelfRenderingExpression(SelfRenderingExpression expression) { + doReplaceExpression( expression ); + } + + @Override + public void visitSqlSelectionExpression(SqlSelectionExpression expression) { + doReplaceExpression( expression ); + } + + @Override + public void visitEntityTypeLiteral(EntityTypeLiteral expression) { + doReplaceExpression( expression ); + } + + @Override + public void visitTuple(SqlTuple tuple) { + doReplaceExpression( tuple ); + } + + @Override + public void visitCollation(Collation collation) { + doReplaceExpression( collation ); + } + + @Override + public void visitParameter(JdbcParameter jdbcParameter) { + doReplaceExpression( jdbcParameter ); + } + + @Override + public void visitJdbcLiteral(JdbcLiteral jdbcLiteral) { + doReplaceExpression( jdbcLiteral ); + } + + @Override + public void visitQueryLiteral(QueryLiteral queryLiteral) { + doReplaceExpression( queryLiteral ); + } + + @Override + public void acceptConvertedQueryLiteral(ConvertedQueryLiteral convertedQueryLiteral) { + doReplaceExpression( convertedQueryLiteral ); + } + + @Override + public void visitUnaryOperationExpression(UnaryOperation unaryOperationExpression) { + doReplaceExpression( unaryOperationExpression ); + } + + @Override + public void visitModifiedSubQueryExpression(ModifiedSubQueryExpression expression) { + doReplaceExpression( expression ); + } + + @Override + public void visitDurationUnit(DurationUnit durationUnit) { + doReplaceExpression( durationUnit ); + } + + @Override + public void visitDuration(Duration duration) { + doReplaceExpression( duration ); + } + + @Override + public void visitConversion(Conversion conversion) { + doReplaceExpression( conversion ); + } + + @Override + public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { + doReplaceExpression( booleanExpressionPredicate ); + } + + @Override + public void visitSqlFragmentPredicate(SqlFragmentPredicate predicate) { + doReplaceExpression( predicate ); + } + + @Override + public void visitBetweenPredicate(BetweenPredicate betweenPredicate) { + final Expression expression = replaceExpression( betweenPredicate.getExpression() ); + final Expression lowerBound = replaceExpression( betweenPredicate.getLowerBound() ); + final Expression upperBound = replaceExpression( betweenPredicate.getUpperBound() ); + if ( expression != betweenPredicate.getExpression() + || lowerBound != betweenPredicate.getLowerBound() + || upperBound != betweenPredicate.getUpperBound() ) { + returnedNode = new BetweenPredicate( + expression, + lowerBound, + upperBound, + betweenPredicate.isNegated(), + betweenPredicate.getExpressionType() + ); + } + else { + returnedNode = betweenPredicate; + } + } + + @Override + public void visitGroupedPredicate(GroupedPredicate groupedPredicate) { + groupedPredicate.getSubPredicate().accept( this ); + if ( returnedNode != groupedPredicate.getSubPredicate() ) { + returnedNode = new GroupedPredicate( (Predicate) returnedNode ); + } + else { + returnedNode = groupedPredicate; + } + } + + @Override + public void visitInListPredicate(InListPredicate inListPredicate) { + final Expression testExpression = replaceExpression( inListPredicate.getTestExpression() ); + List items = null; + final List listExpressions = inListPredicate.getListExpressions(); + for ( int i = 0; i < listExpressions.size(); i++ ) { + final Expression listExpression = listExpressions.get( i ); + final Expression newListExpression = replaceExpression( listExpression ); + if ( newListExpression != listExpression ) { + if ( items == null ) { + items = new ArrayList<>( listExpressions ); + } + items.set( i, newListExpression ); + } + } + if ( testExpression != inListPredicate.getTestExpression() || items != null ) { + returnedNode = new InListPredicate( + testExpression, + items == null ? listExpressions : items, + inListPredicate.isNegated(), + inListPredicate.getExpressionType() + ); + } + else { + returnedNode = inListPredicate; + } + } + + @Override + public void visitInSubQueryPredicate(InSubQueryPredicate inSubQueryPredicate) { + final Expression testExpression = replaceExpression( inSubQueryPredicate.getTestExpression() ); + final QueryPart subQuery = replaceExpression( inSubQueryPredicate.getSubQuery() ); + if ( testExpression != inSubQueryPredicate.getTestExpression() + || subQuery != inSubQueryPredicate.getSubQuery() ) { + returnedNode = new InSubQueryPredicate( + testExpression, + subQuery, + inSubQueryPredicate.isNegated(), + inSubQueryPredicate.getExpressionType() + ); + } + else { + returnedNode = inSubQueryPredicate; + } + } + + @Override + public void visitExistsPredicate(ExistsPredicate existsPredicate) { + final QueryPart queryPart = replaceExpression( existsPredicate.getExpression() ); + if ( queryPart != existsPredicate.getExpression() ) { + returnedNode = new ExistsPredicate( + queryPart, + existsPredicate.isNegated(), + existsPredicate.getExpressionType() + ); + } + else { + returnedNode = existsPredicate; + } + } + + @Override + public void visitJunction(Junction junction) { + final List predicates = junction.getPredicates(); + List newPredicates = null; + for ( int i = 0; i < predicates.size(); i++ ) { + predicates.get( i ).accept( this ); + if ( returnedNode != predicates.get( i ) ) { + if ( newPredicates == null ) { + newPredicates = new ArrayList<>( predicates ); + } + newPredicates.set( i, (Predicate) returnedNode ); + } + } + if ( newPredicates != null ) { + returnedNode = new Junction( + junction.getNature(), + newPredicates, + junction.getExpressionType() + ); + } + else { + returnedNode = junction; + } + } + + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + final Expression matchExpression = replaceExpression( likePredicate.getMatchExpression() ); + final Expression patternExpression = replaceExpression( likePredicate.getPattern() ); + final Expression escapeExpression = likePredicate.getEscapeCharacter() == null + ? null + : replaceExpression( likePredicate.getEscapeCharacter() ); + if ( matchExpression != likePredicate.getMatchExpression() + || patternExpression != likePredicate.getPattern() + || escapeExpression != likePredicate.getEscapeCharacter() ) { + returnedNode = new LikePredicate( + matchExpression, + patternExpression, + escapeExpression, + likePredicate.isNegated(), + likePredicate.isCaseSensitive(), + likePredicate.getExpressionType() + ); + } + else { + returnedNode = likePredicate; + } + } + + @Override + public void visitNegatedPredicate(NegatedPredicate negatedPredicate) { + negatedPredicate.getPredicate().accept( this ); + if ( returnedNode != negatedPredicate.getPredicate() ) { + returnedNode = new NegatedPredicate( (Predicate) returnedNode ); + } + else { + returnedNode = negatedPredicate; + } + } + + @Override + public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) { + final Expression expression = replaceExpression( nullnessPredicate.getExpression() ); + if ( expression != nullnessPredicate.getExpression() ) { + returnedNode = new NullnessPredicate( + expression, + nullnessPredicate.isNegated(), + nullnessPredicate.getExpressionType() + ); + } + else { + returnedNode = nullnessPredicate; + } + } + + @Override + public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) { + final Expression lhs = replaceExpression( comparisonPredicate.getLeftHandExpression() ); + final Expression rhs = replaceExpression( comparisonPredicate.getRightHandExpression() ); + if ( lhs != comparisonPredicate.getLeftHandExpression() + || rhs != comparisonPredicate.getRightHandExpression() ) { + returnedNode = new ComparisonPredicate( + lhs, + comparisonPredicate.getOperator(), + rhs, + comparisonPredicate.getExpressionType() + ); + } + else { + returnedNode = comparisonPredicate; + } + } + + @Override + public void visitSelfRenderingPredicate(SelfRenderingPredicate selfRenderingPredicate) { + final SelfRenderingExpression selfRenderingExpression = replaceExpression( selfRenderingPredicate.getSelfRenderingExpression() ); + if ( selfRenderingExpression != selfRenderingPredicate.getSelfRenderingExpression() ) { + returnedNode = new SelfRenderingPredicate( + selfRenderingExpression + ); + } + else { + returnedNode = selfRenderingPredicate; + } + } + + /* Unsupported */ + + @Override + public void visitSelectStatement(SelectStatement statement) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitDeleteStatement(DeleteStatement statement) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitUpdateStatement(UpdateStatement statement) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitInsertStatement(InsertStatement statement) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitAssignment(Assignment assignment) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitQueryGroup(QueryGroup queryGroup) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitQuerySpec(QuerySpec querySpec) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitSortSpecification(SortSpecification sortSpecification) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitOffsetFetchClause(QueryPart querySpec) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitSelectClause(SelectClause selectClause) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitSqlSelection(SqlSelection sqlSelection) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitFromClause(FromClause fromClause) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitTableGroup(TableGroup tableGroup) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitTableGroupJoin(TableGroupJoin tableGroupJoin) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitNamedTableReference(NamedTableReference tableReference) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitFunctionTableReference(FunctionTableReference tableReference) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitTableReferenceJoin(TableReferenceJoin tableReferenceJoin) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitFilterPredicate(FilterPredicate filterPredicate) { + throw new UnsupportedOperationException(); + } + + @Override + public void visitFilterFragmentPredicate(FilterPredicate.FilterFragmentPredicate fragmentPredicate) { + throw new UnsupportedOperationException(); + } +} 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 32cacc9a12..eefdbc8f3c 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,13 +9,19 @@ package org.hibernate.sql.ast.tree.expression; import java.util.List; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.sqm.sql.internal.DomainResultProducer; import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.basic.BasicResult; /** * @author Christian Beikov */ -public class Over implements Expression { +public class Over implements Expression, DomainResultProducer { private final Expression expression; private final List partitions; @@ -109,6 +115,29 @@ public class Over implements Expression { walker.visitOver( this ); } + @Override + public DomainResult createDomainResult(String resultVariable, DomainResultCreationState creationState) { + final SqlSelection sqlSelection = createSelection( creationState.getSqlAstCreationState() ); + return new BasicResult( + sqlSelection.getValuesArrayPosition(), + resultVariable, + expression.getExpressionType().getJdbcMappings().get( 0 ).getMappedJavaType() + ); + } + + @Override + public void applySqlSelections(DomainResultCreationState creationState) { + createSelection( creationState.getSqlAstCreationState() ); + } + + private SqlSelection createSelection(SqlAstCreationState creationState) { + return creationState.getSqlExpressionResolver().resolveSqlSelection( + this, + expression.getExpressionType().getJdbcMappings().get( 0 ).getMappedJavaType(), + creationState.getCreationContext().getSessionFactory().getTypeConfiguration() + ); + } + public static enum FrameMode { ROWS, RANGE, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/Junction.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/Junction.java index c6e49df4a9..4d9cf04911 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/Junction.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/Junction.java @@ -29,7 +29,7 @@ public class Junction implements Predicate { private final Nature nature; private final JdbcMappingContainer expressionType; - private final List predicates = new ArrayList<>(); + private final List predicates; public Junction() { this( Nature.CONJUNCTION ); @@ -42,6 +42,16 @@ public class Junction implements Predicate { public Junction(Nature nature, JdbcMappingContainer expressionType) { this.nature = nature; this.expressionType = expressionType; + this.predicates = new ArrayList<>(); + } + + public Junction( + Nature nature, + List predicates, + JdbcMappingContainer expressionType) { + this.nature = nature; + this.expressionType = expressionType; + this.predicates = predicates; } public void add(Predicate predicate) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java index 47a9f51d93..8c513b59e7 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QueryPart.java @@ -34,6 +34,15 @@ public abstract class QueryPart implements SqlAstNode, Expression, DomainResultP this.isRoot = isRoot; } + protected QueryPart(boolean isRoot, QueryPart original) { + this.isRoot = isRoot; + this.hasPositionalSortItem = original.hasPositionalSortItem; + this.sortSpecifications = original.sortSpecifications; + this.offsetClauseExpression = original.offsetClauseExpression; + this.fetchClauseExpression = original.fetchClauseExpression; + this.fetchClauseType = original.fetchClauseType; + } + public abstract QuerySpec getFirstQuerySpec(); public abstract QuerySpec getLastQuerySpec(); @@ -100,7 +109,7 @@ public abstract class QueryPart implements SqlAstNode, Expression, DomainResultP public void setFetchClauseExpression(Expression fetchClauseExpression, FetchClauseType fetchClauseType) { if ( fetchClauseExpression == null ) { this.fetchClauseExpression = null; - this.fetchClauseType = null; + this.fetchClauseType = FetchClauseType.ROWS_ONLY; } else { if ( fetchClauseType == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java index fcdcd66b6a..0e8cdc9b94 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/QuerySpec.java @@ -35,22 +35,36 @@ import org.hibernate.type.spi.TypeConfiguration; public class QuerySpec extends QueryPart implements SqlAstNode, PredicateContainer, Expression, DomainResultProducer { private final FromClause fromClause; - private final SelectClause selectClause = new SelectClause(); + private final SelectClause selectClause; private Predicate whereClauseRestrictions; - private boolean hasPositionalGroupItem; private List groupByClauseExpressions = Collections.emptyList(); private Predicate havingClauseRestrictions; public QuerySpec(boolean isRoot) { super( isRoot ); this.fromClause = new FromClause(); + this.selectClause = new SelectClause(); } public QuerySpec(boolean isRoot, int expectedNumberOfRoots) { super( isRoot ); this.fromClause = new FromClause( expectedNumberOfRoots ); + this.selectClause = new SelectClause(); + } + + private QuerySpec(QuerySpec original) { + super( false, original ); + this.fromClause = original.fromClause; + this.selectClause = original.selectClause; + this.whereClauseRestrictions = original.whereClauseRestrictions; + this.groupByClauseExpressions = original.groupByClauseExpressions; + this.havingClauseRestrictions = original.havingClauseRestrictions; + } + + public QuerySpec asSubQuery() { + return isRoot() ? new QuerySpec( this ) : this; } @Override @@ -94,20 +108,8 @@ public class QuerySpec extends QueryPart implements SqlAstNode, PredicateContain return groupByClauseExpressions; } - public boolean hasPositionalGroupItem() { - return hasPositionalGroupItem; - } - public void setGroupByClauseExpressions(List groupByClauseExpressions) { this.groupByClauseExpressions = groupByClauseExpressions == null ? Collections.emptyList() : groupByClauseExpressions; - if ( isRoot() ) { - for ( int i = 0; i < groupByClauseExpressions.size(); i++ ) { - final Expression groupItem = groupByClauseExpressions.get( i ); - if ( groupItem instanceof SqmAliasedNodeRef ) { - hasPositionalGroupItem = true; - } - } - } } public Predicate getHavingClauseRestrictions() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java index 7d9d4974e7..e595ff732d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/OrderedSetAggregateTest.java @@ -10,9 +10,6 @@ import java.util.Arrays; import java.util.Date; import java.util.List; -import org.hibernate.dialect.AbstractHANADialect; -import org.hibernate.dialect.SQLServerDialect; - import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; import org.hibernate.testing.orm.junit.DialectFeatureChecks; @@ -21,11 +18,11 @@ import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; -import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -141,7 +138,6 @@ public class OrderedSetAggregateTest { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsInverseDistributionFunctions.class) - @SkipForDialect(dialectClass = SQLServerDialect.class, reason = "The function is only supported as window function and needs emulation") public void testInverseDistribution(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -153,9 +149,7 @@ public class OrderedSetAggregateTest { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class) - @SkipForDialect(dialectClass = SQLServerDialect.class, reason = "The function is only supported as window function and needs emulation") - @SkipForDialect(dialectClass = AbstractHANADialect.class, matchSubTypes = true, reason = "The function is only supported as window function and needs emulation") - public void testHypotheticalSet(SessionFactoryScope scope) { + public void testHypotheticalSetPercentRank(SessionFactoryScope scope) { scope.inTransaction( session -> { TypedQuery q = session.createQuery( "select percent_rank(5) within group (order by eob.theInt asc) from EntityOfBasics eob", Double.class ); @@ -163,4 +157,30 @@ public class OrderedSetAggregateTest { } ); } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class) + public void testHypotheticalSetRank(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select rank(5) within group (order by eob.theInt asc) from EntityOfBasics eob", Long.class ); + assertEquals( 1L, q.getSingleResult() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class) + public void testHypotheticalSetRankWithGroupByHavingOrderByLimit(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TypedQuery q = session.createQuery( "select eob2.id, rank(5) within group (order by eob.theInt asc) from EntityOfBasics eob cross join EntityOfBasics eob2 group by eob2.id having eob2.id > 1 order by 1,2 offset 1", Tuple.class ); + List resultList = q.getResultList(); + assertEquals( 3, resultList.size() ); + assertEquals( 1L, resultList.get( 0 ).get( 1, Long.class ) ); + assertEquals( 1L, resultList.get( 1 ).get( 1, Long.class ) ); + assertEquals( 1L, resultList.get( 2 ).get( 1, Long.class ) ); + } + ); + } }