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 46ebdefc2d..edb1f88d5c 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 a1d1a6767c..8078d1b7e7 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 34aa543101..1bf92540f4 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, "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/OracleDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java index d3f8a950a8..e27b157885 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,12 @@ 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 f6babfd865..2a646971b7 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/ArrayTypesTestEntity.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/ArrayTypesTestEntity.java new file mode 100644 index 0000000000..f00ae6f226 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/ArrayTypesTestEntity.java @@ -0,0 +1,56 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.schema; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.hibernate.annotations.Array; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + + +@Entity(name = "ArrayTypesTestEntity") +@Table(name = ArrayTypesTestEntity.TABLE_NAME) +public class ArrayTypesTestEntity { + + public static final String TABLE_NAME = "ARRAY_TYPES_TABLE"; + + @Id + @GeneratedValue + Integer id; + @Version + Integer version; + + 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; +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaArrayTypesValidationTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaArrayTypesValidationTestBase.java new file mode 100644 index 0000000000..7df2a7ab9f --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaArrayTypesValidationTestBase.java @@ -0,0 +1,144 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.schema; + + +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.provider.Settings; +import org.hibernate.tool.schema.spi.SchemaManagementException; + +import org.junit.jupiter.api.AfterEach; + +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.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test schema validation at startup for all the supported types: + * - Missing table validation error + * - No validation error when everything is fine + * - TODO: Missing column + * - TODO: Wrong column type + */ +@DisabledFor(value = DB2, reason = "No InformationExtractor for Dialect [org.hibernate.dialect.DB2Dialect..]") +public abstract class SchemaArrayTypesValidationTestBase extends BaseReactiveTest { + + public static class IndividuallyStrategyTest extends SchemaArrayTypesValidationTestBase { + + @Override + protected Configuration constructConfiguration(String hbm2DdlOption) { + final Configuration configuration = super.constructConfiguration( hbm2DdlOption ); + configuration.setProperty( Settings.HBM2DDL_JDBC_METADATA_EXTRACTOR_STRATEGY, INDIVIDUALLY.toString() ); + configuration.setProperty( Settings.SHOW_SQL, System.getProperty( Settings.SHOW_SQL, "true" ) ); + return configuration; + } + } + + public static class GroupedStrategyTest extends SchemaArrayTypesValidationTestBase { + + @Override + protected Configuration constructConfiguration(String hbm2DdlOption) { + final Configuration configuration = super.constructConfiguration( hbm2DdlOption ); + configuration.setProperty( Settings.HBM2DDL_JDBC_METADATA_EXTRACTOR_STRATEGY, GROUPED.toString() ); + configuration.setProperty( Settings.SHOW_SQL, System.getProperty( Settings.SHOW_SQL, "true" ) ); + return configuration; + } + } + + protected Configuration constructConfiguration(String action) { + Configuration configuration = super.constructConfiguration(); + configuration.setProperty( Settings.HBM2DDL_JDBC_METADATA_EXTRACTOR_STRATEGY, INDIVIDUALLY.toString() ); + configuration.setProperty( Settings.HBM2DDL_AUTO, action ); + return configuration; + } + + @BeforeEach + @Override + public void before(VertxTestContext context) { + Configuration createConf = constructConfiguration( "create" ); + createConf.addAnnotatedClass( ArrayTypesTestEntity.class ); + + // Make sure that the extra table is not in the db + Configuration dropConf = constructConfiguration( "drop" ); + dropConf.addAnnotatedClass( Extra.class ); + + test( context, setupSessionFactory( dropConf ) + .thenCompose( v -> factoryManager.stop() ) + .thenCompose( v -> setupSessionFactory( createConf ) ) + .thenCompose( v -> factoryManager.stop() ) + ); + } + + @AfterEach + @Override + public void after(VertxTestContext context) { + super.after( context ); + closeFactory( context ); + } + + // When we have created the table, the validation should pass + @Test + @Timeout(value = 10, timeUnit = MINUTES) + public void testValidationSucceeds(VertxTestContext context) { + Configuration validateConf = constructConfiguration( "validate" ); + validateConf.addAnnotatedClass( ArrayTypesTestEntity.class ); + + StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder() + .applySettings( validateConf.getProperties() ); + test( context, setupSessionFactory( validateConf ) ); + } + + // Validation should fail if a table is missing + @Test + @Timeout(value = 10, timeUnit = MINUTES) + public void testValidationFails(VertxTestContext context) { + Configuration validateConf = constructConfiguration( "validate" ); + validateConf.addAnnotatedClass( ArrayTypesTestEntity.class ); + // The table mapping this entity shouldn't be in the db + validateConf.addAnnotatedClass( Extra.class ); + + final String errorMessage = "Schema-validation: missing table [" + Extra.TABLE_NAME + "]"; + test( context, setupSessionFactory( validateConf ) + .handle( (unused, throwable) -> { + assertNotNull( throwable ); + assertEquals( throwable.getClass(), SchemaManagementException.class ); + assertEquals( throwable.getMessage(), errorMessage ); + return null; + } ) + ); + } + + /** + * An extra entity used for validation, + * it should not be created at start up + */ + @Entity(name = "Extra") + @Table(name = Extra.TABLE_NAME) + public static class Extra { + public static final String TABLE_NAME = "EXTRA_TABLE"; + @Id + @GeneratedValue + private Integer id; + + private String description; + } +} 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 a9b08ac0d6..58ae92517c 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,13 @@ 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.annotations.EnabledFor; +import org.hibernate.reactive.testing.SqlStatementTracker; import org.junit.jupiter.api.Test; @@ -29,11 +34,20 @@ 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.COCKROACHDB; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +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.POSTGRESQL; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; +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; @@ -42,6 +56,34 @@ @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( "stringArrayWithColumnAnnotation varchar(64) array[100]" ), + "generated query for PostgreSQL `create table...`" + ); + + private final static Condition IS_PG_CREATE_TABLE_NO_ANNOTATIONS_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 ); @@ -62,12 +104,48 @@ private void testField( } @Test - public void testStringArrayType(VertxTestContext context) { + public void testStringArrayTypeWithNoAnnotations(VertxTestContext context) { + Basic basic = new Basic(); + String[] dataArray = { "Hello world!", "Hello earth" }; + basic.stringArrayNoAnnotations = dataArray; + + testField( context, basic, found -> { + assertArrayEquals( dataArray, found.stringArrayNoAnnotations ); + // 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 testStringArrayTypeWithArrayAnotation(VertxTestContext context) { Basic basic = new Basic(); String[] dataArray = {"Hello world!", "Hello earth"}; - basic.stringArray = dataArray; + basic.stringArrayWithAnnotation = dataArray; - testField( context, basic, found -> assertArrayEquals( dataArray, found.stringArray ) ); + testField( context, basic, found -> assertArrayEquals( dataArray, found.stringArrayWithAnnotation ) ); + } + + @Test + @DisabledFor(value = DB2, reason = "error executing SQL statement [An error occurred with a DB2 operation, SQLCODE=-302 SQLSTATE=22001]") + @DisabledFor(value = {MYSQL, MARIA}, reason = "errorMessage=Data too long for column 'stringArrayWithColumnAnnotation' at row 1, errorCode=1406, sqlState=22001") + @DisabledFor(value = SQLSERVER, reason = " org.hibernate.exception.SQLGrammarException: error executing SQL statement [{number=2628, state=1, severity=16, " + + "message='String or binary data would be truncated in table 'master.dbo.Basic', column 'stringArrayWithColumnAnnotation'.") + 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 ); + // 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 @@ -265,15 +343,57 @@ public void testBigIntegerArrayType(VertxTestContext context) { } @Test - public void testBigDecimalArrayType(VertxTestContext context) { + @EnabledFor({POSTGRESQL, COCKROACHDB}) + public void testBigDecimalArrayTypeNoAnnotation(VertxTestContext context) { Basic basic = new Basic(); BigDecimal[] dataArray = {BigDecimal.valueOf( 123384967L ), BigDecimal.ZERO}; - basic.bigDecimalArray = dataArray; + basic.bigDecimalArrayNoAnnotations = dataArray; 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( dataArray.length, found.bigDecimalArrayNoAnnotations.length ); + assertEquals( dataArray[0].compareTo( found.bigDecimalArrayNoAnnotations[0] ), 0 ); + assertEquals( dataArray[1].compareTo( found.bigDecimalArrayNoAnnotations[1] ), 0 ); + } ); + } + + @Test + @EnabledFor({POSTGRESQL, COCKROACHDB}) + public void testBigDecimalArrayTypeWithColumnAnnotation(VertxTestContext context) { + Basic basic = new Basic(); + BigDecimal[] dataArray = {BigDecimal.valueOf( 123384967L ), BigDecimal.ZERO}; + basic.bigDecimalArrayWithColumnAnnotation = dataArray; + + testField( context, basic, found -> { + assertEquals( dataArray.length, found.bigDecimalArrayWithColumnAnnotation.length ); + assertEquals( dataArray[0].compareTo( found.bigDecimalArrayWithColumnAnnotation[0] ), 0 ); + assertEquals( dataArray[1].compareTo( found.bigDecimalArrayWithColumnAnnotation[1] ), 0 ); + } ); + } + + @Test + @EnabledFor({POSTGRESQL, COCKROACHDB}) + public void testBigDecimalArrayWithArrayAnnotation(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 ); + } ); + } + + @Test + public void testBigDecimalArrayTypeWithBothAnnotations(VertxTestContext context) { + Basic basic = new Basic(); + BigDecimal[] dataArray = {BigDecimal.valueOf( 123384967L ), BigDecimal.ZERO}; + basic.bigDecimalArrayWithBothAnnotations = dataArray; + + testField( context, basic, found -> { + assertEquals( dataArray.length, found.bigDecimalArrayWithBothAnnotations.length ); + assertEquals( dataArray[0].compareTo( found.bigDecimalArrayWithBothAnnotations[0] ), 0 ); + assertEquals( dataArray[1].compareTo( found.bigDecimalArrayWithBothAnnotations[1] ), 0 ); } ); } @@ -283,7 +403,12 @@ private static class Basic { @Id @GeneratedValue Integer id; - String[] stringArray; + String[] stringArrayNoAnnotations; + @Array(length = 10) // NOTE that the property `length` is required + String[] stringArrayWithAnnotation; + @Array(length = 100) // the maximum length of the SQL array + @Column(length = 64) // the maximum length of the strings in the array + String[] stringArrayWithColumnAnnotation; Boolean[] booleanArray; boolean[] primitiveBooleanArray; Integer[] integerArray; @@ -307,8 +432,18 @@ private static class Basic { // the default column type when creating the schema is too small on some databases @Column(length = 5000) BigInteger[] bigIntegerArray; + + BigDecimal[] bigDecimalArrayNoAnnotations; // FAILS: DB2, MySQL, MariaDB, SqlServer + + @Array(length = 5) + BigDecimal[] bigDecimalArrayWithArrayAnnotation; // FAILS: DB2, MySQL, MariaDB, SqlServer + + @Column(length = 5) + BigDecimal[] bigDecimalArrayWithColumnAnnotation; // FAILS: DB2, MySQL, MariaDB, SqlServer + + @Array(length = 5) @Column(length = 5000) - BigDecimal[] bigDecimalArray; + BigDecimal[] bigDecimalArrayWithBothAnnotations; // FAILS: } enum AnEnum {FIRST, SECOND, THIRD, FOURTH}