HHH-15578 Add min/max emulation for uuid on PostgreSQL

This commit is contained in:
Christian Beikov 2022-10-05 10:40:51 +02:00 committed by Andrea Boriero
parent b392f663c3
commit 5f2d5e3938
3 changed files with 220 additions and 0 deletions

View File

@ -39,6 +39,7 @@ import org.hibernate.dialect.RowLockStrategy;
import org.hibernate.dialect.SelectItemReferenceStrategy;
import org.hibernate.dialect.TimeZoneSupport;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.PostgreSQLMinMaxFunction;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.dialect.identity.PostgreSQLIdentityColumnSupport;
import org.hibernate.dialect.pagination.LimitHandler;
@ -574,6 +575,60 @@ public class PostgreSQLLegacyDialect extends Dialect {
functionFactory.inverseDistributionOrderedSetAggregates();
functionFactory.hypotheticalOrderedSetAggregates();
}
if ( !supportsMinMaxOnUuid() ) {
queryEngine.getSqmFunctionRegistry().register( "min", new PostgreSQLMinMaxFunction( "min" ) );
queryEngine.getSqmFunctionRegistry().register( "max", new PostgreSQLMinMaxFunction( "max" ) );
}
}
/**
* Whether PostgreSQL supports `min(uuid)`/`max(uuid)` which it doesn't by default.
* Since the emulation is not very performant, this can be overridden by users which
* make sure that an aggregate function for uuid exists on their database.
*
* The following definitions can be used for this purpose:
*
* <code>
* create or replace function min(uuid, uuid)
* returns uuid
* immutable parallel safe
* language plpgsql as
* $$
* begin
* return least($1, $2);
* end
* $$;
*
* create aggregate min(uuid) (
* sfunc = min,
* stype = uuid,
* combinefunc = min,
* parallel = safe,
* sortop = operator (<)
* );
*
* create or replace function max(uuid, uuid)
* returns uuid
* immutable parallel safe
* language plpgsql as
* $$
* begin
* return greatest($1, $2);
* end
* $$;
*
* create aggregate max(uuid) (
* sfunc = max,
* stype = uuid,
* combinefunc = max,
* parallel = safe,
* sortop = operator (>)
* );
* </code>
*/
protected boolean supportsMinMaxOnUuid() {
return false;
}
@Override

View File

@ -25,6 +25,7 @@ import org.hibernate.PessimisticLockException;
import org.hibernate.QueryTimeoutException;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.PostgreSQLMinMaxFunction;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.dialect.identity.PostgreSQLIdentityColumnSupport;
import org.hibernate.dialect.pagination.LimitHandler;
@ -556,6 +557,60 @@ public class PostgreSQLDialect extends Dialect {
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions
functionFactory.inverseDistributionOrderedSetAggregates();
functionFactory.hypotheticalOrderedSetAggregates();
if ( !supportsMinMaxOnUuid() ) {
queryEngine.getSqmFunctionRegistry().register( "min", new PostgreSQLMinMaxFunction( "min" ) );
queryEngine.getSqmFunctionRegistry().register( "max", new PostgreSQLMinMaxFunction( "max" ) );
}
}
/**
* Whether PostgreSQL supports `min(uuid)`/`max(uuid)` which it doesn't by default.
* Since the emulation is not very performant, this can be overridden by users which
* make sure that an aggregate function for uuid exists on their database.
*
* The following definitions can be used for this purpose:
*
* <code>
* create or replace function min(uuid, uuid)
* returns uuid
* immutable parallel safe
* language plpgsql as
* $$
* begin
* return least($1, $2);
* end
* $$;
*
* create aggregate min(uuid) (
* sfunc = min,
* stype = uuid,
* combinefunc = min,
* parallel = safe,
* sortop = operator (<)
* );
*
* create or replace function max(uuid, uuid)
* returns uuid
* immutable parallel safe
* language plpgsql as
* $$
* begin
* return greatest($1, $2);
* end
* $$;
*
* create aggregate max(uuid) (
* sfunc = max,
* stype = uuid,
* combinefunc = max,
* parallel = safe,
* sortop = operator (>)
* );
* </code>
*/
protected boolean supportsMinMaxOnUuid() {
return false;
}
@Override

View File

@ -0,0 +1,110 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function;
import java.util.List;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.function.FunctionKind;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.type.SqlTypes;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.COMPARABLE;
/**
* PostgreSQL doesn't support min/max for uuid yet,
* but since that type is comparable we want to support this operation.
* The workaround is to cast uuid to text and aggregate that, which preserves the ordering,
* and finally cast the result back to uuid.
*
* @author Christian Beikov
*/
public class PostgreSQLMinMaxFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
public PostgreSQLMinMaxFunction(String name) {
super(
name,
FunctionKind.AGGREGATE,
new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 1 ), COMPARABLE ),
StandardFunctionReturnTypeResolvers.useFirstNonNull(),
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
);
}
@Override
public void render(SqlAppender sqlAppender, List<? extends SqlAstNode> sqlAstArguments, SqlAstTranslator<?> walker) {
render( sqlAppender, sqlAstArguments, null, walker );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
Predicate filter,
SqlAstTranslator<?> translator) {
final boolean caseWrapper = filter != null && !translator.supportsFilterClause();
sqlAppender.appendSql( getName() );
sqlAppender.appendSql( '(' );
final Expression arg = (Expression) sqlAstArguments.get( 0 );
final String castTarget;
if ( caseWrapper ) {
translator.getCurrentClauseStack().push( Clause.WHERE );
sqlAppender.appendSql( "case when " );
filter.accept( translator );
translator.getCurrentClauseStack().pop();
sqlAppender.appendSql( " then " );
castTarget = renderArgument( sqlAppender, translator, arg );
sqlAppender.appendSql( " else null end)" );
}
else {
castTarget = renderArgument( sqlAppender, translator, arg );
sqlAppender.appendSql( ')' );
if ( filter != null ) {
translator.getCurrentClauseStack().push( Clause.WHERE );
sqlAppender.appendSql( " filter (where " );
filter.accept( translator );
sqlAppender.appendSql( ')' );
translator.getCurrentClauseStack().pop();
}
}
if ( castTarget != null ) {
sqlAppender.appendSql( "::" );
sqlAppender.appendSql( castTarget );
}
}
private String renderArgument(SqlAppender sqlAppender, SqlAstTranslator<?> translator, Expression arg) {
final JdbcMapping sourceMapping = arg.getExpressionType().getJdbcMappings().get( 0 );
// Cast uuid expressions to "text" first, aggregate that, and finally cast to uuid again
if ( sourceMapping.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.UUID ) {
sqlAppender.appendSql( "cast(" );
arg.accept( translator );
sqlAppender.appendSql( " as text)" );
return "uuid";
}
else {
arg.accept( translator );
return null;
}
}
@Override
public String getArgumentListSignature() {
return "(COMPARABLE arg)";
}
}