Skip to content

Commit 80bcd88

Browse files
committed
HHH-16861 HQL ordinal() function
The ordinal function maps to the enum ordinal both fields mapped as ORDINAL and field mapped a STRING generating different SQL in both cases. `ordinal(field)` is equivalent to `cast(enum as Integer`, implementation taken from CastStrEmulation Lexer and parser don't need to be changed as there is nakedIdentifier that matches custom function names
1 parent cbdbb27 commit 80bcd88

File tree

4 files changed

+190
-0
lines changed

4 files changed

+190
-0
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
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.metamodel.mapping.JdbcMapping;
10+
import org.hibernate.query.ReturnableType;
11+
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
12+
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
13+
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
14+
import org.hibernate.sql.ast.SqlAstTranslator;
15+
import org.hibernate.sql.ast.spi.SqlAppender;
16+
import org.hibernate.sql.ast.tree.SqlAstNode;
17+
import org.hibernate.sql.ast.tree.expression.Expression;
18+
import org.hibernate.type.StandardBasicTypes;
19+
import org.hibernate.type.descriptor.java.EnumJavaType;
20+
import org.hibernate.type.descriptor.jdbc.JdbcType;
21+
import org.hibernate.type.spi.TypeConfiguration;
22+
23+
import static java.lang.String.format;
24+
25+
26+
/**
27+
* The HQL {@code ordinal()} function returns the ordinal value of an enum
28+
* <p>
29+
* For enum fields mapped as ORDINAL it's a synonym for {@code cast(x as Integer)}. Same as {@link CastStrEmulation} but for Integer.
30+
* For enum fields mapped as STRING or ENUM it's a case statement that returns the ordinal value.
31+
*
32+
* @author Luca Molteni
33+
*/
34+
public class OrdinalFunction
35+
extends AbstractSqmSelfRenderingFunctionDescriptor {
36+
37+
public OrdinalFunction(TypeConfiguration typeConfiguration) {
38+
super(
39+
"ordinal",
40+
StandardArgumentsValidators.exactly( 1 ),
41+
StandardFunctionReturnTypeResolvers.invariant(
42+
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER )
43+
),
44+
null
45+
);
46+
}
47+
48+
@Override
49+
public void render(
50+
SqlAppender sqlAppender,
51+
List<? extends SqlAstNode> arguments,
52+
ReturnableType<?> returnType,
53+
SqlAstTranslator<?> walker) {
54+
Expression singleExpression = (Expression) arguments.get( 0 );
55+
56+
JdbcMapping singleJdbcMapping = singleExpression.getExpressionType().getSingleJdbcMapping();
57+
JdbcType argumentType = singleJdbcMapping.getJdbcType();
58+
59+
String columnNameExpression = singleExpression.getColumnReference().getColumnExpression();
60+
61+
if ( argumentType.isInteger() ) {
62+
sqlAppender.appendSql( columnNameExpression );
63+
}
64+
else if ( argumentType.isString() || argumentType.isEnum() ) {
65+
66+
EnumJavaType<?> enumJavaType = (EnumJavaType<?>) singleJdbcMapping.getMappedJavaType();
67+
Object[] enumConstants = enumJavaType.getJavaTypeClass().getEnumConstants();
68+
69+
sqlAppender.appendSql( "case" );
70+
for ( Object e : enumConstants ) {
71+
Enum<?> enumValue = (Enum<?>) e;
72+
sqlAppender.appendSql( format( " when %s = ", columnNameExpression ) );
73+
sqlAppender.append( format( "'%s'", enumValue.toString() ) );
74+
sqlAppender.appendSql( " then " );
75+
sqlAppender.append( Integer.toString( enumValue.ordinal() ) );
76+
}
77+
sqlAppender.appendSql( " else -1 " );
78+
sqlAppender.appendSql( " end" );
79+
}
80+
}
81+
82+
@Override
83+
public String getArgumentListSignature() {
84+
return "(ENUM arg)";
85+
}
86+
}

hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ default boolean isArray() {
295295
return isArray( getDdlTypeCode() );
296296
}
297297

298+
default boolean isEnum() {
299+
return getDdlTypeCode() == ENUM;
300+
}
301+
298302
static boolean isArray(int jdbcTypeCode) {
299303
switch ( jdbcTypeCode ) {
300304
case ARRAY:
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)