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 <christian.beikov@gmail.com>
This commit is contained in:
Luca Molteni 2024-09-19 14:20:41 +02:00 committed by Christian Beikov
parent c5db0d38e7
commit f8e4e6e49f
6 changed files with 203 additions and 4 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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
* <p>
* 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<? extends SqlAstNode> 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)";
}
}

View File

@ -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?
};
}

View File

@ -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)

View File

@ -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<Integer> femaleOrdinalFunction = session.createQuery(
"select ordinal(ordinalGender) " +
"from EntityOfBasics e " +
"where e.ordinalGender = :gender",
Integer.class
)
.setParameter( "gender", EntityOfBasics.Gender.FEMALE )
.getResultList();
List<Integer> 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<Integer> 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 ) );
} );
}
}