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.LpadRpadPadEmulation;
|
||||||
import org.hibernate.dialect.function.SqlFunction;
|
import org.hibernate.dialect.function.SqlFunction;
|
||||||
import org.hibernate.dialect.function.TrimFunction;
|
import org.hibernate.dialect.function.TrimFunction;
|
||||||
|
import org.hibernate.dialect.function.OrdinalFunction;
|
||||||
import org.hibernate.dialect.identity.IdentityColumnSupport;
|
import org.hibernate.dialect.identity.IdentityColumnSupport;
|
||||||
import org.hibernate.dialect.identity.IdentityColumnSupportImpl;
|
import org.hibernate.dialect.identity.IdentityColumnSupportImpl;
|
||||||
import org.hibernate.dialect.lock.LockingStrategy;
|
import org.hibernate.dialect.lock.LockingStrategy;
|
||||||
|
@ -1227,6 +1228,11 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
|
||||||
functionContributions.getFunctionRegistry().register( "str",
|
functionContributions.getFunctionRegistry().register( "str",
|
||||||
new CastStrEmulation( typeConfiguration ) );
|
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
|
//format() function for datetimes, emulated on many databases using the
|
||||||
//Oracle-style to_char() function, and on others using their native
|
//Oracle-style to_char() function, and on others using their native
|
||||||
//formatting functions
|
//formatting functions
|
||||||
|
|
|
@ -312,7 +312,8 @@ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExport
|
||||||
private String buildDropTypeSqlString(String arrayTypeName) {
|
private String buildDropTypeSqlString(String arrayTypeName) {
|
||||||
if ( dialect.supportsIfExistsBeforeTypeName() ) {
|
if ( dialect.supportsIfExistsBeforeTypeName() ) {
|
||||||
return "drop type if exists " + arrayTypeName + " force";
|
return "drop type if exists " + arrayTypeName + " force";
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return "drop type " + arrayTypeName + " force";
|
return "drop type " + arrayTypeName + " force";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,7 +321,8 @@ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExport
|
||||||
private String buildDropFunctionSqlString(String functionTypeName) {
|
private String buildDropFunctionSqlString(String functionTypeName) {
|
||||||
if ( supportsIfExistsBeforeFunctionName() ) {
|
if ( supportsIfExistsBeforeFunctionName() ) {
|
||||||
return "drop function if exists " + functionTypeName;
|
return "drop function if exists " + functionTypeName;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return "drop function " + functionTypeName;
|
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
|
@Internal
|
||||||
public static void checkArgumentType(
|
public static void checkArgumentType(
|
||||||
int paramNumber, String functionName, FunctionParameterType type, JdbcType jdbcType, Type javaType) {
|
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
|
// as a special case, we consider a binary column
|
||||||
// comparable when it is mapped by a Java UUID
|
// comparable when it is mapped by a Java UUID
|
||||||
&& !( type == COMPARABLE && isBinaryUuid( jdbcType, javaType ) ) ) {
|
&& !( type == COMPARABLE && isBinaryUuid( jdbcType, javaType ) ) ) {
|
||||||
|
@ -234,7 +234,7 @@ public class ArgumentTypesValidator implements ArgumentsValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Internal
|
@Internal
|
||||||
private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcType) {
|
private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcType, Type javaType) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case COMPARABLE -> jdbcType.isComparable();
|
case COMPARABLE -> jdbcType.isComparable();
|
||||||
case STRING -> jdbcType.isStringLikeExcludingClob();
|
case STRING -> jdbcType.isStringLikeExcludingClob();
|
||||||
|
@ -253,6 +253,7 @@ public class ArgumentTypesValidator implements ArgumentsValidator {
|
||||||
case IMPLICIT_JSON -> jdbcType.isImplicitJson();
|
case IMPLICIT_JSON -> jdbcType.isImplicitJson();
|
||||||
case XML -> jdbcType.isXml();
|
case XML -> jdbcType.isXml();
|
||||||
case IMPLICIT_XML -> jdbcType.isImplicitXml();
|
case IMPLICIT_XML -> jdbcType.isImplicitXml();
|
||||||
|
case ENUM -> javaType instanceof Class<?> clz && clz.isEnum();
|
||||||
default -> true; // TODO: should we throw here?
|
default -> true; // TODO: should we throw here?
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,12 @@ public enum FunctionParameterType {
|
||||||
* @since 7.0
|
* @since 7.0
|
||||||
*/
|
*/
|
||||||
IMPLICIT_JSON,
|
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
|
* Indicates that the argument should be a XML type
|
||||||
* @see org.hibernate.type.SqlTypes#isXmlType(int)
|
* @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