diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index bc51fdbda8a412..9e243507f52772 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -471,6 +471,11 @@ TIP: It is possible to read annotations from the test class or method to control WARNING: While it is possible to use JUnit Jupiter callback interfaces like `BeforeEachCallback`, you might run into classloading issues because Quarkus has to run tests in a custom classloader which JUnit is not aware of. +== Reset the Database after tests + +You can use the `@io.quarkus.test.ResetDatabase` annotation to reset the database after a test has run. This will drop the database, +and recreate the schema. Quarkus can use Liqibase, FlyWay or Hibernate ORM to reset the schema, depending on what is configured. + [[testing_different_profiles]] == Testing Different Profiles diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java index d953511a950b95..e78c6cd8a17d3e 100644 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java +++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java @@ -1,5 +1,7 @@ package io.quarkus.agroal.deployment; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + import java.sql.Driver; import java.util.ArrayList; import java.util.HashMap; @@ -7,9 +9,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Priority; import javax.enterprise.inject.Default; import javax.inject.Singleton; +import javax.interceptor.Interceptor; import javax.sql.XADataSource; import org.jboss.jandex.DotName; @@ -24,9 +29,12 @@ import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.runtime.DataSourcesJdbcBuildTimeConfig; import io.quarkus.agroal.runtime.TransactionIntegration; +import io.quarkus.agroal.runtime.schema.CleanDatabaseInterceptor; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.agroal.spi.JdbcDriverBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.processor.DotNames; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -37,6 +45,7 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -48,6 +57,10 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.narayana.jta.runtime.interceptor.TestTransactionInterceptor; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; @@ -56,6 +69,7 @@ class AgroalProcessor { private static final Logger log = Logger.getLogger(AgroalProcessor.class); + private static final String CLEAN_DATABASE = "io.quarkus.test.ResetDatabase"; private static final DotName DATA_SOURCE = DotName.createSimple(javax.sql.DataSource.class.getName()); @BuildStep @@ -333,4 +347,36 @@ HealthBuildItem addHealthCheck(DataSourcesBuildTimeConfig dataSourcesBuildTimeCo return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck", dataSourcesBuildTimeConfig.healthEnabled); } + + @BuildStep + public DevConsoleTemplateInfoBuildItem devConsoleInfo( + List dbs) { + return new DevConsoleTemplateInfoBuildItem("dbs", + dbs.stream().map(AggregatedDataSourceBuildTimeConfigBuildItem::getName) + .collect(Collectors.toList())); + } + + @BuildStep + @Record(value = STATIC_INIT, optional = true) + DevConsoleRouteBuildItem devConsoleCleanDatabaseHandler(AgroalRecorder recorder) { + return new DevConsoleRouteBuildItem("clean", "POST", recorder.devConsoleCleanDatabaseHandler()); + } + + @BuildStep(onlyIf = IsTest.class) + void cleanDatabaseSupport(BuildProducer generatedBeanBuildItemBuildProducer, + BuildProducer additionalBeans) { + //generate the annotated interceptor with gizmo + //all the logic is in the parent, but we don't have access to the + //binding annotation here + try (ClassCreator c = ClassCreator.builder() + .classOutput(new GeneratedBeanGizmoAdaptor(generatedBeanBuildItemBuildProducer)).className( + CleanDatabaseInterceptor.class.getName() + "Generated") + .superClass(TestTransactionInterceptor.class).build()) { + c.addAnnotation(CLEAN_DATABASE); + c.addAnnotation(Interceptor.class.getName()); + c.addAnnotation(Priority.class).addValue("value", Interceptor.Priority.PLATFORM_BEFORE + 100); + } + additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(CleanDatabaseInterceptor.class) + .addBeanClass(CLEAN_DATABASE).build()); + } } diff --git a/extensions/agroal/deployment/src/main/resources/dev-templates/clean.html b/extensions/agroal/deployment/src/main/resources/dev-templates/clean.html new file mode 100644 index 00000000000000..d667072017f00a --- /dev/null +++ b/extensions/agroal/deployment/src/main/resources/dev-templates/clean.html @@ -0,0 +1,27 @@ +{#include main} + {#title}Clean Databases{/title} + {#body} + + + + + + + + + {#for db in info:dbs} + + + + {/for} + +
DatasourceActions
+ {db} + +
+ + +
+
+ {/body} +{/include} \ No newline at end of file diff --git a/extensions/agroal/deployment/src/main/resources/dev-templates/embedded.html b/extensions/agroal/deployment/src/main/resources/dev-templates/embedded.html new file mode 100644 index 00000000000000..d631a84cd8c97c --- /dev/null +++ b/extensions/agroal/deployment/src/main/resources/dev-templates/embedded.html @@ -0,0 +1,3 @@ + + + Clean Databases diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java index abf8d8af6ae52d..133477a95776b9 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java @@ -1,10 +1,16 @@ package io.quarkus.agroal.runtime; +import java.util.ServiceLoader; import java.util.function.Supplier; import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; +import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; @Recorder public class AgroalRecorder { @@ -29,4 +35,20 @@ public AgroalDataSource get() { }; } + public Handler devConsoleCleanDatabaseHandler() { + // the usual issue of Vert.x hanging on to the first TCCL and setting it on all its threads + final ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); + return new DevConsolePostHandler() { + @Override + protected void handlePost(RoutingContext event, MultiMap form) throws Exception { + String name = form.get("name"); + ServiceLoader dbs = ServiceLoader.load(DatabaseSchemaProvider.class, + Thread.currentThread().getContextClassLoader()); + for (DatabaseSchemaProvider i : dbs) { + i.resetDatabase(name); + } + flashMessage(event, "Action invoked"); + } + }; + } } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/schema/CleanDatabaseInterceptor.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/schema/CleanDatabaseInterceptor.java new file mode 100644 index 00000000000000..4d9fe2e00b202c --- /dev/null +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/schema/CleanDatabaseInterceptor.java @@ -0,0 +1,33 @@ +package io.quarkus.agroal.runtime.schema; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +import javax.interceptor.AroundInvoke; +import javax.interceptor.InvocationContext; + +public class CleanDatabaseInterceptor { + + final List providers; + + public CleanDatabaseInterceptor() { + this.providers = new ArrayList<>(); + ServiceLoader dbs = ServiceLoader.load(DatabaseSchemaProvider.class, + Thread.currentThread().getContextClassLoader()); + for (DatabaseSchemaProvider i : dbs) { + providers.add(i); + } + } + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + try { + return context.proceed(); + } finally { + for (DatabaseSchemaProvider i : providers) { + i.resetAllDatabases(); + } + } + } +} diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/schema/DatabaseSchemaProvider.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/schema/DatabaseSchemaProvider.java new file mode 100644 index 00000000000000..fec42ee3c097b7 --- /dev/null +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/schema/DatabaseSchemaProvider.java @@ -0,0 +1,11 @@ +package io.quarkus.agroal.runtime.schema; + +/** + * A service interface that can be used to reset the database for dev and test mode. + */ +public interface DatabaseSchemaProvider { + + void resetDatabase(String dbName); + + void resetAllDatabases(); +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java new file mode 100644 index 00000000000000..c4641c5394ea39 --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java @@ -0,0 +1,23 @@ +package io.quarkus.flyway.runtime; + +import io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider; + +public class FlywaySchemaProvider implements DatabaseSchemaProvider { + @Override + public void resetDatabase(String dbName) { + for (FlywayContainer i : FlywayRecorder.FLYWAY_CONTAINERS) { + if (i.getDataSourceName().equals(dbName)) { + i.getFlyway().clean(); + i.getFlyway().migrate(); + } + } + } + + @Override + public void resetAllDatabases() { + for (FlywayContainer i : FlywayRecorder.FLYWAY_CONTAINERS) { + i.getFlyway().clean(); + i.getFlyway().migrate(); + } + } +} diff --git a/extensions/flyway/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider b/extensions/flyway/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider new file mode 100644 index 00000000000000..60c52a7bd78e0b --- /dev/null +++ b/extensions/flyway/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider @@ -0,0 +1 @@ +io.quarkus.flyway.runtime.FlywaySchemaProvider diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 9f0434465e4d92..200970c8d9d232 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -125,6 +125,7 @@ import io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor; import io.quarkus.hibernate.orm.runtime.proxies.PreGeneratedProxies; +import io.quarkus.hibernate.orm.runtime.schema.SchemaManagementIntegrator; import io.quarkus.hibernate.orm.runtime.tenant.DataSourceTenantConnectionResolver; import io.quarkus.hibernate.orm.runtime.tenant.TenantConnectionResolver; import io.quarkus.hibernate.orm.runtime.tenant.TenantResolver; @@ -390,6 +391,7 @@ public void build(RecorderContext recorderContext, HibernateOrmRecorder recorder List integrationBuildItems, ProxyDefinitionsBuildItem proxyDefinitions, BuildProducer feature, + LaunchModeBuildItem launchModeBuildItem, BuildProducer beanContainerListener) throws Exception { feature.produce(new FeatureBuildItem(Feature.HIBERNATE_ORM)); @@ -420,6 +422,9 @@ public void build(RecorderContext recorderContext, HibernateOrmRecorder recorder for (String integratorClassName : ServiceUtil.classNamesNamedIn(classLoader, INTEGRATOR_SERVICE_FILE)) { integratorClasses.add((Class) recorderContext.classProxy(integratorClassName)); } + if (launchModeBuildItem.getLaunchMode().isDevOrTest()) { + integratorClasses.add(SchemaManagementIntegrator.class); + } Map> integrationStaticDescriptors = HibernateOrmIntegrationStaticConfiguredBuildItem .collectDescriptors(integrationBuildItems); diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java new file mode 100644 index 00000000000000..77ffced95f7678 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateSchemaRecreateDevConsoleTestCase.java @@ -0,0 +1,37 @@ +package io.quarkus.hibernate.orm; + +import static org.hamcrest.Matchers.is; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class HibernateSchemaRecreateDevConsoleTestCase { + @RegisterExtension + final static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyEntity.class, MyEntityTestResource.class) + .addAsResource("application.properties") + .addAsResource("import.sql")); + + @Test + public void testCleanDatabase() { + RestAssured.when().get("/my-entity/count").then().body(is("2")); + RestAssured.when().get("/my-entity/add").then().body(is("MyEntity:added")); + RestAssured.when().get("/my-entity/count").then().body(is("3")); + RestAssured.with() + .redirects().follow(false).formParam("name", "").post("q/dev/io.quarkus.quarkus-agroal/clean") + .then() + .statusCode(303); + RestAssured.when().get("/my-entity/count").then().body(is("2")); + + } + + private void assertBodyIs(String expectedBody) { + RestAssured.when().get("/my-entity/2").then().body(is(expectedBody)); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/MyEntityTestResource.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/MyEntityTestResource.java index 871e2b818e3542..a2827a372d7274 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/MyEntityTestResource.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/MyEntityTestResource.java @@ -2,6 +2,7 @@ import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.transaction.Transactional; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -25,4 +26,22 @@ public String getName(@PathParam("id") long id) { return "no entity"; } + + @GET + @Path("/add") + @Produces(MediaType.TEXT_PLAIN) + @Transactional + public String add() { + MyEntity entity = new MyEntity(); + entity.setName("added"); + em.persist(entity); + return entity.toString(); + } + + @GET + @Path("/count") + @Produces(MediaType.TEXT_PLAIN) + public int count() { + return em.createQuery("from MyEntity").getResultList().size(); + } } diff --git a/extensions/hibernate-orm/deployment/src/test/resources/import.sql b/extensions/hibernate-orm/deployment/src/test/resources/import.sql index 861dca425e30cf..fec2bf0f4cefed 100644 --- a/extensions/hibernate-orm/deployment/src/test/resources/import.sql +++ b/extensions/hibernate-orm/deployment/src/test/resources/import.sql @@ -1,2 +1,3 @@ INSERT INTO MyEntity(id, name) VALUES(1, 'default sql load script entity'); INSERT INTO MyEntity(id, name) VALUES(2, 'import.sql load script entity'); +alter sequence myEntitySeq restart with 3; \ No newline at end of file diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java index d67bc84a504f64..a8f5d32dca5081 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java @@ -20,6 +20,7 @@ import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationRuntimeDescriptor; import io.quarkus.hibernate.orm.runtime.proxies.PreGeneratedProxies; +import io.quarkus.hibernate.orm.runtime.schema.SchemaManagementIntegrator; import io.quarkus.hibernate.orm.runtime.session.ForwardingSession; import io.quarkus.hibernate.orm.runtime.tenant.DataSourceTenantConnectionResolver; import io.quarkus.runtime.annotations.Recorder; @@ -54,6 +55,10 @@ public void setupPersistenceProvider(HibernateOrmRuntimeConfig hibernateOrmRunti public BeanContainerListener initMetadata(List parsedPersistenceXmlDescriptors, Scanner scanner, Collection> additionalIntegrators, PreGeneratedProxies proxyDefinitions) { + SchemaManagementIntegrator.clearDsMap(); + for (QuarkusPersistenceUnitDefinition i : parsedPersistenceXmlDescriptors) { + SchemaManagementIntegrator.mapDatasource(i.getDataSource(), i.getName()); + } return new BeanContainerListener() { @Override public void created(BeanContainer beanContainer) { @@ -118,5 +123,4 @@ protected Session delegate() { } }; } - } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/schema/SchemaManagementIntegrator.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/schema/SchemaManagementIntegrator.java new file mode 100644 index 00000000000000..552575696c6e7d --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/schema/SchemaManagementIntegrator.java @@ -0,0 +1,164 @@ +package io.quarkus.hibernate.orm.runtime.schema; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.hibernate.tool.schema.SourceType; +import org.hibernate.tool.schema.TargetType; +import org.hibernate.tool.schema.spi.CommandAcceptanceException; +import org.hibernate.tool.schema.spi.ExceptionHandler; +import org.hibernate.tool.schema.spi.ExecutionOptions; +import org.hibernate.tool.schema.spi.SchemaManagementTool; +import org.hibernate.tool.schema.spi.ScriptSourceInput; +import org.hibernate.tool.schema.spi.ScriptTargetOutput; +import org.hibernate.tool.schema.spi.SourceDescriptor; +import org.hibernate.tool.schema.spi.TargetDescriptor; +import org.jboss.logging.Logger; + +import io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.runtime.LaunchMode; + +public class SchemaManagementIntegrator implements Integrator, DatabaseSchemaProvider { + + private static final Logger log = Logger.getLogger(SchemaManagementIntegrator.class); + + private static final Map metadataMap = new ConcurrentHashMap<>(); + private static final Map datasourceToPuMap = new ConcurrentHashMap<>(); + + @Override + public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + metadataMap.put(defaultName(sessionFactory.getName()), new Holder(metadata, sessionFactory, serviceRegistry)); + } + + @Override + public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + metadataMap.remove(defaultName(sessionFactory.getName())); + } + + public static void clearDsMap() { + datasourceToPuMap.clear(); + } + + public static void mapDatasource(String datasource, String pu) { + datasourceToPuMap.put(datasource, pu); + } + + static String defaultName(String name) { + if (name == null) { + return DataSourceUtil.DEFAULT_DATASOURCE_NAME; + } + return name; + } + + public static void recreateDatabases() { + if (!LaunchMode.current().isDevOrTest()) { + throw new IllegalStateException("Can only be used in dev or test mode"); + } + for (String val : metadataMap.keySet()) { + recreateDatabase(val); + } + } + + public static void recreateDatabase(String name) { + if (!LaunchMode.current().isDevOrTest()) { + throw new IllegalStateException("Can only be used in dev or test mode"); + } + Holder val = metadataMap.get(name); + + Object prop = val.sessionFactory.getProperties().get("javax.persistence.schema-generation.database.action"); + if (prop != null && !(prop.toString().equals("none"))) { + //if this is none we assume another framework is doing this (e.g. flyway) + val.sessionFactory.getServiceRegistry().getService(SchemaManagementTool.class).getSchemaDropper(new HashMap()) + .doDrop(val.metadata, new SimpleExecutionOptions(), new SimpleSourceDescriptor(), + new SimpleTargetDescriptor()); + val.sessionFactory.getServiceRegistry().getService(SchemaManagementTool.class).getSchemaCreator(new HashMap()) + .doCreation(val.metadata, new SimpleExecutionOptions(), new SimpleSourceDescriptor(), + new SimpleTargetDescriptor()); + } + //we still clear caches though + val.sessionFactory.getCache().evictAll(); + val.sessionFactory.getCache().evictQueries(); + } + + @Override + public void resetDatabase(String dbName) { + String name = datasourceToPuMap.get(dbName); + if (name == null) { + //not a hibernate DS + return; + } + recreateDatabase(name); + } + + @Override + public void resetAllDatabases() { + recreateDatabases(); + } + + static class Holder { + final Metadata metadata; + final SessionFactoryImplementor sessionFactory; + final SessionFactoryServiceRegistry serviceRegistry; + + Holder(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + this.metadata = metadata; + this.sessionFactory = sessionFactory; + this.serviceRegistry = serviceRegistry; + } + } + + private static class SimpleExecutionOptions implements ExecutionOptions { + @Override + public Map getConfigurationValues() { + return Collections.emptyMap(); + } + + @Override + public boolean shouldManageNamespaces() { + return false; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return new ExceptionHandler() { + @Override + public void handleException(CommandAcceptanceException exception) { + log.error("Failed to recreate schema", exception); + } + }; + } + } + + private static class SimpleSourceDescriptor implements SourceDescriptor { + @Override + public SourceType getSourceType() { + return SourceType.METADATA; + } + + @Override + public ScriptSourceInput getScriptSourceInput() { + return null; + } + } + + private static class SimpleTargetDescriptor implements TargetDescriptor { + @Override + public EnumSet getTargetTypes() { + return EnumSet.of(TargetType.DATABASE); + } + + @Override + public ScriptTargetOutput getScriptTargetOutput() { + return null; + } + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider b/extensions/hibernate-orm/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider new file mode 100644 index 00000000000000..d6954febfd8a5d --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider @@ -0,0 +1 @@ +io.quarkus.hibernate.orm.runtime.schema.SchemaManagementIntegrator \ No newline at end of file diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java new file mode 100644 index 00000000000000..1998d0fbffccfb --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java @@ -0,0 +1,67 @@ +package io.quarkus.liquibase.runtime; + +import javax.enterprise.inject.Any; +import javax.enterprise.inject.UnsatisfiedResolutionException; + +import io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.liquibase.LiquibaseFactory; +import liquibase.Liquibase; +import liquibase.exception.LiquibaseException; + +public class LiquibaseSchemaProvider implements DatabaseSchemaProvider { + @Override + public void resetDatabase(String dbName) { + try { + InjectableInstance liquibaseFactoryInstance = Arc.container() + .select(LiquibaseFactory.class, Any.Literal.INSTANCE); + if (liquibaseFactoryInstance.isUnsatisfied()) { + return; + } + for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { + try { + LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); + if (liquibaseFactory.getDataSourceName().equals(dbName)) { + doReset(liquibaseFactory); + } + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured + } + } + } catch (Exception e) { + throw new IllegalStateException("Error starting Liquibase", e); + } + } + + @Override + public void resetAllDatabases() { + try { + InjectableInstance liquibaseFactoryInstance = Arc.container() + .select(LiquibaseFactory.class, Any.Literal.INSTANCE); + if (liquibaseFactoryInstance.isUnsatisfied()) { + return; + } + for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { + try { + LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); + doReset(liquibaseFactory); + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured + } + } + } catch (Exception e) { + throw new IllegalStateException("Error starting Liquibase", e); + } + } + + public void doReset(LiquibaseFactory liquibaseFactory) throws LiquibaseException { + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.dropAll(); + } + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } + } +} diff --git a/extensions/liquibase/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider b/extensions/liquibase/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider new file mode 100644 index 00000000000000..23d7c6452f4998 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/resources/META-INF/services/io.quarkus.agroal.runtime.schema.DatabaseSchemaProvider @@ -0,0 +1 @@ +io.quarkus.liquibase.runtime.LiquibaseSchemaProvider \ No newline at end of file diff --git a/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Artwork.java b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Artwork.java new file mode 100644 index 00000000000000..ed5275f16c56e3 --- /dev/null +++ b/integration-tests/jpa-h2/src/main/java/io/quarkus/it/jpa/h2/Artwork.java @@ -0,0 +1,33 @@ +package io.quarkus.it.jpa.h2; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Artwork { + + @Id + @GeneratedValue + private Integer id; + + private String name; + + public Integer getId() { + return id; + } + + public Artwork setId(Integer id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Artwork setName(String name) { + this.name = name; + return this; + } +} diff --git a/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java b/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java index 81b29711c351c5..2c7e3b446fee8e 100644 --- a/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java +++ b/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/JPAFunctionalityTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; +import io.quarkus.test.ResetDatabase; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -13,6 +14,7 @@ * is run in both JVM mode and native mode (see also test in subclass). */ @QuarkusTest +@ResetDatabase public class JPAFunctionalityTest { @Test diff --git a/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/ResetDatabaseTestCase.java b/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/ResetDatabaseTestCase.java new file mode 100644 index 00000000000000..d2f90fb42adf51 --- /dev/null +++ b/integration-tests/jpa-h2/src/test/java/io/quarkus/it/jpa/h2/ResetDatabaseTestCase.java @@ -0,0 +1,37 @@ +package io.quarkus.it.jpa.h2; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.ResetDatabase; +import io.quarkus.test.junit.QuarkusTest; + +//if the DB was not cleaned between method invocations then one of these tests would fail +@QuarkusTest +@ResetDatabase +@Transactional +public class ResetDatabaseTestCase { + + @Inject + EntityManager entityManager; + + @Test + public void test1() { + Assertions.assertEquals(0, entityManager.createQuery("from Artwork").getResultList().size()); + Artwork d = new Artwork(); + d.setName("Mona Lisa"); + entityManager.persist(d); + } + + @Test + public void test2() { + Assertions.assertEquals(0, entityManager.createQuery("from Artwork").getResultList().size()); + Artwork d = new Artwork(); + d.setName("Mona Lisa"); + entityManager.persist(d); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/ResetDatabase.java b/test-framework/common/src/main/java/io/quarkus/test/ResetDatabase.java new file mode 100644 index 00000000000000..75547b058b5ba8 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/ResetDatabase.java @@ -0,0 +1,23 @@ +package io.quarkus.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.interceptor.InterceptorBinding; + +/** + * Indicates that the database should be dropped and recreated + * after the method is run. + * + * This requires an extension to be installed that has the ability + * to update the database schema, namely Hibernate ORM, Flyway + * or Liqibase. + */ +@InterceptorBinding +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface ResetDatabase { + +}