diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 7d2e240142..186529d2da 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -32,6 +32,7 @@ import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.criteria.Subquery; import org.hibernate.Incubating; +import org.hibernate.query.sqm.FrameKind; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -897,12 +898,61 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { // Window functions /** - * Create an empty window to use for window and set-order aggregate functions. + * Create an empty {@link JpaWindow} to use with window and aggregate functions. * * @return the empty window */ JpaWindow createWindow(); + /** + * Create a window frame of type {@link FrameKind#UNBOUNDED_PRECEDING} to use with {@link JpaWindow}s. + * + * @return the window frame + */ + JpaWindowFrame frameUnboundedPreceding(); + + /** + * @see #frameBetweenPreceding(Expression) + */ + JpaWindowFrame frameBetweenPreceding(int offset); + + /** + * Create window frame of type {@link FrameKind#OFFSET_PRECEDING} to use with {@link JpaWindow}s. + * + * @param offset the {@code offset} expression + * + * @return the window frame + */ + JpaWindowFrame frameBetweenPreceding(Expression offset); + + /** + * Create a window frame of type {@link FrameKind#CURRENT_ROW} to use with {@link JpaWindow}s. + * + * @return the window frame + */ + JpaWindowFrame frameCurrentRow(); + + /** + * @see #frameBetweenFollowing(Expression) + */ + JpaWindowFrame frameBetweenFollowing(int offset); + + /** + * Create a window frame of type {@link FrameKind#OFFSET_FOLLOWING} to use with {@link JpaWindow}s. + * + * @param offset the {@code offset} expression + * + * @return the window frame + */ + JpaWindowFrame frameBetweenFollowing(Expression offset); + + /** + * Create a window frame of type {@link FrameKind#UNBOUNDED_FOLLOWING} to use with {@link JpaWindow}s. + * + * @return the window frame + */ + JpaWindowFrame frameUnboundedFollowing(); + /** * Create a generic window function expression that will be applied * over the specified {@link JpaWindow window}. @@ -1017,6 +1067,120 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { */ JpaExpression cumeDist(JpaWindow window); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Aggregate functions + + /** + * @see #functionAggregate(String, Class, JpaPredicate, JpaWindow, Expression...) + */ + JpaExpression functionAggregate( + String name, + Class type, + JpaPredicate filter, + Expression... args); + + /** + * @see #functionAggregate(String, Class, JpaPredicate, JpaWindow, Expression...) + */ + JpaExpression functionAggregate( + String name, + Class type, + JpaWindow window, + Expression... args); + + /** + * Create a generic aggregate function expression. + * + * @param name name of the ordered set-aggregate function + * @param type type of this expression + * @param filter optional filter clause + * @param window optional window over which to apply the function + * @param args optional arguments to the function + * @param type of this expression + * + * @return aggregate function expression + */ + JpaExpression functionAggregate( + String name, + Class type, + JpaPredicate filter, + JpaWindow window, + Expression... args); + + /** + * @see #sum(Expression, JpaPredicate, JpaWindow) + */ + JpaExpression sum(Expression argument, JpaPredicate filter); + + /** + * @see #sum(Expression, JpaPredicate, JpaWindow) + */ + JpaExpression sum(Expression argument, JpaWindow window); + + /** + * Create a {@code sum} aggregate function expression. + * + * @param argument argument to the function + * @param filter optional filter clause + * @param window optional window over which to apply the function + * @param type of the input expression + * + * @return aggregate function expression + * + * @see #functionAggregate(String, Class, JpaPredicate, JpaWindow, Expression...) + */ + JpaExpression sum(Expression argument, JpaPredicate filter, JpaWindow window); + + /** + * @see #avg(Expression, JpaPredicate, JpaWindow) + */ + JpaExpression avg(Expression argument, JpaPredicate filter); + + /** + * @see #avg(Expression, JpaPredicate, JpaWindow) + */ + JpaExpression avg(Expression argument, JpaWindow window); + + /** + * Create an {@code avg} aggregate function expression. + * + * @param argument argument to the function + * @param filter optional filter clause + * @param window optional window over which to apply the function + * @param type of the input expression + * + * @return aggregate function expression + * + * @see #functionAggregate(String, Class, JpaPredicate, JpaWindow, Expression...) + */ + JpaExpression avg(Expression argument, JpaPredicate filter, JpaWindow window); + + /** + * @see #count(Expression, JpaPredicate, JpaWindow) + */ + JpaExpression count(Expression argument, JpaPredicate filter); + + /** + * @see #count(Expression, JpaPredicate, JpaWindow) + */ + JpaExpression count(Expression argument, JpaWindow window); + + /** + * Create a {@code count} aggregate function expression. + * + * @param argument argument to the function + * @param filter optional filter clause + * @param window optional window over which to apply the function + * + * @return aggregate function expression + * + * @see #functionAggregate(String, Class, JpaPredicate, JpaWindow, Expression...) + */ + JpaExpression count(Expression argument, JpaPredicate filter, JpaWindow window); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Ordered-Set Aggregate functions + /** * @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, JpaWindow, Expression...) */ @@ -1162,8 +1326,10 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { * @param sortExpression the sort expression * @param sortOrder the sort order * @param nullPrecedence the null precedence - * @return ordered set-aggregate expression * @param type of this expression + * + * @return ordered set-aggregate expression + * * @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, JpaWindow, Expression...) */ JpaExpression mode( @@ -1300,7 +1466,7 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { * * @return ordered set-aggregate expression * - * @see #functionWithinGroup + * @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, JpaWindow, Expression...) */ JpaExpression rank(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression... arguments); @@ -1329,7 +1495,7 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { * * @return ordered set-aggregate expression * - * @see #functionWithinGroup + * @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, JpaWindow, Expression...) */ JpaExpression percentRank( JpaOrder order, diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindow.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindow.java index f531ee9aa7..34982cbdd2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindow.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindow.java @@ -6,6 +6,8 @@ */ package org.hibernate.query.criteria; +import org.hibernate.query.sqm.FrameExclusion; + import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Order; @@ -16,4 +18,12 @@ public interface JpaWindow { JpaWindow partitionBy(Expression... expressions); JpaWindow orderBy(Order... expressions); + + JpaWindow frameRows(JpaWindowFrame startFrame, JpaWindowFrame endFrame); + + JpaWindow frameRange(JpaWindowFrame startFrame, JpaWindowFrame endFrame); + + JpaWindow frameGroups(JpaWindowFrame startFrame, JpaWindowFrame endFrame); + + JpaWindow frameExclude(FrameExclusion frameExclusion); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindowFrame.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindowFrame.java new file mode 100644 index 0000000000..6b17e40d7e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaWindowFrame.java @@ -0,0 +1,20 @@ +/* + * 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 . + */ +package org.hibernate.query.criteria; + +import org.hibernate.query.sqm.FrameKind; + +import jakarta.persistence.criteria.Expression; + +/** + * @author Marco Belladelli + */ +public interface JpaWindowFrame { + FrameKind getKind(); + + Expression getExpression(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index 478526da44..58b917cffe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -48,6 +48,7 @@ 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.criteria.JpaWindowFrame; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -1261,6 +1262,41 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde return criteriaBuilder.createWindow(); } + @Override + public JpaWindowFrame frameUnboundedPreceding() { + return criteriaBuilder.frameUnboundedPreceding(); + } + + @Override + public JpaWindowFrame frameBetweenPreceding(int offset) { + return criteriaBuilder.frameBetweenPreceding( offset ); + } + + @Override + public JpaWindowFrame frameBetweenPreceding(Expression offset) { + return criteriaBuilder.frameBetweenPreceding( offset ); + } + + @Override + public JpaWindowFrame frameCurrentRow() { + return criteriaBuilder.frameCurrentRow(); + } + + @Override + public JpaWindowFrame frameBetweenFollowing(int offset) { + return criteriaBuilder.frameBetweenFollowing( offset ); + } + + @Override + public JpaWindowFrame frameBetweenFollowing(Expression offset) { + return criteriaBuilder.frameBetweenFollowing( offset ); + } + + @Override + public JpaWindowFrame frameUnboundedFollowing() { + return criteriaBuilder.frameUnboundedFollowing(); + } + @Override public JpaExpression windowFunction(String name, Class type, JpaWindow window, Expression... args) { return criteriaBuilder.windowFunction( name, type, window, args ); @@ -1311,6 +1347,75 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde return criteriaBuilder.cumeDist( window ); } + @Override + public JpaExpression functionAggregate( + String name, + Class type, + JpaPredicate filter, + Expression... args) { + return criteriaBuilder.functionAggregate( name, type, filter, args ); + } + + @Override + public JpaExpression functionAggregate(String name, Class type, JpaWindow window, Expression... args) { + return criteriaBuilder.functionAggregate( name, type, window, args ); + } + + @Override + public JpaExpression functionAggregate( + String name, + Class type, + JpaPredicate filter, + JpaWindow window, + Expression... args) { + return criteriaBuilder.functionAggregate( name, type, filter, window, args ); + } + + @Override + public JpaExpression sum(Expression argument, JpaPredicate filter) { + return criteriaBuilder.sum( argument, filter ); + } + + @Override + public JpaExpression sum(Expression argument, JpaWindow window) { + return criteriaBuilder.sum( argument, window ); + } + + @Override + public JpaExpression sum(Expression argument, JpaPredicate filter, JpaWindow window) { + return criteriaBuilder.sum( argument, filter, window ); + } + + @Override + public JpaExpression avg(Expression argument, JpaPredicate filter) { + return criteriaBuilder.avg( argument, filter ); + } + + @Override + public JpaExpression avg(Expression argument, JpaWindow window) { + return criteriaBuilder.avg( argument, window ); + } + + @Override + public JpaExpression avg(Expression argument, JpaPredicate filter, JpaWindow window) { + return criteriaBuilder.avg( argument, filter, window ); + } + + @Override + public JpaExpression count(Expression argument, JpaPredicate filter) { + return criteriaBuilder.count( argument, filter ); + } + + @Override + public JpaExpression count(Expression argument, JpaWindow window) { + return criteriaBuilder.count( argument, window ); + } + + @Override + public JpaExpression count(Expression argument, JpaPredicate filter, JpaWindow window) { + return criteriaBuilder.count( argument, filter, window ); + } + @Override public JpaExpression functionWithinGroup(String name, Class type, JpaOrder order, Expression... args) { return criteriaBuilder.functionWithinGroup( name, type, order, args ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 615059d955..641f8968d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -58,11 +58,13 @@ 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.JpaWindowFrame; 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.FrameKind; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SetOperator; @@ -108,6 +110,7 @@ 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.SqmWindowFrame; import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; @@ -140,6 +143,7 @@ import org.hibernate.service.ServiceRegistry; import org.hibernate.type.BasicType; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.SmallIntJdbcType; import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.Tuple; @@ -2573,14 +2577,45 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, return new SqmWindow( this ); } + @Override + public SqmWindowFrame frameUnboundedPreceding() { + return new SqmWindowFrame( this, FrameKind.UNBOUNDED_PRECEDING ); + } + + @Override + public SqmWindowFrame frameBetweenPreceding(int offset) { + return new SqmWindowFrame( this, FrameKind.OFFSET_PRECEDING, literal( offset ) ); + } + + @Override + public SqmWindowFrame frameBetweenPreceding(Expression offset) { + return new SqmWindowFrame( this, FrameKind.OFFSET_PRECEDING, (SqmExpression) offset ); + } + + @Override + public SqmWindowFrame frameCurrentRow() { + return new SqmWindowFrame( this, FrameKind.CURRENT_ROW ); + } + + @Override + public SqmWindowFrame frameBetweenFollowing(int offset) { + return new SqmWindowFrame( this, FrameKind.OFFSET_FOLLOWING, literal( offset ) ); + } + + @Override + public SqmWindowFrame frameBetweenFollowing(Expression offset) { + return new SqmWindowFrame( this, FrameKind.OFFSET_FOLLOWING, (SqmExpression) offset ); + } + + @Override + public SqmWindowFrame frameUnboundedFollowing() { + return new SqmWindowFrame( this, FrameKind.UNBOUNDED_FOLLOWING ); + } + @Override public SqmExpression windowFunction(String name, Class 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 function = getFunctionDescriptor( name ).generateSqmExpression( - expressionList(args), + expressionList( args ), null, queryEngine, getJpaMetamodel().getTypeConfiguration() @@ -2638,7 +2673,97 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Ordered set-aggregate functions + // Aggregate functions + + @Override + public SqmExpression functionAggregate( + String name, + Class type, + JpaPredicate filter, + Expression... args) { + return functionAggregate( name, type, filter, null, args ); + } + + @Override + public SqmExpression functionAggregate( + String name, + Class type, + JpaWindow window, + Expression... args) { + return functionAggregate( name, type, null, window, args ); + } + + @Override + public SqmExpression functionAggregate( + String name, + Class type, + JpaPredicate filter, + JpaWindow window, + Expression... args) { + SqmPredicate sqmFilter = filter != null ? (SqmPredicate) filter : null; + SqmExpression function = getFunctionDescriptor( name ).generateAggregateSqmExpression( + expressionList( args ), + sqmFilter, + null, + queryEngine, + getJpaMetamodel().getTypeConfiguration() + ); + if ( window == null ) { + return function; + } + else { + return new SqmOver<>( function, (SqmWindow) window ); + } + } + + @Override + public SqmExpression sum(Expression argument, JpaPredicate filter) { + return sum( argument, filter, null ); + } + + @Override + public SqmExpression sum(Expression argument, JpaWindow window) { + return sum( argument, null, window ); + } + + @Override + public SqmExpression sum(Expression argument, JpaPredicate filter, JpaWindow window) { + return functionAggregate( "sum", Number.class, filter, window, argument ); + } + + @Override + public SqmExpression avg(Expression argument, JpaPredicate filter) { + return avg( argument, filter, null ); + } + + @Override + public SqmExpression avg(Expression argument, JpaWindow window) { + return avg( argument, null, window ); + } + + @Override + public SqmExpression avg(Expression argument, JpaPredicate filter, JpaWindow window) { + return functionAggregate( "avg", Double.class, filter, window, argument ); + } + + @Override + public SqmExpression count(Expression argument, JpaPredicate filter) { + return count( argument, filter, null ); + } + + @Override + public SqmExpression count(Expression argument, JpaWindow window) { + return count( argument, null, window ); + } + + @Override + public SqmExpression count(Expression argument, JpaPredicate filter, JpaWindow window) { + return functionAggregate( "count", Long.class, filter, window, argument ); + } + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Ordered-Set Aggregate functions @Override public SqmExpression functionWithinGroup(String name, Class type, JpaOrder order, Expression... args) { @@ -2747,7 +2872,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, JpaWindow window, Expression argument, String separator) { - return listagg( order, filter, window, argument, value( separator, (SqmExpression) argument ) ); + return listagg( order, filter, window, argument, literal( separator ) ); } @Override @@ -2891,6 +3016,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, sortExpression.getJavaType(), sort( (SqmExpression) sortExpression, sortOrder, nullPrecedence ), filter, + window, argument ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java index 77be421157..d2e49c063f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmOver.java @@ -20,8 +20,6 @@ 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 extends AbstractSqmExpression { private final SqmExpression expression; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindow.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindow.java index 491f7fb076..63371fb477 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindow.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindow.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import org.hibernate.query.criteria.JpaWindow; +import org.hibernate.query.criteria.JpaWindowFrame; import org.hibernate.query.sqm.FrameExclusion; import org.hibernate.query.sqm.FrameKind; import org.hibernate.query.sqm.FrameMode; @@ -34,12 +35,12 @@ import static org.hibernate.query.sqm.FrameMode.ROWS; public class SqmWindow extends AbstractSqmNode implements JpaWindow, SqmVisitableNode { private final List> partitions; private final List orderList; - private final FrameMode mode; - private final FrameKind startKind; - private final SqmExpression startExpression; - private final FrameKind endKind; - private final SqmExpression endExpression; - private final FrameExclusion exclusion; + private FrameMode mode; + private FrameKind startKind; + private SqmExpression startExpression; + private FrameKind endKind; + private SqmExpression endExpression; + private FrameExclusion exclusion; public SqmWindow(NodeBuilder nodeBuilder) { this( @@ -108,6 +109,42 @@ public class SqmWindow extends AbstractSqmNode implements JpaWindow, SqmVisitabl return exclusion; } + @Override + public JpaWindow frameRows(JpaWindowFrame startFrame, JpaWindowFrame endFrame) { + return this.setFrames(FrameMode.ROWS, startFrame, endFrame); + } + + @Override + public JpaWindow frameRange(JpaWindowFrame startFrame, JpaWindowFrame endFrame) { + return this.setFrames(FrameMode.RANGE, startFrame, endFrame); + + } + + @Override + public JpaWindow frameGroups(JpaWindowFrame startFrame, JpaWindowFrame endFrame) { + return this.setFrames(FrameMode.GROUPS, startFrame, endFrame); + + } + + private SqmWindow setFrames(FrameMode frameMode, JpaWindowFrame startFrame, JpaWindowFrame endFrame) { + this.mode = frameMode; + if (startFrame != null) { + this.startKind = startFrame.getKind(); + this.startExpression = (SqmExpression) startFrame.getExpression(); + } + if (endFrame != null) { + this.endKind = endFrame.getKind(); + this.endExpression = (SqmExpression) endFrame.getExpression(); + } + return this; + } + + @Override + public JpaWindow frameExclude(FrameExclusion frameExclusion) { + this.exclusion = frameExclusion; + return this; + } + @Override public JpaWindow partitionBy(Expression... expressions) { for (Expression expression : expressions) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindowFrame.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindowFrame.java new file mode 100644 index 0000000000..8ba1fdfa43 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmWindowFrame.java @@ -0,0 +1,57 @@ +/* + * 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 . + */ +package org.hibernate.query.sqm.tree.expression; + +import org.hibernate.query.criteria.JpaWindowFrame; +import org.hibernate.query.sqm.FrameKind; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.tree.AbstractSqmNode; +import org.hibernate.query.sqm.tree.SqmCopyContext; + +/** + * @author Marco Belladelli + */ +public class SqmWindowFrame extends AbstractSqmNode implements JpaWindowFrame { + private final FrameKind kind; + private final SqmExpression expression; + + public SqmWindowFrame(NodeBuilder nodeBuilder, FrameKind kind) { + this( nodeBuilder, kind, null ); + } + + public SqmWindowFrame(NodeBuilder nodeBuilder, FrameKind kind, SqmExpression expression) { + super( nodeBuilder ); + this.kind = kind; + this.expression = expression; + } + + @Override + public FrameKind getKind() { + return kind; + } + + @Override + public SqmExpression getExpression() { + return expression; + } + + @Override + public SqmWindowFrame copy(SqmCopyContext context) { + final SqmWindowFrame existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + return context.registerCopy( + this, + new SqmWindowFrame( + nodeBuilder(), + kind, + expression == null ? null : expression.copy( context ) + ) + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java index 5b61c7ed7a..e203a611fd 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java @@ -10,6 +10,8 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaExpression; import org.hibernate.query.criteria.JpaWindow; @@ -20,10 +22,12 @@ 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.RequiresDialect; 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; @@ -33,6 +37,7 @@ 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 @@ -129,7 +134,11 @@ public class CriteriaOrderedSetAggregateTest { CriteriaQuery cr = cb.createQuery( String.class ); Root root = cr.from( EntityOfBasics.class ); - JpaExpression function = cb.listagg( cb.desc( root.get( "id" ) ), root.get( "theString" ), "," ); + JpaExpression function = cb.listagg( + cb.desc( root.get( "id" ) ), + root.get( "theString" ), + "," + ); cr.select( function ); String result = session.createQuery( cr ).getSingleResult(); @@ -158,6 +167,36 @@ public class CriteriaOrderedSetAggregateTest { } ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class) + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsWindowFunctions.class) + @RequiresDialect(H2Dialect.class) + public void testListaggWithFilterAndWindow(SessionFactoryScope scope) { + // note : not many dbs support this for now but the generated sql is correct + scope.inTransaction( session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery( String.class ); + Root root = cr.from( EntityOfBasics.class ); + + JpaWindow window = cb.createWindow().partitionBy( root.get( "theInt" ) ); + JpaExpression function = cb.listagg( + cb.desc( root.get( "id" ) ), + cb.lt( root.get( "theInt" ), cb.literal( 10 ) ), + window, + root.get( "theString" ), + "," + ); + + cr.select( function ); + List resultList = session.createQuery( cr ).getResultList(); + assertEquals( "5,5", resultList.get( 0 ) ); + assertEquals( "6", resultList.get( 1 ) ); + assertEquals( "7", resultList.get( 2 ) ); + assertNull( resultList.get( 3 ) ); + assertEquals( "5,5", resultList.get( 4 ) ); + } ); + } + @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStringAggregation.class) public void testListaggWithNullsClause(SessionFactoryScope scope) { @@ -199,6 +238,37 @@ public class CriteriaOrderedSetAggregateTest { } ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsInverseDistributionFunctions.class) + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsWindowFunctions.class) + @SkipForDialect(dialectClass = PostgreSQLDialect.class) + public void testInverseDistributionWithWindow(SessionFactoryScope scope) { + // note : PostgreSQL currently does not support ordered-set aggregate functions with OVER clause + scope.inTransaction( session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery( Integer.class ); + Root root = cr.from( EntityOfBasics.class ); + + JpaWindow window = cb.createWindow().partitionBy( root.get( "theInt" ) ); + JpaExpression function = cb.percentileDisc( + cb.literal( 0.5 ), + window, + root.get( "theInt" ), + SortOrder.ASCENDING, + NullPrecedence.FIRST + ); + + cr.select( function ).orderBy( cb.asc( cb.literal( 1 ) ) ); + List resultList = session.createQuery( cr ).getResultList(); + assertEquals( 5, resultList.size() ); + assertEquals( 5, resultList.get( 0 ) ); + assertEquals( 5, resultList.get( 1 ) ); + assertEquals( 6, resultList.get( 2 ) ); + assertEquals( 7, resultList.get( 3 ) ); + assertEquals( 13, resultList.get( 4 ) ); + } ); + } + @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class) public void testHypotheticalSetPercentRank(SessionFactoryScope scope) { @@ -258,5 +328,4 @@ public class CriteriaOrderedSetAggregateTest { assertEquals( 1L, resultList.get( 2 ).get( 1, Long.class ) ); } ); } - } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaWindowFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaWindowFunctionTest.java index fa4b667c29..7f4a182b84 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaWindowFunctionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaWindowFunctionTest.java @@ -29,8 +29,8 @@ 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.Path; import jakarta.persistence.criteria.Root; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -239,16 +239,21 @@ public class CriteriaWindowFunctionTest { ); } - // @Test - public void testSumWithFilterAsWindowFunction(SessionFactoryScope scope) { + @Test + public void testSumWithFilterAndWindow(SessionFactoryScope scope) { scope.inTransaction( session -> { - // todo marco : add 'simple` aggregate functions (sum, avg, count) - TypedQuery q = session.createQuery( - "select sum(eob.theInt) filter (where eob.theInt > 5) over (order by eob.theInt) from EntityOfBasics eob order by eob.theInt", - Long.class - ); - List resultList = q.getResultList(); + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery( Long.class ); + Root root = cr.from( EntityOfBasics.class ); + + Path theInt = root.get( "theInt" ); + JpaWindow window = cb.createWindow().orderBy( cb.asc( theInt ) ); + JpaExpression sum = cb.sum( theInt, cb.gt( theInt, 5 ), window ).asLong(); + + cr.select( sum ).orderBy( cb.asc( theInt, true ) ); + + List resultList = session.createQuery( cr ).getResultList(); assertEquals( 5L, resultList.size() ); assertNull( resultList.get( 0 ) ); assertNull( resultList.get( 1 ) ); @@ -259,16 +264,71 @@ public class CriteriaWindowFunctionTest { ); } - // @Test + @Test + public void testAvgAsWindowFunctionWithoutFilter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery( Double.class ); + Root root = cr.from( EntityOfBasics.class ); + + Path id = root.get( "id" ); + JpaWindow window = cb.createWindow().orderBy( cb.asc( id ) ); + JpaExpression avg = cb.avg( id, window ); + + cr.select( avg ).orderBy( cb.asc( cb.literal( 1 ) ) ); + + List resultList = session.createQuery( cr ).getResultList(); + assertEquals( 5L, resultList.size() ); + assertEquals( 1.0, resultList.get( 0 ) ); + assertEquals( 1.5, resultList.get( 1 ) ); + assertEquals( 2.0, resultList.get( 2 ) ); + assertEquals( 2.5, resultList.get( 3 ) ); + assertEquals( 3.0, resultList.get( 4 ) ); + } + ); + } + + @Test + public void testCountAsWindowFunctionWithFilter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery( Long.class ); + Root root = cr.from( EntityOfBasics.class ); + + JpaWindow window = cb.createWindow(); + JpaExpression count = cb.count( root, cb.gt( root.get( "id" ), 2 ), window ); + + cr.select( count ); + + List resultList = session.createQuery( cr ).getResultList(); + assertEquals( 5L, resultList.size() ); + assertEquals( 3L, resultList.get( 0 ) ); + assertEquals( 3L, resultList.get( 1 ) ); + assertEquals( 3L, resultList.get( 2 ) ); + assertEquals( 3L, resultList.get( 3 ) ); + assertEquals( 3L, resultList.get( 4 ) ); + } + ); + } + + @Test public void testFrame(SessionFactoryScope scope) { scope.inTransaction( session -> { - // todo marco : how should we do window function frames in Criteria ? - TypedQuery q = session.createQuery( - "select first_value(eob.theInt) over (order by eob.id rows between 2 preceding and current row) from EntityOfBasics eob order by eob.id", - Integer.class - ); - List resultList = q.getResultList(); + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery( Integer.class ); + Root root = cr.from( EntityOfBasics.class ); + + JpaWindow window = cb.createWindow() + .orderBy( cb.asc( root.get( "id" ) ) ) + .frameRows( cb.frameBetweenPreceding( 2 ), cb.frameCurrentRow() ); + JpaExpression firstValue = cb.firstValue( root.get( "theInt" ), window ); + + cr.select( firstValue ).orderBy( cb.asc( root.get( "id" ) ) ); + + List resultList = session.createQuery( cr ).getResultList(); assertEquals( 5, resultList.size() ); assertEquals( 5, resultList.get( 0 ) ); assertEquals( 5, resultList.get( 1 ) );