Skip to content

Commit e1a56c5

Browse files
committed
[#2738] Test column type validation in Oracle
1 parent c23033d commit e1a56c5

File tree

3 files changed

+296
-1
lines changed

3 files changed

+296
-1
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/* Hibernate, Relational Persistence for Idiomatic Java
2+
*
3+
* SPDX-License-Identifier: Apache-2.0
4+
* Copyright: Red Hat Inc. and Hibernate Authors
5+
*/
6+
package org.hibernate.reactive.schema;
7+
8+
9+
import java.net.URL;
10+
import java.util.concurrent.CompletionStage;
11+
import java.util.stream.Stream;
12+
13+
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
14+
import org.hibernate.cfg.AvailableSettings;
15+
import org.hibernate.cfg.Configuration;
16+
import org.hibernate.reactive.BaseReactiveTest;
17+
import org.hibernate.reactive.annotations.DisabledFor;
18+
import org.hibernate.reactive.annotations.EnabledFor;
19+
import org.hibernate.reactive.provider.Settings;
20+
import org.hibernate.tool.schema.spi.SchemaManagementException;
21+
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
26+
27+
import io.vertx.junit5.Timeout;
28+
import io.vertx.junit5.VertxTestContext;
29+
import jakarta.persistence.Column;
30+
import jakarta.persistence.Entity;
31+
import jakarta.persistence.GeneratedValue;
32+
import jakarta.persistence.Id;
33+
import jakarta.persistence.Table;
34+
35+
import static java.util.concurrent.TimeUnit.MINUTES;
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.hibernate.cfg.SchemaToolingSettings.HBM2DDL_JDBC_METADATA_EXTRACTOR_STRATEGY;
38+
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2;
39+
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA;
40+
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL;
41+
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE;
42+
import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED;
43+
import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY;
44+
import static org.junit.jupiter.params.provider.Arguments.arguments;
45+
46+
/**
47+
* Test schema validation at startup for all the supported types:
48+
* - Missing table validation error
49+
* - No validation error when everything is fine
50+
* - TODO: Test that validation fails when a column is missing
51+
* - TODO: Test that validation fails when a column is the wrong type
52+
*/
53+
@DisabledFor(value = DB2, reason = "We don't have an information extractor. See https://github.com/hibernate/hibernate-reactive/issues/911")
54+
@DisabledFor(value = { MARIA, MYSQL }, reason = "HHH-18869: Schema creation creates an invalid schema")
55+
public class SchemaValidationTest extends BaseReactiveTest {
56+
57+
static Stream<Arguments> settings() {
58+
return Stream.of(
59+
arguments( INDIVIDUALLY.toString(), null ),
60+
arguments( GROUPED.toString(), null ),
61+
arguments( INDIVIDUALLY.toString(), "VARBINARY" ),
62+
arguments( GROUPED.toString(), "VARBINARY" )
63+
);
64+
}
65+
66+
protected Configuration constructConfiguration(String action, String strategy, String type) {
67+
Configuration configuration = super.constructConfiguration();
68+
configuration.setProperty( Settings.HBM2DDL_AUTO, action );
69+
configuration.setProperty( HBM2DDL_JDBC_METADATA_EXTRACTOR_STRATEGY, strategy );
70+
if ( type != null ) {
71+
// ORM 7 stores arrays as json for MariaDB and MySQL. By setting this property, users
72+
// can keep the behaviour backward compatible. We need to test both.
73+
configuration.setProperty( "hibernate.type.preferred_array_jdbc_type", type );
74+
}
75+
return configuration;
76+
}
77+
78+
@Override
79+
public void before(VertxTestContext context) {
80+
// Do nothing, we prepare everything in the test so that we can use a parameterized test
81+
}
82+
83+
public CompletionStage<Void> setupFactory(String strategy, String type) {
84+
return setupFactory( strategy, type, null );
85+
}
86+
87+
public CompletionStage<Void> setupFactory(String strategy, String type, String importFile) {
88+
Configuration createConf = constructConfiguration( "create", strategy, type );
89+
createConf.addAnnotatedClass( BasicTypesTestEntity.class );
90+
if ( importFile != null ) {
91+
final URL importFileURL = Thread.currentThread()
92+
.getContextClassLoader()
93+
.getResource( importFile );
94+
createConf.setProperty( AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE, importFileURL.getFile() );
95+
}
96+
// Make sure that the extra table is not in the db
97+
Configuration dropConf = constructConfiguration( "drop", strategy, type );
98+
dropConf.addAnnotatedClass( Extra.class );
99+
100+
return setupSessionFactory( dropConf )
101+
.thenCompose( v -> factoryManager.stop() )
102+
.thenCompose( v -> setupSessionFactory( createConf ) )
103+
.thenCompose( v -> factoryManager.stop() );
104+
}
105+
106+
@AfterEach
107+
@Override
108+
public void after(VertxTestContext context) {
109+
super.after( context );
110+
closeFactory( context );
111+
}
112+
113+
@ParameterizedTest
114+
@MethodSource("settings")
115+
@EnabledFor( ORACLE )
116+
public void testOracleColumnTypeValidation(final String strategy, final String type, VertxTestContext context) {
117+
test(
118+
context, setupFactory( strategy, type, "oracle-SchemaValidationTest.sql" )
119+
.thenCompose( v -> {
120+
Configuration validateConf = constructConfiguration( "validate", strategy, type );
121+
validateConf.addAnnotatedClass( Fruit.class );
122+
new StandardServiceRegistryBuilder().applySettings( validateConf.getProperties() );
123+
return setupSessionFactory( validateConf );
124+
} )
125+
);
126+
}
127+
128+
// When we have created the table, the validation should pass
129+
@ParameterizedTest
130+
@MethodSource("settings")
131+
@Timeout(value = 10, timeUnit = MINUTES)
132+
public void testValidationSucceeds(final String strategy, final String type, VertxTestContext context) {
133+
test(
134+
context, setupFactory( strategy, type )
135+
.thenCompose( v -> {
136+
Configuration validateConf = constructConfiguration( "validate", strategy, type );
137+
validateConf.addAnnotatedClass( BasicTypesTestEntity.class );
138+
new StandardServiceRegistryBuilder().applySettings( validateConf.getProperties() );
139+
return setupSessionFactory( validateConf );
140+
} )
141+
);
142+
}
143+
144+
// Validation should fail if a table is missing
145+
@ParameterizedTest
146+
@MethodSource("settings")
147+
@Timeout(value = 10, timeUnit = MINUTES)
148+
public void testValidationFails(String strategy, String type, VertxTestContext context) {
149+
final String errorMessage = "Schema validation: missing table [" + Extra.TABLE_NAME + "]";
150+
test(
151+
context, setupFactory( strategy, type )
152+
.thenCompose( v -> {
153+
Configuration validateConf = constructConfiguration( "validate", strategy, type );
154+
validateConf.addAnnotatedClass( BasicTypesTestEntity.class );
155+
// The table mapping this entity shouldn't be in the db
156+
validateConf.addAnnotatedClass( Extra.class );
157+
return setupSessionFactory( validateConf )
158+
.handle( (unused, throwable) -> {
159+
assertThat( throwable )
160+
.isInstanceOf( SchemaManagementException.class )
161+
.hasMessage( errorMessage );
162+
return null;
163+
} );
164+
} )
165+
);
166+
}
167+
168+
/**
169+
* An extra entity used for validation,
170+
* it should not be created at start up
171+
*/
172+
@Entity(name = "Extra")
173+
@Table(name = Extra.TABLE_NAME)
174+
public static class Extra {
175+
public static final String TABLE_NAME = "EXTRA_TABLE";
176+
@Id
177+
@GeneratedValue
178+
private Integer id;
179+
180+
private String description;
181+
}
182+
183+
@Entity(name = "Fruit")
184+
public static class Fruit {
185+
186+
@Id
187+
@GeneratedValue
188+
private Integer id;
189+
190+
@Column(name = "something_name", nullable = false, updatable = false)
191+
private String name;
192+
193+
public Fruit() {
194+
}
195+
196+
public Fruit(String name) {
197+
this.name = name;
198+
}
199+
200+
public Integer getId() {
201+
return id;
202+
}
203+
204+
public void setId(Integer id) {
205+
this.id = id;
206+
}
207+
208+
public String getName() {
209+
return name;
210+
}
211+
212+
public void setName(String name) {
213+
this.name = name;
214+
}
215+
216+
@Override
217+
public String toString() {
218+
return "Fruit{" + id + "," + name + '}';
219+
}
220+
}
221+
}

hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTestBase.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
package org.hibernate.reactive.schema;
77

88

9+
import java.net.URL;
10+
911
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
12+
import org.hibernate.cfg.AvailableSettings;
1013
import org.hibernate.cfg.Configuration;
1114
import org.hibernate.reactive.BaseReactiveTest;
12-
import org.hibernate.reactive.provider.Settings;
1315
import org.hibernate.reactive.annotations.DisabledFor;
16+
import org.hibernate.reactive.annotations.EnabledFor;
17+
import org.hibernate.reactive.provider.Settings;
1418
import org.hibernate.tool.schema.spi.SchemaManagementException;
1519

1620
import org.junit.jupiter.api.AfterEach;
@@ -19,13 +23,15 @@
1923

2024
import io.vertx.junit5.Timeout;
2125
import io.vertx.junit5.VertxTestContext;
26+
import jakarta.persistence.Column;
2227
import jakarta.persistence.Entity;
2328
import jakarta.persistence.GeneratedValue;
2429
import jakarta.persistence.Id;
2530
import jakarta.persistence.Table;
2631

2732
import static java.util.concurrent.TimeUnit.MINUTES;
2833
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2;
34+
import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE;
2935
import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED;
3036
import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY;
3137
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -74,6 +80,11 @@ public void before(VertxTestContext context) {
7480
Configuration createConf = constructConfiguration( "create" );
7581
createConf.addAnnotatedClass( BasicTypesTestEntity.class );
7682

83+
final URL importFileURL = Thread.currentThread()
84+
.getContextClassLoader()
85+
.getResource( "oracle-SchemaValidationTest.sql" );
86+
createConf.setProperty( AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE, importFileURL.getFile() );
87+
7788
// Make sure that the extra table is not in the db
7889
Configuration dropConf = constructConfiguration( "drop" );
7990
dropConf.addAnnotatedClass( Extra.class );
@@ -92,6 +103,18 @@ public void after(VertxTestContext context) {
92103
closeFactory( context );
93104
}
94105

106+
@Test
107+
@Timeout(value = 10, timeUnit = MINUTES)
108+
@EnabledFor( ORACLE )
109+
public void testOracleColumnTypeValidation(VertxTestContext context) {
110+
Configuration validateConf = constructConfiguration( "validate" );
111+
validateConf.addAnnotatedClass( Fruit.class );
112+
113+
StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder()
114+
.applySettings( validateConf.getProperties() );
115+
test( context, setupSessionFactory( validateConf ) );
116+
}
117+
95118
// When we have created the table, the validation should pass
96119
@Test
97120
@Timeout(value = 10, timeUnit = MINUTES)
@@ -139,4 +162,43 @@ public static class Extra {
139162

140163
private String description;
141164
}
165+
166+
@Entity(name = "Fruit")
167+
public static class Fruit {
168+
169+
@Id
170+
@GeneratedValue
171+
private Integer id;
172+
173+
@Column(name = "something_name", nullable = false, updatable = false)
174+
private String name;
175+
176+
public Fruit() {
177+
}
178+
179+
public Fruit(String name) {
180+
this.name = name;
181+
}
182+
183+
public Integer getId() {
184+
return id;
185+
}
186+
187+
public void setId(Integer id) {
188+
this.id = id;
189+
}
190+
191+
public String getName() {
192+
return name;
193+
}
194+
195+
public void setName(String name) {
196+
this.name = name;
197+
}
198+
199+
@Override
200+
public String toString() {
201+
return "Fruit{" + id + "," + name + '}';
202+
}
203+
}
142204
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Import file for testing schema validation in SchemaValidationTest
2+
drop table if exists Fruit cascade constraints
3+
drop sequence if exists Fruit_SEQ
4+
5+
-- Create the table manually, so that we can check if the validation succeeds
6+
create sequence fruit_seq start with 1 increment by 50;
7+
create table Fruit (id number(10,0) not null, something_name nvarchar2(20) not null, primary key (id))
8+
9+
INSERT INTO fruit(id, something_name) VALUES (1, 'Cherry');
10+
INSERT INTO fruit(id, something_name) VALUES (2, 'Apple');
11+
INSERT INTO fruit(id, something_name) VALUES (3, 'Banana');
12+
ALTER SEQUENCE fruit_seq RESTART start with 4;

0 commit comments

Comments
 (0)