Skip to content

Commit f8e4e6e

Browse files
lucamoltenibeikov
andcommitted
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>
1 parent c5db0d3 commit f8e4e6e

File tree

6 files changed

+203
-4
lines changed

6 files changed

+203
-4
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import org.hibernate.dialect.function.LpadRpadPadEmulation;
6969
import org.hibernate.dialect.function.SqlFunction;
7070
import org.hibernate.dialect.function.TrimFunction;
71+
import org.hibernate.dialect.function.OrdinalFunction;
7172
import org.hibernate.dialect.identity.IdentityColumnSupport;
7273
import org.hibernate.dialect.identity.IdentityColumnSupportImpl;
7374
import org.hibernate.dialect.lock.LockingStrategy;
@@ -1227,6 +1228,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
12271228
functionContributions.getFunctionRegistry().register( "str",
12281229
new CastStrEmulation( typeConfiguration ) );
12291230

1231+
// Function to convert enum mapped as Ordinal to their ordinal value
1232+
1233+
functionContributions.getFunctionRegistry().register( "ordinal",
1234+
new OrdinalFunction( typeConfiguration ) );
1235+
12301236
//format() function for datetimes, emulated on many databases using the
12311237
//Oracle-style to_char() function, and on others using their native
12321238
//formatting functions

hibernate-core/src/main/java/org/hibernate/dialect/OracleUserDefinedTypeExporter.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,15 +312,17 @@ public String[] getSqlDropStrings(UserDefinedArrayType userDefinedType, Metadata
312312
private String buildDropTypeSqlString(String arrayTypeName) {
313313
if ( dialect.supportsIfExistsBeforeTypeName() ) {
314314
return "drop type if exists " + arrayTypeName + " force";
315-
} else {
315+
}
316+
else {
316317
return "drop type " + arrayTypeName + " force";
317318
}
318319
}
319320

320321
private String buildDropFunctionSqlString(String functionTypeName) {
321322
if ( supportsIfExistsBeforeFunctionName() ) {
322323
return "drop function if exists " + functionTypeName;
323-
} else {
324+
}
325+
else {
324326
return "drop function " + functionTypeName;
325327
}
326328
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.dialect.function;
6+
7+
import java.util.List;
8+
9+
import org.hibernate.QueryException;
10+
import org.hibernate.metamodel.mapping.JdbcMapping;
11+
import org.hibernate.query.ReturnableType;
12+
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
13+
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
14+
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
15+
import org.hibernate.sql.ast.SqlAstTranslator;
16+
import org.hibernate.sql.ast.spi.SqlAppender;
17+
import org.hibernate.sql.ast.tree.SqlAstNode;
18+
import org.hibernate.sql.ast.tree.expression.Expression;
19+
import org.hibernate.type.SqlTypes;
20+
import org.hibernate.type.StandardBasicTypes;
21+
import org.hibernate.type.descriptor.java.EnumJavaType;
22+
import org.hibernate.type.descriptor.jdbc.JdbcType;
23+
import org.hibernate.type.spi.TypeConfiguration;
24+
25+
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ENUM;
26+
27+
28+
/**
29+
* The HQL {@code ordinal()} function returns the ordinal value of an enum
30+
* <p>
31+
* For enum fields mapped as ORDINAL it's a synonym for {@code cast(x as Integer)}. Same as {@link CastStrEmulation} but for Integer.
32+
* For enum fields mapped as STRING or ENUM it's a case statement that returns the ordinal value.
33+
*
34+
* @author Luca Molteni
35+
*/
36+
public class OrdinalFunction
37+
extends AbstractSqmSelfRenderingFunctionDescriptor {
38+
39+
public OrdinalFunction(TypeConfiguration typeConfiguration) {
40+
super(
41+
"ordinal",
42+
new ArgumentTypesValidator( null, ENUM ),
43+
StandardFunctionReturnTypeResolvers.invariant(
44+
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER )
45+
),
46+
null
47+
);
48+
}
49+
50+
@Override
51+
public void render(
52+
SqlAppender sqlAppender,
53+
List<? extends SqlAstNode> arguments,
54+
ReturnableType<?> returnType,
55+
SqlAstTranslator<?> walker) {
56+
Expression singleExpression = (Expression) arguments.get( 0 );
57+
58+
JdbcMapping singleJdbcMapping = singleExpression.getExpressionType().getSingleJdbcMapping();
59+
JdbcType argumentType = singleJdbcMapping.getJdbcType();
60+
61+
if ( argumentType.isInteger() ) {
62+
singleExpression.accept( walker );
63+
}
64+
else if ( argumentType.isString() || argumentType.getDefaultSqlTypeCode() == SqlTypes.ENUM ) {
65+
66+
EnumJavaType<?> enumJavaType = (EnumJavaType<?>) singleJdbcMapping.getMappedJavaType();
67+
Object[] enumConstants = enumJavaType.getJavaTypeClass().getEnumConstants();
68+
69+
sqlAppender.appendSql( "case " );
70+
singleExpression.accept( walker );
71+
for ( Object e : enumConstants ) {
72+
Enum<?> enumValue = (Enum<?>) e;
73+
sqlAppender.appendSql( " when " );
74+
sqlAppender.appendSingleQuoteEscapedString( (String) singleJdbcMapping.convertToRelationalValue(
75+
enumValue.toString() ) );
76+
sqlAppender.appendSql( " then " );
77+
sqlAppender.appendSql( enumValue.ordinal() );
78+
}
79+
sqlAppender.appendSql( " end" );
80+
}
81+
else {
82+
throw new QueryException( "Unsupported enum type passed to 'ordinal()' function: " + argumentType );
83+
}
84+
}
85+
86+
@Override
87+
public String getArgumentListSignature() {
88+
return "(ENUM arg)";
89+
}
90+
}

hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private int validateArgument(int paramNumber, JdbcMappingContainer expressionTyp
220220
@Internal
221221
public static void checkArgumentType(
222222
int paramNumber, String functionName, FunctionParameterType type, JdbcType jdbcType, Type javaType) {
223-
if ( !isCompatible( type, jdbcType )
223+
if ( !isCompatible( type, jdbcType, javaType )
224224
// as a special case, we consider a binary column
225225
// comparable when it is mapped by a Java UUID
226226
&& !( type == COMPARABLE && isBinaryUuid( jdbcType, javaType ) ) ) {
@@ -234,7 +234,7 @@ private static boolean isBinaryUuid(JdbcType jdbcType, Type javaType) {
234234
}
235235

236236
@Internal
237-
private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcType) {
237+
private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcType, Type javaType) {
238238
return switch (type) {
239239
case COMPARABLE -> jdbcType.isComparable();
240240
case STRING -> jdbcType.isStringLikeExcludingClob();
@@ -253,6 +253,7 @@ private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcTyp
253253
case IMPLICIT_JSON -> jdbcType.isImplicitJson();
254254
case XML -> jdbcType.isXml();
255255
case IMPLICIT_XML -> jdbcType.isImplicitXml();
256+
case ENUM -> javaType instanceof Class<?> clz && clz.isEnum();
256257
default -> true; // TODO: should we throw here?
257258
};
258259
}

hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ public enum FunctionParameterType {
9898
* @since 7.0
9999
*/
100100
IMPLICIT_JSON,
101+
/**
102+
* Indicates that the argument should be an ENUM type
103+
* @see org.hibernate.type.SqlTypes#isEnumType(int)
104+
* @since 7.0
105+
*/
106+
ENUM,
101107
/**
102108
* Indicates that the argument should be a XML type
103109
* @see org.hibernate.type.SqlTypes#isXmlType(int)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.hql;
6+
7+
import java.util.List;
8+
9+
import org.hibernate.testing.orm.domain.gambit.EntityOfBasics;
10+
import org.hibernate.testing.orm.junit.DomainModel;
11+
import org.hibernate.testing.orm.junit.Jira;
12+
import org.hibernate.testing.orm.junit.SessionFactory;
13+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
14+
import org.junit.jupiter.api.BeforeAll;
15+
import org.junit.jupiter.api.Test;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
@DomainModel(annotatedClasses = {
20+
EntityOfBasics.class,
21+
EntityOfBasics.Gender.class,
22+
})
23+
@SessionFactory
24+
@Jira("https://hibernate.atlassian.net/browse/HHH-16861")
25+
public class EnumTest {
26+
27+
28+
@BeforeAll
29+
public void setUp(SessionFactoryScope scope) {
30+
scope.inTransaction(session -> {
31+
EntityOfBasics male = new EntityOfBasics();
32+
male.setId( 20_000_000 );
33+
male.setGender( EntityOfBasics.Gender.MALE ); // Ordinal 0
34+
male.setOrdinalGender( EntityOfBasics.Gender.MALE ); // Ordinal 0
35+
36+
EntityOfBasics female = new EntityOfBasics();
37+
female.setId( 20_000_001 );
38+
female.setGender( EntityOfBasics.Gender.FEMALE ); // Ordinal 1
39+
female.setOrdinalGender( EntityOfBasics.Gender.FEMALE ); // Ordinal 1
40+
41+
session.persist( male );
42+
session.persist( female );
43+
});
44+
}
45+
46+
47+
@Test
48+
public void testOrdinalFunctionOnOrdinalEnum(SessionFactoryScope scope) {
49+
scope.inTransaction( session -> {
50+
51+
List<Integer> femaleOrdinalFunction = session.createQuery(
52+
"select ordinal(ordinalGender) " +
53+
"from EntityOfBasics e " +
54+
"where e.ordinalGender = :gender",
55+
Integer.class
56+
)
57+
.setParameter( "gender", EntityOfBasics.Gender.FEMALE )
58+
.getResultList();
59+
60+
List<Integer> femaleWithCast = session.createQuery(
61+
"select cast(e.ordinalGender as Integer) " +
62+
"from EntityOfBasics e " +
63+
"where e.ordinalGender = :gender",
64+
Integer.class
65+
)
66+
.setParameter( "gender", EntityOfBasics.Gender.FEMALE )
67+
.getResultList();
68+
69+
assertThat( femaleOrdinalFunction ).hasSize( 1 );
70+
assertThat( femaleOrdinalFunction ).hasSameElementsAs( femaleWithCast );
71+
} );
72+
73+
}
74+
75+
76+
@Test
77+
public void testOrdinalFunctionOnStringEnum(SessionFactoryScope scope) {
78+
scope.inTransaction( session -> {
79+
List<Integer> femaleOrdinalFromString = session.createQuery(
80+
"select ordinal(gender)" +
81+
"from EntityOfBasics e " +
82+
"where e.gender = :gender",
83+
Integer.class
84+
)
85+
.setParameter( "gender", EntityOfBasics.Gender.FEMALE )
86+
.getResultList();
87+
88+
assertThat( femaleOrdinalFromString ).hasSize( 1 );
89+
assertThat( femaleOrdinalFromString ).hasSameElementsAs( List.of( 1 ) );
90+
} );
91+
92+
}
93+
94+
}

0 commit comments

Comments
 (0)