From 76c903e82b6486ddd3987905bd8c9bebba7955b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20NO=C3=8BL?= Date: Mon, 19 May 2025 10:33:47 +0200 Subject: [PATCH] HHH-19472: native queries can return Object[] --- .../jpa/spi/NativeQueryArrayTransformer.java | 22 +++ .../query/sql/internal/NativeQueryImpl.java | 6 +- .../hql/SingleSelectionArrayResultTest.java | 173 ++++++++++++------ 3 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java new file mode 100644 index 000000000000..461c100d6ff0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.jpa.spi; + +import org.hibernate.query.TupleTransformer; + +/** + * A {@link TupleTransformer} for handling {@code Object[]} results from native queries. + * + * @since 7.0 + */ +public class NativeQueryArrayTransformer implements TupleTransformer { + + public static final NativeQueryArrayTransformer INSTANCE = new NativeQueryArrayTransformer(); + + @Override + public Object[] transformTuple(Object[] tuple, String[] aliases) { + return tuple; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 0c7ab0380387..07e3535da8ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -25,6 +25,7 @@ import org.hibernate.FlushMode; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.jpa.spi.NativeQueryArrayTransformer; import org.hibernate.jpa.spi.NativeQueryConstructorTransformer; import org.hibernate.jpa.spi.NativeQueryListTransformer; import org.hibernate.jpa.spi.NativeQueryMapTransformer; @@ -377,7 +378,10 @@ else if ( Map.class.equals( resultClass ) ) { else if ( List.class.equals( resultClass ) ) { return NativeQueryListTransformer.INSTANCE; } - else if ( resultClass != Object.class && resultClass != Object[].class ) { + else if ( Object[].class.equals( resultClass )) { + return NativeQueryArrayTransformer.INSTANCE; + } + else if ( resultClass != Object.class ) { // TODO: this is extremely fragile and probably a bug if ( isClass( resultClass ) && !hasJavaTypeDescriptor( resultClass ) ) { // not a basic type, so something we can attempt diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java index 0a3577b076e1..e99c9b54d8b6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java @@ -4,81 +4,144 @@ */ package org.hibernate.orm.test.hql; +import java.util.stream.Stream; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hibernate.query.SelectionQuery; + import org.hibernate.testing.orm.domain.gambit.BasicEntity; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import static org.assertj.core.api.Assertions.assertThat; /** * @author Marco Belladelli */ -@DomainModel( annotatedClasses = BasicEntity.class ) +@DomainModel(annotatedClasses = BasicEntity.class) @SessionFactory -@Jira( "https://hibernate.atlassian.net/browse/HHH-18450" ) +@Jira("https://hibernate.atlassian.net/browse/HHH-18450") +@Jira("https://hibernate.atlassian.net/browse/HHH-19472") +@RequiresDialects({@RequiresDialect(H2Dialect.class), @RequiresDialect(PostgreSQLDialect.class)}) public class SingleSelectionArrayResultTest { - @Test - public void testArrayResult(SessionFactoryScope scope) { + + static class TestArguments implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + Arguments.of( "select 1", null, null ), + Arguments.of( "select cast(1 as integer)", null, null ), + Arguments.of( "select id from BasicEntity", null, null ), + Arguments.of( "select cast(id as integer) from BasicEntity", null, null ), + Arguments.of( "select ?1", 1, 1 ), + Arguments.of( "select :p1", "p1", 1 ), + Arguments.of( "select cast(?1 as integer)", 1, 1 ), + Arguments.of( "select cast(:p1 as integer)", "p1", 1 ) + ); + } + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testNativeQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + NativeQuery query = session.createNativeQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testSelectionQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + SelectionQuery query = session.createSelectionQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testNativeQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { scope.inTransaction( session -> { - assertThat( session.createQuery( - "select 1", - Object[].class - ).getSingleResult() ).containsExactly( 1 ); - assertThat( session.createQuery( - "select cast(1 as integer)", - Object[].class - ).getSingleResult() ).containsExactly( 1 ); - assertThat( session.createSelectionQuery( - "select id from BasicEntity", - Object[].class - ).getSingleResult() ).containsExactly( 1 ); - assertThat( session.createSelectionQuery( - "select cast(id as integer) from BasicEntity", - Object[].class - ).getSingleResult() ).containsExactly( 1 ); - assertThat( session.createSelectionQuery( - "select ?1", - Object[].class - ).setParameter( 1, 1 ).getSingleResult() ).containsExactly( 1 ); - assertThat( session.createQuery( - "select cast(:p1 as integer)", - Object[].class - ).setParameter( "p1", 1 ).getSingleResult() ).containsExactly( 1 ); + NativeQuery query = session.createNativeQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); } ); } - @Test - public void testNormalResult(SessionFactoryScope scope) { + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testSelectionQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { scope.inTransaction( session -> { - assertThat( session.createQuery( - "select 1", - Object.class - ).getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); - assertThat( session.createQuery( - "select cast(1 as integer)", - Object.class - ).getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); - assertThat( session.createSelectionQuery( - "select id from BasicEntity", - Object.class - ).getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); - assertThat( session.createSelectionQuery( - "select cast(id as integer) from BasicEntity", - Object.class - ).getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); - assertThat( session.createSelectionQuery( - "select ?1", - Object.class - ).setParameter( 1, 1 ).getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); - assertThat( session.createQuery( - "select cast(:p1 as integer)", - Object.class - ).setParameter( "p1", 1 ).getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + SelectionQuery query = session.createSelectionQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); } ); }