HHH-15654 Criteria APIs for window and ordered-set aggregate functions

This commit is contained in:
Marco Belladelli 2022-11-30 17:40:23 +01:00 committed by Christian Beikov
parent bc36eb3eeb
commit 2f1f6870b6
12 changed files with 1389 additions and 180 deletions

View File

@ -892,4 +892,240 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
*/
@Incubating
JpaSearchOrder desc(JpaCteCriteriaAttribute x, boolean nullsFirst);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Window functions
/**
* Create an empty window to use for window and set-order aggregate functions.
*
* @return the empty window
*/
JpaWindow createWindow();
/**
* Create a generic window function expression that will be applied
* over the specified {@link JpaWindow window}.
*
* @param name name of the window function
* @param type type of this expression
* @param window window over which the function will be applied
* @param args arguments to the function
* @param <T> type of this expression
*
* @return window function expression
*/
<T> JpaExpression<T> windowFunction(String name, Class<T> type, JpaWindow window, Expression<?>... args);
/**
* Create a {@code row_number} window function expression.
*
* @param window window over which the function will be applied
*
* @return window function expression
*
* @see #windowFunction
*/
JpaExpression<Long> rowNumber(JpaWindow window);
/**
* Create a {@code first_value} window function expression.
*
* @param argument argument expression to pass to {@code first_value}
* @param window window over which the function will be applied
* @param <T> type of the expression
*
* @return window function expression
*
* @see #windowFunction
*/
<T> JpaExpression<T> firstValue(Expression<T> argument, JpaWindow window);
/**
* Create a {@code last_value} window function expression.
*
* @param argument argument expression to pass to {@code last_value}
* @param window window over which the function will be applied
* @param <T> type of the expression
*
* @return window function expression
*
* @see #windowFunction
*/
<T> JpaExpression<T> lastValue(Expression<T> argument, JpaWindow window);
/**
* Create a {@code nth_value} window function expression.
*
* @param argument argument expression to pass to {@code nth_value}
* @param n the {@code N} argument for the function
* @param window window over which the function will be applied
* @param <T> type of the expression
*
* @return window function expression
*
* @see #windowFunction
*/
<T> JpaExpression<T> nthValue(Expression<T> argument, Expression<Integer> n, JpaWindow window);
/**
* Create a {@code rank} window function expression.
*
* @param window window over which the function will be applied
*
* @return window function expression
*
* @see #windowFunction
*/
JpaExpression<Long> rank(JpaWindow window);
/**
* Create a {@code dense_rank} window function expression.
*
* @param window window over which the function will be applied
*
* @return window function expression
*
* @see #windowFunction
*/
JpaExpression<Long> denseRank(JpaWindow window);
/**
* Create a {@code percent_rank} window function expression.
*
* @param window window over which the function will be applied
*
* @return window function expression
*
* @see #windowFunction
*/
JpaExpression<Double> percentRank(JpaWindow window);
/**
* Create a {@code cume_dist} window function expression.
*
* @param window window over which the function will be applied
*
* @return window function expression
*
* @see #windowFunction
*/
JpaExpression<Double> cumeDist(JpaWindow window);
/**
* Create a generic ordered set-aggregate function expression.
*
* @param name name of the ordered set-aggregate function
* @param type type of this expression
* @param order order by clause used in within group
* @param filter optional filter clause
* @param args optional arguments to the function
* @param <T> type of this expression
*
* @return ordered set-aggregate function expression
*/
<T> JpaExpression<T> functionWithinGroup(
String name,
Class<T> type,
JpaOrder order,
JpaPredicate filter,
Expression<?>... args);
/**
* Create a generic ordered set-aggregate function expression.
*
* @param name name of the ordered set-aggregate function
* @param type type of this expression
* @param order order by clause used in within group
* @param args optional arguments to the function
* @param <T> type of this expression
*
* @return ordered set-aggregate function expression
*
* @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, Expression...) functionWithinGroup
*/
<T> JpaExpression<T> functionWithinGroup(String name, Class<T> type, JpaOrder order, Expression<?>... args);
/**
* Create a {@code listagg} ordered set-aggregate function expression.
*
* @param order order by clause used in within group
* @param filter optional filter clause
* @param argument values to join
* @param separator the separator used to join the values
*
* @return ordered set-aggregate expression
*
* @see #functionWithinGroup
*/
JpaExpression<String> listagg(
JpaOrder order,
JpaPredicate filter,
Expression<String> argument,
Expression<String> separator);
/**
* Create a {@code mode} ordered set-aggregate function expression.
*
* @param order order by clause used in within group
* @param filter optional filter clause
* @param argument argument to the function
*
* @return ordered set-aggregate expression
*
* @see #functionWithinGroup
*/
JpaExpression<?> mode(JpaOrder order, JpaPredicate filter, Expression<?> argument);
/**
* Create a {@code percentile_cont} ordered set-aggregate function expression.
*
* @param order order by clause used in within group
* @param filter optional filter clause
* @param argument argument to the function
*
* @return ordered set-aggregate expression
*
* @see #functionWithinGroup
*/
JpaExpression<Integer> percentileCont(JpaOrder order, JpaPredicate filter, Expression<? extends Number> argument);
/**
* Create a {@code percentile_disc} ordered set-aggregate function expression.
*
* @param order order by clause used in within group
* @param filter optional filter clause
* @param argument argument to the function
*
* @return ordered set-aggregate expression
*
* @see #functionWithinGroup
*/
JpaExpression<Integer> percentileDisc(JpaOrder order, JpaPredicate filter, Expression<? extends Number> argument);
/**
* Create a {@code rank} ordered set-aggregate function expression.
*
* @param order order by clause used in within group
* @param filter optional filter clause
* @param argument argument to the function
*
* @return ordered set-aggregate expression
*
* @see #functionWithinGroup
*/
JpaExpression<Long> rank(JpaOrder order, JpaPredicate filter, Expression<Integer> argument);
/**
* Create a {@code percent_rank} ordered set-aggregate function expression.
*
* @param order order by clause used in within group
* @param filter optional filter clause
* @param argument argument to the function
*
* @return ordered set-aggregate expression
*
* @see #functionWithinGroup
*/
JpaExpression<Double> percentRank(JpaOrder order, JpaPredicate filter, Expression<Integer> argument);
}

View File

@ -0,0 +1,19 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.query.criteria;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
/**
* @author Marco Belladelli
*/
public interface JpaWindow {
JpaWindow partitionBy(Expression<?>... expressions);
JpaWindow orderBy(Order... expressions);
}

View File

@ -47,6 +47,7 @@ import org.hibernate.query.criteria.JpaSelection;
import org.hibernate.query.criteria.JpaSetJoin;
import org.hibernate.query.criteria.JpaSimpleCase;
import org.hibernate.query.criteria.JpaSubQuery;
import org.hibernate.query.criteria.JpaWindow;
import org.hibernate.query.sqm.NullPrecedence;
import org.hibernate.query.sqm.SortOrder;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
@ -1254,4 +1255,109 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
public JpaSearchOrder desc(JpaCteCriteriaAttribute x, boolean nullsFirst) {
return criteriaBuilder.desc( x, nullsFirst );
}
@Override
public JpaWindow createWindow() {
return criteriaBuilder.createWindow();
}
@Override
public <T> JpaExpression<T> windowFunction(String name, Class<T> type, JpaWindow window, Expression<?>... args) {
return criteriaBuilder.windowFunction( name, type, window, args );
}
@Override
public JpaExpression<Long> rowNumber(JpaWindow window) {
return criteriaBuilder.rowNumber( window );
}
@Override
public <T> JpaExpression<T> firstValue(Expression<T> argument, JpaWindow window) {
return criteriaBuilder.firstValue( argument, window );
}
@Override
public <T> JpaExpression<T> lastValue(Expression<T> argument, JpaWindow window) {
return criteriaBuilder.lastValue( argument, window );
}
@Override
public <T> JpaExpression<T> nthValue(Expression<T> argument, Expression<Integer> n, JpaWindow window) {
return criteriaBuilder.nthValue( argument, n, window );
}
@Override
public JpaExpression<Long> rank(JpaWindow window) {
return criteriaBuilder.rank( window );
}
@Override
public JpaExpression<Long> denseRank(JpaWindow window) {
return criteriaBuilder.denseRank( window );
}
@Override
public JpaExpression<Double> percentRank(JpaWindow window) {
return criteriaBuilder.percentRank( window );
}
@Override
public JpaExpression<Double> cumeDist(JpaWindow window) {
return criteriaBuilder.cumeDist( window );
}
@Override
public <T> JpaExpression<T> functionWithinGroup(
String name,
Class<T> type,
JpaOrder order,
JpaPredicate filter,
Expression<?>... args) {
return criteriaBuilder.functionWithinGroup( name, type, order, filter, args );
}
@Override
public <T> JpaExpression<T> functionWithinGroup(String name, Class<T> type, JpaOrder order, Expression<?>... args) {
return criteriaBuilder.functionWithinGroup( name, type, order, args );
}
@Override
public JpaExpression<String> listagg(
JpaOrder order,
JpaPredicate filter,
Expression<String> argument,
Expression<String> separator) {
return criteriaBuilder.listagg( order, filter, argument, separator );
}
@Override
public JpaExpression<?> mode(JpaOrder order, JpaPredicate filter, Expression<?> argument) {
return criteriaBuilder.mode( order, filter, argument );
}
@Override
public JpaExpression<Integer> percentileCont(
JpaOrder order,
JpaPredicate filter,
Expression<? extends Number> argument) {
return criteriaBuilder.percentileCont( order, filter, argument );
}
@Override
public JpaExpression<Integer> percentileDisc(
JpaOrder order,
JpaPredicate filter,
Expression<? extends Number> argument) {
return criteriaBuilder.percentileDisc( order, filter, argument );
}
@Override
public JpaExpression<Long> rank(JpaOrder order, JpaPredicate filter, Expression<Integer> argument) {
return criteriaBuilder.rank( order, filter, argument );
}
@Override
public JpaExpression<Double> percentRank(JpaOrder order, JpaPredicate filter, Expression<Integer> argument) {
return criteriaBuilder.percentRank( order, filter, argument );
}
}

View File

@ -63,6 +63,7 @@ import org.hibernate.query.sqm.tree.expression.SqmToDuration;
import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation;
import org.hibernate.query.sqm.tree.expression.SqmWindow;
import org.hibernate.query.sqm.tree.from.SqmAttributeJoin;
import org.hibernate.query.sqm.tree.from.SqmCrossJoin;
import org.hibernate.query.sqm.tree.from.SqmCteJoin;
@ -259,6 +260,8 @@ public interface SemanticQueryWalker<T> {
T visitOver(SqmOver<?> over);
T visitWindow(SqmWindow widow);
T visitOverflow(SqmOverflow<?> sqmOverflow);
T visitCoalesce(SqmCoalesce<?> sqmCoalesce);

View File

@ -43,32 +43,34 @@ import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.metamodel.model.domain.DomainType;
import org.hibernate.metamodel.model.domain.JpaMetamodel;
import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.BindableType;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.SemanticException;
import org.hibernate.query.criteria.spi.CriteriaBuilderExtension;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaCteCriteriaAttribute;
import org.hibernate.query.criteria.JpaSearchOrder;
import org.hibernate.query.criteria.JpaSubQuery;
import org.hibernate.query.sqm.BinaryArithmeticOperator;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.NullPrecedence;
import org.hibernate.query.sqm.SetOperator;
import org.hibernate.query.sqm.SortOrder;
import org.hibernate.query.sqm.TrimSpec;
import org.hibernate.query.sqm.UnaryArithmeticOperator;
import org.hibernate.query.criteria.JpaCoalesce;
import org.hibernate.query.criteria.JpaCompoundSelection;
import org.hibernate.query.criteria.JpaCriteriaQuery;
import org.hibernate.query.criteria.JpaCteCriteriaAttribute;
import org.hibernate.query.criteria.JpaExpression;
import org.hibernate.query.criteria.JpaOrder;
import org.hibernate.query.criteria.JpaPredicate;
import org.hibernate.query.criteria.JpaSearchOrder;
import org.hibernate.query.criteria.JpaSelection;
import org.hibernate.query.criteria.JpaSubQuery;
import org.hibernate.query.criteria.JpaWindow;
import org.hibernate.query.criteria.ValueHandlingMode;
import org.hibernate.query.criteria.spi.CriteriaBuilderExtension;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.BinaryArithmeticOperator;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.NullPrecedence;
import org.hibernate.query.sqm.SetOperator;
import org.hibernate.query.sqm.SortOrder;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.SqmQuerySource;
import org.hibernate.query.sqm.TrimSpec;
import org.hibernate.query.sqm.UnaryArithmeticOperator;
import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
@ -89,7 +91,6 @@ import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmSetJoin;
import org.hibernate.query.sqm.tree.domain.SqmSingularJoin;
import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter;
import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter;
import org.hibernate.query.sqm.tree.expression.SqmBinaryArithmetic;
import org.hibernate.query.sqm.tree.expression.SqmCaseSearched;
import org.hibernate.query.sqm.tree.expression.SqmCaseSimple;
@ -102,9 +103,12 @@ 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.SqmModifiedSubQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmOver;
import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation;
import org.hibernate.query.sqm.tree.expression.SqmWindow;
import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter;
import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
import org.hibernate.query.sqm.tree.predicate.SqmBetweenPredicate;
@ -123,6 +127,7 @@ import org.hibernate.query.sqm.tree.predicate.SqmPredicate;
import org.hibernate.query.sqm.tree.select.SqmDynamicInstantiation;
import org.hibernate.query.sqm.tree.select.SqmDynamicInstantiationArgument;
import org.hibernate.query.sqm.tree.select.SqmJpaCompoundSelection;
import org.hibernate.query.sqm.tree.select.SqmOrderByClause;
import org.hibernate.query.sqm.tree.select.SqmQueryGroup;
import org.hibernate.query.sqm.tree.select.SqmQueryPart;
import org.hibernate.query.sqm.tree.select.SqmSelectQuery;
@ -2559,4 +2564,148 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext,
throw new InvalidObjectException( "Could not find a SessionFactory [uuid=" + uuid + ",name=" + name + "]" );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Window functions
@Override
public SqmWindow createWindow() {
return new SqmWindow( this );
}
@Override
public <T> SqmExpression<T> windowFunction(String name, Class<T> type, JpaWindow window, Expression<?>... args) {
// todo marco : some functions (`rank` and `percent_rank`) are not registered as window functions
// but as ordered set-aggregate functions, but they can serve both purposes
// getting an error when calling generateWindowSqmExpression
// (@see CommonFunctionFactory#hypotheticalOrderedSetAggregates)
SqmExpression<T> function = getFunctionDescriptor( name ).generateSqmExpression(
expressionList(args),
null,
queryEngine,
getJpaMetamodel().getTypeConfiguration()
);
return new SqmOver<>( function, (SqmWindow) window );
}
@Override
public SqmExpression<Long> rowNumber(JpaWindow window) {
return windowFunction( "row_number", Long.class, window );
}
@Override
@SuppressWarnings("unchecked")
public <T> SqmExpression<T> firstValue(Expression<T> argument, JpaWindow window) {
return (SqmExpression<T>) windowFunction( "first_value", argument.getJavaType(), window, argument );
}
@Override
@SuppressWarnings("unchecked")
public <T> SqmExpression<T> lastValue(Expression<T> argument, JpaWindow window) {
return (SqmExpression<T>) windowFunction( "last_value", argument.getJavaType(), window, argument );
}
@Override
@SuppressWarnings("unchecked")
public <T> SqmExpression<T> nthValue(Expression<T> argument, Expression<Integer> n, JpaWindow window) {
return (SqmExpression<T>) windowFunction( "nth_value", argument.getJavaType(), window, argument, n );
}
@Override
public SqmExpression<Long> rank(JpaWindow window) {
return windowFunction( "rank", Long.class, window );
}
@Override
public SqmExpression<Long> denseRank(JpaWindow window) {
return windowFunction( "dense_rank", Long.class, window );
}
@Override
public SqmExpression<Double> percentRank(JpaWindow window) {
return windowFunction( "percent_rank", Double.class, window );
}
@Override
public SqmExpression<Double> cumeDist(JpaWindow window) {
return windowFunction( "cume_dist", Double.class, window );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Ordered set-aggregate functions
@Override
public <T> SqmExpression<T> functionWithinGroup(
String name,
Class<T> type,
JpaOrder order,
JpaPredicate filter,
Expression<?>... args) {
// todo marco : we could also pass a Window (?)
// set aggregate function support OVER clauses
// eg. https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions110.htm
SqmOrderByClause withinGroupClause = new SqmOrderByClause();
if ( order != null ) {
withinGroupClause.addSortSpecification( (SqmSortSpecification) order );
}
SqmPredicate sqmFilter = filter != null ? (SqmPredicate) filter : null;
return getFunctionDescriptor( name ).generateOrderedSetAggregateSqmExpression(
expressionList( args ),
sqmFilter,
withinGroupClause,
null,
queryEngine,
getJpaMetamodel().getTypeConfiguration()
);
}
@Override
public <T> SqmExpression<T> functionWithinGroup(
String name,
Class<T> type,
JpaOrder order,
Expression<?>... args) {
return functionWithinGroup( name, type, order, null, args );
}
@Override
public SqmExpression<String> listagg(
JpaOrder order,
JpaPredicate filter,
Expression<String> argument,
Expression<String> separator) {
return functionWithinGroup( "listagg", String.class, order, filter, argument, separator );
}
@Override
public SqmExpression<?> mode(JpaOrder order, JpaPredicate filter, Expression<?> argument) {
return functionWithinGroup( "mode", argument.getJavaType(), order, filter, argument );
}
@Override
public SqmExpression<Integer> percentileCont(
JpaOrder order,
JpaPredicate filter,
Expression<? extends Number> argument) {
return functionWithinGroup( "percentile_cont", Integer.class, order, filter, argument );
}
@Override
public SqmExpression<Integer> percentileDisc(
JpaOrder order,
JpaPredicate filter,
Expression<? extends Number> argument) {
return functionWithinGroup( "percentile_disc", Integer.class, order, filter, argument );
}
@Override
public SqmExpression<Long> rank(JpaOrder order, JpaPredicate filter, Expression<Integer> argument) {
return functionWithinGroup( "rank", Long.class, order, filter, argument );
}
@Override
public SqmExpression<Double> percentRank(JpaOrder order, JpaPredicate filter, Expression<Integer> argument) {
return functionWithinGroup( "percent_rank", Double.class, order, filter, argument );
}
}

View File

@ -67,6 +67,7 @@ import org.hibernate.query.sqm.tree.expression.SqmToDuration;
import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation;
import org.hibernate.query.sqm.tree.expression.SqmWindow;
import org.hibernate.query.sqm.tree.from.SqmAttributeJoin;
import org.hibernate.query.sqm.tree.from.SqmCrossJoin;
import org.hibernate.query.sqm.tree.from.SqmCteJoin;
@ -847,6 +848,11 @@ public class SqmTreePrinter implements SemanticQueryWalker<Object> {
return null;
}
@Override
public Object visitWindow(SqmWindow window) {
return null;
}
@Override
public Object visitWhereClause(SqmWhereClause whereClause) {
if ( whereClause != null && whereClause.getPredicate() != null ) {

View File

@ -69,6 +69,7 @@ import org.hibernate.query.sqm.tree.expression.SqmToDuration;
import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation;
import org.hibernate.query.sqm.tree.expression.SqmWindow;
import org.hibernate.query.sqm.tree.from.SqmAttributeJoin;
import org.hibernate.query.sqm.tree.from.SqmCrossJoin;
import org.hibernate.query.sqm.tree.from.SqmCteJoin;
@ -704,19 +705,25 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker<Obj
@Override
public Object visitOver(SqmOver<?> over) {
over.getExpression().accept( this );
for ( SqmExpression<?> partition : over.getPartitions() ) {
over.getWindow().accept( this );
return over;
}
@Override
public Object visitWindow(SqmWindow window) {
for ( SqmExpression<?> partition : window.getPartitions() ) {
partition.accept( this );
}
for ( SqmSortSpecification sqmSortSpecification : over.getOrderList() ) {
for ( SqmSortSpecification sqmSortSpecification : window.getOrderList() ) {
visitSortSpecification( sqmSortSpecification );
}
if ( over.getStartExpression() != null ) {
over.getStartExpression().accept( this );
if ( window.getStartExpression() != null ) {
window.getStartExpression().accept( this );
}
if ( over.getEndExpression() != null ) {
over.getEndExpression().accept( this );
if ( window.getEndExpression() != null ) {
window.getEndExpression().accept( this );
}
return over;
return window;
}
@Override

View File

@ -5696,24 +5696,29 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
public Object visitOver(SqmOver<?> over) {
currentClauseStack.push( Clause.OVER );
final Expression expression = (Expression) over.getExpression().accept( this );
final List<Expression> partitions = new ArrayList<>(over.getPartitions().size());
for ( SqmExpression<?> partition : over.getPartitions() ) {
final List<Expression> partitions = new ArrayList<>(over.getWindow().getPartitions().size());
for ( SqmExpression<?> partition : over.getWindow().getPartitions() ) {
partitions.add( (Expression) partition.accept( this ) );
}
final List<SortSpecification> orderList = new ArrayList<>( over.getOrderList().size() );
for ( SqmSortSpecification sortSpecification : over.getOrderList() ) {
final List<SortSpecification> orderList = new ArrayList<>( over.getWindow().getOrderList().size() );
for ( SqmSortSpecification sortSpecification : over.getWindow().getOrderList() ) {
orderList.add( visitSortSpecification( sortSpecification ) );
}
final Over<Object> 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()
over.getWindow().getMode(),
over.getWindow().getStartKind(),
over.getWindow().getStartExpression() == null ?
null :
(Expression) over.getWindow().getStartExpression().accept( this ),
over.getWindow().getEndKind(),
over.getWindow().getEndExpression() == null ?
null :
(Expression) over.getWindow().getEndExpression().accept( this ),
over.getWindow().getExclusion()
);
currentClauseStack.pop();
return overExpression;

View File

@ -6,7 +6,6 @@
*/
package org.hibernate.query.sqm.tree.expression;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.query.sqm.FrameExclusion;
@ -19,18 +18,23 @@ import org.hibernate.query.sqm.tree.select.SqmSortSpecification;
/**
* @author Christian Beikov
* @author Marco Belladelli
*/
// todo marco : changed this for window functions, but isn't this API ?
// what changed ? getter methods + renderFrameKind moved to SqmWindow
public class SqmOver<T> extends AbstractSqmExpression<T> {
private final SqmExpression<T> expression;
private final List<SqmExpression<?>> partitions;
private final List<SqmSortSpecification> 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;
private final SqmWindow window;
public SqmOver(
SqmExpression<T> expression,
SqmWindow window) {
super( expression.getNodeType(), expression.nodeBuilder() );
this.expression = expression;
this.window = window;
}
public SqmOver(
SqmExpression<T> expression,
@ -42,16 +46,20 @@ public class SqmOver<T> extends AbstractSqmExpression<T> {
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;
this(
expression,
new SqmWindow(
expression.nodeBuilder(),
partitions,
orderList,
mode,
startKind,
startExpression,
endKind,
endExpression,
exclusion
)
);
}
@Override
@ -60,26 +68,11 @@ public class SqmOver<T> extends AbstractSqmExpression<T> {
if ( existing != null ) {
return existing;
}
final List<SqmExpression<?>> partitions = new ArrayList<>(this.partitions.size());
for ( SqmExpression<?> partition : this.partitions ) {
partitions.add( partition.copy( context ) );
}
final List<SqmSortSpecification> orderList = new ArrayList<>(this.orderList.size());
for ( SqmSortSpecification sortSpecification : this.orderList ) {
orderList.add( sortSpecification.copy( context ) );
}
final SqmOver<T> 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
window.copy( context )
)
);
copyTo( over, context );
@ -90,36 +83,8 @@ public class SqmOver<T> extends AbstractSqmExpression<T> {
return expression;
}
public List<SqmExpression<?>> getPartitions() {
return partitions;
}
public List<SqmSortSpecification> 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;
public SqmWindow getWindow() {
return window;
}
@Override
@ -136,91 +101,7 @@ public class SqmOver<T> extends AbstractSqmExpression<T> {
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;
}
}
window.appendHqlString( sb );
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 );
}
}
}

View File

@ -0,0 +1,249 @@
/*
* 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.criteria.JpaWindow;
import org.hibernate.query.sqm.FrameExclusion;
import org.hibernate.query.sqm.FrameKind;
import org.hibernate.query.sqm.FrameMode;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SemanticQueryWalker;
import org.hibernate.query.sqm.tree.AbstractSqmNode;
import org.hibernate.query.sqm.tree.SqmCopyContext;
import org.hibernate.query.sqm.tree.SqmVisitableNode;
import org.hibernate.query.sqm.tree.select.SqmSortSpecification;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
import static org.hibernate.query.sqm.FrameExclusion.NO_OTHERS;
import static org.hibernate.query.sqm.FrameKind.CURRENT_ROW;
import static org.hibernate.query.sqm.FrameKind.UNBOUNDED_PRECEDING;
import static org.hibernate.query.sqm.FrameMode.ROWS;
/**
* @author Marco Belladelli
*/
public class SqmWindow extends AbstractSqmNode implements JpaWindow, SqmVisitableNode {
private final List<SqmExpression<?>> partitions;
private final List<SqmSortSpecification> 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 SqmWindow(NodeBuilder nodeBuilder) {
this(
nodeBuilder,
new ArrayList<>(),
new ArrayList<>(),
ROWS,
UNBOUNDED_PRECEDING,
null,
CURRENT_ROW,
null,
NO_OTHERS
);
}
public SqmWindow(
NodeBuilder nodeBuilder,
List<SqmExpression<?>> partitions,
List<SqmSortSpecification> orderList,
FrameMode mode,
FrameKind startKind,
SqmExpression<?> startExpression,
FrameKind endKind,
SqmExpression<?> endExpression,
FrameExclusion exclusion) {
super( nodeBuilder );
this.partitions = partitions;
this.orderList = orderList;
this.mode = mode;
this.startKind = startKind;
this.startExpression = startExpression;
this.endKind = endKind;
this.endExpression = endExpression;
this.exclusion = exclusion;
}
public List<SqmExpression<?>> getPartitions() {
return partitions;
}
public List<SqmSortSpecification> 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 JpaWindow partitionBy(Expression<?>... expressions) {
for (Expression<?> expression : expressions) {
this.partitions.add((SqmExpression<?>) expression);
}
return this;
}
@Override
public JpaWindow orderBy(Order... orders) {
for (Order order : orders) {
this.orderList.add( (SqmSortSpecification) order );
}
return this;
}
public SqmWindow copy(SqmCopyContext context) {
final SqmWindow existing = context.getCopy( this );
if ( existing != null ) {
return existing;
}
final List<SqmExpression<?>> partitionsCopy = new ArrayList<>( this.partitions.size() );
for ( SqmExpression<?> partition : this.partitions ) {
partitions.add( partition.copy( context ) );
}
final List<SqmSortSpecification> orderListCopy = new ArrayList<>( this.orderList.size() );
for ( SqmSortSpecification sortSpecification : this.orderList ) {
orderList.add( sortSpecification.copy( context ) );
}
return context.registerCopy(
this,
new SqmWindow(
nodeBuilder(),
partitionsCopy,
orderListCopy,
mode,
startKind,
startExpression == null ? null : startExpression.copy( context ),
endKind,
endExpression == null ? null : endExpression.copy( context ),
exclusion
)
);
}
@Override
public <X> X accept(SemanticQueryWalker<X> walker) {
return walker.visitWindow( this );
}
@Override
public void appendHqlString(StringBuilder sb) {
boolean needsWhitespace = false;
if ( !this.partitions.isEmpty() ) {
needsWhitespace = true;
sb.append( "partition by " );
this.partitions.get( 0 ).appendHqlString( sb );
for ( int i = 1; i < this.partitions.size(); i++ ) {
sb.append( ',' );
this.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 == ROWS && startKind == UNBOUNDED_PRECEDING && endKind == CURRENT_ROW && exclusion == 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 == 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;
}
}
}
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 );
}
}
}

View File

@ -0,0 +1,266 @@
/*
* 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.criteria;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaExpression;
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.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Root;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Marco Belladelli
*/
@ServiceRegistry
@DomainModel(standardModels = StandardDomainModel.GAMBIT)
@SessionFactory
public class CriteriaOrderedSetAggregateTest {
@BeforeEach
public void prepareData(SessionFactoryScope scope) {
scope.inTransaction( em -> {
Date now = new Date();
EntityOfBasics entity1 = new EntityOfBasics();
entity1.setId( 1 );
entity1.setTheString( "5" );
entity1.setTheInt( 5 );
entity1.setTheInteger( -1 );
entity1.setTheDouble( 5.0 );
entity1.setTheDate( now );
entity1.setTheBoolean( true );
em.persist( entity1 );
EntityOfBasics entity2 = new EntityOfBasics();
entity2.setId( 2 );
entity2.setTheString( "6" );
entity2.setTheInt( 6 );
entity2.setTheInteger( -2 );
entity2.setTheDouble( 6.0 );
entity2.setTheBoolean( true );
em.persist( entity2 );
EntityOfBasics entity3 = new EntityOfBasics();
entity3.setId( 3 );
entity3.setTheString( "7" );
entity3.setTheInt( 7 );
entity3.setTheInteger( 3 );
entity3.setTheDouble( 7.0 );
entity3.setTheBoolean( false );
entity3.setTheDate( new Date( now.getTime() + 200000L ) );
em.persist( entity3 );
EntityOfBasics entity4 = new EntityOfBasics();
entity4.setId( 4 );
entity4.setTheString( "13" );
entity4.setTheInt( 13 );
entity4.setTheInteger( 4 );
entity4.setTheDouble( 13.0 );
entity4.setTheBoolean( false );
entity4.setTheDate( new Date( now.getTime() + 300000L ) );
em.persist( entity4 );
EntityOfBasics entity5 = new EntityOfBasics();
entity5.setId( 5 );
entity5.setTheString( "5" );
entity5.setTheInt( 5 );
entity5.setTheInteger( 5 );
entity5.setTheDouble( 9.0 );
entity5.setTheBoolean( false );
em.persist( entity5 );
} );
}
@AfterEach
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction( session -> session.createQuery( "delete from EntityOfBasics" ).executeUpdate() );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class)
public void testListaggWithoutOrder(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<String> cr = cb.createQuery( String.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<String> function = cb.listagg( null, null, root.get( "theString" ), cb.literal( "," ) );
cr.select( function );
List<String> elements = Arrays.asList( session.createQuery( cr ).getSingleResult().split( "," ) );
List<String> expectedElements = List.of( "13", "5", "5", "6", "7" );
elements.sort( String.CASE_INSENSITIVE_ORDER );
assertEquals( expectedElements, elements );
} );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class)
public void testListagg(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<String> cr = cb.createQuery( String.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<String> function = cb.listagg(
cb.desc( root.get( "id" ) ),
null,
root.get( "theString" ),
cb.literal( "," )
);
cr.select( function );
String result = session.createQuery( cr ).getSingleResult();
assertEquals( "5,13,7,6,5", result );
} );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class)
public void testListaggWithFilter(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<String> cr = cb.createQuery( String.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<String> function = cb.listagg(
cb.desc( root.get( "id" ) ),
cb.lt( root.get( "theInt" ), cb.literal( 10 ) ),
root.get( "theString" ),
cb.literal( "," )
);
cr.select( function );
String result = session.createQuery( cr ).getSingleResult();
assertEquals( "5,7,6,5", result );
} );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class)
public void testListaggWithNullsClause(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<String> cr = cb.createQuery( String.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<String> function = cb.listagg(
cb.desc( root.get( "id" ), true ),
null,
root.get( "theString" ),
cb.literal( "," )
);
cr.select( function );
String result = session.createQuery( cr ).getSingleResult();
assertEquals( "5,13,7,6,5", result );
} );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsInverseDistributionFunctions.class)
public void testInverseDistribution(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Integer> cr = cb.createQuery( Integer.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<Integer> function = cb.percentileDisc(
cb.asc( root.get( "theInt" ) ),
null,
cb.literal( 0.5 )
);
cr.select( function );
Integer result = session.createQuery( cr ).getSingleResult();
assertEquals( 6, result );
} );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class)
public void testHypotheticalSetPercentRank(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Double> cr = cb.createQuery( Double.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<Double> function = cb.percentRank( cb.asc( root.get( "theInt" ) ), null, cb.literal( 5 ) );
cr.select( function );
Double result = session.createQuery( cr ).getSingleResult();
assertEquals( 0.0D, result );
} );
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class)
public void testHypotheticalSetRank(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Long> cr = cb.createQuery( Long.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaExpression<Long> function = cb.rank( cb.asc( root.get( "theInt" ) ), null, cb.literal( 5 ) );
cr.select( function );
Long result = session.createQuery( cr ).getSingleResult();
assertEquals( 1L, result );
} );
}
// @Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class)
public void testHypotheticalSetRankWithGroupByHavingOrderByLimit(SessionFactoryScope scope) {
scope.inTransaction( session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Tuple> cr = cb.createQuery( Tuple.class );
Root<EntityOfBasics> root1 = cr.from( EntityOfBasics.class );
Root<EntityOfBasics> root2 = cr.from( EntityOfBasics.class );
JpaExpression<Long> function = cb.rank( cb.asc( root1.get( "theInt" ) ), null, cb.literal( 5 ) );
cr.multiselect( root2.get( "id" ), function )
.groupBy( root2.get( "id" ) ).having( cb.gt( root2.get( "id" ), cb.literal( 1 ) ) )
.orderBy( cb.asc( cb.literal( 1 ) ), cb.asc( cb.literal( 2 ) ) );
// todo marco : this test causes problems but only with mssql and db2, the sql obtained is not correct.
// we are trying to get something like this query:
// 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
List<Tuple> resultList = session.createQuery( cr ).setFirstResult( 1 ).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 ) );
} );
}
}

View File

@ -0,0 +1,282 @@
/*
* 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.criteria;
import java.util.Date;
import java.util.List;
import org.hibernate.dialect.DB2Dialect;
import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaExpression;
import org.hibernate.query.criteria.JpaWindow;
import org.hibernate.testing.orm.domain.StandardDomainModel;
import org.hibernate.testing.orm.domain.gambit.EntityOfBasics;
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* @author Marco Belladelli
*/
@ServiceRegistry
@DomainModel(standardModels = StandardDomainModel.GAMBIT)
@SessionFactory
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsWindowFunctions.class)
public class CriteriaWindowFunctionTest {
@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
public void testRowNumberWithoutOrder(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Long> cr = cb.createQuery( Long.class );
cr.from( EntityOfBasics.class );
JpaWindow window = cb.createWindow();
JpaExpression<Long> rowNumber = cb.rowNumber( window );
cr.select( rowNumber ).orderBy( cb.asc( cb.literal( 1 ) ) );
List<Long> resultList = session.createQuery( cr ).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
public void testFirstValue(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Integer> cr = cb.createQuery( Integer.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaWindow window = cb.createWindow().orderBy( cb.desc( root.get( "theInt" ) ) );
JpaExpression<Integer> firstValue = cb.firstValue( root.get( "theInt" ), window );
cr.select( firstValue ).orderBy( cb.asc( cb.literal( 1 ) ) );
List<Integer> resultList = session.createQuery( cr ).getResultList();
assertEquals( 5, resultList.size() );
assertEquals( 13, resultList.get( 0 ) );
assertEquals( 13, resultList.get( 1 ) );
assertEquals( 13, resultList.get( 2 ) );
assertEquals( 13, resultList.get( 3 ) );
assertEquals( 13, resultList.get( 4 ) );
}
);
}
@Test
@SkipForDialect(dialectClass = SQLServerDialect.class)
@SkipForDialect(dialectClass = DB2Dialect.class)
public void testNthValue(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Integer> cr = cb.createQuery( Integer.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaWindow window = cb.createWindow().orderBy( cb.desc( root.get( "theInt" ) ) );
JpaExpression<Integer> nthValue = cb.nthValue(
root.get( "theInt" ),
cb.literal( 2 ),
window
);
// todo marco : db2 throws java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
// on getResultList() line, and SqlServer doesn't support nth_value function (even tho it's in Dialect)
cr.select( nthValue ).orderBy( cb.asc( cb.literal( 1 ), true ) );
List<Integer> resultList = session.createQuery( cr ).getResultList();
assertEquals( 5, resultList.size() );
assertNull( resultList.get( 0 ) );
assertEquals( 7, resultList.get( 1 ) );
assertEquals( 7, resultList.get( 2 ) );
assertEquals( 7, resultList.get( 3 ) );
assertEquals( 7, resultList.get( 4 ) );
}
);
}
@Test
public void testRank(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Long> cr = cb.createQuery( Long.class );
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaWindow window = cb.createWindow()
.partitionBy( root.get( "theInt" ) )
.orderBy( cb.asc( root.get( "id" ) ) );
JpaExpression<Long> rank = cb.rank( window );
cr.select( rank ).orderBy( cb.asc( cb.literal( 1 ) ) );
List<Long> resultList = session.createQuery( cr ).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
public void testReusableWindow(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Tuple> cr = cb.createTupleQuery();
Root<EntityOfBasics> root = cr.from( EntityOfBasics.class );
JpaWindow window = cb.createWindow()
.partitionBy( root.get( "theInt" ) )
.orderBy( cb.asc( root.get( "id" ) ) );
JpaExpression<Long> rowNumber = cb.rowNumber( window );
JpaExpression<Double> percentRank = cb.percentRank( window );
JpaExpression<Double> cumeDist = cb.cumeDist( window );
cr.multiselect( rowNumber, percentRank, cumeDist ).orderBy( cb.asc( cb.literal( 1 ) ) );
List<Tuple> resultList = session.createQuery( cr ).getResultList();
assertEquals( 5, resultList.size() );
assertEquals( 0D, resultList.get( 0 ).get( 1 ) );
assertEquals( 0D, resultList.get( 1 ).get( 1 ) );
assertEquals( 0D, resultList.get( 2 ).get( 1 ) );
assertEquals( 0D, resultList.get( 3 ).get( 1 ) );
assertEquals( 1D, resultList.get( 4 ).get( 1 ) );
assertEquals( 1D, resultList.get( 4 ).get( 2 ) );
}
);
}
// @Test
public void testSumWithFilterAsWindowFunction(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
// todo marco : add filter clause predicate to CriteriaBuilder ?
// problem with getting the window functions as aggregate, @see SqmCriteriaNodeBuilder#windowFunction
TypedQuery<Long> 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<Long> 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
public void testFrame(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
// todo marco : how should we do window function frames in Criteria ?
TypedQuery<Integer> 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<Integer> 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 ) );
}
);
}
}