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 f566d03caf27..4118eec33340 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -36,6 +36,7 @@ import org.hibernate.dialect.function.LpadRpadPadEmulation; import org.hibernate.dialect.function.OrdinalFunction; import org.hibernate.dialect.function.SqlFunction; +import org.hibernate.dialect.function.StringFunction; import org.hibernate.dialect.function.TrimFunction; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupportImpl; @@ -1218,6 +1219,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionContributions.getFunctionRegistry().register( "ordinal", new OrdinalFunction( typeConfiguration ) ); + // Function to convert enum mapped as String to their string value + + functionContributions.getFunctionRegistry().register( "string", + new StringFunction( 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/function/StringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/StringFunction.java new file mode 100644 index 000000000000..9b4835822531 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/StringFunction.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.model.domain.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 java.util.List; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ENUM; + + +/** + * The HQL {@code string()} function returns the string value of an enum + *

+ * For enum fields mapped as STRING or ENUM it's a synonym for {@code cast(x as String)}. Same as {@link CastStrEmulation}. + * For enum fields mapped as ORDINAL it's a case statement that returns the bane if enum. + * + * @author Luca Molteni, Cedomir Igaly + */ +public class StringFunction + extends AbstractSqmSelfRenderingFunctionDescriptor { + + public StringFunction(TypeConfiguration typeConfiguration) { + super( + "string", + new ArgumentTypesValidator( null, ENUM ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) + ), + 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.isString() || argumentType.getDefaultSqlTypeCode() == SqlTypes.ENUM ) { + singleExpression.accept( walker ); + } + else if ( argumentType.isInteger() ) { + 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.appendSql( enumValue.ordinal() ); + sqlAppender.appendSql( " then " ); + sqlAppender.appendSingleQuoteEscapedString( enumValue.name() ); + } + sqlAppender.appendSql( " end" ); + } + else { + throw new QueryException( "Unsupported enum type passed to 'string()' function: " + argumentType ); + } + } + + @Override + public String getArgumentListSignature() { + return "(ENUM arg)"; + } +} 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 index 8b64001b1061..30163fa481bc 100644 --- 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 @@ -4,8 +4,6 @@ */ 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; @@ -14,6 +12,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; @DomainModel(annotatedClasses = { @@ -22,6 +22,7 @@ }) @SessionFactory @Jira("https://hibernate.atlassian.net/browse/HHH-16861") +@Jira("https://hibernate.atlassian.net/browse/HHH-18708") public class EnumTest { @@ -97,4 +98,56 @@ public void testOrdinalFunctionOnStringEnum(SessionFactoryScope scope) { } + @Test + public void testStringFunctionOnStringEnum(SessionFactoryScope scope) { + scope.inTransaction( session -> { + + List femaleStringFunction = session.createQuery( + "select string(gender) " + + "from EntityOfBasics e " + + "where e.gender = :gender", + String.class + ) + .setParameter( "gender", EntityOfBasics.Gender.FEMALE ) + .getResultList(); + + List femaleWithCast = session.createQuery( + "select cast(e.gender as String) " + + "from EntityOfBasics e " + + "where e.gender = :gender", + String.class + ) + .setParameter( "gender", EntityOfBasics.Gender.FEMALE ) + .getResultList(); + + assertThat( femaleStringFunction ).hasSize( 1 ); + assertThat( femaleStringFunction ).hasSameElementsAs( femaleWithCast ); + } ); + + } + + @Test + public void testStringFunctionOnOrdinalEnum(SessionFactoryScope scope) { + scope.inTransaction( session -> { + //tag::hql-string-function-example[] + // enum Gender { + // MALE, + // FEMALE, + // OTHER + //} + List femaleStringFromString = session.createQuery( + "select string(ordinalGender)" + + "from EntityOfBasics e " + + "where e.ordinalGender = :gender", + String.class ) + .setParameter( "gender", EntityOfBasics.Gender.FEMALE ) + .getResultList(); + // This will return List.of(1) + //end::hql-string-function-example[] + assertThat( femaleStringFromString ).hasSize( 1 ); + assertThat( femaleStringFromString ).hasSameElementsAs( List.of( "FEMALE" ) ); + } ); + + } + }