HHH-17375 Support casting array to string and add optional third argument to array_to_string for null element handling
This commit is contained in:
parent
8ec90b8fb1
commit
64dd9e657c
|
@ -1202,7 +1202,7 @@ The following functions deal with SQL array types, which are not supported on ev
|
|||
| `array_trim()` | Creates array copy trimming the last _N_ elements
|
||||
| `array_fill()` | Creates array filled with the same element _N_ times
|
||||
| `array_fill_list()` | Like `array_fill`, but returns the result as `List<?>`
|
||||
| `array_to_string()` | String representation of non-null array elements
|
||||
| `array_to_string()` | String representation of array
|
||||
|===
|
||||
|
||||
[[hql-array-constructor-functions]]
|
||||
|
@ -1546,9 +1546,11 @@ include::{array-example-dir-hql}/ArrayFillTest.java[tags=hql-array-fill-example]
|
|||
====
|
||||
|
||||
[[hql-array-to-string-functions]]
|
||||
===== `array_to_string()`
|
||||
===== `array_to_string()` or `cast(array as String)`
|
||||
|
||||
Concatenates the non-null array elements with a separator, as specified by the arguments.
|
||||
Concatenates the array elements with a separator, as specified by the arguments.
|
||||
Null values are filtered, but the optional third argument can be specified to define a default value to use
|
||||
when a `null` array element is encountered.
|
||||
Returns `null` if the first argument is `null`.
|
||||
|
||||
[[hql-array-to-string-example]]
|
||||
|
@ -1559,6 +1561,17 @@ include::{array-example-dir-hql}/ArrayToStringTest.java[tags=hql-array-to-string
|
|||
----
|
||||
====
|
||||
|
||||
Alternatively, it is also possible to use `cast(array as String)`,
|
||||
which is a short version of `concat('[', array_to_string(array, ',', 'null'), ']')`.
|
||||
|
||||
[[hql-array-to-string-hql-example]]
|
||||
====
|
||||
[source, JAVA, indent=0]
|
||||
----
|
||||
include::{array-example-dir-hql}/ArrayToStringTest.java[tags=hql-array-to-string-hql-example]
|
||||
----
|
||||
====
|
||||
|
||||
[[hql-user-defined-functions]]
|
||||
==== Native and user-defined functions
|
||||
|
||||
|
|
|
@ -104,6 +104,11 @@ public class PostgresPlusLegacyDialect extends PostgreSQLLegacyDialect {
|
|||
return super.timestampdiffPattern( unit, fromTemporalType, toTemporalType );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmptyStringTreatedAsNull() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int registerResultSetOutParameter(CallableStatement statement, int col) throws SQLException {
|
||||
statement.registerOutParameter( col, Types.REF );
|
||||
|
|
|
@ -248,13 +248,16 @@ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExport
|
|||
"return res; " +
|
||||
"end;",
|
||||
"create or replace function " + arrayTypeName + "_to_string(arr in " + arrayTypeName +
|
||||
", sep in varchar2) return varchar2 deterministic is " +
|
||||
", sep in varchar2, nullVal in varchar2) return varchar2 deterministic is " +
|
||||
"res varchar2(4000):=''; begin " +
|
||||
"if arr is null or sep is null then return null; end if; " +
|
||||
"for i in 1 .. arr.count loop " +
|
||||
"if arr(i) is not null then " +
|
||||
"if length(res)<>0 then res:=res||sep; end if; " +
|
||||
"res:=res||arr(i); " +
|
||||
"elsif nullVal is not null then " +
|
||||
"if length(res)<>0 then res:=res||sep; end if; " +
|
||||
"res:=res||nullVal; " +
|
||||
"end if; " +
|
||||
"end loop; " +
|
||||
"return res; " +
|
||||
|
|
|
@ -104,6 +104,11 @@ public class PostgresPlusDialect extends PostgreSQLDialect {
|
|||
return super.timestampdiffPattern( unit, fromTemporalType, toTemporalType );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmptyStringTreatedAsNull() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int registerResultSetOutParameter(CallableStatement statement, int col) throws SQLException {
|
||||
statement.registerOutParameter( col, Types.REF );
|
||||
|
|
|
@ -10,10 +10,14 @@ import java.sql.Types;
|
|||
import java.util.List;
|
||||
|
||||
import org.hibernate.dialect.Dialect;
|
||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||
import org.hibernate.query.ReturnableType;
|
||||
import org.hibernate.query.sqm.CastType;
|
||||
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
|
||||
import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression;
|
||||
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
|
||||
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
|
||||
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
|
||||
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
|
||||
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
|
||||
|
@ -23,6 +27,8 @@ import org.hibernate.sql.ast.spi.SqlAppender;
|
|||
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||
import org.hibernate.sql.ast.tree.expression.Expression;
|
||||
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
|
||||
import org.hibernate.type.BasicType;
|
||||
|
||||
/**
|
||||
* ANSI SQL-inspired {@code cast()} function, where the target types
|
||||
|
@ -71,9 +77,59 @@ public class CastFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
|
|||
final JdbcMapping targetJdbcMapping = castTarget.getExpressionType().getSingleJdbcMapping();
|
||||
final CastType targetType = getCastType( targetJdbcMapping );
|
||||
|
||||
String cast = dialect.castPattern( sourceType, targetType );
|
||||
if ( sourceType == CastType.OTHER && targetType == CastType.STRING
|
||||
&& sourceMapping.getJdbcType().isArray() ) {
|
||||
renderCastArrayToString( sqlAppender, arguments.get( 0 ), dialect, walker );
|
||||
}
|
||||
else {
|
||||
String cast = dialect.castPattern( sourceType, targetType );
|
||||
|
||||
new PatternRenderer( cast ).render( sqlAppender, arguments, walker );
|
||||
new PatternRenderer( cast ).render( sqlAppender, arguments, walker );
|
||||
}
|
||||
}
|
||||
|
||||
public static void renderCastArrayToString(
|
||||
SqlAppender sqlAppender,
|
||||
SqlAstNode arrayArgument,
|
||||
Dialect dialect,
|
||||
SqlAstTranslator<?> walker) {
|
||||
final SessionFactoryImplementor sessionFactory = walker.getSessionFactory();
|
||||
final BasicType<?> stringType = sessionFactory.getTypeConfiguration()
|
||||
.getBasicTypeForJavaType( String.class );
|
||||
final SqmFunctionRegistry functionRegistry = sessionFactory.getQueryEngine()
|
||||
.getSqmFunctionRegistry();
|
||||
final SqmFunctionDescriptor concatDescriptor = functionRegistry.findFunctionDescriptor( "concat" );
|
||||
final SqmFunctionDescriptor arrayToStringDescriptor = functionRegistry.findFunctionDescriptor( "array_to_string" );
|
||||
final boolean caseWhen = dialect.isEmptyStringTreatedAsNull();
|
||||
if ( caseWhen ) {
|
||||
sqlAppender.append( "case when " );
|
||||
arrayArgument.accept( walker );
|
||||
sqlAppender.append( " is null then null else " );
|
||||
}
|
||||
|
||||
( (AbstractSqmSelfRenderingFunctionDescriptor) concatDescriptor ).render(
|
||||
sqlAppender,
|
||||
List.of(
|
||||
new QueryLiteral<>( "[", stringType ),
|
||||
new SelfRenderingFunctionSqlAstExpression(
|
||||
"array_to_string",
|
||||
( (AbstractSqmSelfRenderingFunctionDescriptor) arrayToStringDescriptor ),
|
||||
List.of(
|
||||
arrayArgument,
|
||||
new QueryLiteral<>( ",", stringType ),
|
||||
new QueryLiteral<>( "null", stringType )
|
||||
),
|
||||
stringType,
|
||||
stringType
|
||||
),
|
||||
new QueryLiteral<>( "]", stringType )
|
||||
),
|
||||
stringType,
|
||||
walker
|
||||
);
|
||||
if ( caseWhen ) {
|
||||
sqlAppender.append( " end" );
|
||||
}
|
||||
}
|
||||
|
||||
private CastType getCastType(JdbcMapping sourceMapping) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.hibernate.type.SqlTypes;
|
|||
import org.hibernate.type.StandardBasicTypes;
|
||||
import org.hibernate.type.spi.TypeConfiguration;
|
||||
|
||||
import static org.hibernate.dialect.function.CastFunction.renderCastArrayToString;
|
||||
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
|
||||
|
||||
public class CastingConcatFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
|
||||
|
@ -97,12 +98,16 @@ public class CastingConcatFunction extends AbstractSqmSelfRenderingFunctionDescr
|
|||
|
||||
private void renderAsString(SqlAppender sqlAppender, SqlAstTranslator<?> translator, Expression expression) {
|
||||
final JdbcMapping sourceMapping = expression.getExpressionType().getSingleJdbcMapping();
|
||||
final CastType sourceType = sourceMapping.getCastType();
|
||||
// No need to cast if we already have a string
|
||||
if ( sourceMapping.getCastType() == CastType.STRING ) {
|
||||
if ( sourceType == CastType.STRING ) {
|
||||
translator.render( expression, argumentRenderingMode );
|
||||
}
|
||||
else if ( sourceType == CastType.OTHER && sourceMapping.getJdbcType().isArray() ) {
|
||||
renderCastArrayToString( sqlAppender, expression, dialect, translator );
|
||||
}
|
||||
else {
|
||||
final String cast = dialect.castPattern( sourceMapping.getCastType(), CastType.STRING );
|
||||
final String cast = dialect.castPattern( sourceType, CastType.STRING );
|
||||
new PatternRenderer( cast.replace( "?2", concatArgumentCastType ), argumentRenderingMode )
|
||||
.render( sqlAppender, Collections.singletonList( expression ), translator );
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ import org.hibernate.sql.ast.tree.select.QuerySpec;
|
|||
import org.hibernate.type.StandardBasicTypes;
|
||||
import org.hibernate.type.spi.TypeConfiguration;
|
||||
|
||||
import static org.hibernate.dialect.function.CastFunction.renderCastArrayToString;
|
||||
|
||||
/**
|
||||
* @author Christian Beikov
|
||||
*/
|
||||
|
@ -401,13 +403,18 @@ public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
|
|||
}
|
||||
else {
|
||||
final JdbcMapping sourceMapping = realArg.getExpressionType().getSingleJdbcMapping();
|
||||
final CastType sourceType = sourceMapping.getCastType();
|
||||
// No need to cast if we already have a string
|
||||
if ( sourceMapping.getCastType() == CastType.STRING ) {
|
||||
if ( sourceType == CastType.STRING ) {
|
||||
translator.render( realArg, defaultArgumentRenderingMode );
|
||||
return false;
|
||||
}
|
||||
else if ( sourceType == CastType.OTHER && sourceMapping.getJdbcType().isArray() ) {
|
||||
renderCastArrayToString( sqlAppender, realArg, dialect, translator );
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
final String cast = dialect.castPattern( sourceMapping.getCastType(), CastType.STRING );
|
||||
final String cast = dialect.castPattern( sourceType, CastType.STRING );
|
||||
new PatternRenderer( cast.replace( "?2", concatArgumentCastType ) )
|
||||
.render( sqlAppender, Collections.singletonList( realArg ), translator );
|
||||
return false;
|
||||
|
|
|
@ -36,18 +36,20 @@ public class ArrayAndElementArgumentValidator extends ArrayArgumentValidator {
|
|||
TypeConfiguration typeConfiguration) {
|
||||
final BasicType<?> expectedElementType = getElementType( arguments, functionName, typeConfiguration );
|
||||
for ( int elementIndex : elementIndexes ) {
|
||||
final SqmTypedNode<?> elementArgument = arguments.get( elementIndex );
|
||||
final SqmExpressible<?> elementType = elementArgument.getExpressible().getSqmType();
|
||||
if ( expectedElementType != null && elementType != null && expectedElementType != elementType ) {
|
||||
throw new FunctionArgumentException(
|
||||
String.format(
|
||||
"Parameter %d of function '%s()' has type %s, but argument is of type '%s'",
|
||||
elementIndex,
|
||||
functionName,
|
||||
expectedElementType.getJavaTypeDescriptor().getTypeName(),
|
||||
elementType.getTypeName()
|
||||
)
|
||||
);
|
||||
if ( elementIndex < arguments.size() ) {
|
||||
final SqmTypedNode<?> elementArgument = arguments.get( elementIndex );
|
||||
final SqmExpressible<?> elementType = elementArgument.getExpressible().getSqmType();
|
||||
if ( expectedElementType != null && elementType != null && expectedElementType != elementType ) {
|
||||
throw new FunctionArgumentException(
|
||||
String.format(
|
||||
"Parameter %d of function '%s()' has type %s, but argument is of type '%s'",
|
||||
elementIndex,
|
||||
functionName,
|
||||
expectedElementType.getJavaTypeDescriptor().getTypeName(),
|
||||
elementType.getTypeName()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,13 +34,16 @@ public class ArrayToStringFunction extends AbstractSqmSelfRenderingFunctionDescr
|
|||
"array_to_string",
|
||||
FunctionKind.NORMAL,
|
||||
StandardArgumentsValidators.composite(
|
||||
new ArgumentTypesValidator( null, ANY, STRING ),
|
||||
ArrayArgumentValidator.DEFAULT_INSTANCE
|
||||
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY ),
|
||||
new ArrayAndElementArgumentValidator( 0, 2 )
|
||||
),
|
||||
StandardFunctionReturnTypeResolvers.invariant(
|
||||
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
|
||||
),
|
||||
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING )
|
||||
StandardFunctionArgumentTypeResolvers.composite(
|
||||
new ArrayAndElementArgumentTypeResolver( 0, 2 ),
|
||||
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -54,6 +57,10 @@ public class ArrayToStringFunction extends AbstractSqmSelfRenderingFunctionDescr
|
|||
sqlAstArguments.get( 0 ).accept( walker );
|
||||
sqlAppender.appendSql( ',' );
|
||||
sqlAstArguments.get( 1 ).accept( walker );
|
||||
if ( sqlAstArguments.size() > 2 ) {
|
||||
sqlAppender.appendSql( ',' );
|
||||
sqlAstArguments.get( 2 ).accept( walker );
|
||||
}
|
||||
sqlAppender.appendSql( ')' );
|
||||
}
|
||||
|
||||
|
|
|
@ -37,11 +37,22 @@ public class H2ArrayToStringFunction extends ArrayToStringFunction {
|
|||
SqlAstTranslator<?> walker) {
|
||||
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
|
||||
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
|
||||
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
|
||||
sqlAppender.append( "case when " );
|
||||
arrayExpression.accept( walker );
|
||||
sqlAppender.append( " is not null then coalesce((select listagg(array_get(" );
|
||||
sqlAppender.append( " is not null then coalesce((select listagg(" );
|
||||
if ( defaultExpression != null ) {
|
||||
sqlAppender.append( "coalesce(" );
|
||||
}
|
||||
sqlAppender.append( "array_get(" );
|
||||
arrayExpression.accept( walker );
|
||||
sqlAppender.append(",i.idx)," );
|
||||
sqlAppender.append(",i.idx)" );
|
||||
if ( defaultExpression != null ) {
|
||||
sqlAppender.append( "," );
|
||||
defaultExpression.accept( walker );
|
||||
sqlAppender.append( ")" );
|
||||
}
|
||||
sqlAppender.append("," );
|
||||
separatorExpression.accept( walker );
|
||||
sqlAppender.append( ") within group (order by i.idx) from system_range(1,");
|
||||
sqlAppender.append( Integer.toString( maximumArraySize ) );
|
||||
|
|
|
@ -33,9 +33,20 @@ public class HSQLArrayToStringFunction extends ArrayToStringFunction {
|
|||
SqlAstTranslator<?> walker) {
|
||||
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
|
||||
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
|
||||
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
|
||||
sqlAppender.append( "case when " );
|
||||
arrayExpression.accept( walker );
|
||||
sqlAppender.append( " is not null then coalesce((select group_concat(t.val order by t.idx separator " );
|
||||
sqlAppender.append( " is not null then coalesce((select group_concat(" );
|
||||
if ( defaultExpression != null ) {
|
||||
sqlAppender.append( "coalesce(" );
|
||||
}
|
||||
sqlAppender.append( "t.val" );
|
||||
if ( defaultExpression != null ) {
|
||||
sqlAppender.append( "," );
|
||||
defaultExpression.accept( walker );
|
||||
sqlAppender.append( ")" );
|
||||
}
|
||||
sqlAppender.append( " order by t.idx separator " );
|
||||
// HSQLDB doesn't like non-literals as separator
|
||||
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
|
||||
sqlAppender.append( ") from unnest(");
|
||||
|
|
|
@ -46,6 +46,13 @@ public class OracleArrayToStringFunction extends ArrayToStringFunction {
|
|||
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( ')' );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -293,6 +293,21 @@ public interface JdbcType extends Serializable {
|
|||
|| isIntervalType( ddlTypeCode );
|
||||
}
|
||||
|
||||
default boolean isArray() {
|
||||
return isArray( getDdlTypeCode() );
|
||||
}
|
||||
|
||||
static boolean isArray(int jdbcTypeCode) {
|
||||
switch ( jdbcTypeCode ) {
|
||||
case ARRAY:
|
||||
case STRUCT_ARRAY:
|
||||
case JSON_ARRAY:
|
||||
case XML_ARRAY:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
default CastType getCastType() {
|
||||
return getCastType( getDdlTypeCode() );
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test;
|
|||
import jakarta.persistence.Tuple;
|
||||
import org.assertj.core.api.Assertions;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.isOneOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
@ -59,16 +61,29 @@ public class ArrayToStringTest {
|
|||
public void test(SessionFactoryScope scope) {
|
||||
scope.inSession( em -> {
|
||||
//tag::hql-array-to-string-example[]
|
||||
List<String> results = em.createQuery( "select array_to_string(e.theArray, ',') from EntityWithArrays e", String.class )
|
||||
List<String> results = em.createQuery( "select array_to_string(e.theArray, ',') from EntityWithArrays e order by e.id", String.class )
|
||||
.getResultList();
|
||||
//end::hql-array-to-string-example[]
|
||||
assertEquals( 3, results.size() );
|
||||
// We expect an empty string, but Oracle returns NULL instead of empty strings
|
||||
Assertions.assertThat( results.get( 0 ) ).isNullOrEmpty();
|
||||
assertEquals( "abc,def", results.get( 1 ) );
|
||||
assertNull( results.get( 2 ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullValue(SessionFactoryScope scope) {
|
||||
scope.inSession( em -> {
|
||||
List<String> results = em.createQuery( "select array_to_string(e.theArray, ',', 'null') from EntityWithArrays e order by e.id", String.class )
|
||||
.getResultList();
|
||||
assertEquals( 3, results.size() );
|
||||
Assertions.assertThat( results.get( 0 ) ).isNullOrEmpty();
|
||||
assertEquals( "abc,null,def", results.get( 1 ) );
|
||||
assertNull( results.get( 2 ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNodeBuilderArray(SessionFactoryScope scope) {
|
||||
scope.inSession( em -> {
|
||||
|
@ -99,4 +114,30 @@ public class ArrayToStringTest {
|
|||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCast(SessionFactoryScope scope) {
|
||||
scope.inSession( em -> {
|
||||
//tag::hql-array-to-string-hql-example[]
|
||||
List<String> results = em.createQuery( "select cast(e.theArray as String) from EntityWithArrays e order by e.id", String.class )
|
||||
.getResultList();
|
||||
//end::hql-array-to-string-hql-example[]
|
||||
assertEquals( 3, results.size() );
|
||||
assertEquals( "[]", results.get( 0 ) );
|
||||
assertEquals( "[abc,null,def]", results.get( 1 ) );
|
||||
assertNull( results.get( 2 ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStr(SessionFactoryScope scope) {
|
||||
scope.inSession( em -> {
|
||||
List<String> results = em.createQuery( "select str(e.theArray) from EntityWithArrays e order by e.id", String.class )
|
||||
.getResultList();
|
||||
assertEquals( 3, results.size() );
|
||||
assertEquals( "[]", results.get( 0 ) );
|
||||
assertEquals( "[abc,null,def]", results.get( 1 ) );
|
||||
assertNull( results.get( 2 ) );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue