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:
parent
c5db0d38e7
commit
f8e4e6e49f
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)";
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ) );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue