From 427aa62ae4773104e79dfbe2fcea6d378ecbd37c Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Tue, 6 Feb 2024 15:33:38 -0600 Subject: [PATCH 1/4] [#1641] added postgres datatype codes for array types --- .../PostgreSqlReactiveInformationExtractorImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java index b041e78e4..d6bcb00a3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java @@ -141,7 +141,7 @@ protected T processImportedKeysResultSet( @Override protected int dataTypeCode(String typeName) { - // Copied from PostgreSQLDialect. + // Copied from PostgreSQLDialect.resolveSqlTypeCode // Not ideal, but it should work for now // It would be nice to be able to get the correct code some way switch ( typeName ) { @@ -162,6 +162,11 @@ protected int dataTypeCode(String typeName) { return SqlTypes.TIMESTAMP_UTC; case "bytea": return Types.VARBINARY; + case "_numeric": + case "_bool": + case "_int8": + case "_varchar": + return Types.ARRAY; default: return 0; } From c95080146f890ce06383c16d7ae6a964a41c3231 Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Tue, 6 Feb 2024 15:33:55 -0600 Subject: [PATCH 2/4] [#1641] tests for @Array annotations --- .../reactive/containers/DB2Database.java | 5 + .../containers/MSSQLServerDatabase.java | 5 + .../reactive/containers/MySQLDatabase.java | 5 + .../reactive/containers/OracleDatabase.java | 5 + .../containers/PostgreSQLDatabase.java | 5 + .../reactive/schema/BasicTypesTestEntity.java | 28 +++++ .../reactive/types/JavaTypesArrayTest.java | 112 +++++++++++++++++- 7 files changed, 162 insertions(+), 3 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java index 46ebdefc2..edb1f88d5 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java @@ -73,6 +73,11 @@ class DB2Database implements TestableDatabase { expectedDBTypeForClass.put( Character.class, "CHARACTER" ); expectedDBTypeForClass.put( char.class, "CHARACTER" ); expectedDBTypeForClass.put( String.class, "VARCHAR" ); + expectedDBTypeForClass.put( String[].class, "VARBINARY" ); + expectedDBTypeForClass.put( Long[].class, "VARBINARY" ); + expectedDBTypeForClass.put( BigDecimal[].class, "VARBINARY" ); + expectedDBTypeForClass.put( BigInteger[].class, "VARBINARY" ); + expectedDBTypeForClass.put( Boolean[].class, "VARBINARY" ); }} /** diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java index a1d1a6767..8078d1b7e 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java @@ -82,6 +82,11 @@ class MSSQLServerDatabase implements TestableDatabase { expectedDBTypeForClass.put( Character.class, "char" ); expectedDBTypeForClass.put( char.class, "char" ); expectedDBTypeForClass.put( String.class, "varchar" ); + expectedDBTypeForClass.put( String[].class, "varbinary" ); + expectedDBTypeForClass.put( Long[].class, "varbinary" ); + expectedDBTypeForClass.put( BigDecimal[].class, "varbinary" ); + expectedDBTypeForClass.put( BigInteger[].class, "varbinary" ); + expectedDBTypeForClass.put( Boolean[].class, "varbinary" ); }} /** diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java index 34aa54310..01006b567 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java @@ -73,6 +73,11 @@ class MySQLDatabase implements TestableDatabase { expectedDBTypeForClass.put( Character.class, "char" ); expectedDBTypeForClass.put( char.class, "char" ); expectedDBTypeForClass.put( String.class, "varchar" ); + expectedDBTypeForClass.put( String[].class, "varchar" ); + expectedDBTypeForClass.put( Long[].class, "varbinary" ); + expectedDBTypeForClass.put( BigDecimal[].class, "varbinary" ); + expectedDBTypeForClass.put( BigInteger[].class, "varbinary" ); + expectedDBTypeForClass.put( Boolean[].class, "varbinary" ); }}; /** diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java index d3f8a950a..6f157192e 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java @@ -81,6 +81,11 @@ class OracleDatabase implements TestableDatabase { expectedDBTypeForClass.put( Character.class, "CHAR" ); expectedDBTypeForClass.put( char.class, "CHAR" ); expectedDBTypeForClass.put( String.class, "VARCHAR2" ); + expectedDBTypeForClass.put( String[].class, "STRINGARRAY" ); + expectedDBTypeForClass.put( Long[].class, "LONGARRAY" ); + expectedDBTypeForClass.put( BigDecimal[].class, "BIGDECIMALARRAY" ); + expectedDBTypeForClass.put( BigInteger[].class, "BIGINTEGERARRAY" ); + expectedDBTypeForClass.put( Boolean[].class, "BOOLEANARRAY" ); } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java index f6babfd86..2a646971b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java @@ -73,6 +73,11 @@ class PostgreSQLDatabase implements TestableDatabase { expectedDBTypeForClass.put( Character.class, "character" ); expectedDBTypeForClass.put( char.class, "character" ); expectedDBTypeForClass.put( String.class, "character varying" ); + expectedDBTypeForClass.put( String[].class, "ARRAY" ); + expectedDBTypeForClass.put( Long[].class, "ARRAY" ); + expectedDBTypeForClass.put( BigDecimal[].class, "ARRAY" ); + expectedDBTypeForClass.put( BigInteger[].class, "ARRAY" ); + expectedDBTypeForClass.put( Boolean[].class, "ARRAY" ); }} /** diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/BasicTypesTestEntity.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/BasicTypesTestEntity.java index 75567438d..bb06c4e40 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/BasicTypesTestEntity.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/BasicTypesTestEntity.java @@ -18,6 +18,9 @@ import java.util.Date; import java.util.TimeZone; import java.util.UUID; + +import org.hibernate.annotations.Array; + import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; @@ -104,6 +107,31 @@ public class BasicTypesTestEntity { Serializable serializable; + String[] stringArray; + + @Array(length = 5) + String[] stringArrayAnnotated; + + Long[] longArray; + + @Array(length = 5) + Long[] longArrayAnnotated; + + BigDecimal[] bigDecimalArray; + + @Array(length = 5) + BigDecimal[] bigDecimalArrayAnnotated; + + BigInteger[] bigIntegerArray; + + @Array(length = 5) + BigInteger[] bigIntegerArrayAnnotated; + + Boolean[] fieldBooleanArray; + + @Array(length = 5) + Boolean[] fieldBooleanArrayAnnotated; + public BasicTypesTestEntity() { } public BasicTypesTestEntity(String name) { diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java index a9b08ac0d..139ad6b78 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java @@ -17,8 +17,12 @@ import java.util.UUID; import java.util.function.Consumer; +import org.hibernate.annotations.Array; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; import org.hibernate.reactive.BaseReactiveTest; import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.testing.SqlStatementTracker; import org.junit.jupiter.api.Test; @@ -29,19 +33,51 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.assertj.core.api.Condition; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @Timeout(value = 10, timeUnit = MINUTES) -@DisabledFor( value = ORACLE, reason = "Vert.x does not support arrays for Oracle" ) +@DisabledFor(value = ORACLE, reason = "Vert.x does not support arrays for Oracle") public class JavaTypesArrayTest extends BaseReactiveTest { + private static SqlStatementTracker sqlTracker; + + private final static Condition IS_PG_CREATE_TABLE_QUERY = new Condition<>( + s -> s.toLowerCase().startsWith( "create table" ) && s.contains( "stringArrayWithArrayAnnotation varchar(255) array[5]," ), + "generated query for PostgreSQL `create table...`" + ); + + private final static Condition IS_PG_CREATE_TABLE_NO_ARRAY_ANNOTATION_QUERY = new Condition<>( + s -> s.toLowerCase().startsWith( "create table" ) && s.contains( "stringArray varchar(255) array," ), + "generated query for PostgreSQL `create table...`" + ); + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + sqlTracker = new SqlStatementTracker( JavaTypesArrayTest::filterCreateTable, configuration.getProperties() ); + return configuration; + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean filterCreateTable(String s) { + return s.toLowerCase().startsWith( "create table" ); + } + @Override protected Set> annotatedEntities() { return Set.of( Basic.class ); @@ -64,12 +100,59 @@ private void testField( @Test public void testStringArrayType(VertxTestContext context) { Basic basic = new Basic(); - String[] dataArray = {"Hello world!", "Hello earth"}; + String[] dataArray = { "Hello world!", "Hello earth" }; basic.stringArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.stringArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.stringArray ); + // PostgreSQL is the only DB that changes it's `create table...` statement to include array information + // This test checks that the logged query is correct and contains "array[100]" + if ( dbType() == POSTGRESQL ) { + assertThat( sqlTracker.getLoggedQueries() ).have( IS_PG_CREATE_TABLE_NO_ARRAY_ANNOTATION_QUERY ); + } + } ); + } + + @Test + public void testStringArrayTypeWithArrayAnnotation(VertxTestContext context) { + Basic basic = new Basic(); + String[] dataArray = {"Hello world!", "Hello earth"}; + basic.stringArrayWithArrayAnnotation = dataArray; + + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.stringArrayWithArrayAnnotation ); + // PostgreSQL is the only DB that changes it's `create table...` statement to include array information + // This test checks that the logged query is correct and contains "array[100]" + if ( dbType() == POSTGRESQL ) { + assertThat( sqlTracker.getLoggedQueries() ).have( IS_PG_CREATE_TABLE_QUERY ); + } + } ); } + @Test + public void testStringArrayTypeWithColumnAnnotation(VertxTestContext context) { + Basic basic = new Basic(); + String[] dataArray = { "Hello world!", "Hello earth" }; + basic.stringArrayWithColumnAnnotation = dataArray; + + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.stringArrayWithColumnAnnotation ); + } ); + } + + @Test + public void testStringArrayTypeWithBothAnnotations(VertxTestContext context) { + Basic basic = new Basic(); + String[] dataArray = { "Hello world!", "Hello earth" }; + basic.stringArrayWithBothAnnotations = dataArray; + + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.stringArrayWithBothAnnotations ); + } ); + } + + + @Test public void testBooleanArrayType(VertxTestContext context) { Basic basic = new Basic(); @@ -277,6 +360,19 @@ public void testBigDecimalArrayType(VertxTestContext context) { } ); } + @Test + public void testBigDecimalArrayTypeWithArrayAnnotation(VertxTestContext context) { + Basic basic = new Basic(); + BigDecimal[] dataArray = {BigDecimal.valueOf( 123384967L ), BigDecimal.ZERO}; + basic.bigDecimalArrayWithArrayAnnotation = dataArray; + + testField( context, basic, found -> { + assertEquals( dataArray.length, found.bigDecimalArrayWithArrayAnnotation.length ); + assertEquals( dataArray[0].compareTo( found.bigDecimalArrayWithArrayAnnotation[0] ), 0 ); + assertEquals( dataArray[1].compareTo( found.bigDecimalArrayWithArrayAnnotation[1] ), 0 ); + } ); + } + @Entity(name = "Basic") @Table(name = "Basic") private static class Basic { @@ -284,6 +380,13 @@ private static class Basic { @GeneratedValue Integer id; String[] stringArray; + @Array(length = 5) + String[] stringArrayWithArrayAnnotation; + @Column(length = 255) + String[] stringArrayWithColumnAnnotation; + @Array(length = 5) + @Column(length = 255) + String[] stringArrayWithBothAnnotations; Boolean[] booleanArray; boolean[] primitiveBooleanArray; Integer[] integerArray; @@ -309,6 +412,9 @@ private static class Basic { BigInteger[] bigIntegerArray; @Column(length = 5000) BigDecimal[] bigDecimalArray; + @Array(length = 5) + @Column(length = 5000) + BigDecimal[] bigDecimalArrayWithArrayAnnotation; } enum AnEnum {FIRST, SECOND, THIRD, FOURTH} From c061826db6073b7233b55dcbfab73c0223e6c547 Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Tue, 6 Feb 2024 15:34:03 -0600 Subject: [PATCH 3/4] [#1641] tests for @Struct annotations --- .../reactive/StructComponentTest.java | 131 +++++++++++++ .../reactive/StructEmbeddableTest.java | 180 ++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructComponentTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructEmbeddableTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructComponentTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructComponentTest.java new file mode 100644 index 000000000..19586c0e9 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructComponentTest.java @@ -0,0 +1,131 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; + +import org.hibernate.annotations.Struct; +import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.COCKROACHDB; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Timeout(value = 10, timeUnit = MINUTES) +@DisabledFor(value = ORACLE, reason = "see issue https://github.com/hibernate/hibernate-reactive/issues/1855") +@DisabledFor(value = {SQLSERVER, MYSQL, MARIA, COCKROACHDB}, reason = "ORM does not support @Struct for these databases") +public class StructComponentTest extends BaseReactiveTest { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + static Book book = createBook(); + static Publisher ePublisher; + static Publisher pPublisher; + + @Override + protected Collection> annotatedEntities() { + return List.of( Book.class ); + } + + private static Book createBook() { + ePublisher = new Publisher(); + ePublisher.setName( "ebooks" ); + ePublisher.setPubId( 5 ); + + pPublisher = new Publisher(); + pPublisher.setName( "paperbooks" ); + pPublisher.setPubId( 25 ); + + Book book = new Book(); + book.title = "Hibernate"; + book.author = "Steve"; + book.ebookPublisher = ePublisher; + book.paperBackPublisher = pPublisher; + return book; + } + + @BeforeEach + public void populateDB(VertxTestContext context) { + test( context, getSessionFactory() + .withTransaction( session -> session.persist( book ) + .thenCompose( v -> session.flush() ) ) + ); + } + + @Test + public void testStructComponent(VertxTestContext context) { + test( context, openSession() + .thenCompose( s2 -> s2.find( Book.class, book.id ) ) + .thenAccept( resultBook -> { + assertNotNull( resultBook ); + assertEquals( book.title, resultBook.title ); + assertEquals( book.ebookPublisher.pubId, resultBook.ebookPublisher.pubId ); + assertEquals( book.paperBackPublisher.pubId, resultBook.paperBackPublisher.pubId ); + } ) + ); + } + + @Entity(name = "Book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String author; + + @Column(name = "ebook_publisher") + private Publisher ebookPublisher; + private Publisher paperBackPublisher; + } + + @Embeddable + @Struct( name = "publisher_type") + public static class Publisher { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + private Integer pubId; + + public Integer getPubId() { + return pubId; + } + + public void setPubId(Integer pubId) { + this.pubId = pubId; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructEmbeddableTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructEmbeddableTest.java new file mode 100644 index 000000000..c0b51c737 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/StructEmbeddableTest.java @@ -0,0 +1,180 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.awt.Point; +import java.util.Collection; +import java.util.List; + +import org.hibernate.annotations.Struct; +import org.hibernate.reactive.annotations.DisabledFor; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.COCKROACHDB; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Timeout(value = 10, timeUnit = MINUTES) +@DisabledFor(value = ORACLE, reason = "see issue https://github.com/hibernate/hibernate-reactive/issues/1855") +@DisabledFor(value = {SQLSERVER, MYSQL, MARIA, COCKROACHDB}, reason = "ORM does not support @Struct for these databases") +public class StructEmbeddableTest extends BaseReactiveTest { + static RecordStructHolder holder1; + static RecordStructHolder holder2; + + @Override + protected Collection> annotatedEntities() { + return List.of( RecordStructHolder.class ); + } + + @BeforeEach + public void populateDB(VertxTestContext context) { + holder1 = new RecordStructHolder( 1L, new NamedPoint( "first", 1, 1 ) ); + holder2 = new RecordStructHolder( 2L, new NamedPoint( "second", 2, 2 ) ); + holder1.simpleStringHolder = new SimpleStringHolder( "column a","column b","column c" ); + + test( context, getSessionFactory() + .withTransaction( session -> session.persist( holder1, holder2 ) + .thenCompose( v -> session.flush() ) ) + ); + } + + @Test + public void testFindAndUpdate(VertxTestContext context) { + test( context, openSession() + .thenCompose( s2 -> s2.find( RecordStructHolder.class, holder1.id ) + .thenAccept( resultHolder -> { + assertNotNull( resultHolder ); + assertEquals( holder1.getThePoint().getPoint(), resultHolder.getThePoint().getPoint() ); + resultHolder.setThePoint( new NamedPoint( "third", 3, 3 ) ); + assertEquals( "third", resultHolder.getThePoint().name ); + } ) + .thenCompose( vv -> s2.flush() ) + .thenCompose( vv -> s2.find( RecordStructHolder.class, holder1.id ) + .thenAccept( found -> assertEquals( "third", found.getThePoint().getName() ) ) ) + ) + ); + } + + @Test + public void testSelectionItems(VertxTestContext context) { + test( context, openSession() + .thenCompose( s -> s.createSelectionQuery( "from RecordStructHolder where id = ?1", RecordStructHolder.class ) + .setParameter( 1, holder1.getId() ) + .getResultList() ) + .thenAccept( holders -> { + assertNotNull( holders ); + final RecordStructHolder holder = holders.get( 0 ); + assertEquals( holder1.getThePoint().getPoint(), holder.getThePoint().getPoint() ); + } ) + ); + } + + @Test + public void testEmbeddedColumnOrder(VertxTestContext context) { + test( context, openSession() + .thenCompose( s2 -> s2.find( RecordStructHolder.class, holder1.id ) + .thenAccept( resultHolder -> { + assertNotNull( resultHolder ); + assertEquals( holder1.getThePoint().getPoint(), resultHolder.getThePoint().getPoint() ); + assertEquals( "column a", holder1.simpleStringHolder.aColumn ); + assertEquals( "column b", holder1.simpleStringHolder.bColumn ); + assertEquals( "column c", holder1.simpleStringHolder.cColumn ); + } ) + ) + ); + } + + @Entity(name = "RecordStructHolder") + public static class RecordStructHolder { + @Id + private Long id; + @Struct(name = "my_point_type") + private NamedPoint thePoint; + + private SimpleStringHolder simpleStringHolder; + + public RecordStructHolder() { + } + + public RecordStructHolder(Long id, NamedPoint thePoint) { + this.id = id; + this.thePoint = thePoint; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public NamedPoint getThePoint() { + return thePoint; + } + + public void setThePoint(NamedPoint point) { + this.thePoint = point; + } + } + + @Embeddable + static class NamedPoint { + public String name; + public Point point; + + public NamedPoint() { + } + + public NamedPoint(String name, Integer x, Integer y) { + this.point = new Point( x, y ); + this.name = name; + } + + public String getName() { + return name; + } + + public Point getPoint() { + return point; + } + } + + // By default, the order of columns is based on the alphabetical ordering of the embeddable type attribute names. + // This class has column names re-defined using @Column annotation "name" attribute and will reverse the column order + @Embeddable + @Struct(name = "simple_string_holder") + static class SimpleStringHolder { + @Column(name = "c") + public String aColumn; + @Column(name = "b") + public String bColumn; + @Column(name = "a") + public String cColumn; + + public SimpleStringHolder() {} + + public SimpleStringHolder(String aColumn, String bColumn, String cColumn) { + this.aColumn = aColumn; + this.bColumn = bColumn; + this.cColumn = cColumn; + } + } +} From 5e2a1e7b92bb179bccc786e51fe5753e54ec0ecf Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Thu, 8 Feb 2024 13:48:47 +0100 Subject: [PATCH 4/4] [#1641] Refactor JavaTypesArrayTest * Remove duplicated code * Change some values so that are not equal to the default * Test the column definition for all databases --- .../reactive/types/JavaTypesArrayTest.java | 214 +++++++++++++----- 1 file changed, 162 insertions(+), 52 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java index 139ad6b78..66fb6dbbd 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JavaTypesArrayTest.java @@ -16,7 +16,9 @@ import java.util.Set; import java.util.UUID; import java.util.function.Consumer; +import java.util.function.Predicate; +import org.hibernate.AssertionFailure; import org.hibernate.annotations.Array; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.Configuration; @@ -33,35 +35,30 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; -import org.assertj.core.api.Condition; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; -import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +/** + * Test that we handle arrays as basic types and the @{@link Array} annotation in combination with @{@link Column}. + *

+ * Specifying the length doesn't seem to have any effect at the moment. + * We use it when creating the table with Postgres, but Postgres ignore it anyway. + * All the other dbs will save the array as a `varbinary` column and length is set using @{@link Column} + */ @Timeout(value = 10, timeUnit = MINUTES) @DisabledFor(value = ORACLE, reason = "Vert.x does not support arrays for Oracle") public class JavaTypesArrayTest extends BaseReactiveTest { private static SqlStatementTracker sqlTracker; - private final static Condition IS_PG_CREATE_TABLE_QUERY = new Condition<>( - s -> s.toLowerCase().startsWith( "create table" ) && s.contains( "stringArrayWithArrayAnnotation varchar(255) array[5]," ), - "generated query for PostgreSQL `create table...`" - ); - - private final static Condition IS_PG_CREATE_TABLE_NO_ARRAY_ANNOTATION_QUERY = new Condition<>( - s -> s.toLowerCase().startsWith( "create table" ) && s.contains( "stringArray varchar(255) array," ), - "generated query for PostgreSQL `create table...`" - ); - @Override protected Configuration constructConfiguration() { Configuration configuration = super.constructConfiguration(); @@ -75,7 +72,7 @@ protected void addServices(StandardServiceRegistryBuilder builder) { } private static boolean filterCreateTable(String s) { - return s.toLowerCase().startsWith( "create table" ); + return s.toLowerCase().startsWith( "create table basic " ); } @Override @@ -100,16 +97,12 @@ private void testField( @Test public void testStringArrayType(VertxTestContext context) { Basic basic = new Basic(); - String[] dataArray = { "Hello world!", "Hello earth" }; + String[] dataArray = {"Hello world!", "Hello earth"}; basic.stringArray = dataArray; testField( context, basic, found -> { assertArrayEquals( dataArray, found.stringArray ); - // PostgreSQL is the only DB that changes it's `create table...` statement to include array information - // This test checks that the logged query is correct and contains "array[100]" - if ( dbType() == POSTGRESQL ) { - assertThat( sqlTracker.getLoggedQueries() ).have( IS_PG_CREATE_TABLE_NO_ARRAY_ANNOTATION_QUERY ); - } + validateArrayColumn( "stringArray", null, 255 ); } ); } @@ -121,38 +114,34 @@ public void testStringArrayTypeWithArrayAnnotation(VertxTestContext context) { testField( context, basic, found -> { assertArrayEquals( dataArray, found.stringArrayWithArrayAnnotation ); - // PostgreSQL is the only DB that changes it's `create table...` statement to include array information - // This test checks that the logged query is correct and contains "array[100]" - if ( dbType() == POSTGRESQL ) { - assertThat( sqlTracker.getLoggedQueries() ).have( IS_PG_CREATE_TABLE_QUERY ); - } + validateArrayColumn( "stringArrayWithArrayAnnotation", 5, null ); } ); } @Test public void testStringArrayTypeWithColumnAnnotation(VertxTestContext context) { Basic basic = new Basic(); - String[] dataArray = { "Hello world!", "Hello earth" }; + String[] dataArray = {"Hello world!", "Hello earth"}; basic.stringArrayWithColumnAnnotation = dataArray; testField( context, basic, found -> { assertArrayEquals( dataArray, found.stringArrayWithColumnAnnotation ); + validateArrayColumn( "stringArrayWithColumnAnnotation", null, 200 ); } ); } @Test public void testStringArrayTypeWithBothAnnotations(VertxTestContext context) { Basic basic = new Basic(); - String[] dataArray = { "Hello world!", "Hello earth" }; + String[] dataArray = {"Hello world!", "Hello earth"}; basic.stringArrayWithBothAnnotations = dataArray; testField( context, basic, found -> { assertArrayEquals( dataArray, found.stringArrayWithBothAnnotations ); + validateArrayColumn( "stringArrayWithBothAnnotations", 5, 200 ); } ); } - - @Test public void testBooleanArrayType(VertxTestContext context) { Basic basic = new Basic(); @@ -160,6 +149,7 @@ public void testBooleanArrayType(VertxTestContext context) { basic.booleanArray = dataArray; testField( context, basic, found -> assertArrayEquals( dataArray, found.booleanArray ) ); + validateArrayColumn( "booleanArray", null, null ); } @Test @@ -168,7 +158,10 @@ public void testPrimitiveBooleanArrayType(VertxTestContext context) { boolean[] dataArray = {true, false, true}; basic.primitiveBooleanArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.primitiveBooleanArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.primitiveBooleanArray ); + validateArrayColumn( "primitiveBooleanArray", null, null ); + } ); } @Test @@ -177,7 +170,10 @@ public void testIntegerArrayType(VertxTestContext context) { Integer[] dataArray = {null, Integer.MIN_VALUE, 2, Integer.MAX_VALUE}; basic.integerArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.integerArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.integerArray ); + validateArrayColumn( "integerArray", null, null ); + } ); } @Test @@ -186,7 +182,10 @@ public void testPrimitiveIntegerArrayType(VertxTestContext context) { int[] dataArray = {1, 2, 3}; basic.primitiveIntegerArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.primitiveIntegerArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.primitiveIntegerArray ); + validateArrayColumn( "primitiveIntegerArray", null, null ); + } ); } @Test @@ -195,7 +194,10 @@ public void testLongArrayType(VertxTestContext context) { Long[] dataArray = {Long.MIN_VALUE, Long.MAX_VALUE, 3L, null}; basic.longArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.longArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.longArray ); + validateArrayColumn( "longArray", null, null ); + } ); } @Test @@ -204,7 +206,10 @@ public void testPrimitiveLongArrayType(VertxTestContext context) { long[] dataArray = {Long.MIN_VALUE, Long.MAX_VALUE, 3L}; basic.primitiveLongArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.primitiveLongArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.primitiveLongArray ); + validateArrayColumn( "primitiveLongArray", null, null ); + } ); } @Test @@ -213,7 +218,10 @@ public void testFloatArrayType(VertxTestContext context) { Float[] dataArray = {12.562f, null, 13.562f}; basic.floatArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.floatArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.floatArray ); + validateArrayColumn( "floatArray", null, null ); + } ); } @Test @@ -222,7 +230,10 @@ public void testPrimitiveFloatArrayType(VertxTestContext context) { float[] dataArray = {12.562f, 13.562f}; basic.primitiveFloatArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.primitiveFloatArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.primitiveFloatArray ); + validateArrayColumn( "primitiveFloatArray", null, null ); + } ); } @Test @@ -231,7 +242,10 @@ public void testDoubleArrayType(VertxTestContext context) { Double[] dataArray = {12.562d, null, 13.562d}; basic.doubleArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.doubleArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.doubleArray ); + validateArrayColumn( "doubleArray", null, null ); + } ); } @Test @@ -240,7 +254,10 @@ public void testPrimitiveDoubleArrayType(VertxTestContext context) { double[] dataArray = {12.562d, 13.562d}; basic.primitiveDoubleArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.primitiveDoubleArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.primitiveDoubleArray ); + validateArrayColumn( "primitiveDoubleArray", null, null ); + } ); } @Test @@ -253,7 +270,10 @@ public void testUUIDArrayType(VertxTestContext context) { }; basic.uuidArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.uuidArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.uuidArray ); + validateArrayColumn( "uuidArray", null, null ); + } ); } @Test @@ -262,7 +282,10 @@ public void testEnumArrayType(VertxTestContext context) { AnEnum[] dataArray = {AnEnum.FOURTH, AnEnum.FIRST, AnEnum.THIRD}; basic.enumArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.enumArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.enumArray ); + validateArrayColumn( "enumArray", null, null ); + } ); } @Test @@ -271,7 +294,10 @@ public void testShortArrayType(VertxTestContext context) { Short[] dataArray = {512, 112, null, 0}; basic.shortArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.shortArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.shortArray ); + validateArrayColumn( "shortArray", null, null ); + } ); } @Test @@ -280,7 +306,10 @@ public void testPrimitiveShortArrayType(VertxTestContext context) { short[] dataArray = {500, 32, -1}; basic.primitiveShortArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.primitiveShortArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.primitiveShortArray ); + validateArrayColumn( "primitiveShortArray", null, null ); + } ); } @Test @@ -294,7 +323,10 @@ public void testLocalDateArrayType(VertxTestContext context) { }; basic.localDateArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.localDateArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.localDateArray ); + validateArrayColumn( "localDateArray", null, null ); + } ); } @Test @@ -303,7 +335,10 @@ public void testDateArrayType(VertxTestContext context) { Date[] dataArray = {Calendar.getInstance().getTime(), Calendar.getInstance().getTime()}; basic.dateArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.dateArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.dateArray ); + validateArrayColumn( "dateArray", null, null ); + } ); } @Test @@ -317,7 +352,10 @@ public void testLocalTimeArrayType(VertxTestContext context) { }; basic.localTimeArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.localTimeArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.localTimeArray ); + validateArrayColumn( "localTimeArray", null, null ); + } ); } @Test @@ -335,7 +373,10 @@ public void testLocalDateTimeArrayType(VertxTestContext context) { }; basic.localDateTimeArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.localDateTimeArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.localDateTimeArray ); + validateArrayColumn( "localDateTimeArray", null, null ); + } ); } @Test @@ -344,7 +385,10 @@ public void testBigIntegerArrayType(VertxTestContext context) { BigInteger[] dataArray = {BigInteger.TEN, BigInteger.ZERO}; basic.bigIntegerArray = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.bigIntegerArray ) ); + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.bigIntegerArray ); + validateArrayColumn( "bigIntegerArray", null, 5000 ); + } ); } @Test @@ -355,8 +399,9 @@ public void testBigDecimalArrayType(VertxTestContext context) { testField( context, basic, found -> { assertEquals( dataArray.length, found.bigDecimalArray.length ); - assertEquals( dataArray[0].compareTo( found.bigDecimalArray[0] ), 0 ); - assertEquals( dataArray[1].compareTo( found.bigDecimalArray[1] ), 0 ); + assertEquals( 0, dataArray[0].compareTo( found.bigDecimalArray[0] ) ); + assertEquals( 0, dataArray[1].compareTo( found.bigDecimalArray[1] ) ); + validateArrayColumn( "bigDecimalArray", null, 5000 ); } ); } @@ -368,11 +413,76 @@ public void testBigDecimalArrayTypeWithArrayAnnotation(VertxTestContext context) testField( context, basic, found -> { assertEquals( dataArray.length, found.bigDecimalArrayWithArrayAnnotation.length ); - assertEquals( dataArray[0].compareTo( found.bigDecimalArrayWithArrayAnnotation[0] ), 0 ); - assertEquals( dataArray[1].compareTo( found.bigDecimalArrayWithArrayAnnotation[1] ), 0 ); + assertEquals( 0, dataArray[0].compareTo( found.bigDecimalArrayWithArrayAnnotation[0] ) ); + assertEquals( 0, dataArray[1].compareTo( found.bigDecimalArrayWithArrayAnnotation[1] ) ); + validateArrayColumn( "bigDecimalArrayWithArrayAnnotation", 5, 5000 ); } ); } + + private void validateArrayColumn(String columnName, Integer arrayLength, Integer columnLength) { + assertThat( sqlTracker.getLoggedQueries() ) + .allMatch( arrayColumnPredicate( columnName, arrayLength, columnLength ) ); + } + + // A predicate that checks we apply the right size to the array when required + private static Predicate arrayColumnPredicate(String columnName, Integer arrayLength, Integer columnLength) { + switch ( dbType() ) { + case POSTGRESQL: + case COCKROACHDB: + return postgresPredicate( columnName, arrayLength, columnLength ); + case MYSQL: + case MARIA: + case SQLSERVER: + case DB2: + return arrayAsVarbinaryPredicate( columnName, columnLength ); + default: + throw new AssertionFailure( "Unexpected database: " + dbType() ); + } + } + + /** + * For Postgres, we expect arrays to be defined as {@code array}. + *

+ * For example: {@code varchar(255) array[2]} + *

+ */ + private static Predicate postgresPredicate(String columnName, Integer arrayLength, Integer columnLength) { + StringBuilder regexBuilder = new StringBuilder(); + regexBuilder.append( ".*" ); + + regexBuilder.append( columnName ).append( " \\w+" ); + // Column length only affects arrays of strings + if ( columnLength != null && columnName.startsWith( "string" ) ) { + regexBuilder.append( "\\(" ).append( columnLength ).append( "\\)" ); + } + else { + // for some types we have a default size. For example: `varchar(255)` or `numeric(38,0)` + regexBuilder.append( "(\\(\\d+(,\\d+)?\\))?" ); + } + regexBuilder.append( " array" ); + if ( arrayLength != null ) { + regexBuilder.append( "\\[" ).append( arrayLength ).append( "\\]" ); + } + regexBuilder.append( ".*" ); + return s -> s.matches( regexBuilder.toString() ); + } + + private static Predicate arrayAsVarbinaryPredicate(String columnName, Integer columnLength) { + StringBuilder regexBuilder = new StringBuilder(); + // Example of correct query definition: columnName varbinary(255) + regexBuilder.append( columnName ).append( " varbinary" ).append( "(" ); + if ( columnLength != null ) { + regexBuilder.append( columnLength ); + } + else { + // Default size + regexBuilder.append( 255 ); + } + regexBuilder.append( ")" ); + return s -> s.contains( regexBuilder.toString() ); + } + @Entity(name = "Basic") @Table(name = "Basic") private static class Basic { @@ -382,10 +492,10 @@ private static class Basic { String[] stringArray; @Array(length = 5) String[] stringArrayWithArrayAnnotation; - @Column(length = 255) + @Column(length = 200) String[] stringArrayWithColumnAnnotation; @Array(length = 5) - @Column(length = 255) + @Column(length = 200) String[] stringArrayWithBothAnnotations; Boolean[] booleanArray; boolean[] primitiveBooleanArray;