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; } 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; + } + } +} 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..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,9 +16,15 @@ 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; import org.hibernate.reactive.BaseReactiveTest; import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.testing.SqlStatementTracker; import org.junit.jupiter.api.Test; @@ -33,15 +39,42 @@ 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; 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" ) +@DisabledFor(value = ORACLE, reason = "Vert.x does not support arrays for Oracle") public class JavaTypesArrayTest extends BaseReactiveTest { + private static SqlStatementTracker sqlTracker; + + @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 basic " ); + } + @Override protected Set> annotatedEntities() { return Set.of( Basic.class ); @@ -67,7 +100,46 @@ public void testStringArrayType(VertxTestContext context) { 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 ); + validateArrayColumn( "stringArray", null, 255 ); + } ); + } + + @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 ); + validateArrayColumn( "stringArrayWithArrayAnnotation", 5, null ); + } ); + } + + @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 ); + validateArrayColumn( "stringArrayWithColumnAnnotation", null, 200 ); + } ); + } + + @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 ); + validateArrayColumn( "stringArrayWithBothAnnotations", 5, 200 ); + } ); } @Test @@ -77,6 +149,7 @@ public void testBooleanArrayType(VertxTestContext context) { basic.booleanArray = dataArray; testField( context, basic, found -> assertArrayEquals( dataArray, found.booleanArray ) ); + validateArrayColumn( "booleanArray", null, null ); } @Test @@ -85,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 @@ -94,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 @@ -103,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 @@ -112,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 @@ -121,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 @@ -130,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 @@ -139,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 @@ -148,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 @@ -157,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 @@ -170,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 @@ -179,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 @@ -188,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 @@ -197,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 @@ -211,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 @@ -220,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 @@ -234,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 @@ -252,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 @@ -261,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 @@ -272,11 +399,90 @@ 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 ); + } ); + } + + @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( 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 { @@ -284,6 +490,13 @@ private static class Basic { @GeneratedValue Integer id; String[] stringArray; + @Array(length = 5) + String[] stringArrayWithArrayAnnotation; + @Column(length = 200) + String[] stringArrayWithColumnAnnotation; + @Array(length = 5) + @Column(length = 200) + String[] stringArrayWithBothAnnotations; Boolean[] booleanArray; boolean[] primitiveBooleanArray; Integer[] integerArray; @@ -309,6 +522,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}