Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.metamodel.mapping.SqlTypedMapping;
import org.hibernate.query.sqm.CastType;
import org.hibernate.query.sqm.IntervalType;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
Expand Down Expand Up @@ -1097,6 +1098,18 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
return "cast(null as " + castType + ")";
}

// Add override for the newer signature to access columnDefinition
@Override
public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) {
final String castTypeName = sqlType.getColumnDefinition();
if ( castTypeName != null ) {
return "cast(null as " + castTypeName + ")";
}

return getSelectClauseNullString( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode(), typeConfiguration );
}


private static String castType(DdlType descriptor) {
final String typeName = descriptor.getTypeName( Size.length( Size.DEFAULT_LENGTH ) );
//trim off the length/precision/scale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -962,9 +962,18 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi

@Override
public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) {
final String castTypeName = typeConfiguration.getDdlTypeRegistry()
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), typeConfiguration.getDdlTypeRegistry() );
String castTypeName = sqlType.getColumnDefinition();

if ( castTypeName == null ) {
castTypeName = typeConfiguration.getDdlTypeRegistry()
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
.getCastTypeName(
sqlType.toSize(),
(SqlExpressible) sqlType.getJdbcMapping(),
typeConfiguration.getDdlTypeRegistry()
);
}

return "cast(null as " + castTypeName + ")";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.community.dialect;

import java.util.ArrayList;
import java.util.List;

import org.hibernate.cfg.AvailableSettings;
import org.hibernate.resource.jdbc.spi.StatementInspector;

import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;

// Even though we are testing PostgreSQLLegacyDialect, the test environment runs a standard PostgreSQL instance,
// which is detected as PostgreSQLDialect. To prevent the test from being skipped, we require PostgreSQLDialect.
// The actual PostgreSQLLegacyDialect is enforced internally via the @Setting annotation.
@RequiresDialect(org.hibernate.dialect.PostgreSQLDialect.class)
@DomainModel(annotatedClasses = {
PostgreSQLLegacyDialectTest.BaseEntity.class,
PostgreSQLLegacyDialectTest.InetEntity.class,
PostgreSQLLegacyDialectTest.EmptyEntity.class
})
@SessionFactory
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the standard statement collector instead. Same for the other PostgreSQL test

Suggested change
@SessionFactory
@SessionFactory(useCollectingStatementInspector = true)
@ServiceRegistry(
settings = {
@Setting(
name = AvailableSettings.DIALECT,
value = "org.hibernate.community.dialect.PostgreSQLLegacyDialect"
),
@Setting(
name = AvailableSettings.STATEMENT_INSPECTOR,
value = "org.hibernate.community.dialect.PostgreSQLLegacyDialectTest$SqlSpy"
)
}

)
public class PostgreSQLLegacyDialectTest {

public static final List<String> SQL_LOG = new ArrayList<>();

@BeforeEach
protected void setupTest(SessionFactoryScope scope) {
SQL_LOG.clear();
scope.inTransaction(
(session) -> {
session.createNativeQuery(
"insert " +
"into inet_entity (id, ipAddress) " +
"values (1, '192.168.0.1'::inet)"
)
.executeUpdate();
session.persist( new EmptyEntity() );
}
);
SQL_LOG.clear();
}

@Test
@JiraKey(value = "HHH-19974")
public void testCastNullString(SessionFactoryScope scope) {
scope.inTransaction(
(session) -> {
String entityName = BaseEntity.class.getName();

List<BaseEntity> results = session.createQuery(
"select r from " + entityName + " r", BaseEntity.class
).list();

boolean foundCast = false;
for ( String sql : SQL_LOG ) {
if ( sql.contains( "cast(null as inet)" ) ) {
foundCast = true;
break;
}
}

Assertions.assertTrue( foundCast, "must contains 'cast(null as inet)' clause." );
}
);
}


@Entity(name = "root_entity")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public static abstract class BaseEntity {
@Id
@GeneratedValue
private Long id;
}

@Entity(name = "inet_entity")
public static class InetEntity extends BaseEntity {

@Column(columnDefinition = "inet")
private String ipAddress;

public InetEntity() {
}
}

@Entity(name = "empty_entity")
public static class EmptyEntity extends BaseEntity {
}

public static class SqlSpy implements StatementInspector {
@Override
public String inspect(String sql) {
SQL_LOG.add( sql.toLowerCase() );
return sql;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -944,12 +944,16 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi

@Override
public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) {
final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry();
final String castTypeName = ddlTypeRegistry
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), ddlTypeRegistry );
// PostgreSQL assumes a plain null literal in the select statement to be of type text,
// which can lead to issues in e.g. the union subclass strategy, so do a cast
String castTypeName = sqlType.getColumnDefinition();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that this is the substance of the proposed change?

But this doesn't look right to me. If this getColumnDefinition() method returns the content of @Column(columnDefinition), then that is not a column type and should never be treated as such.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are absolutely right.

If columnDefinition contains constraints like default ... or not null, passing it directly to cast() will indeed cause a syntax error. I missed that edge case.

The underlying issue is: The field is mapped as String in Java, so Hibernate currently defaults to casting the null literal as varchar. However, the actual DB column is inet, and PostgreSQL throws a type mismatch error (UNION types inet and character varying cannot be matched).

Since we can't safely use columnDefinition, is there a recommended way in Hibernate to extract only the SQL type name (e.g. inet) for these native types, or should we register these types explicitly in the Dialect to avoid the varchar fallback?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably do it by registering a DdlType and a JdbcType. Something like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean it looks like we already have this stuff built in:

You just write:

public InetAddress addr;

Or if you want to use a string:

@JdbcTypeCode(SqlTypes.INET) public String addrStr;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gavinking

I have verified your suggestions.

You are absolutely right regarding the inet type.
I confirmed that both approaches work perfectly without any changes to the dialect:

  1. Using @JdbcTypeCode(SqlTypes.INET) on a String field.
  2. Using the InetAddress Java type directly (e.g., private InetAddress addr;).

In both cases, the test passes and Hibernate correctly generates cast(null as inet) in the SQL.
So, at least for types that can be mapped to a standard SqlTypes (or have a built-in mapping like InetAddress), the current mechanism works fine if the mapping is configured correctly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, regarding InformixDialect:

I noticed that InformixDialect currently only overrides the deprecated getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) method.

Even if we drop the columnDefinition logic (as agreed), would you be open to a PR that refactors InformixDialect to override the new getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) signature instead?

@Override public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) { final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); final String castTypeName = ddlTypeRegistry	.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )	.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), ddlTypeRegistry ); return "cast(null as " + castTypeName + ")"; }

This would align the dialect with the modern API and make future type handling improvements easier.


if ( castTypeName == null ) {
final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry();
castTypeName = ddlTypeRegistry
.getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() )
.getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), ddlTypeRegistry );
// PostgreSQL assumes a plain null literal in the select statement to be of type text,
// which can lead to issues in e.g. the union subclass strategy, so do a cast
}
return "cast(null as " + castTypeName + ")";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect;


import java.util.ArrayList;
import java.util.List;

import org.hibernate.cfg.AvailableSettings;
import org.hibernate.resource.jdbc.spi.StatementInspector;

import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;

@RequiresDialect(PostgreSQLDialect.class)
@DomainModel(annotatedClasses = {
PostgreSQLDialectTest.BaseEntity.class,
PostgreSQLDialectTest.InetEntity.class,
PostgreSQLDialectTest.EmptyEntity.class
})
@SessionFactory
@ServiceRegistry(
settings = @Setting(
name = AvailableSettings.STATEMENT_INSPECTOR,
value = "org.hibernate.dialect.PostgreSQLDialectTest$SqlSpy"
)
)
public class PostgreSQLDialectTest {

public static final List<String> SQL_LOG = new ArrayList<>();

@BeforeEach
protected void setupTest(SessionFactoryScope scope) {
SQL_LOG.clear();
scope.inTransaction(
(session) -> {
session.createNativeQuery(
"insert " +
"into inet_entity (id, ipAddress) " +
"values (1, '192.168.0.1'::inet)"
)
.executeUpdate();
session.persist( new EmptyEntity() );
}
);
SQL_LOG.clear();
}

@Test
@JiraKey(value = "HHH-19974")
public void testCastNullString(SessionFactoryScope scope) {
scope.inTransaction(
(session) -> {
String entityName = BaseEntity.class.getName();

List<BaseEntity> results = session.createQuery(
"select r from " + entityName + " r", BaseEntity.class
).list();

boolean foundCast = false;
for ( String sql : SQL_LOG ) {
if ( sql.contains( "cast(null as inet)" ) ) {
foundCast = true;
break;
}
}

Assertions.assertTrue( foundCast, "must contains 'cast(null as inet)' clause." );
}
);
}


@Entity(name = "root_entity")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public static abstract class BaseEntity {
@Id
@GeneratedValue
private Long id;
}

@Entity(name = "inet_entity")
public static class InetEntity extends BaseEntity {

@Column(columnDefinition = "inet")
private String ipAddress;

public InetEntity() {
}
}

@Entity(name = "empty_entity")
public static class EmptyEntity extends BaseEntity {
}

public static class SqlSpy implements StatementInspector {
@Override
public String inspect(String sql) {
SQL_LOG.add( sql.toLowerCase() );
return sql;
}
}

}