From f8e4e6e49f5b85b719633a8f55193c0f4ae4e608 Mon Sep 17 00:00:00 2001 From: Luca Molteni Date: Thu, 19 Sep 2024 14:20:41 +0200 Subject: [PATCH] HHH-16861 HQL ordinal() function The `ordinal` function returns the `ordinal` property of Java enums, for both enums mapped as ORDINAL and enums mapped as STRING generating different SQL in each case `ordinal(field)` is equivalent to `cast(enum as Integer)`, implementation taken from CastStrEmulation when used on ordinal mapped enums. Lexer and parser don't need to be changed as there is nakedIdentifier that matches custom function names `ordinal` function is validated to work only on Java enum fields Use convertToRelationalValue to generate enum value inside the SQL query Co-authored-by: Christian Beikov --- .../java/org/hibernate/dialect/Dialect.java | 6 ++ .../OracleUserDefinedTypeExporter.java | 6 +- .../dialect/function/OrdinalFunction.java | 90 ++++++++++++++++++ .../function/ArgumentTypesValidator.java | 5 +- .../function/FunctionParameterType.java | 6 ++ .../org/hibernate/orm/test/hql/EnumTest.java | 94 +++++++++++++++++++ 6 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/OrdinalFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/hql/EnumTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index d4de91e6bb..af7fb977d3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -68,6 +68,7 @@ import org.hibernate.dialect.function.LocatePositionEmulation; import org.hibernate.dialect.function.LpadRpadPadEmulation; import org.hibernate.dialect.function.SqlFunction; import org.hibernate.dialect.function.TrimFunction; +import org.hibernate.dialect.function.OrdinalFunction; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupportImpl; import org.hibernate.dialect.lock.LockingStrategy; @@ -1227,6 +1228,11 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun functionContributions.getFunctionRegistry().register( "str", new CastStrEmulation( typeConfiguration ) ); + // Function to convert enum mapped as Ordinal to their ordinal value + + functionContributions.getFunctionRegistry().register( "ordinal", + new OrdinalFunction( typeConfiguration ) ); + //format() function for datetimes, emulated on many databases using the //Oracle-style to_char() function, and on others using their native //formatting functions diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleUserDefinedTypeExporter.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleUserDefinedTypeExporter.java index b4d7fd0e39..c9cbe48372 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleUserDefinedTypeExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleUserDefinedTypeExporter.java @@ -312,7 +312,8 @@ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExport private String buildDropTypeSqlString(String arrayTypeName) { if ( dialect.supportsIfExistsBeforeTypeName() ) { return "drop type if exists " + arrayTypeName + " force"; - } else { + } + else { return "drop type " + arrayTypeName + " force"; } } @@ -320,7 +321,8 @@ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExport private String buildDropFunctionSqlString(String functionTypeName) { if ( supportsIfExistsBeforeFunctionName() ) { return "drop function if exists " + functionTypeName; - } else { + } + else { return "drop function " + functionTypeName; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/OrdinalFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/OrdinalFunction.java new file mode 100644 index 0000000000..f743d4c32b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/OrdinalFunction.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.type.SqlTypes; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.descriptor.java.EnumJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ENUM; + + +/** + * The HQL {@code ordinal()} function returns the ordinal value of an enum + *

+ * For enum fields mapped as ORDINAL it's a synonym for {@code cast(x as Integer)}. Same as {@link CastStrEmulation} but for Integer. + * For enum fields mapped as STRING or ENUM it's a case statement that returns the ordinal value. + * + * @author Luca Molteni + */ +public class OrdinalFunction + extends AbstractSqmSelfRenderingFunctionDescriptor { + + public OrdinalFunction(TypeConfiguration typeConfiguration) { + super( + "ordinal", + new ArgumentTypesValidator( null, ENUM ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER ) + ), + null + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + Expression singleExpression = (Expression) arguments.get( 0 ); + + JdbcMapping singleJdbcMapping = singleExpression.getExpressionType().getSingleJdbcMapping(); + JdbcType argumentType = singleJdbcMapping.getJdbcType(); + + if ( argumentType.isInteger() ) { + singleExpression.accept( walker ); + } + else if ( argumentType.isString() || argumentType.getDefaultSqlTypeCode() == SqlTypes.ENUM ) { + + EnumJavaType enumJavaType = (EnumJavaType) singleJdbcMapping.getMappedJavaType(); + Object[] enumConstants = enumJavaType.getJavaTypeClass().getEnumConstants(); + + sqlAppender.appendSql( "case " ); + singleExpression.accept( walker ); + for ( Object e : enumConstants ) { + Enum enumValue = (Enum) e; + sqlAppender.appendSql( " when " ); + sqlAppender.appendSingleQuoteEscapedString( (String) singleJdbcMapping.convertToRelationalValue( + enumValue.toString() ) ); + sqlAppender.appendSql( " then " ); + sqlAppender.appendSql( enumValue.ordinal() ); + } + sqlAppender.appendSql( " end" ); + } + else { + throw new QueryException( "Unsupported enum type passed to 'ordinal()' function: " + argumentType ); + } + } + + @Override + public String getArgumentListSignature() { + return "(ENUM arg)"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java index 05cb05fdb8..721f49f3c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java @@ -220,7 +220,7 @@ public class ArgumentTypesValidator implements ArgumentsValidator { @Internal public static void checkArgumentType( int paramNumber, String functionName, FunctionParameterType type, JdbcType jdbcType, Type javaType) { - if ( !isCompatible( type, jdbcType ) + if ( !isCompatible( type, jdbcType, javaType ) // as a special case, we consider a binary column // comparable when it is mapped by a Java UUID && !( type == COMPARABLE && isBinaryUuid( jdbcType, javaType ) ) ) { @@ -234,7 +234,7 @@ public class ArgumentTypesValidator implements ArgumentsValidator { } @Internal - private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcType) { + private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcType, Type javaType) { return switch (type) { case COMPARABLE -> jdbcType.isComparable(); case STRING -> jdbcType.isStringLikeExcludingClob(); @@ -253,6 +253,7 @@ public class ArgumentTypesValidator implements ArgumentsValidator { case IMPLICIT_JSON -> jdbcType.isImplicitJson(); case XML -> jdbcType.isXml(); case IMPLICIT_XML -> jdbcType.isImplicitXml(); + case ENUM -> javaType instanceof Class clz && clz.isEnum(); default -> true; // TODO: should we throw here? }; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java index d2ba1aa06c..cdf9d20fde 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java @@ -98,6 +98,12 @@ public enum FunctionParameterType { * @since 7.0 */ IMPLICIT_JSON, + /** + * Indicates that the argument should be an ENUM type + * @see org.hibernate.type.SqlTypes#isEnumType(int) + * @since 7.0 + */ + ENUM, /** * Indicates that the argument should be a XML type * @see org.hibernate.type.SqlTypes#isXmlType(int) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/EnumTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/EnumTest.java new file mode 100644 index 0000000000..be69ee55c1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/EnumTest.java @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import java.util.List; + +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + EntityOfBasics.class, + EntityOfBasics.Gender.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-16861") +public class EnumTest { + + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction(session -> { + EntityOfBasics male = new EntityOfBasics(); + male.setId( 20_000_000 ); + male.setGender( EntityOfBasics.Gender.MALE ); // Ordinal 0 + male.setOrdinalGender( EntityOfBasics.Gender.MALE ); // Ordinal 0 + + EntityOfBasics female = new EntityOfBasics(); + female.setId( 20_000_001 ); + female.setGender( EntityOfBasics.Gender.FEMALE ); // Ordinal 1 + female.setOrdinalGender( EntityOfBasics.Gender.FEMALE ); // Ordinal 1 + + session.persist( male ); + session.persist( female ); + }); + } + + + @Test + public void testOrdinalFunctionOnOrdinalEnum(SessionFactoryScope scope) { + scope.inTransaction( session -> { + + List femaleOrdinalFunction = session.createQuery( + "select ordinal(ordinalGender) " + + "from EntityOfBasics e " + + "where e.ordinalGender = :gender", + Integer.class + ) + .setParameter( "gender", EntityOfBasics.Gender.FEMALE ) + .getResultList(); + + List femaleWithCast = session.createQuery( + "select cast(e.ordinalGender as Integer) " + + "from EntityOfBasics e " + + "where e.ordinalGender = :gender", + Integer.class + ) + .setParameter( "gender", EntityOfBasics.Gender.FEMALE ) + .getResultList(); + + assertThat( femaleOrdinalFunction ).hasSize( 1 ); + assertThat( femaleOrdinalFunction ).hasSameElementsAs( femaleWithCast ); + } ); + + } + + + @Test + public void testOrdinalFunctionOnStringEnum(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List femaleOrdinalFromString = session.createQuery( + "select ordinal(gender)" + + "from EntityOfBasics e " + + "where e.gender = :gender", + Integer.class + ) + .setParameter( "gender", EntityOfBasics.Gender.FEMALE ) + .getResultList(); + + assertThat( femaleOrdinalFromString ).hasSize( 1 ); + assertThat( femaleOrdinalFromString ).hasSameElementsAs( List.of( 1 ) ); + } ); + + } + +}