Skip to content

Commit d32037f

Browse files
committed
HHH-19974 Refactor InformixDialect to override the new getSelectClauseNullString signature.
- this change utilizes the DdlTypeRegistry to dynamically determine the correct cast type name, ensuring better compatibility and correctness for null handling in select clauses on Informix. - Added a reproduction test case for InformixDialect to verify the fix. Signed-off-by: namucy <[email protected]>
1 parent 82d913d commit d32037f

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import org.hibernate.exception.LockAcquisitionException;
4141
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
4242
import org.hibernate.mapping.CheckConstraint;
43+
import org.hibernate.metamodel.mapping.SqlExpressible;
44+
import org.hibernate.metamodel.mapping.SqlTypedMapping;
4345
import org.hibernate.query.sqm.CastType;
4446
import org.hibernate.query.sqm.IntervalType;
4547
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
@@ -1097,6 +1099,17 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
10971099
return "cast(null as " + castType + ")";
10981100
}
10991101

1102+
// Add override for the newer signature to access columnDefinition
1103+
@Override
1104+
public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) {
1105+
final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry();
1106+
final String castTypeName = ddlTypeRegistry
1107+
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
1108+
.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), ddlTypeRegistry );
1109+
return "cast(null as " + castTypeName + ")";
1110+
}
1111+
1112+
11001113
private static String castType(DdlType descriptor) {
11011114
final String typeName = descriptor.getTypeName( Size.length( Size.DEFAULT_LENGTH ) );
11021115
//trim off the length/precision/scale
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.community.dialect;
6+
7+
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.GeneratedValue;
10+
import jakarta.persistence.Id;
11+
import jakarta.persistence.Inheritance;
12+
import jakarta.persistence.InheritanceType;
13+
import jakarta.persistence.Lob;
14+
import org.hibernate.annotations.JdbcTypeCode;
15+
import org.hibernate.cfg.AvailableSettings;
16+
import org.hibernate.testing.jdbc.SQLStatementInspector;
17+
import org.hibernate.testing.orm.junit.DomainModel;
18+
import org.hibernate.testing.orm.junit.JiraKey;
19+
import org.hibernate.testing.orm.junit.RequiresDialect;
20+
import org.hibernate.testing.orm.junit.ServiceRegistry;
21+
import org.hibernate.testing.orm.junit.SessionFactory;
22+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
23+
import org.hibernate.testing.orm.junit.Setting;
24+
import org.hibernate.type.SqlTypes;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
28+
import java.time.LocalDateTime;
29+
import java.util.List;
30+
import java.util.stream.Stream;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
35+
@RequiresDialect(org.hibernate.community.dialect.InformixDialect.class)
36+
@DomainModel(annotatedClasses = {
37+
InformixUnionNullCastingTest.BaseEntity.class,
38+
InformixUnionNullCastingTest.LvarcharEntity.class,
39+
InformixUnionNullCastingTest.DatetimeEntity.class,
40+
InformixUnionNullCastingTest.ClobEntity.class,
41+
InformixUnionNullCastingTest.EmptyEntity.class
42+
})
43+
@SessionFactory(useCollectingStatementInspector = true)
44+
@ServiceRegistry(
45+
settings = {
46+
@Setting(
47+
name = AvailableSettings.DIALECT,
48+
value = "org.hibernate.community.dialect.InformixDialect"
49+
),
50+
@Setting(name = AvailableSettings.SHOW_SQL, value = "true"),
51+
@Setting(name = AvailableSettings.FORMAT_SQL, value = "true"),
52+
}
53+
)
54+
public class InformixUnionNullCastingTest {
55+
56+
@BeforeEach
57+
protected void setupTest(SessionFactoryScope scope) {
58+
scope.inTransaction(
59+
(session) -> {
60+
session.persist(new EmptyEntity());
61+
62+
LvarcharEntity lvarcharEntity = new LvarcharEntity();
63+
lvarcharEntity.longContent = "This is a very long string designed to be stored as LVARCHAR in Informix.";
64+
session.persist(lvarcharEntity);
65+
66+
ClobEntity clobEntity = new ClobEntity();
67+
clobEntity.clobContent = "This is a CLOB content that should be cast correctly in union queries.";
68+
session.persist(clobEntity);
69+
70+
DatetimeEntity datetimeEntity = new DatetimeEntity();
71+
datetimeEntity.eventTime = LocalDateTime.now();
72+
session.persist(datetimeEntity);
73+
}
74+
);
75+
}
76+
77+
@Test
78+
@JiraKey(value = "HHH-19974")
79+
public void testInformixSpecialTypeCasting(SessionFactoryScope scope) {
80+
scope.inTransaction(session ->
81+
session.createQuery(
82+
"select r from root_entity r",
83+
BaseEntity.class
84+
).list()
85+
);
86+
87+
SQLStatementInspector inspector = scope.getCollectingStatementInspector();
88+
List<String> sqlQueries = inspector.getSqlQueries();
89+
90+
sqlQueries.forEach(System.out::println);
91+
92+
Stream.of(
93+
new CastExpectation("LVARCHAR", "cast(null as lvarchar)"),
94+
new CastExpectation("DATETIME", "cast(null as datetime", "year to"),
95+
new CastExpectation("CLOB", "cast(null as clob)")
96+
).forEach(expectation -> verifyCastings(sqlQueries, expectation));
97+
}
98+
99+
100+
private void verifyCastings(List<String> sqlQueries, CastExpectation expectation) {
101+
assertThat(sqlQueries)
102+
.as("SQL should contain proper cast for type: %s", expectation.typeName)
103+
.anyMatch(sql -> {
104+
String lowerSql = sql.toLowerCase();
105+
return expectation.requiredPatterns.stream()
106+
.allMatch(lowerSql::contains);
107+
});
108+
}
109+
110+
/**
111+
* A simple record to hold expectation data for verification.
112+
*
113+
* @param typeName The name of the type, used for error messaging.
114+
* @param requiredPatterns The list of lowercase string patterns that must be present in the generated SQL.
115+
*/
116+
record CastExpectation(String typeName, List<String> requiredPatterns) {
117+
CastExpectation(String typeName, String... patterns) {
118+
this(typeName, List.of(patterns));
119+
}
120+
}
121+
122+
123+
@Entity(name = "root_entity")
124+
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
125+
public static abstract class BaseEntity {
126+
@Id
127+
@GeneratedValue
128+
private Long id;
129+
}
130+
131+
@Entity(name = "lvarchar_entity")
132+
public static class LvarcharEntity extends BaseEntity {
133+
134+
@JdbcTypeCode(SqlTypes.LONGVARCHAR)
135+
public String longContent;
136+
}
137+
138+
@Entity(name = "datetime_entity")
139+
public static class DatetimeEntity extends BaseEntity {
140+
141+
public LocalDateTime eventTime;
142+
}
143+
144+
@Entity(name = "clob_entity")
145+
public static class ClobEntity extends BaseEntity {
146+
147+
@Lob
148+
@JdbcTypeCode(SqlTypes.CLOB)
149+
public String clobContent;
150+
}
151+
152+
@Entity(name = "empty_entity")
153+
public static class EmptyEntity extends BaseEntity {
154+
}
155+
156+
}

0 commit comments

Comments
 (0)