Skip to content

Commit f358c29

Browse files
committed
HHH-18780 Use column type information to generate union subclass null casts
1 parent 6d8137f commit f358c29

File tree

10 files changed

+995
-33
lines changed

10 files changed

+995
-33
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
5656
import org.hibernate.internal.util.JdbcExceptionHelper;
5757
import org.hibernate.metamodel.mapping.EntityMappingType;
58+
import org.hibernate.metamodel.mapping.SqlExpressible;
59+
import org.hibernate.metamodel.mapping.SqlTypedMapping;
5860
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
5961
import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport;
6062
import org.hibernate.procedure.spi.CallableStatementSupport;
@@ -217,6 +219,7 @@ protected String castType(int sqlTypeCode) {
217219
case NCHAR:
218220
case VARCHAR:
219221
case NVARCHAR:
222+
return "varchar";
220223
case LONG32VARCHAR:
221224
case LONG32NVARCHAR:
222225
return "text";
@@ -872,6 +875,14 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
872875
return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")";
873876
}
874877

878+
@Override
879+
public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) {
880+
final String castTypeName = typeConfiguration.getDdlTypeRegistry()
881+
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
882+
.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), typeConfiguration.getDdlTypeRegistry() );
883+
return "cast(null as " + castTypeName + ")";
884+
}
885+
875886
@Override
876887
public boolean supportsCommentOn() {
877888
return true;

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
import org.hibernate.mapping.Table;
117117
import org.hibernate.mapping.UserDefinedType;
118118
import org.hibernate.metamodel.mapping.EntityMappingType;
119+
import org.hibernate.metamodel.mapping.SqlTypedMapping;
119120
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
120121
import org.hibernate.persister.entity.Lockable;
121122
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
@@ -3115,11 +3116,31 @@ public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() {
31153116
* @param sqlType The {@link Types} type code.
31163117
* @param typeConfiguration The type configuration
31173118
* @return The appropriate select clause value fragment.
3119+
* @deprecated Use {@link #getSelectClauseNullString(SqlTypedMapping, TypeConfiguration)} instead
31183120
*/
3121+
@Deprecated(forRemoval = true)
31193122
public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) {
31203123
return "null";
31213124
}
31223125

3126+
/**
3127+
* Given a type mapping, return the expression
3128+
* for a literal null value of that type, to use in a {@code select}
3129+
* clause.
3130+
* <p>
3131+
* The {@code select} query will be an element of a {@code UNION}
3132+
* or {@code UNION ALL}.
3133+
*
3134+
* @implNote Some databases require an explicit type cast.
3135+
*
3136+
* @param sqlTypeMapping The type mapping.
3137+
* @param typeConfiguration The type configuration
3138+
* @return The appropriate select clause value fragment.
3139+
*/
3140+
public String getSelectClauseNullString(SqlTypedMapping sqlTypeMapping, TypeConfiguration typeConfiguration) {
3141+
return getSelectClauseNullString( sqlTypeMapping.getJdbcMapping().getJdbcType().getDdlTypeCode(), typeConfiguration );
3142+
}
3143+
31233144
/**
31243145
* Does this dialect support {@code UNION ALL}?
31253146
*

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.hibernate.mapping.Table;
5656
import org.hibernate.mapping.UserDefinedType;
5757
import org.hibernate.metamodel.mapping.EntityMappingType;
58+
import org.hibernate.metamodel.mapping.SqlTypedMapping;
5859
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
5960
import org.hibernate.persister.entity.Lockable;
6061
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
@@ -731,6 +732,11 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
731732
return wrapped.getSelectClauseNullString( sqlType, typeConfiguration );
732733
}
733734

735+
@Override
736+
public String getSelectClauseNullString(SqlTypedMapping sqlTypeMapping, TypeConfiguration typeConfiguration) {
737+
return wrapped.getSelectClauseNullString( sqlTypeMapping, typeConfiguration );
738+
}
739+
734740
@Override
735741
public boolean supportsUnionAll() {
736742
return wrapped.supportsUnionAll();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
5353
import org.hibernate.internal.util.JdbcExceptionHelper;
5454
import org.hibernate.metamodel.mapping.EntityMappingType;
55+
import org.hibernate.metamodel.mapping.SqlExpressible;
56+
import org.hibernate.metamodel.mapping.SqlTypedMapping;
5557
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
5658
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
5759
import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport;
@@ -247,6 +249,7 @@ protected String castType(int sqlTypeCode) {
247249
case NCHAR:
248250
case VARCHAR:
249251
case NVARCHAR:
252+
return "varchar";
250253
case LONG32VARCHAR:
251254
case LONG32NVARCHAR:
252255
return "text";
@@ -939,6 +942,17 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
939942
return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")";
940943
}
941944

945+
@Override
946+
public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) {
947+
final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry();
948+
final String castTypeName = ddlTypeRegistry
949+
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
950+
.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), ddlTypeRegistry );
951+
// PostgreSQL assumes a plain null literal in the select statement to be of type text,
952+
// which can lead to issues in e.g. the union subclass strategy, so do a cast
953+
return "cast(null as " + castTypeName + ")";
954+
}
955+
942956
@Override
943957
public String quoteCollation(String collation) {
944958
return '\"' + collation + '\"';

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88

99
import org.hibernate.engine.jdbc.Size;
1010

11+
import org.checkerframework.checker.nullness.qual.Nullable;
12+
1113
/**
1214
* Models the type of a thing that can be used as an expression in a SQL query
1315
*
1416
* @author Christian Beikov
1517
*/
1618
public interface SqlTypedMapping {
19+
@Nullable
1720
String getColumnDefinition();
18-
Long getLength();
19-
Integer getPrecision();
20-
Integer getScale();
21-
Integer getTemporalPrecision();
21+
@Nullable Long getLength();
22+
@Nullable Integer getPrecision();
23+
@Nullable Integer getScale();
24+
@Nullable Integer getTemporalPrecision();
2225
default boolean isLob() {
2326
return getJdbcMapping().getJdbcType().isLob();
2427
}

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,30 @@
99
import org.hibernate.metamodel.mapping.JdbcMapping;
1010
import org.hibernate.metamodel.mapping.SqlTypedMapping;
1111

12+
import org.checkerframework.checker.nullness.qual.Nullable;
13+
1214
/**
1315
* @author Christian Beikov
1416
*/
1517
public class SqlTypedMappingImpl implements SqlTypedMapping {
1618

17-
private final String columnDefinition;
18-
private final Long length;
19-
private final Integer precision;
20-
private final Integer scale;
21-
private final Integer temporalPrecision;
19+
private final @Nullable String columnDefinition;
20+
private final @Nullable Long length;
21+
private final @Nullable Integer precision;
22+
private final @Nullable Integer scale;
23+
private final @Nullable Integer temporalPrecision;
2224
private final JdbcMapping jdbcMapping;
2325

26+
public SqlTypedMappingImpl(JdbcMapping jdbcMapping) {
27+
this( null, null, null, null, null, jdbcMapping );
28+
}
29+
2430
public SqlTypedMappingImpl(
25-
String columnDefinition,
26-
Long length,
27-
Integer precision,
28-
Integer scale,
29-
Integer temporalPrecision,
31+
@Nullable String columnDefinition,
32+
@Nullable Long length,
33+
@Nullable Integer precision,
34+
@Nullable Integer scale,
35+
@Nullable Integer temporalPrecision,
3036
JdbcMapping jdbcMapping) {
3137
// Save memory by using interned strings. Probability is high that we have multiple duplicate strings
3238
this.columnDefinition = columnDefinition == null ? null : columnDefinition.intern();
@@ -38,27 +44,27 @@ public SqlTypedMappingImpl(
3844
}
3945

4046
@Override
41-
public String getColumnDefinition() {
47+
public @Nullable String getColumnDefinition() {
4248
return columnDefinition;
4349
}
4450

4551
@Override
46-
public Long getLength() {
52+
public @Nullable Long getLength() {
4753
return length;
4854
}
4955

5056
@Override
51-
public Integer getPrecision() {
57+
public @Nullable Integer getPrecision() {
5258
return precision;
5359
}
5460

5561
@Override
56-
public Integer getTemporalPrecision() {
62+
public @Nullable Integer getTemporalPrecision() {
5763
return temporalPrecision;
5864
}
5965

6066
@Override
61-
public Integer getScale() {
67+
public @Nullable Integer getScale() {
6268
return scale;
6369
}
6470

hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.hibernate.metamodel.mapping.SelectableMapping;
4848
import org.hibernate.metamodel.mapping.TableDetails;
4949
import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess;
50+
import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl;
5051
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
5152
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
5253
import org.hibernate.persister.spi.PersisterCreationContext;
@@ -492,8 +493,7 @@ protected String generateSubquery(PersistentClass model, Metadata mapping) {
492493
subquery.append( "select " );
493494
for ( Column col : columns ) {
494495
if ( !table.containsColumn( col ) ) {
495-
int sqlType = col.getSqlTypeCode( mapping );
496-
subquery.append( dialect.getSelectClauseNullString( sqlType, getFactory().getTypeConfiguration() ) )
496+
subquery.append( getSelectClauseNullString( col, dialect ) )
497497
.append(" as ");
498498
}
499499
subquery.append( col.getQuotedName( dialect ) )
@@ -508,6 +508,23 @@ protected String generateSubquery(PersistentClass model, Metadata mapping) {
508508
return subquery.append( ")" ).toString();
509509
}
510510

511+
private String getSelectClauseNullString(Column col, Dialect dialect) {
512+
return dialect.getSelectClauseNullString(
513+
new SqlTypedMappingImpl(
514+
col.getTypeName(),
515+
col.getLength(),
516+
col.getPrecision(),
517+
col.getScale(),
518+
col.getTemporalPrecision(),
519+
col.getValue().getSelectableType(
520+
col.getValue().getBuildingContext().getMetadataCollector(),
521+
col.getTypeIndex()
522+
)
523+
),
524+
getFactory().getTypeConfiguration()
525+
);
526+
}
527+
511528
protected String generateSubquery(Map<String, EntityNameUse> entityNameUses) {
512529
if ( !hasSubclasses() ) {
513530
return getTableName();
@@ -574,9 +591,7 @@ protected String generateSubquery(Map<String, EntityNameUse> entityNameUses) {
574591
if ( selectableMapping == null ) {
575592
// If there is no selectable mapping for a table name, we render a null expression
576593
selectableMapping = selectableMappings.values().iterator().next();
577-
final int sqlType = selectableMapping.getJdbcMapping().getJdbcType()
578-
.getDdlTypeCode();
579-
buf.append( dialect.getSelectClauseNullString( sqlType, getFactory().getTypeConfiguration() ) )
594+
buf.append( dialect.getSelectClauseNullString( selectableMapping, getFactory().getTypeConfiguration() ) )
580595
.append( " as " );
581596
}
582597
if ( selectableMapping.isFormula() ) {

hibernate-core/src/test/java/org/hibernate/orm/test/dialect/PostgreSQLDialectTestCase.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.sql.SQLException;
1212

1313
import org.hibernate.JDBCException;
14+
import org.hibernate.Length;
1415
import org.hibernate.LockMode;
1516
import org.hibernate.LockOptions;
1617
import org.hibernate.PessimisticLockException;
@@ -29,8 +30,14 @@
2930
import org.hibernate.mapping.Table;
3031
import org.hibernate.mapping.UniqueKey;
3132

33+
import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl;
34+
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
35+
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
3236
import org.hibernate.testing.TestForIssue;
3337
import org.hibernate.testing.junit4.BaseUnitTestCase;
38+
import org.hibernate.testing.orm.junit.JiraKey;
39+
40+
import org.hibernate.type.spi.TypeConfiguration;
3441
import org.junit.Test;
3542

3643
import org.mockito.Mockito;
@@ -153,6 +160,41 @@ public void testAlterTableDropConstraintString() {
153160
assertEquals("alter table if exists table_name drop constraint if exists unique_something", sql );
154161
}
155162

163+
@Test
164+
@JiraKey( value = "HHH-18780" )
165+
public void testTextVsVarchar() {
166+
PostgreSQLDialect dialect = new PostgreSQLDialect();
167+
168+
final TypeConfiguration typeConfiguration = new TypeConfiguration();
169+
final SqmFunctionRegistry functionRegistry = new SqmFunctionRegistry();
170+
typeConfiguration.scope( new DialectFeatureChecks.FakeMetadataBuildingContext( typeConfiguration, functionRegistry ) );
171+
final DialectFeatureChecks.FakeTypeContributions typeContributions = new DialectFeatureChecks.FakeTypeContributions( typeConfiguration );
172+
final DialectFeatureChecks.FakeFunctionContributions functionContributions = new DialectFeatureChecks.FakeFunctionContributions(
173+
dialect,
174+
typeConfiguration,
175+
functionRegistry
176+
);
177+
dialect.contribute( typeContributions, typeConfiguration.getServiceRegistry() );
178+
dialect.initializeFunctionRegistry( functionContributions );
179+
final String varcharNullString = dialect.getSelectClauseNullString(
180+
new SqlTypedMappingImpl( typeConfiguration.getBasicTypeForJavaType( String.class ) ),
181+
typeConfiguration
182+
);
183+
final String textNullString = dialect.getSelectClauseNullString(
184+
new SqlTypedMappingImpl(
185+
null,
186+
(long) Length.LONG32,
187+
null,
188+
null,
189+
null,
190+
typeConfiguration.getBasicTypeForJavaType( String.class )
191+
),
192+
typeConfiguration
193+
);
194+
assertEquals("cast(null as varchar)", varcharNullString);
195+
assertEquals("cast(null as text)", textNullString);
196+
}
197+
156198
private static class MockSqlStringGenerationContext implements SqlStringGenerationContext {
157199

158200
@Override

hibernate-core/src/test/java/org/hibernate/orm/test/hql/joinedSubclass/JoinedSubclassNativeQueryTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
import org.hibernate.cfg.AvailableSettings;
1010
import org.hibernate.engine.spi.SessionFactoryImplementor;
11-
import org.hibernate.type.SqlTypes;
11+
import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl;
1212

1313
import org.hibernate.testing.TestForIssue;
1414
import org.hibernate.testing.orm.junit.DomainModel;
1515
import org.hibernate.testing.orm.junit.SessionFactory;
1616
import org.hibernate.testing.orm.junit.SessionFactoryScope;
17+
import org.hibernate.type.spi.TypeConfiguration;
1718
import org.junit.jupiter.api.AfterAll;
1819
import org.junit.jupiter.api.Assertions;
1920
import org.junit.jupiter.api.BeforeAll;
@@ -63,10 +64,14 @@ public void testJoinedInheritanceNativeQuery(SessionFactoryScope scope) {
6364
scope.inTransaction(
6465
session -> {
6566
final SessionFactoryImplementor sessionFactory = scope.getSessionFactory();
67+
final TypeConfiguration typeConfiguration = sessionFactory.getTypeConfiguration();
6668
final String nullColumnString = sessionFactory
6769
.getJdbcServices()
6870
.getDialect()
69-
.getSelectClauseNullString( SqlTypes.VARCHAR, sessionFactory.getTypeConfiguration() );
71+
.getSelectClauseNullString(
72+
new SqlTypedMappingImpl( typeConfiguration.getBasicTypeForJavaType( String.class ) ),
73+
typeConfiguration
74+
);
7075
// PostgreSQLDialect#getSelectClauseNullString produces e.g. `null::text` which we interpret as parameter,
7176
// so workaround this problem by configuring to ignore JDBC parameters
7277
session.setProperty( AvailableSettings.NATIVE_IGNORE_JDBC_PARAMETERS, true );

0 commit comments

Comments
 (0)