Skip to content

HHH-18780 Use column type information to generate union subclass null casts #10663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

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 @@ -55,6 +55,8 @@
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.SqlExpressible;
import org.hibernate.metamodel.mapping.SqlTypedMapping;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport;
import org.hibernate.procedure.spi.CallableStatementSupport;
Expand Down Expand Up @@ -217,6 +219,7 @@ protected String castType(int sqlTypeCode) {
case NCHAR:
case VARCHAR:
case NVARCHAR:
return "varchar";
case LONG32VARCHAR:
case LONG32NVARCHAR:
return "text";
Expand Down Expand Up @@ -872,6 +875,14 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")";
}

@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() );
return "cast(null as " + castTypeName + ")";
}

@Override
public boolean supportsCommentOn() {
return true;
Expand Down
21 changes: 21 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
import org.hibernate.mapping.Table;
import org.hibernate.mapping.UserDefinedType;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.SqlTypedMapping;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
Expand Down Expand Up @@ -3115,11 +3116,31 @@
* @param sqlType The {@link Types} type code.
* @param typeConfiguration The type configuration
* @return The appropriate select clause value fragment.
* @deprecated Use {@link #getSelectClauseNullString(SqlTypedMapping, TypeConfiguration)} instead
*/
@Deprecated(forRemoval = true)
public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) {
return "null";
}

/**
* Given a type mapping, return the expression
* for a literal null value of that type, to use in a {@code select}
* clause.
* <p>
* The {@code select} query will be an element of a {@code UNION}
* or {@code UNION ALL}.
*
* @implNote Some databases require an explicit type cast.
*
* @param sqlTypeMapping The type mapping.
* @param typeConfiguration The type configuration
* @return The appropriate select clause value fragment.
*/
public String getSelectClauseNullString(SqlTypedMapping sqlTypeMapping, TypeConfiguration typeConfiguration) {
return getSelectClauseNullString( sqlTypeMapping.getJdbcMapping().getJdbcType().getDdlTypeCode(), typeConfiguration );
}

/**
* Does this dialect support {@code UNION ALL}?
*
Expand Down Expand Up @@ -3217,7 +3238,7 @@
* {@code is false}, or {@code false} if it does not. The
* default is {@code is false}.
*/
public boolean supportsIsTrue() {

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
Dialect.getSelectClauseNullString
should be avoided because it has been deprecated.
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.hibernate.mapping.Table;
import org.hibernate.mapping.UserDefinedType;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.SqlTypedMapping;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
Expand Down Expand Up @@ -731,6 +732,11 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
return wrapped.getSelectClauseNullString( sqlType, typeConfiguration );
}

@Override
public String getSelectClauseNullString(SqlTypedMapping sqlTypeMapping, TypeConfiguration typeConfiguration) {
return wrapped.getSelectClauseNullString( sqlTypeMapping, typeConfiguration );
}

@Override
public boolean supportsUnionAll() {
return wrapped.supportsUnionAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.SqlExpressible;
import org.hibernate.metamodel.mapping.SqlTypedMapping;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport;
Expand Down Expand Up @@ -247,6 +249,7 @@ protected String castType(int sqlTypeCode) {
case NCHAR:
case VARCHAR:
case NVARCHAR:
return "varchar";
case LONG32VARCHAR:
case LONG32NVARCHAR:
return "text";
Expand Down Expand Up @@ -939,6 +942,17 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi
return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")";
}

@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
return "cast(null as " + castTypeName + ")";
}

@Override
public String quoteCollation(String collation) {
return '\"' + collation + '\"';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@

import java.util.List;

import org.hibernate.metamodel.mapping.JdbcMappingContainer;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.ast.tree.select.SortSpecification;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;

import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Oracle array_to_string function.
Expand All @@ -37,22 +39,89 @@ public void render(
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
final String arrayTypeName = DdlTypeHelper.getTypeName(
( (Expression) sqlAstArguments.get( 0 ) ).getExpressionType(),
walker.getSessionFactory().getTypeConfiguration()
);
sqlAppender.append( arrayTypeName );
sqlAppender.append( "_to_string(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 1 ).accept( walker );
if ( sqlAstArguments.size() > 2 ) {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final JdbcMappingContainer expressionType = (arrayExpression).getExpressionType();
if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression
&& ArrayAggFunction.FUNCTION_NAME.equals( ( (FunctionExpression) arrayExpression ).getFunctionName() ) ) {
final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression
= (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) arrayExpression;
// When the array argument is an aggregate expression, we access its contents directly
final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 );
final @Nullable Expression defaultExpression =
sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
final List<SortSpecification> withinGroup = functionExpression.getWithinGroup();
final Predicate filter = functionExpression.getFilter();

sqlAppender.append( "listagg(" );
if ( filter != null ) {
sqlAppender.appendSql( "case when " );
walker.getCurrentClauseStack().push( Clause.WHERE );
filter.accept( walker );
walker.getCurrentClauseStack().pop();
sqlAppender.appendSql( " then " );
}
if ( defaultExpression != null ) {
sqlAppender.append( "coalesce(" );
}
arrayElementExpression.accept( walker );
if ( defaultExpression != null ) {
sqlAppender.append( ',' );
defaultExpression.accept( walker );
sqlAppender.append( ')' );
}
if ( filter != null ) {
sqlAppender.appendSql( " else null end" );
}
sqlAppender.append( ',' );
sqlAstArguments.get( 2 ).accept( walker );
sqlAstArguments.get( 1 ).accept( walker );
sqlAppender.appendSql( ')' );

if ( withinGroup != null && !withinGroup.isEmpty() ) {
walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP );
sqlAppender.appendSql( " within group (order by " );
withinGroup.get( 0 ).accept( walker );
for ( int i = 1; i < withinGroup.size(); i++ ) {
sqlAppender.appendSql( ',' );
withinGroup.get( i ).accept( walker );
}
sqlAppender.appendSql( ')' );
walker.getCurrentClauseStack().pop();
}
}
else if ( expressionType.getSingleJdbcMapping().getJdbcType().getDefaultSqlTypeCode() == SqlTypes.JSON ) {
sqlAppender.append( "(select listagg(" );
if ( sqlAstArguments.size() > 2 ) {
sqlAppender.append( "coalesce(t.v," );
sqlAstArguments.get( 2 ).accept( walker );
sqlAppender.append( ")," );
}
else {
sqlAppender.append( "t.v," );
}

sqlAstArguments.get( 1 ).accept( walker );
sqlAppender.append( ") from json_table(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ",'$[*]' columns (v path '$')) t)" );
}
else {
sqlAppender.append( ",null" );
final String arrayTypeName = DdlTypeHelper.getTypeName(
expressionType,
walker.getSessionFactory().getTypeConfiguration()
);
sqlAppender.append( arrayTypeName );
sqlAppender.append( "_to_string(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 1 ).accept( walker );
if ( sqlAstArguments.size() > 2 ) {
sqlAppender.append( ',' );
sqlAstArguments.get( 2 ).accept( walker );
}
else {
sqlAppender.append( ",null" );
}
sqlAppender.append( ')' );
}
sqlAppender.append( ')' );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@

import org.hibernate.engine.jdbc.Size;

import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Models the type of a thing that can be used as an expression in a SQL query
*
* @author Christian Beikov
*/
public interface SqlTypedMapping {
@Nullable
String getColumnDefinition();
Long getLength();
Integer getPrecision();
Integer getScale();
Integer getTemporalPrecision();
@Nullable Long getLength();
@Nullable Integer getPrecision();
@Nullable Integer getScale();
@Nullable Integer getTemporalPrecision();
default boolean isLob() {
return getJdbcMapping().getJdbcType().isLob();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,30 @@
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.SqlTypedMapping;

import org.checkerframework.checker.nullness.qual.Nullable;

/**
* @author Christian Beikov
*/
public class SqlTypedMappingImpl implements SqlTypedMapping {

private final String columnDefinition;
private final Long length;
private final Integer precision;
private final Integer scale;
private final Integer temporalPrecision;
private final @Nullable String columnDefinition;
private final @Nullable Long length;
private final @Nullable Integer precision;
private final @Nullable Integer scale;
private final @Nullable Integer temporalPrecision;
private final JdbcMapping jdbcMapping;

public SqlTypedMappingImpl(JdbcMapping jdbcMapping) {
this( null, null, null, null, null, jdbcMapping );
}

public SqlTypedMappingImpl(
String columnDefinition,
Long length,
Integer precision,
Integer scale,
Integer temporalPrecision,
@Nullable String columnDefinition,
@Nullable Long length,
@Nullable Integer precision,
@Nullable Integer scale,
@Nullable Integer temporalPrecision,
JdbcMapping jdbcMapping) {
// Save memory by using interned strings. Probability is high that we have multiple duplicate strings
this.columnDefinition = columnDefinition == null ? null : columnDefinition.intern();
Expand All @@ -38,27 +44,27 @@ public SqlTypedMappingImpl(
}

@Override
public String getColumnDefinition() {
public @Nullable String getColumnDefinition() {
return columnDefinition;
}

@Override
public Long getLength() {
public @Nullable Long getLength() {
return length;
}

@Override
public Integer getPrecision() {
public @Nullable Integer getPrecision() {
return precision;
}

@Override
public Integer getTemporalPrecision() {
public @Nullable Integer getTemporalPrecision() {
return temporalPrecision;
}

@Override
public Integer getScale() {
public @Nullable Integer getScale() {
return scale;
}

Expand Down
Loading
Loading