From 49af62c74a0e17834eb2fbae264702003f80e6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hermann?= Date: Tue, 15 Oct 2024 07:47:52 -0400 Subject: [PATCH] flyway multitenant extension --- bom/application/pom.xml | 10 + .../java/io/quarkus/deployment/Feature.java | 1 + docs/src/main/asciidoc/flyway.adoc | 2 +- .../flyway-multitenant/deployment/pom.xml | 97 +++++ .../multitenant/deployment/FlywayEnabled.java | 24 ++ ...ywayMultiTenantAlwaysEnabledProcessor.java | 14 + .../FlywayMultiTenantCallbacksLocator.java | 105 ++++++ .../FlywayMultiTenantProcessor.java | 355 ++++++++++++++++++ .../devui/FlywayDevUIProcessor.java | 52 +++ .../dev-ui/qwc-flyway-datasources.js | 175 +++++++++ .../java/db/migration/V1_0_1__Update.java | 18 + .../java/db/migration/V1_0_2__Update.java | 39 ++ .../multitenant/test/DevModeTestEndpoint.java | 22 ++ .../FlywayDevModeCreateFromHibernateTest.java | 113 ++++++ .../FlywayDevModeModifyMigrationTest.java | 58 +++ .../multitenant/test/FlywayDevModeTest.java | 43 +++ ...AtStartExistingSchemaHistoryTableTest.java | 40 ++ .../FlywayExtensionBaselineAtStartTest.java | 39 ++ ...nBaselineOnMigrateNamedDataSourceTest.java | 43 +++ ...OnMigrateNamedDataSourcesInactiveTest.java | 70 ++++ .../FlywayExtensionBaselineOnMigrateTest.java | 41 ++ .../test/FlywayExtensionCDICallback.java | 48 +++ .../test/FlywayExtensionCallback.java | 47 +++ .../test/FlywayExtensionCallback2.java | 39 ++ .../test/FlywayExtensionCallbackTest.java | 66 ++++ ...ayExtensionCleanAndMigrateAtStartTest.java | 50 +++ ...ndMigrateAtStartWithJavaMigrationTest.java | 86 +++++ .../test/FlywayExtensionCleanAtStartTest.java | 51 +++ ...DefaultDatasourceDynamicInjectionTest.java | 38 ++ ...eDefaultDatasourceStaticInjectionTest.java | 46 +++ ...seNamedDataSourceDynamicInjectionTest.java | 49 +++ ...lseNamedDataSourceStaticInjectionTest.java | 57 +++ ...yExtensionConfigDefaultDataSourceTest.java | 35 ++ ...figDefaultDataSourceWithoutFlywayTest.java | 38 ++ .../test/FlywayExtensionConfigFixture.java | 186 +++++++++ ...ayExtensionConfigMultiDataSourcesTest.java | 72 ++++ ...figMultiDataSourcesWithoutDefaultTest.java | 50 +++ ...nfigNamedDataSourceWithoutDefaultTest.java | 39 ++ ...onfigNamedDataSourceWithoutFlywayTest.java | 40 ++ ...DefaultDatasourceDynamicInjectionTest.java | 38 ++ ...gDefaultDatasourceStaticInjectionTest.java | 45 +++ ...ngNamedDataSourceDynamicInjectionTest.java | 48 +++ ...ingNamedDataSourceStaticInjectionTest.java | 56 +++ .../test/FlywayExtensionDisabledTest.java | 31 ++ ...FlywayExtensionFilesystemResourceTest.java | 60 +++ .../test/FlywayExtensionInitSqlTest.java | 44 +++ ...efaultDatasourceConfigActiveFalseTest.java | 42 +++ ...tStartDefaultDatasourceUrlMissingTest.java | 41 ++ ...sionMigrateAtStartNamedDataSourceTest.java | 44 +++ ...tNamedDatasourceConfigActiveFalseTest.java | 52 +++ ...rtNamedDatasourceConfigUrlMissingTest.java | 51 +++ ...yExtensionMigrateAtStartSubfolderTest.java | 39 ++ .../FlywayExtensionMigrateAtStartTest.java | 39 ++ .../FlywayExtensionRepairAtStartTest.java | 81 ++++ .../FlywayExtensionValidateAtStartTest.java | 23 ++ .../FlywayExtensionWithCustomizerTest.java | 66 ++++ ...ExtensionWithJavaMigrationDevModeTest.java | 40 ++ ...nWithJavaMigrationDevModeTestEndpoint.java | 37 ++ ...MultipleDatasourcesAndCustomizersTest.java | 113 ++++++ .../test/FlywayH2TestCustomizer.java | 81 ++++ .../FlywayMultiDataSourcesDevModeTest.java | 35 ++ .../multitenant/test/FlywayTestResources.java | 8 + .../flyway/multitenant/test/Fruit.java | 35 ++ .../test/MultiDataSourcesDevModeEndpoint.java | 50 +++ .../baseline-at-start-config.properties | 8 + ...ing-schema-history-table-config.properties | 8 + ...ine-on-migrate-named-datasource.properties | 10 + ...rate-named-datasources-inactive.properties | 24 ++ .../resources/baseline-on-migrate.properties | 10 + .../test/resources/callback-config.properties | 10 + .../src/test/resources/callback-init-data.sql | 4 + ...ean-and-migrate-at-start-config.properties | 13 + ...t-start-with-fs-resource-config.properties | 13 + .../clean-at-start-config.properties | 14 + .../test/resources/config-empty.properties | 1 + ...tasource-with-customizer-config.properties | 9 + ...fault-datasource-without-flyway.properties | 4 + .../config-for-default-datasource.properties | 19 + ...ig-for-missing-named-datasource.properties | 19 + ...asource-with-customizers-config.properties | 24 ++ ...ple-datasources-without-default.properties | 41 ++ ...config-for-multiple-datasources.properties | 61 +++ ...amed-datasource-without-default.properties | 19 + ...named-datasource-without-flyway.properties | 4 + .../subfolder/V1.0.0__Quarkus.sql | 5 + .../db/migration/V1.0.0__Quarkus.sql | 5 + .../db/migration/V1.0.3__Quarkus_Callback.sql | 4 + .../test/resources/disabled-config.properties | 8 + .../src/test/resources/h2-init-data.sql | 3 + .../h2-init-schema-history-table.sql | 13 + .../test/resources/init-sql-config.properties | 8 + ...t-start-config-named-datasource.properties | 8 + .../migrate-at-start-config.properties | 7 + ...grate-at-start-subfolder-config.properties | 8 + .../repair-at-start-config.properties | 7 + .../validate-at-start-config.properties | 7 + extensions/flyway-multitenant/pom.xml | 21 ++ extensions/flyway-multitenant/runtime/pom.xml | 93 +++++ .../multitenant/FlywayPersistenceUnit.java | 83 ++++ .../FlywayMultiTenantBuildTimeConfig.java | 50 +++ .../FlywayMultiTenantContainerProducer.java | 100 +++++ .../FlywayMultiTenantContainerUtil.java | 20 + .../FlywayMultiTenantRuntimeConfig.java | 40 ++ .../multitenant/runtime/FlywayRecorder.java | 184 +++++++++ .../runtime/QuarkusPathLocationScanner.java | 120 ++++++ .../runtime/devui/FlywayDevUIRecorder.java | 20 + .../runtime/devui/FlywayJsonRpcService.java | 231 ++++++++++++ .../resources/META-INF/quarkus-extension.yaml | 14 + .../runtime/FlywayCreatorTest.java | 295 +++++++++++++++ .../quarkus/flyway/runtime/FlywayCreator.java | 15 +- extensions/pom.xml | 1 + .../vertx/utils/VertxOutputStream.java | 1 - 112 files changed, 5278 insertions(+), 5 deletions(-) create mode 100644 extensions/flyway-multitenant/deployment/pom.xml create mode 100644 extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayEnabled.java create mode 100644 extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantAlwaysEnabledProcessor.java create mode 100644 extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantCallbacksLocator.java create mode 100644 extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantProcessor.java create mode 100644 extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/devui/FlywayDevUIProcessor.java create mode 100644 extensions/flyway-multitenant/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_1__Update.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_2__Update.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/DevModeTestEndpoint.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeCreateFromHibernateTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeModifyMigrationTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartExistingSchemaHistoryTableTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCDICallback.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback2.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallbackTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAtStartTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceDynamicInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceStaticInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceDynamicInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceStaticInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigFixture.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceDynamicInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceStaticInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceDynamicInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceStaticInjectionTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionDisabledTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionFilesystemResourceTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionInitSqlTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceUrlMissingTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigUrlMissingTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartSubfolderTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionRepairAtStartTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionValidateAtStartTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithCustomizerTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTestEndpoint.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithMultipleDatasourcesAndCustomizersTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayH2TestCustomizer.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayMultiDataSourcesDevModeTest.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayTestResources.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/Fruit.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/MultiDataSourcesDevModeEndpoint.java create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-existing-schema-history-table-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/callback-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/callback-init-data.sql create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/clean-at-start-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-empty.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-with-customizer-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-missing-named-datasource.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasource-with-customizers-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-default.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/db/migration-subfolder/subfolder/V1.0.0__Quarkus.sql create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.0__Quarkus.sql create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.3__Quarkus_Callback.sql create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/disabled-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/h2-init-data.sql create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/h2-init-schema-history-table.sql create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/init-sql-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-subfolder-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/repair-at-start-config.properties create mode 100644 extensions/flyway-multitenant/deployment/src/test/resources/validate-at-start-config.properties create mode 100644 extensions/flyway-multitenant/pom.xml create mode 100644 extensions/flyway-multitenant/runtime/pom.xml create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/FlywayPersistenceUnit.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantBuildTimeConfig.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerProducer.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerUtil.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantRuntimeConfig.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayRecorder.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/QuarkusPathLocationScanner.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayDevUIRecorder.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayJsonRpcService.java create mode 100644 extensions/flyway-multitenant/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 extensions/flyway-multitenant/runtime/src/test/java/io/quarkus/flyway/multitenant/runtime/FlywayCreatorTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index a72f136ec4f11..55329d77ac6a9 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1060,6 +1060,16 @@ quarkus-flyway-deployment ${project.version} + + io.quarkus + quarkus-flyway-multitenant + ${project.version} + + + io.quarkus + quarkus-flyway-multitenant-deployment + ${project.version} + io.quarkus quarkus-flyway-postgresql diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 497136eab4c92..cb07132ccb38e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -25,6 +25,7 @@ public enum Feature { ELASTICSEARCH_REST_HIGH_LEVEL_CLIENT, ELASTICSEARCH_JAVA_CLIENT, FLYWAY, + FLYWAY_MULTITENANT, GRPC_CLIENT, GRPC_SERVER, HIBERNATE_ORM, diff --git a/docs/src/main/asciidoc/flyway.adoc b/docs/src/main/asciidoc/flyway.adoc index 84120b19321e2..cf2ed63da0a10 100644 --- a/docs/src/main/asciidoc/flyway.adoc +++ b/docs/src/main/asciidoc/flyway.adoc @@ -315,7 +315,7 @@ NOTE: Without configuration, Flyway is set up for every datasource using the def == Customizing Flyway -In cases where Flyway needs to be configured in addition to the configuration options that Quarkus provides, the `io.quarkus.flyway.FlywayConfigurationCustomizer` class comes in handy. +In cases where Flyway needs to be configured in addition to the configuration options that Quarkus provides, the `io.quarkus.flyway.multitenant.FlywayConfigurationCustomizer` class comes in handy. To customize Flyway for the default datasource, simply add a bean like so: diff --git a/extensions/flyway-multitenant/deployment/pom.xml b/extensions/flyway-multitenant/deployment/pom.xml new file mode 100644 index 0000000000000..7cac9aa1071e2 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/pom.xml @@ -0,0 +1,97 @@ + + + + quarkus-flyway-multitenant-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-flyway-multitenant-deployment + Quarkus - Flyway - Multitenant - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-flyway-deployment + + + io.quarkus + quarkus-hibernate-orm-deployment + + + io.quarkus + quarkus-flyway-multitenant + + + io.quarkus + quarkus-vertx-http-dev-ui-tests + test + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-test-h2 + test + + + io.quarkus + quarkus-jdbc-h2-deployment + test + + + io.quarkus + quarkus-resteasy-jackson-deployment + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + -AlegacyConfigRoot=true + + + + + + + + diff --git a/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayEnabled.java b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayEnabled.java new file mode 100644 index 0000000000000..904ca4cccb1de --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayEnabled.java @@ -0,0 +1,24 @@ +package io.quarkus.flyway.multitenant.deployment; + +import java.util.function.BooleanSupplier; + +import io.quarkus.flyway.multitenant.runtime.FlywayMultiTenantBuildTimeConfig; + +/** + * Supplier that can be used to only run build steps + * if the Flyway extension is enabled. + */ +public class FlywayEnabled implements BooleanSupplier { + + private final FlywayMultiTenantBuildTimeConfig config; + + FlywayEnabled(FlywayMultiTenantBuildTimeConfig config) { + this.config = config; + } + + @Override + public boolean getAsBoolean() { + return config.enabled; + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantAlwaysEnabledProcessor.java b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantAlwaysEnabledProcessor.java new file mode 100644 index 0000000000000..8d1084b549a9d --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantAlwaysEnabledProcessor.java @@ -0,0 +1,14 @@ +package io.quarkus.flyway.multitenant.deployment; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +public class FlywayMultiTenantAlwaysEnabledProcessor { + + @BuildStep + void build(BuildProducer featureProducer) { + featureProducer.produce(new FeatureBuildItem(Feature.FLYWAY_MULTITENANT)); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantCallbacksLocator.java b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantCallbacksLocator.java new file mode 100644 index 0000000000000..14be0508d114c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantCallbacksLocator.java @@ -0,0 +1,105 @@ +package io.quarkus.flyway.multitenant.deployment; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.flywaydb.core.api.callback.Callback; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.flyway.multitenant.runtime.FlywayMultiTenantBuildTimeConfig; + +/** + * Logic to locate and process Flyway {@link Callback} classes. + * This class also helps to keep the {@link FlywayMultiTenantProcessor} class as lean as possible to make it easier to maintain + */ +class FlywayMultiTenantCallbacksLocator { + private final Collection persistenceUnitNames; + private final FlywayMultiTenantBuildTimeConfig flywayBuildConfig; + private final CombinedIndexBuildItem combinedIndexBuildItem; + private final BuildProducer reflectiveClassProducer; + + private FlywayMultiTenantCallbacksLocator(Collection persistenceUnitNames, + FlywayMultiTenantBuildTimeConfig flywayBuildConfig, + CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer reflectiveClassProducer) { + this.persistenceUnitNames = persistenceUnitNames; + this.flywayBuildConfig = flywayBuildConfig; + this.combinedIndexBuildItem = combinedIndexBuildItem; + this.reflectiveClassProducer = reflectiveClassProducer; + } + + public static FlywayMultiTenantCallbacksLocator with(Collection persistenceUnitNames, + FlywayMultiTenantBuildTimeConfig flywayBuildConfig, + CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer reflectiveClassProducer) { + return new FlywayMultiTenantCallbacksLocator(persistenceUnitNames, flywayBuildConfig, combinedIndexBuildItem, + reflectiveClassProducer); + } + + /** + * Main logic to identify callbacks and return them to be processed by the {@link FlywayMultiTenantProcessor} + * + * @return Map containing the callbacks for each datasource. The datasource name is the map key + * @exception ClassNotFoundException if the {@link Callback} class cannot be located by the Quarkus class loader + * @exception InstantiationException if the {@link Callback} class represents an abstract class. + * @exception InvocationTargetException if the underlying constructor throws an exception. + * @exception IllegalAccessException if the {@link Callback} constructor is enforcing Java language access control + * and the underlying constructor is inaccessible + */ + public Map> getCallbacks() + throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException { + final Map> callbacks = new HashMap<>(); + for (String dataSourceName : persistenceUnitNames) { + final Collection instances = callbacksForPersistenceUnit(dataSourceName); + callbacks.put(dataSourceName, instances); + } + return callbacks; + } + + /** + * + * Reads the configuration, instantiates the {@link Callback} class. Also, adds it to the reflective producer + * + * @return List of callbacks for the datasource + * @exception ClassNotFoundException if the {@link Callback} class cannot be located by the Quarkus class loader + * @exception InstantiationException if the {@link Callback} class represents an abstract class. + * @exception InvocationTargetException if the underlying constructor throws an exception. + * @exception IllegalAccessException if the {@link Callback} constructor is enforcing Java language access control + * and the underlying constructor is inaccessible + */ + private Collection callbacksForPersistenceUnit(String persistenceUnitName) + throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException { + final Optional> callbackConfig = flywayBuildConfig + .getConfigForPersistenceUnitName(persistenceUnitName).callbacks; + if (!callbackConfig.isPresent()) { + return Collections.emptyList(); + } + final Collection callbacks = callbackConfig.get(); + final Collection instances = new ArrayList<>(callbacks.size()); + for (String callback : callbacks) { + final ClassInfo clazz = combinedIndexBuildItem.getIndex().getClassByName(DotName.createSimple(callback)); + Objects.requireNonNull(clazz, + "Flyway callback not found, please verify the fully qualified name for the class: " + callback); + if (Modifier.isAbstract(clazz.flags()) || !clazz.hasNoArgsConstructor()) { + throw new IllegalArgumentException( + "Invalid Flyway callback. It shouldn't be abstract and must have a default constructor"); + } + final Class clazzType = Class.forName(callback, false, Thread.currentThread().getContextClassLoader()); + final Callback instance = (Callback) clazzType.getConstructors()[0].newInstance(); + instances.add(instance); + reflectiveClassProducer + .produce(ReflectiveClassBuildItem.builder(clazz.name().toString()).build()); + } + return instances; + } +} diff --git a/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantProcessor.java b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantProcessor.java new file mode 100644 index 0000000000000..495d5dc25e79f --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/FlywayMultiTenantProcessor.java @@ -0,0 +1,355 @@ +package io.quarkus.flyway.multitenant.deployment; + +import static io.quarkus.datasource.common.runtime.DataSourceUtil.DEFAULT_DATASOURCE_NAME; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.quarkus.flyway.multitenant.FlywayPersistenceUnit; +import io.quarkus.flyway.runtime.FlywayContainer; +import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig; +import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem; +import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Singleton; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.migration.JavaMigration; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; +import org.jboss.logging.Logger; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; +import io.quarkus.agroal.spi.JdbcDataSourceSchemaReadyBuildItem; +import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Produce; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; +import io.quarkus.deployment.builditem.InitTaskBuildItem; +import io.quarkus.deployment.builditem.InitTaskCompletedBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; +import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.flyway.multitenant.runtime.FlywayMultiTenantBuildTimeConfig; +import io.quarkus.flyway.multitenant.runtime.FlywayMultiTenantContainerProducer; +import io.quarkus.flyway.multitenant.runtime.FlywayRecorder; +import io.quarkus.flyway.multitenant.runtime.FlywayMultiTenantRuntimeConfig; +import io.quarkus.runtime.util.ClassPathUtils; + +@BuildSteps(onlyIf = FlywayEnabled.class) +class FlywayMultiTenantProcessor { + + private static final String CLASSPATH_APPLICATION_MIGRATIONS_PROTOCOL = "classpath"; + + private static final String FLYWAY_CONTAINER_BEAN_NAME_PREFIX = "flyway_container_"; + private static final String FLYWAY_BEAN_NAME_PREFIX = "flyway_"; + + private static final DotName JAVA_MIGRATION = DotName.createSimple(JavaMigration.class.getName()); + + private static final Logger LOGGER = Logger.getLogger(FlywayMultiTenantProcessor.class); + + @Record(STATIC_INIT) + @BuildStep + MigrationStateBuildItem build(BuildProducer resourceProducer, + BuildProducer reflectiveClassProducer, + BuildProducer hotDeploymentProducer, + FlywayRecorder recorder, + RecorderContext context, + CombinedIndexBuildItem combinedIndexBuildItem, + List jdbcDataSourceBuildItems, + FlywayMultiTenantBuildTimeConfig flywayMultiTenantBuildTimeConfig) throws Exception { + + Collection dataSourceNames = getDataSourceNames(jdbcDataSourceBuildItems); + Map> applicationMigrationsToDs = new HashMap<>(); + for (var dataSourceName : dataSourceNames) { + FlywayDataSourceBuildTimeConfig flywayPersistenceUnitBuildTimeConfig = flywayMultiTenantBuildTimeConfig + .getConfigForPersistenceUnitName(dataSourceName); + + Collection migrationLocations = discoverApplicationMigrations( + flywayPersistenceUnitBuildTimeConfig.locations); + applicationMigrationsToDs.put(dataSourceName, migrationLocations); + } + Set datasourcesWithMigrations = new HashSet<>(); + Set datasourcesWithoutMigrations = new HashSet<>(); + for (var e : applicationMigrationsToDs.entrySet()) { + if (e.getValue().isEmpty()) { + datasourcesWithoutMigrations.add(e.getKey()); + } else { + datasourcesWithMigrations.add(e.getKey()); + } + } + + Collection applicationMigrations = applicationMigrationsToDs.values().stream().collect(HashSet::new, + AbstractCollection::addAll, HashSet::addAll); + for (String applicationMigration : applicationMigrations) { + Location applicationMigrationLocation = new Location(applicationMigration); + String applicationMigrationPath = applicationMigrationLocation.getPath(); + + if ((applicationMigrationPath != null) && + // we don't include .class files in the watched files because that messes up live reload + !applicationMigrationPath.endsWith(".class")) { + hotDeploymentProducer.produce(new HotDeploymentWatchedFileBuildItem(applicationMigrationPath)); + } + } + recorder.setApplicationMigrationFiles(applicationMigrations); + + Set> javaMigrationClasses = new HashSet<>(); + addJavaMigrations(combinedIndexBuildItem.getIndex().getAllKnownImplementors(JAVA_MIGRATION), context, + reflectiveClassProducer, javaMigrationClasses); + recorder.setApplicationMigrationClasses(javaMigrationClasses); + + final Map> callbacks = FlywayMultiTenantCallbacksLocator.with( + dataSourceNames, + flywayMultiTenantBuildTimeConfig, + combinedIndexBuildItem, + reflectiveClassProducer).getCallbacks(); + recorder.setApplicationCallbackClasses(callbacks); + + resourceProducer.produce(new NativeImageResourceBuildItem(applicationMigrations.toArray(new String[0]))); + return new MigrationStateBuildItem(datasourcesWithMigrations, datasourcesWithoutMigrations); + } + + @SuppressWarnings("unchecked") + private void addJavaMigrations(Collection candidates, RecorderContext context, + BuildProducer reflectiveClassProducer, + Set> javaMigrationClasses) { + for (ClassInfo javaMigration : candidates) { + if (Modifier.isAbstract(javaMigration.flags())) { + continue; + } + javaMigrationClasses.add((Class) context.classProxy(javaMigration.name().toString())); + reflectiveClassProducer.produce( + ReflectiveClassBuildItem.builder(javaMigration.name().toString()).build()); + } + } + + @BuildStep + @Produce(SyntheticBeansRuntimeInitBuildItem.class) + @Consume(LoggingSetupBuildItem.class) + @Record(ExecutionTime.RUNTIME_INIT) + void createBeans(FlywayRecorder recorder, + List persistenceUnits, + List sqlGeneratorBuildItems, + BuildProducer additionalBeans, + BuildProducer syntheticBeanBuildItemBuildProducer, + MigrationStateBuildItem migrationsBuildItem, + FlywayMultiTenantBuildTimeConfig flywayMultiTenantBuildTimeConfig) { + // make a FlywayContainerProducer bean + additionalBeans.produce( + AdditionalBeanBuildItem.builder().addBeanClasses(FlywayMultiTenantContainerProducer.class).setUnremovable() + .setDefaultScope(DotNames.SINGLETON).build()); + // add the @FlywayDataSource class otherwise it won't be registered as a qualifier + additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(FlywayDataSource.class).build()); + + for (var persistenceUnit : persistenceUnits) { + String persistenceUnitName = persistenceUnit.getPersistenceUnitName(); + String dataSourceName = persistenceUnit.getConfig().getDataSource().orElse(DEFAULT_DATASOURCE_NAME); + boolean multiTenant = persistenceUnit.getConfig().getMultiTenancyStrategy() == MultiTenancyStrategy.SCHEMA; + + boolean hasMigrations = migrationsBuildItem.hasMigrations.contains(persistenceUnitName); + boolean createPossible = false; + if (!hasMigrations) { + createPossible = sqlGeneratorBuildItems.stream().anyMatch(s -> s.getDatabaseName().equals(persistenceUnitName)); + } + + SyntheticBeanBuildItem.ExtendedBeanConfigurator flywayContainerConfigurator = SyntheticBeanBuildItem + .configure(FlywayContainer.class) + .scope(multiTenant ? Dependent.class : Singleton.class) + .setRuntimeInit() + .unremovable() + .addInjectionPoint(ClassType.create(DotName.createSimple(FlywayMultiTenantContainerProducer.class))) + .addInjectionPoint(ClassType.create(DotName.createSimple(DataSources.class))) + .createWith(recorder.flywayContainerFunction(dataSourceName, persistenceUnitName, multiTenant, + hasMigrations, createPossible)); + + AnnotationInstance flywayContainerQualifier; + + String containerBeanName = FLYWAY_CONTAINER_BEAN_NAME_PREFIX + persistenceUnitName; + flywayContainerConfigurator.name(containerBeanName); + + flywayContainerConfigurator.addQualifier().annotation(DotNames.NAMED).addValue("value", containerBeanName).done(); + flywayContainerConfigurator.addQualifier().annotation(FlywayPersistenceUnit.class) + .addValue("value", persistenceUnitName) + .done(); + flywayContainerConfigurator.priority(5); + + flywayContainerQualifier = AnnotationInstance.builder(FlywayPersistenceUnit.class).add("value", persistenceUnitName) + .build(); + + syntheticBeanBuildItemBuildProducer.produce(flywayContainerConfigurator.done()); + + SyntheticBeanBuildItem.ExtendedBeanConfigurator flywayConfigurator = SyntheticBeanBuildItem + .configure(Flyway.class) + .scope(multiTenant ? Dependent.class : Singleton.class) + .setRuntimeInit() + .unremovable() + .addInjectionPoint(ClassType.create(DotName.createSimple(FlywayContainer.class)), flywayContainerQualifier) + .createWith(recorder.flywayFunction(persistenceUnitName, multiTenant)); + + if (multiTenant) { + flywayConfigurator.addInjectionPoint( + ClassType.create(DotName.createSimple(InjectionPoint.class))); + } + + String flywayBeanName = FLYWAY_BEAN_NAME_PREFIX + persistenceUnitName; + flywayConfigurator.name(flywayBeanName); + flywayConfigurator.priority(5); + + flywayConfigurator.addQualifier().annotation(DotNames.NAMED).addValue("value", flywayBeanName).done(); + flywayConfigurator.addQualifier().annotation(FlywayPersistenceUnit.class).addValue("value", persistenceUnitName) + .done(); + + syntheticBeanBuildItemBuildProducer.produce(flywayConfigurator.done()); + } + } + + @BuildStep + @Consume(BeanContainerBuildItem.class) + @Record(ExecutionTime.RUNTIME_INIT) + public ServiceStartBuildItem startActions(FlywayRecorder recorder, + FlywayMultiTenantRuntimeConfig config, + BuildProducer schemaReadyBuildItem, + BuildProducer initializationCompleteBuildItem, + List jdbcDataSourceBuildItems, + MigrationStateBuildItem migrationsBuildItem) { + + Collection dataSourceNames = getDataSourceNames(jdbcDataSourceBuildItems); + + for (String dataSourceName : dataSourceNames) { + recorder.doStartActions(dataSourceName); + } + + // once we are done running the migrations, we produce a build item indicating that the + // schema is "ready" + schemaReadyBuildItem.produce(new JdbcDataSourceSchemaReadyBuildItem(migrationsBuildItem.hasMigrations)); + initializationCompleteBuildItem.produce(new InitTaskCompletedBuildItem("flyway")); + return new ServiceStartBuildItem("flyway"); + } + + @BuildStep + public InitTaskBuildItem configureInitTask(ApplicationInfoBuildItem app) { + return InitTaskBuildItem.create() + .withName(app.getName() + "-flyway-init") + .withTaskEnvVars(Map.of("QUARKUS_INIT_AND_EXIT", "true", "QUARKUS_FLYWAY_ENABLED", "true")) + .withAppEnvVars(Map.of("QUARKUS_FLYWAY_ENABLED", "false")) + .withSharedEnvironment(true) + .withSharedFilesystem(true); + } + + private Set getDataSourceNames(List jdbcDataSourceBuildItems) { + Set result = new HashSet<>(jdbcDataSourceBuildItems.size()); + for (JdbcDataSourceBuildItem item : jdbcDataSourceBuildItems) { + result.add(item.getName()); + } + return result; + } + + private Collection discoverApplicationMigrations(Collection locations) + throws IOException { + LinkedHashSet applicationMigrationResources = new LinkedHashSet<>(); + // Locations can be a comma separated list + for (String location : locations) { + location = normalizeLocation(location); + if (location.startsWith(Location.FILESYSTEM_PREFIX)) { + applicationMigrationResources.add(location); + continue; + } + + String finalLocation = location; + ClassPathUtils.consumeAsPaths(Thread.currentThread().getContextClassLoader(), location, path -> { + Set applicationMigrations = null; + try { + applicationMigrations = FlywayMultiTenantProcessor.this.getApplicationMigrationsFromPath(finalLocation, + path); + } catch (IOException e) { + LOGGER.warnv(e, + "Can't process files in path %s", path); + } + if (applicationMigrations != null) { + applicationMigrationResources.addAll(applicationMigrations); + } + }); + } + return applicationMigrationResources; + } + + private String normalizeLocation(String location) { + if (location == null) { + throw new IllegalStateException("Flyway migration location may not be null."); + } + + // Strip any 'classpath:' protocol prefixes because they are assumed + // but not recognized by ClassLoader.getResources() + if (location.startsWith(CLASSPATH_APPLICATION_MIGRATIONS_PROTOCOL + ':')) { + location = location.substring(CLASSPATH_APPLICATION_MIGRATIONS_PROTOCOL.length() + 1); + if (location.startsWith("/")) { + location = location.substring(1); + } + } + if (!location.endsWith("/")) { + location += "/"; + } + + return location; + } + + private Set getApplicationMigrationsFromPath(final String location, final Path rootPath) + throws IOException { + + try (final Stream pathStream = Files.walk(rootPath)) { + return pathStream.filter(Files::isRegularFile) + .map(it -> Paths.get(location, rootPath.relativize(it).toString()).normalize().toString()) + // we don't want windows paths here since the paths are going to be used as classpath paths anyway + .map(it -> it.replace('\\', '/')) + .peek(it -> LOGGER.debugf("Discovered path: %s", it)) + .collect(Collectors.toSet()); + } + } + + public static final class MigrationStateBuildItem extends SimpleBuildItem { + + final Set hasMigrations; + final Set missingMigrations; + + MigrationStateBuildItem(Set hasMigrations, Set missingMigrations) { + this.hasMigrations = hasMigrations; + this.missingMigrations = missingMigrations; + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/devui/FlywayDevUIProcessor.java b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/devui/FlywayDevUIProcessor.java new file mode 100644 index 0000000000000..697fee8f13a4e --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/main/java/io/quarkus/flyway/multitenant/deployment/devui/FlywayDevUIProcessor.java @@ -0,0 +1,52 @@ +package io.quarkus.flyway.multitenant.deployment.devui; + +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.flyway.multitenant.runtime.FlywayMultiTenantBuildTimeConfig; +import io.quarkus.flyway.multitenant.runtime.devui.FlywayDevUIRecorder; +import io.quarkus.flyway.multitenant.runtime.devui.FlywayJsonRpcService; + +public class FlywayDevUIProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + @Record(value = RUNTIME_INIT, optional = true) + CardPageBuildItem create(FlywayDevUIRecorder recorder, FlywayMultiTenantBuildTimeConfig buildTimeConfig, + List generatorBuildItem, + CurateOutcomeBuildItem curateOutcomeBuildItem) { + + Map> initialSqlSuppliers = new HashMap<>(); + for (JdbcInitialSQLGeneratorBuildItem buildItem : generatorBuildItem) { + initialSqlSuppliers.put(buildItem.getDatabaseName(), buildItem.getSqlSupplier()); + } + + String artifactId = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getArtifactId(); + + recorder.setInitialSqlSuppliers(initialSqlSuppliers, artifactId); + + CardPageBuildItem card = new CardPageBuildItem(); + + card.addPage(Page.webComponentPageBuilder() + .componentLink("qwc-flyway-datasources.js") + .dynamicLabelJsonRPCMethodName("getNumberOfDatasources") + .icon("font-awesome-solid:database")); + return card; + } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem registerJsonRpcBackend() { + return new JsonRPCProvidersBuildItem(FlywayJsonRpcService.class); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js b/extensions/flyway-multitenant/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js new file mode 100644 index 0000000000000..0057a64087972 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js @@ -0,0 +1,175 @@ +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/grid'; +import 'qui-alert'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import { dialogRenderer } from '@vaadin/dialog/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import '@vaadin/progress-bar'; +import { notifier } from 'notifier'; + +export class QwcFlywayDatasources extends QwcHotReloadElement { + + jsonRpc = new JsonRpc(this); + + static styles = css` + .button { + cursor: pointer; + } + .clearIcon { + color: var(--lumo-warning-text-color); + }`; + + static properties = { + _ds: {state: true}, + _selectedDs: {state: true}, + _createDialogOpened: {state: true} + } + + constructor() { + super(); + this._ds = null; + this._selectedDs = null; + this._createDialogOpened = false; + } + + connectedCallback() { + super.connectedCallback(); + this.hotReload(); + } + + hotReload(){ + this.jsonRpc.getDatasources().then(jsonRpcResponse => { + this._ds = jsonRpcResponse.result; + }); + } + + render() { + if (this._ds) { + return this._renderDataSourceTable(); + } else { + return html``; + } + } + + _renderDataSourceTable() { + return html`${this._renderCreateDialog()} + + + + + + `; + } + + _actionRenderer(ds) { + return html`${this._renderMigrationButtons(ds)} + ${this._renderCreateButton(ds)}`; + } + + _renderMigrationButtons(ds) { + if(ds.hasMigrations){ + return html` + this._clean(ds)} class="button"> + Clean + + this._migrate(ds)} class="button"> + Migrate + `; + } + } + + _renderCreateButton(ds) { + if(ds.createPossible){ + return html` + this._showCreateDialog(ds)} class="button" title="Set up basic files for Flyway migrations to work. Initial file in db/migrations will be created and you can then add additional migration files"> + Create Initial Migration File + `; + } + } + + _nameRenderer(ds) { + return html`${ds.name}`; + } + + _showCreateDialog(ds){ + this._selectedDs = ds; + this._createDialogOpened = true; + } + + _renderCreateDialog(){ + return html` this._renderCreateDialogForm(), "Create")} + >`; + } + + _renderCreateDialogForm(){ + let title = this._selectedDs.name + " Datasource"; + return html`${title}
+ Set up an initial file from Hibernate ORM schema generation for Flyway migrations to work.
+ If you say yes, an initial file in db/migrations will be
+ created and you can then add additional migration files as documented. + ${this._renderDialogButtons(this._selectedDs)} + `; + } + + _renderDialogButtons(ds){ + return html`
+ this._create(this._selectedDs)}>Create + Cancel +
`; + } + + _clean(ds) { + if (confirm('This will drop all objects (tables, views, procedures, triggers, ...) in the configured schema. Do you want to continue?')) { + this.jsonRpc.clean({ds: ds.name}).then(jsonRpcResponse => { + this._showResultNotification(jsonRpcResponse.result); + }); + } + } + + _migrate(ds) { + this.jsonRpc.migrate({ds: ds.name}).then(jsonRpcResponse => { + this._showResultNotification(jsonRpcResponse.result); + }); + } + + _create(ds) { + this.jsonRpc.create({ds: ds.name}).then(jsonRpcResponse => { + this._showResultNotification(jsonRpcResponse.result); + this._selectedDs = null; + this._createDialogOpened = false; + this.hotReload(); + }); + } + + _cancelCreate(){ + this._selectedDs = null; + this._createDialogOpened = false; + } + + _showResultNotification(response){ + if(response.type === "success"){ + notifier.showInfoMessage(response.message + " (" + response.number + ")"); + }else{ + notifier.showWarningMessage(response.message); + } + } + +} +customElements.define('qwc-flyway-datasources', QwcFlywayDatasources); diff --git a/extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_1__Update.java b/extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_1__Update.java new file mode 100644 index 0000000000000..38855b7bd4554 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_1__Update.java @@ -0,0 +1,18 @@ +package db.migration; + +import java.sql.Statement; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +/** + * Migration class for some testcases. + */ +public class V1_0_1__Update extends BaseJavaMigration { + @Override + public void migrate(Context context) throws Exception { + try (Statement statement = context.getConnection().createStatement()) { + statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1001, 'test')"); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_2__Update.java b/extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_2__Update.java new file mode 100644 index 0000000000000..3181a849404d5 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/db/migration/V1_0_2__Update.java @@ -0,0 +1,39 @@ +package db.migration; + +import java.sql.Statement; + +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.migration.Context; +import org.flywaydb.core.api.migration.JavaMigration; + +/** + * Migration class for some testcases. + */ +public class V1_0_2__Update implements JavaMigration { + @Override + public MigrationVersion getVersion() { + return MigrationVersion.fromVersion("1.0.2"); + } + + @Override + public String getDescription() { + return getClass().getSimpleName(); + } + + @Override + public Integer getChecksum() { + return null; + } + + @Override + public boolean canExecuteInTransaction() { + return true; + } + + @Override + public void migrate(Context context) throws Exception { + try (Statement statement = context.getConnection().createStatement()) { + statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1002, 'test')"); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/DevModeTestEndpoint.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/DevModeTestEndpoint.java new file mode 100644 index 0000000000000..3c540864967bf --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/DevModeTestEndpoint.java @@ -0,0 +1,22 @@ +package io.quarkus.flyway.multitenant.test; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.flywaydb.core.Flyway; + +@Path("/fly") +public class DevModeTestEndpoint { + + @Inject + Instance flyway; + + @GET + public boolean present() { + flyway.get(); + return true; + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeCreateFromHibernateTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeCreateFromHibernateTest.java new file mode 100644 index 0000000000000..3e7c3407d17be --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeCreateFromHibernateTest.java @@ -0,0 +1,113 @@ +package io.quarkus.flyway.multitenant.test; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.transaction.UserTransaction; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.hamcrest.CoreMatchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.databind.JsonNode; + +import io.quarkus.devui.tests.DevUIJsonRPCTest; +import io.quarkus.runtime.Startup; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class FlywayDevModeCreateFromHibernateTest extends DevUIJsonRPCTest { + + public FlywayDevModeCreateFromHibernateTest() { + super("io.quarkus.quarkus-flyway"); + } + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(FlywayDevModeCreateFromHibernateTest.class, Endpoint.class, Fruit.class) + .addAsResource(new StringAsset( + "quarkus.flyway.locations=db/create"), "application.properties")); + + @Test + public void testGenerateMigrationFromHibernate() throws Exception { + RestAssured.get("fruit").then().statusCode(200) + .body("[0].name", CoreMatchers.is("Orange")); + + Map params = Map.of("ds", ""); + JsonNode devuiresponse = super.executeJsonRPCMethod("create", params); + + Assertions.assertNotNull(devuiresponse); + String type = devuiresponse.get("type").asText(); + Assertions.assertNotNull(type); + Assertions.assertEquals("success", type); + + config.modifySourceFile(Fruit.class, s -> s.replace("Fruit {", "Fruit {\n" + + " \n" + + " private String color;\n" + + "\n" + + " public String getColor() {\n" + + " return color;\n" + + " }\n" + + "\n" + + " public Fruit setColor(String color) {\n" + + " this.color = color;\n" + + " return this;\n" + + " }")); + //added a field, should now fail (if hibernate were still in charge this would work) + RestAssured.get("fruit").then().statusCode(500); + //now update out sql + config.modifyResourceFile("db/create/V1.0.0__quarkus-flyway-deployment.sql", new Function() { + @Override + public String apply(String s) { + return s + "\nalter table FRUIT add column color VARCHAR;"; + } + }); + // TODO: This still fails. + // RestAssured.get("fruit").then().statusCode(200) + // .body("[0].name", CoreMatchers.is("Orange")); + } + + @Path("/fruit") + @Startup + public static class Endpoint { + + @Inject + EntityManager entityManager; + + @Inject + UserTransaction tx; + + @GET + public List list() { + return entityManager.createQuery("from Fruit", Fruit.class).getResultList(); + } + + @PostConstruct + @Transactional + public void add() throws Exception { + tx.begin(); + try { + Fruit f = new Fruit(); + f.setName("Orange"); + entityManager.persist(f); + tx.commit(); + } catch (Exception e) { + tx.rollback(); + throw e; + } + } + + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeModifyMigrationTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeModifyMigrationTest.java new file mode 100644 index 0000000000000..df566757ff467 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeModifyMigrationTest.java @@ -0,0 +1,58 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.hamcrest.Matchers.is; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.function.Function; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class FlywayDevModeModifyMigrationTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(RowCountEndpoint.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); + + @Test + public void testModifyingExistingMigrationScriptCausesRestart() { + RestAssured.get("/row-count").then().statusCode(200).body(is("0")); + config.modifyResourceFile("db/migration/V1.0.0__Quarkus.sql", new Function() { + @Override + public String apply(String s) { + return s + '\n' + "INSERT INTO quarked_flyway VALUES (1001, 'test')"; + } + }); + RestAssured.get("/row-count").then().statusCode(200).body(is("1")); + } + + @Path("/row-count") + public static class RowCountEndpoint { + + @Inject + AgroalDataSource dataSource; + + @GET + public int rowCount() throws SQLException { + try (Connection connection = dataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet countQuery = stat.executeQuery("select count(1) from quarked_flyway")) { + return countQuery.first() ? countQuery.getInt(1) : 0; + } + } + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeTest.java new file mode 100644 index 0000000000000..e8a3540d38174 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayDevModeTest.java @@ -0,0 +1,43 @@ +package io.quarkus.flyway.multitenant.test; + +import java.util.function.Function; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +/** + * Flyway needs a datasource to work. + * This tests assures, that an error occurs, + * as soon as the default flyway configuration points to a missing default datasource. + */ +public class FlywayDevModeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(DevModeTestEndpoint.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("config-empty.properties", "application.properties")); + + @Test + @DisplayName("Injecting (default) flyway should fail if there is no datasource configured") + public void testAddingFlyway() { + RestAssured.get("fly").then().statusCode(500); + config.modifyResourceFile("application.properties", new Function() { + @Override + public String apply(String s) { + return "quarkus.datasource.db-kind=h2\n" + + "quarkus.datasource.username=sa\n" + + "quarkus.datasource.password=sa\n" + + "quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-dev-mode;DB_CLOSE_DELAY=-1\n" + + "quarkus.flyway.migrate-at-start=true"; + } + }); + RestAssured.get("/fly").then().statusCode(200); + + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartExistingSchemaHistoryTableTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartExistingSchemaHistoryTableTest.java new file mode 100644 index 0000000000000..40128c14eaaad --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartExistingSchemaHistoryTableTest.java @@ -0,0 +1,40 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionBaselineAtStartExistingSchemaHistoryTableTest { + @Inject + Flyway flyway; + + static final FlywayH2TestCustomizer customizer = FlywayH2TestCustomizer + .withDbName("quarkus-baseline-at-start-existing-schema-history") + .withPort(11309) + .withInitSqlFile("src/test/resources/h2-init-schema-history-table.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(customizer::startH2) + .setAfterAllCustomizer(customizer::stopH2) + .withApplicationRoot((jar) -> jar + .addClass(FlywayH2TestCustomizer.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("baseline-at-start-existing-schema-history-table-config.properties", + "application.properties")); + + @Test + @DisplayName("Baseline at start is not executed against existing schema-history-table") + public void testFlywayConfigInjection() { + MigrationInfo migrationInfo = flyway.info().current(); + assertNull(migrationInfo, "Flyway baseline was executed on existing schema history table"); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartTest.java new file mode 100644 index 0000000000000..c917e7ce8c02f --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineAtStartTest.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionBaselineAtStartTest { + @Inject + Flyway flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("baseline-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Baseline at start is executed against empty schema") + public void testFlywayConfigInjection() { + MigrationInfo migrationInfo = flyway.info().current(); + assertNotNull(migrationInfo, "No Flyway migration was executed"); + assertTrue(migrationInfo.getType().isBaseline(), "Flyway migration is not a baseline"); + String currentVersion = migrationInfo + .getVersion() + .toString(); + + assertEquals("1.0.1", currentVersion); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java new file mode 100644 index 0000000000000..ccf8b2999e6c9 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourceTest.java @@ -0,0 +1,43 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionBaselineOnMigrateNamedDataSourceTest { + + @Inject + @FlywayDataSource("users") + Flyway flyway; + + static final FlywayH2TestCustomizer customizer = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-baseline-on-named-ds") + .withPort(11302) + .withInitSqlFile("src/test/resources/h2-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(customizer::startH2) + .setAfterAllCustomizer(customizer::stopH2) + .withApplicationRoot((jar) -> jar + .addClass(FlywayH2TestCustomizer.class) + .addAsResource("baseline-on-migrate-named-datasource.properties", "application.properties")); + + @Test + @DisplayName("Create history table correctly") + public void testFlywayInitialBaselineInfo() { + MigrationInfo baselineInfo = flyway.info().applied()[0]; + + assertEquals("0.0.1", baselineInfo.getVersion().getVersion()); + assertEquals("Initial description for test", baselineInfo.getDescription()); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java new file mode 100644 index 0000000000000..d7212a5fa6648 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java @@ -0,0 +1,70 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest { + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("laptops") + Flyway flywayLaptops; + + static final FlywayH2TestCustomizer customizerUsers = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-baseline-on-named-ds-users") + .withPort(11302) + .withInitSqlFile("src/test/resources/h2-init-data.sql"); + + static final FlywayH2TestCustomizer customizerLaptops = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-baseline-on-named-ds-laptops") + .withPort(11303) + .withInitSqlFile("src/test/resources/h2-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(new Runnable() { + @Override + public void run() { + customizerUsers.startH2(); + customizerLaptops.startH2(); + } + }) + .setAfterAllCustomizer(new Runnable() { + @Override + public void run() { + customizerUsers.stopH2(); + customizerLaptops.stopH2(); + } + }) + .withApplicationRoot((jar) -> jar + .addClass(FlywayH2TestCustomizer.class) + .addAsResource("baseline-on-migrate-named-datasources-inactive.properties", "application.properties")); + + @Test + @DisplayName("Create history table correctly") + public void testFlywayInitialBaselineInfo() { + MigrationInfo baselineInfo = flywayUsers.info().applied()[0]; + + assertEquals("0.0.1", baselineInfo.getVersion().getVersion()); + assertEquals("Initial description for test", baselineInfo.getDescription()); + } + + @Test + @DisplayName("History table not created if inactive") + public void testFlywayInitialBaselineInfoInactive() { + assertEquals(0, flywayLaptops.info().applied().length); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateTest.java new file mode 100644 index 0000000000000..ef7ab233c6b78 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionBaselineOnMigrateTest.java @@ -0,0 +1,41 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionBaselineOnMigrateTest { + + @Inject + Flyway flyway; + + static final FlywayH2TestCustomizer customizer = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-baseline-on-migrate") + .withPort(11301) + .withInitSqlFile("src/test/resources/h2-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(customizer::startH2) + .setAfterAllCustomizer(customizer::stopH2) + .withApplicationRoot((jar) -> jar + .addClass(FlywayH2TestCustomizer.class) + .addAsResource("baseline-on-migrate.properties", "application.properties")); + + @Test + @DisplayName("Create history table correctly") + public void testFlywayInitialBaselineInfo() { + MigrationInfo baselineInfo = flyway.info().applied()[0]; + + assertEquals("0.0.1", baselineInfo.getVersion().getVersion()); + assertEquals("Initial description for test", baselineInfo.getDescription()); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCDICallback.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCDICallback.java new file mode 100644 index 0000000000000..8f551fbd4cdb5 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCDICallback.java @@ -0,0 +1,48 @@ +package io.quarkus.flyway.multitenant.test; + +import java.util.Arrays; +import java.util.List; + +import jakarta.enterprise.inject.spi.CDI; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; + +public class FlywayExtensionCDICallback implements Callback { + + public static List DEFAULT_EVENTS = Arrays.asList( + Event.BEFORE_BASELINE, + Event.AFTER_BASELINE, + Event.BEFORE_MIGRATE, + Event.BEFORE_EACH_MIGRATE, + Event.AFTER_EACH_MIGRATE, + Event.AFTER_VERSIONED, + Event.AFTER_MIGRATE, + Event.AFTER_MIGRATE_OPERATION_FINISH); + + @Override + public boolean supports(Event event, Context context) { + return DEFAULT_EVENTS.contains(event); + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return true; + } + + @Override + public void handle(Event event, Context context) { + try { + CDI.current().select(Flyway.class).get(); + } catch (Exception exception) { + throw new IllegalStateException(exception); + } + } + + @Override + public String getCallbackName() { + return "Quarked Flyway Callback with CDI"; + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback.java new file mode 100644 index 0000000000000..7dcc7228063af --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback.java @@ -0,0 +1,47 @@ +package io.quarkus.flyway.multitenant.test; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; + +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; + +public class FlywayExtensionCallback implements Callback { + + public static List DEFAULT_EVENTS = Arrays.asList( + Event.BEFORE_BASELINE, + Event.AFTER_BASELINE, + Event.BEFORE_MIGRATE, + Event.BEFORE_EACH_MIGRATE, + Event.AFTER_EACH_MIGRATE, + Event.AFTER_VERSIONED, + Event.AFTER_MIGRATE, + Event.AFTER_MIGRATE_OPERATION_FINISH); + + @Override + public boolean supports(Event event, Context context) { + return DEFAULT_EVENTS.contains(event); + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return true; + } + + @Override + public void handle(Event event, Context context) { + try (Statement stmt = context.getConnection().createStatement()) { + stmt.executeUpdate("INSERT INTO quarked_callback(name) VALUES('" + event.getId() + "')"); + } catch (SQLException exception) { + throw new IllegalStateException(exception); + } + } + + @Override + public String getCallbackName() { + return "Quarked Flyway Callback"; + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback2.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback2.java new file mode 100644 index 0000000000000..26875ac9eb24c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallback2.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.multitenant.test; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; + +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; + +public class FlywayExtensionCallback2 implements Callback { + + public static List DEFAULT_EVENTS = Arrays.asList(Event.AFTER_MIGRATE); + + @Override + public boolean supports(Event event, Context context) { + return DEFAULT_EVENTS.contains(event); + } + + @Override + public boolean canHandleInTransaction(Event event, Context context) { + return true; + } + + @Override + public void handle(Event event, Context context) { + try (Statement stmt = context.getConnection().createStatement()) { + stmt.executeUpdate("INSERT INTO quarked_callback(name) VALUES('" + event.getId() + "')"); + } catch (SQLException exception) { + throw new IllegalStateException(exception); + } + } + + @Override + public String getCallbackName() { + return "Quarked Flyway Callback 2"; + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallbackTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallbackTest.java new file mode 100644 index 0000000000000..f63702e457fb1 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCallbackTest.java @@ -0,0 +1,66 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionCallbackTest { + + // Quarkus built object + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + static final FlywayH2TestCustomizer customizer = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-callback") + .withPort(11303) + .withInitSqlFile("src/test/resources/callback-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(customizer::startH2) + .setAfterAllCustomizer(customizer::stopH2) + .withApplicationRoot((jar) -> jar + .addClasses(FlywayH2TestCustomizer.class, + FlywayExtensionCallback.class, FlywayExtensionCallback2.class, FlywayExtensionCDICallback.class) + .addAsResource("db/migration/V1.0.3__Quarkus_Callback.sql") + .addAsResource("callback-config.properties", "application.properties")); + + @Test + @DisplayName("Migrates at start correctly and executes callback") + public void testFlywayCallback() throws SQLException { + MigrationInfo migrationInfo = flyway.info().current(); + assertNotNull(migrationInfo, "No Flyway migration was executed"); + + String currentVersion = migrationInfo.getVersion().toString(); + // Expected to be 1.0.3 as migration runs at start + assertEquals("1.0.3", currentVersion); + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select COUNT(name) from quarked_callback")) { + assertTrue(executeQuery.next(), "Table exists but it is empty"); + int count = executeQuery.getInt(1); + // Expect one row for each callback invoked by Flyway + int expected = FlywayExtensionCallback.DEFAULT_EVENTS.size() + FlywayExtensionCallback2.DEFAULT_EVENTS.size(); + assertEquals(expected, count); + } + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartTest.java new file mode 100644 index 0000000000000..708999cecb83a --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartTest.java @@ -0,0 +1,50 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.h2.jdbc.JdbcSQLSyntaxErrorException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionCleanAndMigrateAtStartTest { + + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Clean and migrate at start correctly") + public void testFlywayConfigInjection() throws SQLException { + + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select * from fake_existing_tbl")) { + fail("fake_existing_tbl should not exist"); + } catch (JdbcSQLSyntaxErrorException e) { + // expected fake_existing_tbl does not exist + } + } + String currentVersion = flyway.info().current().getVersion().toString(); + assertEquals("1.0.0", currentVersion, "Expected to be 1.0.0 as migration runs at start"); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java new file mode 100644 index 0000000000000..fb9a24c3e55ec --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java @@ -0,0 +1,86 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.migration.Context; +import org.flywaydb.core.api.migration.JavaMigration; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import db.migration.V1_0_1__Update; +import db.migration.V1_0_2__Update; +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest { + + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(V1_0_1__Update.class, V1_0_2__Update.class, V9_9_9__Update.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Clean and migrate at start correctly") + public void testFlywayConfigInjection() throws SQLException { + + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet countQuery = stat.executeQuery("select count(1) from quarked_flyway")) { + assertTrue(countQuery.first()); + assertEquals(2, + countQuery.getInt(1), + "Table 'quarked_flyway' does not contain the expected number of rows"); + } + } + String currentVersion = flyway.info().current().getVersion().toString(); + assertEquals("1.0.2", currentVersion, "Expected to be 1.0.2 as there is a SQL and two Java migration scripts"); + } + + public static class V9_9_9__Update implements JavaMigration { + @Override + public MigrationVersion getVersion() { + return MigrationVersion.fromVersion("9.9.9"); + } + + @Override + public String getDescription() { + return getClass().getSimpleName(); + } + + @Override + public Integer getChecksum() { + return null; + } + + @Override + public boolean canExecuteInTransaction() { + return true; + } + + @Override + public void migrate(Context context) throws Exception { + try (Statement statement = context.getConnection().createStatement()) { + statement.executeUpdate("INSERT INTO quarked_flyway VALUES (9999, 'should-not-be-added')"); + } + } + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAtStartTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAtStartTest.java new file mode 100644 index 0000000000000..92e7b03ff6f1e --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionCleanAtStartTest.java @@ -0,0 +1,51 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.h2.jdbc.JdbcSQLSyntaxErrorException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionCleanAtStartTest { + + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Clean at start correctly") + public void testFlywayConfigInjection() throws SQLException { + + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select * from fake_existing_tbl")) { + fail("fake_existing_tbl should not exist"); + } catch (JdbcSQLSyntaxErrorException e) { + // expected fake_existing_tbl does not exist + } + } + MigrationInfo current = flyway.info().current(); + assertNull(current, "Info is not null"); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceDynamicInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceDynamicInjectionTest.java new file mode 100644 index 0000000000000..3e0267dceba3c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceDynamicInjectionTest.java @@ -0,0 +1,38 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseDefaultDatasourceDynamicInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + Instance flyway; + + @Test + @DisplayName("If the default datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceStaticInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceStaticInjectionTest.java new file mode 100644 index 0000000000000..d13f47384f9ec --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseDefaultDatasourceStaticInjectionTest.java @@ -0,0 +1,46 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseDefaultDatasourceStaticInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + @DisplayName("If the default datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(myBean::useFlyway) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Flyway flyway; + + public void useFlyway() { + flyway.getConfiguration(); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceDynamicInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceDynamicInjectionTest.java new file mode 100644 index 0000000000000..ded80952c3b4a --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceDynamicInjectionTest.java @@ -0,0 +1,49 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseNamedDataSourceDynamicInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flyway; + + @Test + @DisplayName("If a named datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceStaticInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceStaticInjectionTest.java new file mode 100644 index 0000000000000..dfdbe367d4ffc --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigActiveFalseNamedDataSourceStaticInjectionTest.java @@ -0,0 +1,57 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseNamedDataSourceStaticInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + MyBean myBean; + + @Test + @DisplayName("If a named datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(myBean::useFlyway) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @FlywayDataSource("users") + Flyway flyway; + + public void useFlyway() { + flyway.getConfiguration(); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceTest.java new file mode 100644 index 0000000000000..77450ba535768 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceTest.java @@ -0,0 +1,35 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigDefaultDataSourceTest { + + @Inject + Flyway flyway; + + @Inject + FlywayExtensionConfigFixture fixture; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(FlywayExtensionConfigFixture.class) + .addClasses(FlywayExtensionCallback.class, FlywayExtensionCallback2.class) + .addAsResource("config-for-default-datasource.properties", "application.properties")); + + @Test + @DisplayName("Reads flyway configuration for default datasource correctly") + public void testFlywayConfigInjection() { + fixture.assertAllConfigurationSettings(flyway.getConfiguration(), ""); + assertFalse(fixture.migrateAtStart("")); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java new file mode 100644 index 0000000000000..2230061e4eb37 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest.java @@ -0,0 +1,38 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +/** + * Assures, that Flyway can also be used without any configuration, + * provided, that at least a datasource is configured. + */ +public class FlywayExtensionConfigDefaultDataSourceWithoutFlywayTest { + + @Inject + Flyway flyway; + + @Inject + FlywayExtensionConfigFixture fixture; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-default-datasource-without-flyway.properties", "application.properties")); + + @Test + @DisplayName("Reads predefined default flyway configuration for default datasource correctly") + public void testFlywayDefaultConfigInjection() { + fixture.assertDefaultConfigurationSettings(flyway.getConfiguration()); + assertFalse(fixture.migrateAtStart("")); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigFixture.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigFixture.java new file mode 100644 index 0000000000000..6f2bfea492925 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigFixture.java @@ -0,0 +1,186 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.configuration.FluentConfiguration; + +/** + * This fixture provides access to read the expected and the actual configuration of flyway. + * It also provides a method combining all assertions to be reused for multiple tests. + */ +@ApplicationScoped +public class FlywayExtensionConfigFixture { + + @Inject + Config config; + + public void assertAllConfigurationSettings(Configuration configuration, String dataSourceName) { + assertEquals(locations(configuration), locations(dataSourceName)); + assertEquals(sqlMigrationPrefix(configuration), sqlMigrationPrefix(dataSourceName)); + assertEquals(repeatableSqlMigrationPrefix(configuration), repeatableSqlMigrationPrefix(dataSourceName)); + assertEquals(tableName(configuration), tableName(dataSourceName)); + assertEquals(schemaNames(configuration), schemaNames(dataSourceName)); + + assertEquals(connectRetries(configuration), connectRetries(dataSourceName)); + + assertEquals(baselineOnMigrate(configuration), baselineOnMigrate(dataSourceName)); + assertEquals(baselineVersion(configuration), baselineVersion(dataSourceName)); + assertEquals(baselineDescription(configuration), baselineDescription(dataSourceName)); + assertEquals(callbacks(configuration), callbacks(dataSourceName)); + } + + public void assertDefaultConfigurationSettings(Configuration configuration) { + FluentConfiguration defaultConfiguration = Flyway.configure(); + assertEquals(locations(configuration), locations(defaultConfiguration)); + assertEquals(sqlMigrationPrefix(configuration), sqlMigrationPrefix(defaultConfiguration)); + assertEquals(repeatableSqlMigrationPrefix(configuration), repeatableSqlMigrationPrefix(defaultConfiguration)); + assertEquals(tableName(configuration), tableName(defaultConfiguration)); + assertEquals(schemaNames(configuration), schemaNames(defaultConfiguration)); + + assertEquals(connectRetries(configuration), connectRetries(defaultConfiguration)); + + assertEquals(baselineOnMigrate(configuration), baselineOnMigrate(defaultConfiguration)); + assertEquals(baselineVersion(configuration), baselineVersion(defaultConfiguration)); + assertEquals(baselineDescription(configuration), baselineDescription(defaultConfiguration)); + assertEquals(callbacks(configuration), callbacks(defaultConfiguration)); + } + + public int callbacks(Configuration configuration) { + return configuration.getCallbacks().length; + } + + public int callbacks(String datasourceName) { + return getStringValue("quarkus.flyway.%s.callbacks", datasourceName).split(",").length; + } + + public int connectRetries(String datasourceName) { + return getIntValue("quarkus.flyway.%s.connect-retries", datasourceName); + } + + public int connectRetries(Configuration configuration) { + return configuration.getConnectRetries(); + } + + public String schemaNames(String datasourceName) { + return getStringValue("quarkus.flyway.%s.schemas", datasourceName); + } + + public String schemaNames(Configuration configuration) { + return Arrays.stream(configuration.getSchemas()).collect(Collectors.joining(",")); + } + + public String tableName(String datasourceName) { + return getStringValue("quarkus.flyway.%s.table", datasourceName); + } + + public String tableName(Configuration configuration) { + return configuration.getTable(); + } + + public String locations(String datasourceName) { + return getStringValue("quarkus.flyway.%s.locations", datasourceName); + } + + public String locations(Configuration configuration) { + return Arrays.stream(configuration.getLocations()).map(Location::getPath).collect(Collectors.joining(",")); + } + + public String sqlMigrationPrefix(String datasourceName) { + return getStringValue("quarkus.flyway.%s.sql-migration-prefix", datasourceName); + } + + public String sqlMigrationPrefix(Configuration configuration) { + return configuration.getSqlMigrationPrefix(); + } + + public String repeatableSqlMigrationPrefix(String datasourceName) { + return getStringValue("quarkus.flyway.%s.repeatable-sql-migration-prefix", datasourceName); + } + + public String repeatableSqlMigrationPrefix(Configuration configuration) { + return configuration.getRepeatableSqlMigrationPrefix(); + } + + public boolean baselineOnMigrate(String datasourceName) { + return getBooleanValue("quarkus.flyway.%s.baseline-on-migrate", datasourceName); + } + + public boolean baselineOnMigrate(Configuration configuration) { + return configuration.isBaselineOnMigrate(); + } + + public String baselineVersion(String datasourceName) { + return getStringValue("quarkus.flyway.%s.baseline-version", datasourceName); + } + + public String baselineVersion(Configuration configuration) { + return configuration.getBaselineVersion().getVersion(); + } + + public String baselineDescription(String datasourceName) { + return getStringValue("quarkus.flyway.%s.baseline-description", datasourceName); + } + + public String baselineDescription(Configuration configuration) { + return configuration.getBaselineDescription(); + } + + public boolean migrateAtStart(String datasourceName) { + return getBooleanValue("quarkus.flyway.migrate-at-start", datasourceName); + } + + public String username(String datasourceName) { + return getStringValue("quarkus.flyway.%s.username", datasourceName); + } + + public String password(String datasourceName) { + return getStringValue("quarkus.flyway.%s.password", datasourceName); + } + + public String jdbcUrl(String datasourceName) { + return getStringValue("quarkus.flyway.%s.jdbc-url", datasourceName); + } + + private String getStringValue(String parameterName, String datasourceName) { + return getValue(parameterName, datasourceName, String.class); + } + + private int getIntValue(String parameterName, String datasourceName) { + return getValue(parameterName, datasourceName, Integer.class); + } + + private boolean getBooleanValue(String parameterName, String datasourceName) { + return getValue(parameterName, datasourceName, Boolean.class); + } + + private T getValue(String parameterName, String datasourceName, Class type) { + return getValue(parameterName, datasourceName, type, this::log); + } + + private T getValue(String parameterName, String datasourceName, Class type, Consumer logger) { + String propertyName = fillin(parameterName, datasourceName); + T propertyValue = config.getValue(propertyName, type); + logger.accept("Config property " + propertyName + " = " + propertyValue); + return propertyValue; + } + + private void log(String content) { + //activate for debugging + // System.out.println(content); + } + + private String fillin(String propertyName, String datasourceName) { + return String.format(propertyName, datasourceName).replace("..", "."); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesTest.java new file mode 100644 index 0000000000000..29cb982575b25 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesTest.java @@ -0,0 +1,72 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Test a full configuration with default and two named datasources plus their flyway settings. + */ +public class FlywayExtensionConfigMultiDataSourcesTest { + + @Inject + FlywayExtensionConfigFixture fixture; + + @Inject + Flyway flyway; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("inventory") + Flyway flywayInventory; + + @Inject + @Named("flyway_inventory") + Flyway flywayNamedInventory; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(FlywayExtensionConfigFixture.class, FlywayExtensionCallback.class) + .addAsResource("config-for-multiple-datasources.properties", "application.properties")); + + @Test + @DisplayName("Reads default flyway configuration for default datasource correctly") + public void testFlywayDefaultConfigInjection() { + fixture.assertAllConfigurationSettings(flyway.getConfiguration(), ""); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration for datasource named 'users' correctly") + public void testFlywayConfigNamedUsersInjection() { + fixture.assertAllConfigurationSettings(flywayUsers.getConfiguration(), "users"); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration for datasource named 'inventory' correctly") + public void testFlywayConfigNamedInventoryInjection() { + fixture.assertAllConfigurationSettings(flywayInventory.getConfiguration(), "inventory"); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration directly named 'inventory_flyway' correctly") + public void testFlywayConfigDirectlyNamedInventoryInjection() { + fixture.assertAllConfigurationSettings(flywayNamedInventory.getConfiguration(), "inventory"); + assertFalse(fixture.migrateAtStart("")); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java new file mode 100644 index 0000000000000..e28d251bd2069 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest.java @@ -0,0 +1,50 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Test a full configuration with default and two named datasources plus their flyway settings. + */ +public class FlywayExtensionConfigMultiDataSourcesWithoutDefaultTest { + + @Inject + FlywayExtensionConfigFixture fixture; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("inventory") + Flyway flywayInventory; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(FlywayExtensionConfigFixture.class, FlywayExtensionCallback.class) + .addAsResource("config-for-multiple-datasources-without-default.properties", "application.properties")); + + @Test + @DisplayName("Reads flyway configuration for datasource named 'users' without default datasource correctly") + public void testFlywayConfigNamedUsersInjection() { + fixture.assertAllConfigurationSettings(flywayUsers.getConfiguration(), "users"); + assertFalse(fixture.migrateAtStart("")); + } + + @Test + @DisplayName("Reads flyway configuration for datasource named 'inventory' without default datasource correctly") + public void testFlywayConfigNamedInventoryInjection() { + fixture.assertAllConfigurationSettings(flywayInventory.getConfiguration(), "inventory"); + assertFalse(fixture.migrateAtStart("")); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java new file mode 100644 index 0000000000000..c6a9f7e8986dc --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutDefaultTest.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Test a full configuration with default and two named datasources plus their flyway settings. + */ +public class FlywayExtensionConfigNamedDataSourceWithoutDefaultTest { + + @Inject + FlywayExtensionConfigFixture fixture; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(FlywayExtensionConfigFixture.class, FlywayExtensionCallback.class) + .addAsResource("config-for-named-datasource-without-default.properties", "application.properties")); + + @Test + @DisplayName("Reads flyway configuration for datasource named 'users' without default datasource correctly") + public void testFlywayConfigNamedUsersInjection() { + fixture.assertAllConfigurationSettings(flywayUsers.getConfiguration(), "users"); + assertFalse(fixture.migrateAtStart("")); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java new file mode 100644 index 0000000000000..f217cf784ddea --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigNamedDataSourceWithoutFlywayTest.java @@ -0,0 +1,40 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Assures, that flyway can also be used without any configuration, + * provided, that at least a named datasource is configured. + */ +public class FlywayExtensionConfigNamedDataSourceWithoutFlywayTest { + + @Inject + @FlywayDataSource("users") + Flyway flyway; + + @Inject + FlywayExtensionConfigFixture fixture; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(FlywayExtensionConfigFixture.class) + .addAsResource("config-for-named-datasource-without-flyway.properties", "application.properties")); + + @Test + @DisplayName("Reads predefined default flyway configuration for named datasource correctly") + public void testFlywayDefaultConfigInjection() { + fixture.assertDefaultConfigurationSettings(flyway.getConfiguration()); + assertFalse(fixture.migrateAtStart("users")); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceDynamicInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceDynamicInjectionTest.java new file mode 100644 index 0000000000000..13a93b830e35a --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceDynamicInjectionTest.java @@ -0,0 +1,38 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigUrlMissingDefaultDatasourceDynamicInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + // The URL won't be missing if dev services are enabled + .overrideConfigKey("quarkus.devservices.enabled", "false"); + + @Inject + Instance flyway; + + @Test + @DisplayName("If the URL is missing for the default datasource, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' is not configured.", + "To solve this, configure datasource ''.", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceStaticInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceStaticInjectionTest.java new file mode 100644 index 0000000000000..7021d4e8c08f7 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingDefaultDatasourceStaticInjectionTest.java @@ -0,0 +1,45 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigUrlMissingDefaultDatasourceStaticInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + // The URL won't be missing if dev services are enabled + .overrideConfigKey("quarkus.devservices.enabled", "false"); + + @Inject + MyBean myBean; + + @Test + @DisplayName("If the URL is missing for the default datasource, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(() -> myBean.useFlyway()) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' is not configured.", + "To solve this, configure datasource ''.", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Flyway flyway; + + public void useFlyway() { + flyway.getConfiguration(); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceDynamicInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceDynamicInjectionTest.java new file mode 100644 index 0000000000000..78a9f553c6205 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceDynamicInjectionTest.java @@ -0,0 +1,48 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigUrlMissingNamedDataSourceDynamicInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + // The URL won't be missing if dev services are enabled + .overrideConfigKey("quarkus.devservices.enabled", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flyway; + + @Test + @DisplayName("If the URL is missing for a named datasource, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' is not configured.", + "To solve this, configure datasource 'users'.", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceStaticInjectionTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceStaticInjectionTest.java new file mode 100644 index 0000000000000..d9d30ee20b58e --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionConfigUrlMissingNamedDataSourceStaticInjectionTest.java @@ -0,0 +1,56 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigUrlMissingNamedDataSourceStaticInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + // The URL won't be missing if dev services are enabled + .overrideConfigKey("quarkus.devservices.enabled", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + MyBean myBean; + + @Test + @DisplayName("If the URL is missing for a named datasource, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(() -> myBean.useFlyway()) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' is not configured.", + "To solve this, configure datasource 'users'.", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @FlywayDataSource("users") + Flyway flyway; + + public void useFlyway() { + flyway.getConfiguration(); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionDisabledTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionDisabledTest.java new file mode 100644 index 0000000000000..926eea83f5f8c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionDisabledTest.java @@ -0,0 +1,31 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionDisabledTest { + + @Inject + Instance flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("disabled-config.properties", "application.properties")); + + @Test + @DisplayName("No Flyway instance available if disabled") + public void testFlywayConfigInjection() { + assertTrue(flyway.isUnsatisfied()); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionFilesystemResourceTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionFilesystemResourceTest.java new file mode 100644 index 0000000000000..3f47ba56287f8 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionFilesystemResourceTest.java @@ -0,0 +1,60 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.h2.jdbc.JdbcSQLSyntaxErrorException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import db.migration.V1_0_1__Update; +import db.migration.V1_0_2__Update; +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionFilesystemResourceTest { + + @Inject + Flyway flyway; + + @Inject + AgroalDataSource defaultDataSource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(V1_0_1__Update.class, V1_0_2__Update.class) + .addAsResource("clean-and-migrate-at-start-with-fs-resource-config.properties", "application.properties")); + + @Test + @DisplayName("Clean and migrate at start correctly") + public void testFlywayConfigInjection() throws SQLException { + + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select * from fake_existing_tbl")) { + fail("fake_existing_tbl should not exist. Clean was run at start"); + } catch (JdbcSQLSyntaxErrorException e) { + // expected fake_existing_tbl does not exist + } + try (ResultSet countQuery = stat.executeQuery("select count(1) from quarked_flyway")) { + assertTrue(countQuery.first()); + assertEquals(2, + countQuery.getInt(1), + "Table 'quarked_flyway' does not contain the expected number of rows"); + } + } + String currentVersion = flyway.info().current().getVersion().toString(); + assertEquals("1.0.3", currentVersion, "Expected to be 1.0.3 as there is a SQL and two Java migration scripts"); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionInitSqlTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionInitSqlTest.java new file mode 100644 index 0000000000000..164058dad5656 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionInitSqlTest.java @@ -0,0 +1,44 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionInitSqlTest { + // Quarkus built object + @Inject + DataSource datasource; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("init-sql-config.properties", "application.properties")); + + @Test + @DisplayName("Check if initSql is invoked") + public void testFlywayInitSql() throws SQLException { + int var = 0; + try (Connection con = datasource.getConnection(); + PreparedStatement ps = con.prepareStatement("SELECT ONE_HUNDRED"); + ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + var = rs.getInt(1); + } + } + assertEquals(100, var, "Init SQL was not executed"); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..a17daf98aca41 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java @@ -0,0 +1,42 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.flyway.migrate-at-start", "true"); + + @Inject + Instance flyway; + + @Test + @DisplayName("If the default datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceUrlMissingTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceUrlMissingTest.java new file mode 100644 index 0000000000000..d926452bce0e0 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartDefaultDatasourceUrlMissingTest.java @@ -0,0 +1,41 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartDefaultDatasourceUrlMissingTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.flyway.migrate-at-start", "true") + // The URL won't be missing if dev services are enabled + .overrideConfigKey("quarkus.devservices.enabled", "false"); + + @Inject + Instance flyway; + + @Test + @DisplayName("If there is no config for the default datasource, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' is not configured.", + "To solve this, configure datasource ''.", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java new file mode 100644 index 0000000000000..d61c8ffd005c6 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java @@ -0,0 +1,44 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Same as {@link FlywayExtensionMigrateAtStartTest} for named datasources. + */ +public class FlywayExtensionMigrateAtStartNamedDataSourceTest { + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("migrate-at-start-config-named-datasource.properties", "application.properties")); + + @Test + @DisplayName("Migrates at start for datasource named 'users' correctly") + public void testFlywayConfigInjection() { + MigrationInfo migrationInfo = flywayUsers.info().current(); + assertNotNull(migrationInfo, "No Flyway migration was executed"); + + String currentVersion = migrationInfo + .getVersion() + .toString(); + // Expected to be 1.0.0 as migration runs at start + assertEquals("1.0.0", currentVersion); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..2f2724e7795dc --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java @@ -0,0 +1,52 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.datasource.users.active", "false") + .overrideConfigKey("quarkus.flyway.users.migrate-at-start", "true") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flyway; + + @Test + @DisplayName("If a named datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigUrlMissingTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigUrlMissingTest.java new file mode 100644 index 0000000000000..ecd58cb725583 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigUrlMissingTest.java @@ -0,0 +1,51 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartNamedDatasourceConfigUrlMissingTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.flyway.users.migrate-at-start", "true") + // The URL won't be missing if dev services are enabled + .overrideConfigKey("quarkus.devservices.enabled", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flyway; + + @Test + @DisplayName("If there is no config for a named datasource, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flyway::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' is not configured.", + "To solve this, configure datasource 'users'.", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartSubfolderTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartSubfolderTest.java new file mode 100644 index 0000000000000..7877dabb10415 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartSubfolderTest.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartSubfolderTest { + // Quarkus built object + @Inject + Flyway flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration-subfolder/subfolder/V1.0.0__Quarkus.sql") + .addAsResource("migrate-at-start-subfolder-config.properties", "application.properties")); + + @Test + @DisplayName("Migrates at start correctly") + public void testFlywayConfigInjection() { + MigrationInfo migrationInfo = flyway.info().current(); + assertNotNull(migrationInfo, "No Flyway migration was executed"); + + String currentVersion = migrationInfo + .getVersion() + .toString(); + // Expected to be 1.0.0 as migration runs at start + assertEquals("1.0.0", currentVersion); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartTest.java new file mode 100644 index 0000000000000..7458667c4a102 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionMigrateAtStartTest.java @@ -0,0 +1,39 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartTest { + // Quarkus built object + @Inject + Flyway flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("migrate-at-start-config.properties", "application.properties")); + + @Test + @DisplayName("Migrates at start correctly") + public void testFlywayConfigInjection() { + MigrationInfo migrationInfo = flyway.info().current(); + assertNotNull(migrationInfo, "No Flyway migration was executed"); + + String currentVersion = migrationInfo + .getVersion() + .toString(); + + assertEquals("1.0.0", currentVersion); + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionRepairAtStartTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionRepairAtStartTest.java new file mode 100644 index 0000000000000..f0f3e76517859 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionRepairAtStartTest.java @@ -0,0 +1,81 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.flywaydb.core.Flyway; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class FlywayExtensionRepairAtStartTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(FlywayResource.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("repair-at-start-config.properties", "application.properties")) + .setLogRecordPredicate(r -> true) + .setAllowFailedStart(true); + + @Test + @DisplayName("Repair at start works correctly") + public void testRepairUsingDevMode() { + assertThat(RestAssured.get("/flyway/current-version").then().statusCode(200).extract().asString()).isEqualTo("1.0.0"); + + config.clearLogRecords(); + config.modifyResourceFile("db/migration/V1.0.0__Quarkus.sql", s -> s + "\nNONSENSE STATEMENT CHANGING CHECKSUM;"); + config.modifyResourceFile("application.properties", s -> s + "\nquarkus.flyway.validate-on-migrate=true"); + + // trigger application restart + RestAssured.get("/"); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(config.getLogRecords()).anySatisfy(r -> { + assertThat(r.getMessage()).contains("Failed to start application"); + assertThat(ExceptionUtil.getRootCause(r.getThrown()).getMessage()) + .contains("Migration checksum mismatch for migration version 1.0.0"); + }); + RestAssured.get("/flyway/current-version").then().statusCode(500); + }); + + config.clearLogRecords(); + config.modifyResourceFile("application.properties", s -> s + "\nquarkus.flyway.repair-at-start=true"); + + // trigger application restart + RestAssured.get("/"); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(config.getLogRecords()).anySatisfy( + r -> assertThat(r.getMessage()).contains("Successfully repaired schema history table")); + assertThat(RestAssured.get("/flyway/current-version").then().statusCode(200).extract().asString()) + .isEqualTo("1.0.0"); + }); + } + + @Path("flyway") + public static class FlywayResource { + @Inject + Flyway flyway; + + @Path("current-version") + @GET + public String currentVersion() { + return flyway.info().current().getVersion().toString(); + } + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionValidateAtStartTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionValidateAtStartTest.java new file mode 100644 index 0000000000000..bd1b3df360cf0 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionValidateAtStartTest.java @@ -0,0 +1,23 @@ +package io.quarkus.flyway.multitenant.test; + +import org.flywaydb.core.api.exception.FlywayValidateException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionValidateAtStartTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("validate-at-start-config.properties", "application.properties")) + .setExpectedException(FlywayValidateException.class); + + @Test + public void shouldNeverBeCalled() { + + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithCustomizerTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithCustomizerTest.java new file mode 100644 index 0000000000000..8b13adf3a99f2 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithCustomizerTest.java @@ -0,0 +1,66 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.flyway.FlywayConfigurationCustomizer; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionWithCustomizerTest { + + @Inject + AgroalDataSource defaultDataSource; + + static final FlywayH2TestCustomizer customizer = FlywayH2TestCustomizer + .withDbName("quarkus-customizer") + .withPort(11303) + .withInitSqlFile("src/test/resources/callback-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(customizer::startH2) + .setAfterAllCustomizer(customizer::stopH2) + .withApplicationRoot((jar) -> jar + .addClasses(FlywayH2TestCustomizer.class, + AddCallbacksCustomizer.class, + FlywayExtensionCallback.class, FlywayExtensionCallback2.class, FlywayExtensionCDICallback.class) + .addAsResource("db/migration/V1.0.3__Quarkus_Callback.sql") + .addAsResource("config-for-default-datasource-with-customizer-config.properties", + "application.properties")); + + @Test + public void testCustomizer() throws SQLException { + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select COUNT(name) from quarked_callback")) { + assertTrue(executeQuery.next(), "Table exists but it is empty"); + int count = executeQuery.getInt(1); + // Expect one row for each callback invoked by Flyway + int expected = FlywayExtensionCallback.DEFAULT_EVENTS.size() + FlywayExtensionCallback2.DEFAULT_EVENTS.size(); + assertEquals(expected, count); + } + } + } + + @Singleton + public static class AddCallbacksCustomizer implements FlywayConfigurationCustomizer { + + @Override + public void customize(FluentConfiguration configuration) { + configuration.callbacks(new FlywayExtensionCallback(), new FlywayExtensionCallback2(), + new FlywayExtensionCDICallback()); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTest.java new file mode 100644 index 0000000000000..3b6d92127bb42 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTest.java @@ -0,0 +1,40 @@ +package io.quarkus.flyway.multitenant.test; + +import static io.restassured.RestAssured.get; +import static org.hamcrest.Matchers.is; + +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import db.migration.V1_0_1__Update; +import db.migration.V1_0_2__Update; +import io.quarkus.test.QuarkusDevModeTest; + +public class FlywayExtensionWithJavaMigrationDevModeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(V1_0_1__Update.class, V1_0_2__Update.class, + FlywayExtensionWithJavaMigrationDevModeTestEndpoint.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); + + @Test + public void test() throws SQLException { + get("/fly") + .then() + .statusCode(200) + .body(is("2/1.0.2")); + + config.modifySourceFile(FlywayExtensionWithJavaMigrationDevModeTestEndpoint.class, s -> s.replace("/fly", "/flyway")); + + get("/flyway") + .then() + .statusCode(200) + .body(is("2/1.0.2")); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTestEndpoint.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTestEndpoint.java new file mode 100644 index 0000000000000..35c5984cbab2c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithJavaMigrationDevModeTestEndpoint.java @@ -0,0 +1,37 @@ +package io.quarkus.flyway.multitenant.test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.flywaydb.core.Flyway; + +import io.agroal.api.AgroalDataSource; + +@Path("/fly") +public class FlywayExtensionWithJavaMigrationDevModeTestEndpoint { + + @Inject + AgroalDataSource defaultDataSource; + + @Inject + Flyway flyway; + + @GET + public String result() throws Exception { + int count = 0; + try (Connection connection = defaultDataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet countQuery = stat.executeQuery("select count(1) from quarked_flyway")) { + countQuery.first(); + count = countQuery.getInt(1); + } + } + String currentVersion = flyway.info().current().getVersion().toString(); + + return count + "/" + currentVersion; + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithMultipleDatasourcesAndCustomizersTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithMultipleDatasourcesAndCustomizersTest.java new file mode 100644 index 0000000000000..a261dbaab3d24 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayExtensionWithMultipleDatasourcesAndCustomizersTest.java @@ -0,0 +1,113 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.flyway.FlywayConfigurationCustomizer; +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionWithMultipleDatasourcesAndCustomizersTest { + + @Inject + AgroalDataSource defaultDataSource; + + @Inject + @Named("users") + AgroalDataSource usersDataSource; + + @Inject + @Named("inventory") + AgroalDataSource inventoryDataSource; + + static final FlywayH2TestCustomizer h2ForDefault = FlywayH2TestCustomizer + .withDbName("quarkus-default-customizer") + .withPort(11303) + .withInitSqlFile("src/test/resources/callback-init-data.sql"); + + static final FlywayH2TestCustomizer h2ForUsers = FlywayH2TestCustomizer + .withDbName("quarkus-users-customizer") + .withPort(11304) + .withInitSqlFile("src/test/resources/callback-init-data.sql"); + + static final FlywayH2TestCustomizer h2ForInventory = FlywayH2TestCustomizer + .withDbName("quarkus-inventory-customizer") + .withPort(11305) + .withInitSqlFile("src/test/resources/callback-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(new Runnable() { + @Override + public void run() { + h2ForDefault.startH2(); + h2ForUsers.startH2(); + h2ForInventory.startH2(); + } + }) + .setAfterAllCustomizer(new Runnable() { + @Override + public void run() { + h2ForDefault.stopH2(); + h2ForUsers.stopH2(); + h2ForInventory.stopH2(); + } + }) + .withApplicationRoot((jar) -> jar + .addClasses(FlywayH2TestCustomizer.class, + AddCallbacksCustomizerForDefaultDS.class, + FlywayExtensionCallback.class, FlywayExtensionCallback2.class, FlywayExtensionCDICallback.class) + .addAsResource("db/migration/V1.0.3__Quarkus_Callback.sql") + .addAsResource("config-for-multiple-datasource-with-customizers-config.properties", + "application.properties")); + + @Test + public void testCustomizers() throws SQLException { + assertEventCount(defaultDataSource, FlywayExtensionCallback.DEFAULT_EVENTS.size()); + assertEventCount(usersDataSource, FlywayExtensionCallback2.DEFAULT_EVENTS.size()); + assertEventCount(inventoryDataSource, 0); + } + + private void assertEventCount(AgroalDataSource dataSource, int expectedEventCount) throws SQLException { + try (Connection connection = dataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet executeQuery = stat.executeQuery("select COUNT(name) from quarked_callback")) { + assertTrue(executeQuery.next(), "Table exists but it is empty"); + int count = executeQuery.getInt(1); + assertEquals(expectedEventCount, count); + } + } + } + + @Singleton + public static class AddCallbacksCustomizerForDefaultDS implements FlywayConfigurationCustomizer { + + @Override + public void customize(FluentConfiguration configuration) { + configuration.callbacks(new FlywayExtensionCallback()); + } + } + + @Singleton + @FlywayDataSource("users") + public static class AddCallbacksCustomizerForUsersDS implements FlywayConfigurationCustomizer { + + @Override + public void customize(FluentConfiguration configuration) { + configuration.callbacks(new FlywayExtensionCallback2()); + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayH2TestCustomizer.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayH2TestCustomizer.java new file mode 100644 index 0000000000000..2b137bbc769e6 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayH2TestCustomizer.java @@ -0,0 +1,81 @@ +package io.quarkus.flyway.multitenant.test; + +import static java.util.Objects.requireNonNull; + +import java.nio.charset.Charset; +import java.sql.SQLException; +import java.util.concurrent.ThreadLocalRandom; + +import org.h2.tools.RunScript; +import org.h2.tools.Server; + +public class FlywayH2TestCustomizer { + private String initSqlFile; + private String dbName; + private int port = ThreadLocalRandom.current().nextInt(49152, 65535); + private Server tcpServer; + + protected FlywayH2TestCustomizer() { + } + + private FlywayH2TestCustomizer(String dbName) { + this.dbName = dbName; + } + + public static FlywayH2TestCustomizer withDbName(String dbName) { + return new FlywayH2TestCustomizer(dbName); + } + + public FlywayH2TestCustomizer withPort(int port) { + this.port = port; + return this; + } + + public FlywayH2TestCustomizer withInitSqlFile(String initSqlFile) { + this.initSqlFile = initSqlFile; + return this; + } + + void startH2() { + try { + tcpServer = Server.createTcpServer("-tcpPort", String.valueOf(port), "-ifNotExists"); + tcpServer.start(); + System.out.println("[INFO] Custom H2 database started in TCP server mode; server status: " + tcpServer.getStatus()); + if (initSqlFile != null) { + executeInitSQL(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + void executeInitSQL() { + requireNonNull(dbName, "Flyway-H2: Default db for init-sql must be specified!"); + requireNonNull(initSqlFile, "Flyway-H2: init-sql must be specified!"); + final String url = buildDbURL(); + try { + System.out.println("[INFO] Custom H2 Initializing DB: " + url); + RunScript.execute( + url, + "sa", + "sa", + initSqlFile, + Charset.defaultCharset(), + false); + } catch (SQLException exception) { + throw new RuntimeException(exception); + } + } + + public String buildDbURL() { + return "jdbc:h2:tcp://localhost:" + port + "/mem:" + dbName + ";DB_CLOSE_DELAY=-1"; + } + + public void stopH2() { + if (tcpServer != null) { + tcpServer.stop(); + System.out.println("[INFO] Custom H2 database was shut down; server status: " + tcpServer.getStatus()); + tcpServer = null; + } + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayMultiDataSourcesDevModeTest.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayMultiDataSourcesDevModeTest.java new file mode 100644 index 0000000000000..fd54e05d85fe5 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayMultiDataSourcesDevModeTest.java @@ -0,0 +1,35 @@ +package io.quarkus.flyway.multitenant.test; + +import static org.hamcrest.CoreMatchers.containsString; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +// see https://github.com/quarkusio/quarkus/issues/9415 +public class FlywayMultiDataSourcesDevModeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(MultiDataSourcesDevModeEndpoint.class, FlywayExtensionCallback.class) + .addAsResource("config-for-multiple-datasources.properties", "application.properties")); + + @Test + public void testProperConfigApplied() { + RestAssured.get("/fly").then() + .statusCode(200) + .body(containsString("db/location1,db/location2")); + + RestAssured.get("/fly?name=users").then() + .statusCode(200) + .body(containsString("db/users/location1,db/users/location2")); + + RestAssured.get("/fly?name=inventory").then() + .statusCode(200) + .body(containsString("db/inventory/location1,db/inventory/location")); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayTestResources.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayTestResources.java new file mode 100644 index 0000000000000..0b25adf733ca1 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/FlywayTestResources.java @@ -0,0 +1,8 @@ +package io.quarkus.flyway.multitenant.test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; + +@QuarkusTestResource(H2DatabaseTestResource.class) +public class FlywayTestResources { +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/Fruit.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/Fruit.java new file mode 100644 index 0000000000000..13986afe6035c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/Fruit.java @@ -0,0 +1,35 @@ +package io.quarkus.flyway.multitenant.test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Fruit { + @Id + @GeneratedValue + private Integer id; + + @Column(length = 40, unique = true) + private String name; + + public Fruit() { + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/MultiDataSourcesDevModeEndpoint.java b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/MultiDataSourcesDevModeEndpoint.java new file mode 100644 index 0000000000000..08c42193fbbb2 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/java/io/quarkus/flyway/multitenant/test/MultiDataSourcesDevModeEndpoint.java @@ -0,0 +1,50 @@ +package io.quarkus.flyway.multitenant.test; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.configuration.Configuration; + +import io.quarkus.flyway.FlywayDataSource; + +@Path("/fly") +public class MultiDataSourcesDevModeEndpoint { + + @Inject + Flyway flywayDefault; + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("inventory") + Flyway flywayInventory; + + @GET + @Produces("text/plain") + public String locations(@QueryParam("name") @DefaultValue("default") String name) { + Configuration configuration; + if ("default".equals(name)) { + configuration = flywayDefault.getConfiguration(); + } else if ("users".equals(name)) { + configuration = flywayUsers.getConfiguration(); + } else if ("inventory".equals(name)) { + configuration = flywayInventory.getConfiguration(); + } else { + throw new RuntimeException("Flyway " + name + " not found"); + } + + return Arrays.stream(configuration.getLocations()).map(Location::getPath).collect(Collectors.joining(",")); + } + +} diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-config.properties new file mode 100644 index 0000000000000..b476e073dc59f --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-config.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:quarkus-baseline-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.baseline-at-start=true +quarkus.flyway.baseline-version=1.0.1 \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-existing-schema-history-table-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-existing-schema-history-table-config.properties new file mode 100644 index 0000000000000..23d6fbbc2303c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-at-start-existing-schema-history-table-config.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost:11309/mem:quarkus-baseline-at-start-existing-schema-history;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.baseline-at-start=true +quarkus.flyway.baseline-version=1.0.1 \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties new file mode 100644 index 0000000000000..bdae3e948bf8c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasource.properties @@ -0,0 +1,10 @@ +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost:11302/mem:quarkus-flyway-baseline-on-named-ds +# Flyway config properties +quarkus.flyway.users.migrate-at-start=true +quarkus.flyway.users.table=test_flyway_history +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=0.0.1 +quarkus.flyway.users.baseline-description=Initial description for test diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties new file mode 100644 index 0000000000000..d816d7b5a8451 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties @@ -0,0 +1,24 @@ +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost:11302/mem:quarkus-flyway-baseline-on-named-ds-users + +# Flyway config properties +quarkus.flyway.users.migrate-at-start=true +quarkus.flyway.users.table=test_flyway_history +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=0.0.1 +quarkus.flyway.users.baseline-description=Initial description for test + +quarkus.datasource.laptops.db-kind=h2 +quarkus.datasource.laptops.username=sa +quarkus.datasource.laptops.password=sa +quarkus.datasource.laptops.jdbc.url=jdbc:h2:tcp://localhost:11302/mem:quarkus-flyway-baseline-on-named-ds-laptops + +# Flyway config properties +quarkus.flyway.laptops.active=false +quarkus.flyway.laptops.migrate-at-start=true +quarkus.flyway.laptops.table=test_flyway_history +quarkus.flyway.laptops.baseline-on-migrate=true +quarkus.flyway.laptops.baseline-version=0.0.1 +quarkus.flyway.laptops.baseline-description=Initial description for test diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate.properties b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate.properties new file mode 100644 index 0000000000000..db84abca3e79c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/baseline-on-migrate.properties @@ -0,0 +1,10 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost:11301/mem:quarkus-flyway-baseline-on-migrate +# Flyway config properties +quarkus.flyway.migrate-at-start=true +quarkus.flyway.table=test_flyway_history +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.baseline-description=Initial description for test diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/callback-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/callback-config.properties new file mode 100644 index 0000000000000..f5d91dc0e7c56 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/callback-config.properties @@ -0,0 +1,10 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost:11303/mem:quarkus-flyway-callback;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback,io.quarkus.flyway.test.FlywayExtensionCallback2,io.quarkus.flyway.test.FlywayExtensionCDICallback diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/callback-init-data.sql b/extensions/flyway-multitenant/deployment/src/test/resources/callback-init-data.sql new file mode 100644 index 0000000000000..0adea0251a6cc --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/callback-init-data.sql @@ -0,0 +1,4 @@ +CREATE TABLE quarked_callback +( + name VARCHAR(120) +); diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-config.properties new file mode 100644 index 0000000000000..26c07c53ca008 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-config.properties @@ -0,0 +1,13 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:quarkus-flyway;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.clean-at-start=true +quarkus.flyway.migrate-at-start=true +quarkus.flyway.table=test_flyway_history +quarkus.flyway.baseline-on-migrate=false +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.baseline-description=Initial description for test +quarkus.flyway.users.locations=db/migration \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties new file mode 100644 index 0000000000000..65afb9365aa52 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties @@ -0,0 +1,13 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:quarkus-flyway;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.clean-at-start=true +quarkus.flyway.migrate-at-start=true +quarkus.flyway.table=test_flyway_history +quarkus.flyway.baseline-on-migrate=false +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.baseline-description=Initial description for test +quarkus.flyway.locations=filesystem:src/test/resources/db/migration,classpath:db/migration diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/clean-at-start-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/clean-at-start-config.properties new file mode 100644 index 0000000000000..35ab099fc98f0 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/clean-at-start-config.properties @@ -0,0 +1,14 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-clean-at-start;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS TEST + +# Flyway config properties +quarkus.flyway.clean-at-start=true +quarkus.flyway.migrate-at-start=false +quarkus.flyway.table=test_flyway_history +quarkus.flyway.default-schema=PUBLIC +quarkus.flyway.schemas=TEST,PUBLIC +quarkus.flyway.baseline-on-migrate=false +quarkus.flyway.baseline-version=0.0.1 +quarkus.flyway.baseline-description=Initial description for test diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-empty.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-empty.properties new file mode 100644 index 0000000000000..7484177fc8b23 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-empty.properties @@ -0,0 +1 @@ +quarkus.datasource.devservices.enabled=false \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-with-customizer-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-with-customizer-config.properties new file mode 100644 index 0000000000000..c7551b0da4e5c --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-with-customizer-config.properties @@ -0,0 +1,9 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost:11303/mem:quarkus-customizer;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=0.0.1 diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties new file mode 100644 index 0000000000000..e56388ea03880 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource-without-flyway.properties @@ -0,0 +1,4 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource.properties new file mode 100644 index 0000000000000..3c8e026ab10f9 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-default-datasource.properties @@ -0,0 +1,19 @@ +#default datasource +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.connect-retries=10 +quarkus.flyway.connect-retries-interval=100ms +quarkus.flyway.schemas=TEST_SCHEMA +quarkus.flyway.table=flyway_quarkus_history +quarkus.flyway.locations=db/location1,db/location2 +quarkus.flyway.sql-migration-prefix=X +quarkus.flyway.repeatable-sql-migration-prefix=K +quarkus.flyway.migrate-at-start=false +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=2.0.1 +quarkus.flyway.baseline-description=Initial description +quarkus.flyway.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback,io.quarkus.flyway.test.FlywayExtensionCallback2 diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-missing-named-datasource.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-missing-named-datasource.properties new file mode 100644 index 0000000000000..86e971af9d39e --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-missing-named-datasource.properties @@ -0,0 +1,19 @@ +# Datasource for "inventory" +quarkus.datasource.inventory.db-kind=h2 +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.jdbc.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.jdbc.max-size=12 +quarkus.datasource.inventory.jdbc.transactions=xa + +# Flyway configuration for missing "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.connect-retries-interval=1 +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasource-with-customizers-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasource-with-customizers-config.properties new file mode 100644 index 0000000000000..f03c201799ab5 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasource-with-customizers-config.properties @@ -0,0 +1,24 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost:11303/mem:quarkus-default-customizer;DB_CLOSE_DELAY=-1 + +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost:11304/mem:quarkus-users-customizer;DB_CLOSE_DELAY=-1 + +quarkus.datasource.inventory.db-kind=h2 +quarkus.datasource.inventory.username=sa +quarkus.datasource.inventory.password=sa +quarkus.datasource.inventory.jdbc.url=jdbc:h2:tcp://localhost:11305/mem:quarkus-inventory-customizer;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true + +quarkus.flyway.users.migrate-at-start=true +quarkus.flyway.users.baseline-on-migrate=true + +quarkus.flyway.inventory.migrate-at-start=true +quarkus.flyway.inventory.baseline-on-migrate=true diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties new file mode 100644 index 0000000000000..bc58f668ce8f6 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources-without-default.properties @@ -0,0 +1,41 @@ +# Datasource for "users" +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=username1 +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.jdbc.max-size=11 + +# Datasource for "inventory" +quarkus.datasource.inventory.db-kind=h2 +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.jdbc.driver=org.h2.jdbcx.JdbcDataSource +quarkus.datasource.inventory.jdbc.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.jdbc.max-size=12 +quarkus.datasource.inventory.jdbc.transactions=xa + +# Flyway configuration for "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.connect-retries-interval=2s +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description +quarkus.flyway.users.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback + +# Flyway configuration for "inventory" datasource +quarkus.flyway.inventory.connect-retries=12 +quarkus.flyway.inventory.connect-retries-interval=2s +quarkus.flyway.inventory.schemas=INVENTORY_TEST_SCHEMA +quarkus.flyway.inventory.table=inventory_flyway_quarkus_history +quarkus.flyway.inventory.locations=db/inventory/location1,db/inventory/location2 +quarkus.flyway.inventory.sql-migration-prefix=I +quarkus.flyway.inventory.repeatable-sql-migration-prefix=N +quarkus.flyway.inventory.migrate-at-start=false +quarkus.flyway.inventory.baseline-on-migrate=true +quarkus.flyway.inventory.baseline-version=3.0.1 +quarkus.flyway.inventory.baseline-description=Inventory initial description +quarkus.flyway.inventory.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources.properties new file mode 100644 index 0000000000000..6edbdc877e858 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-multiple-datasources.properties @@ -0,0 +1,61 @@ +# Datasource default +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 + +# Datasource for "users" +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=username1 +quarkus.datasource.users.jdbc.driver=org.h2.Driver +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.jdbc.max-size=11 + +# Datasource for "inventory" +quarkus.datasource.inventory.db-kind=h2 +quarkus.datasource.inventory.username=username2 +quarkus.datasource.inventory.jdbc.driver=org.h2.jdbcx.JdbcDataSource +quarkus.datasource.inventory.jdbc.url=jdbc:h2:tcp://localhost/mem:inventory +quarkus.datasource.inventory.jdbc.max-size=12 + +# Flyway configuration for default datasource +quarkus.flyway.connect-retries=10 +quarkus.flyway.connect-retries-interval=1000ms +quarkus.flyway.schemas=TEST_SCHEMA +quarkus.flyway.table=flyway_quarkus_history +quarkus.flyway.locations=db/location1,db/location2 +quarkus.flyway.sql-migration-prefix=X +quarkus.flyway.repeatable-sql-migration-prefix=K +quarkus.flyway.migrate-at-start=false +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=2.0.1 +quarkus.flyway.baseline-description=Initial description +quarkus.flyway.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback + +# Flyway configuration for "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.connect-retries-interval=10s +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description +quarkus.flyway.users.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback + +# Flyway configuration for "inventory" datasource +quarkus.flyway.inventory.connect-retries=12 +quarkus.flyway.inventory.connect-retries-interval=2s +quarkus.flyway.inventory.schemas=INVENTORY_TEST_SCHEMA +quarkus.flyway.inventory.table=inventory_flyway_quarkus_history +quarkus.flyway.inventory.locations=db/inventory/location1,db/inventory/location2 +quarkus.flyway.inventory.sql-migration-prefix=I +quarkus.flyway.inventory.repeatable-sql-migration-prefix=N +quarkus.flyway.inventory.migrate-at-start=false +quarkus.flyway.inventory.baseline-on-migrate=true +quarkus.flyway.inventory.baseline-version=3.0.1 +quarkus.flyway.inventory.baseline-description=Inventory initial description +quarkus.flyway.inventory.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-default.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-default.properties new file mode 100644 index 0000000000000..20c1cab3ea521 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-default.properties @@ -0,0 +1,19 @@ +# Datasource for "users" +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=username1 +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost/mem:users +quarkus.datasource.users.jdbc.max-size=11 + +# Flyway configuration for "users" datasource +quarkus.flyway.users.connect-retries=11 +quarkus.flyway.users.connect-retries-interval=12 +quarkus.flyway.users.schemas=USERS_TEST_SCHEMA +quarkus.flyway.users.table=users_flyway_quarkus_history +quarkus.flyway.users.locations=db/users/location1,db/users/location2 +quarkus.flyway.users.sql-migration-prefix=U +quarkus.flyway.users.repeatable-sql-migration-prefix=S +quarkus.flyway.users.migrate-at-start=false +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=2.0.1 +quarkus.flyway.users.baseline-description=Users initial description +quarkus.flyway.users.callbacks=io.quarkus.flyway.multitenant.test.FlywayExtensionCallback diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties new file mode 100644 index 0000000000000..25294da95bacc --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/config-for-named-datasource-without-flyway.properties @@ -0,0 +1,4 @@ +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/db/migration-subfolder/subfolder/V1.0.0__Quarkus.sql b/extensions/flyway-multitenant/deployment/src/test/resources/db/migration-subfolder/subfolder/V1.0.0__Quarkus.sql new file mode 100644 index 0000000000000..241cad5122ccb --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/db/migration-subfolder/subfolder/V1.0.0__Quarkus.sql @@ -0,0 +1,5 @@ +CREATE TABLE quarked_flyway +( + id INT, + name VARCHAR(20) +); \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.0__Quarkus.sql b/extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.0__Quarkus.sql new file mode 100644 index 0000000000000..241cad5122ccb --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.0__Quarkus.sql @@ -0,0 +1,5 @@ +CREATE TABLE quarked_flyway +( + id INT, + name VARCHAR(20) +); \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.3__Quarkus_Callback.sql b/extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.3__Quarkus_Callback.sql new file mode 100644 index 0000000000000..1f46dcd82ef47 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/db/migration/V1.0.3__Quarkus_Callback.sql @@ -0,0 +1,4 @@ +CREATE TABLE quarked_callback_2 +( + name VARCHAR(120) +); \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/disabled-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/disabled-config.properties new file mode 100644 index 0000000000000..7bf03c9db53df --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/disabled-config.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.enabled=false +quarkus.flyway.migrate-at-start=true diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/h2-init-data.sql b/extensions/flyway-multitenant/deployment/src/test/resources/h2-init-data.sql new file mode 100644 index 0000000000000..f7bab27cb014f --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/h2-init-data.sql @@ -0,0 +1,3 @@ +-- We need an existing table in the database to test the creation of the +-- Flyway history table with the correct baseline version set. +CREATE TABLE IF NOT EXISTS fake_existing_tbl; \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/h2-init-schema-history-table.sql b/extensions/flyway-multitenant/deployment/src/test/resources/h2-init-schema-history-table.sql new file mode 100644 index 0000000000000..fafb04b764139 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/h2-init-schema-history-table.sql @@ -0,0 +1,13 @@ +CREATE TABLE "flyway_schema_history" ( + "installed_rank" integer NOT NULL, + "version" character varying(50), + "description" character varying(200) NOT NULL, + "type" character varying(20) NOT NULL, + "script" character varying(1000) NOT NULL, + "checksum" integer, + "installed_by" character varying(100) NOT NULL, + "installed_on" timestamp without time zone DEFAULT now() NOT NULL, + "execution_time" integer NOT NULL, + "success" boolean NOT NULL, + CONSTRAINT flyway_schema_history_pk PRIMARY KEY ("installed_rank") +); \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/init-sql-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/init-sql-config.properties new file mode 100644 index 0000000000000..a59d04d5e3072 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/init-sql-config.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:init-sql-config;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.migrate-at-start=true +quarkus.flyway.init-sql=CREATE CONSTANT IF NOT EXISTS ONE_HUNDRED VALUE 100 \ No newline at end of file diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties b/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties new file mode 100644 index 0000000000000..c7ca3a02a4830 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties @@ -0,0 +1,8 @@ +#quarkus.log.category."io.quarkus.flyway".level=DEBUG +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start-users;DB_CLOSE_DELAY=-1 + +# Flyway config properties for datasource named users +quarkus.flyway.users.migrate-at-start=true diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config.properties new file mode 100644 index 0000000000000..0ba1305214873 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-config.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.migrate-at-start=true diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-subfolder-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-subfolder-config.properties new file mode 100644 index 0000000000000..c57bf8416f737 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/migrate-at-start-subfolder-config.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start-subfolder;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.locations=db/migration-subfolder +quarkus.flyway.migrate-at-start=true diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/repair-at-start-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/repair-at-start-config.properties new file mode 100644 index 0000000000000..564b875154758 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/repair-at-start-config.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-repair-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.migrate-at-start=true diff --git a/extensions/flyway-multitenant/deployment/src/test/resources/validate-at-start-config.properties b/extensions/flyway-multitenant/deployment/src/test/resources/validate-at-start-config.properties new file mode 100644 index 0000000000000..a0810ce17fb35 --- /dev/null +++ b/extensions/flyway-multitenant/deployment/src/test/resources/validate-at-start-config.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-validate-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.validate-at-start=true diff --git a/extensions/flyway-multitenant/pom.xml b/extensions/flyway-multitenant/pom.xml new file mode 100644 index 0000000000000..4853a0c18aec5 --- /dev/null +++ b/extensions/flyway-multitenant/pom.xml @@ -0,0 +1,21 @@ + + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-flyway-multitenant-parent + Quarkus - Flyway - Multitenant + pom + + runtime + deployment + + diff --git a/extensions/flyway-multitenant/runtime/pom.xml b/extensions/flyway-multitenant/runtime/pom.xml new file mode 100644 index 0000000000000..51a924effc1de --- /dev/null +++ b/extensions/flyway-multitenant/runtime/pom.xml @@ -0,0 +1,93 @@ + + + + quarkus-flyway-multitenant-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-flyway-multitenant + Quarkus - Flyway - Multitenant - Runtime + Handle your database schema migrations + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-flyway + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-narayana-jta + + + io.quarkus + quarkus-datasource-common + + + org.graalvm.sdk + nativeimage + provided + + + io.vertx + vertx-web + true + + + io.quarkus + quarkus-vertx-http + true + + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + io.quarkus.flyway + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + -AlegacyConfigRoot=true + + + + + + + + diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/FlywayPersistenceUnit.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/FlywayPersistenceUnit.java new file mode 100644 index 0000000000000..c115b6502b945 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/FlywayPersistenceUnit.java @@ -0,0 +1,83 @@ +package io.quarkus.flyway.multitenant; + +import static io.quarkus.flyway.runtime.FlywayCreator.TENANT_ID_DEFAULT; +import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.enterprise.util.Nonbinding; +import jakarta.inject.Named; +import jakarta.inject.Qualifier; + +/** + * Qualifier used to specify which datasource will be used and therefore which Flyway instance will be injected. + *

+ * Flyway instances can also be qualified by name using @{@link Named}. + * The name is the datasource name prefixed by "flyway_". + */ +@Target({ METHOD, FIELD, PARAMETER, TYPE }) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface FlywayPersistenceUnit { + + String value() default DEFAULT_PERSISTENCE_UNIT_NAME; + + @Nonbinding + String tenantId() default TENANT_ID_DEFAULT; + + /** + * Supports inline instantiation of the {@link FlywayPersistenceUnit} qualifier. + */ + public static final class FlywayPersistenceUnitLiteral extends AnnotationLiteral + implements FlywayPersistenceUnit { + + public static final FlywayPersistenceUnitLiteral INSTANCE = of(""); + + private static final long serialVersionUID = 1L; + + private final String value; + + private final String tenanId; + + public static FlywayPersistenceUnitLiteral of(String value) { + return of(value, TENANT_ID_DEFAULT); + } + + public static FlywayPersistenceUnitLiteral of(String value, String tenantId) { + return new FlywayPersistenceUnitLiteral(value, tenantId); + } + + public static FlywayPersistenceUnitLiteral ofDefault(String tenantId) { + return new FlywayPersistenceUnitLiteral(DEFAULT_PERSISTENCE_UNIT_NAME, tenantId); + } + + @Override + public String value() { + return value; + } + + @Override + public String tenantId() { + return tenanId; + } + + private FlywayPersistenceUnitLiteral(String value, String tenantId) { + this.value = value; + this.tenanId = tenantId; + } + + @Override + public String toString() { + return "FlywayDataSourceLiteral [value=" + value + "]"; + } + } +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantBuildTimeConfig.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantBuildTimeConfig.java new file mode 100644 index 0000000000000..eb1b97ce4e163 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantBuildTimeConfig.java @@ -0,0 +1,50 @@ +package io.quarkus.flyway.multitenant.runtime; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "flyway", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public final class FlywayMultiTenantBuildTimeConfig { + + /** + * Gets the {@link FlywayDataSourceBuildTimeConfig} for the given datasource name. + */ + public FlywayDataSourceBuildTimeConfig getConfigForPersistenceUnitName(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return defaultDataSource; + } + return namedDataSources.getOrDefault(dataSourceName, FlywayDataSourceBuildTimeConfig.defaultConfig()); + } + + /** + * Whether Flyway is enabled *during the build*. + * + * If Flyway is disabled, the Flyway beans won't be created and Flyway won't be usable. + * + * @asciidoclet + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; + + /** + * Flyway configuration for the default datasource. + */ + @ConfigItem(name = ConfigItem.PARENT) + public FlywayDataSourceBuildTimeConfig defaultDataSource; + + /** + * Named datasources. + */ + @ConfigItem(name = ConfigItem.PARENT) + @ConfigDocMapKey("datasource-name") + @ConfigDocSection + public Map namedDataSources = Collections.emptyMap(); +} \ No newline at end of file diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerProducer.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerProducer.java new file mode 100644 index 0000000000000..03412a12d9a32 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerProducer.java @@ -0,0 +1,100 @@ +package io.quarkus.flyway.multitenant.runtime; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.sql.DataSource; + +import io.quarkus.flyway.runtime.FlywayContainer; +import io.quarkus.flyway.runtime.FlywayCreator; +import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig; +import io.quarkus.flyway.runtime.FlywayDataSourceRuntimeConfig; +import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.callback.Callback; + +import io.quarkus.arc.All; +import io.quarkus.arc.InstanceHandle; + +import io.quarkus.flyway.FlywayConfigurationCustomizer; +import io.quarkus.flyway.multitenant.FlywayPersistenceUnit; + +/** + * This class is sort of a producer for {@link Flyway}. + * + * It isn't a CDI producer in the literal sense, but it is marked as a bean + * and it's {@code createFlyway} method is called at runtime in order to produce + * the actual {@code Flyway} objects. + * + * CDI scopes and qualifiers are set up at build-time, which is why this class is devoid of + * any CDI annotations + * + */ +public class FlywayMultiTenantContainerProducer { + + private final FlywayMultiTenantRuntimeConfig flywayMultiTenantRuntimeConfig; + private final FlywayMultiTenantBuildTimeConfig flywayBuildConfig; + + private final List> configCustomizerInstances; + + public FlywayMultiTenantContainerProducer(FlywayMultiTenantRuntimeConfig flywayMultiTenantRuntimeConfig, + FlywayMultiTenantBuildTimeConfig flywayBuildConfig, + @All List> configCustomizerInstances) { + this.flywayMultiTenantRuntimeConfig = flywayMultiTenantRuntimeConfig; + this.flywayBuildConfig = flywayBuildConfig; + this.configCustomizerInstances = configCustomizerInstances; + } + + public FlywayContainer createFlyway(DataSource dataSource, String persistenceUnitName, String tenantId, + boolean hasMigrations, + boolean createPossible) { + FlywayDataSourceRuntimeConfig matchingRuntimeConfig = flywayMultiTenantRuntimeConfig + .getConfigForPersistenceUnitName(persistenceUnitName); + FlywayDataSourceBuildTimeConfig matchingBuildTimeConfig = flywayBuildConfig + .getConfigForPersistenceUnitName(persistenceUnitName); + final Collection callbacks = QuarkusPathLocationScanner.callbacksForPersistenceUnit(persistenceUnitName); + final Flyway flyway = new FlywayCreator(matchingRuntimeConfig, matchingBuildTimeConfig, matchingConfigCustomizers( + configCustomizerInstances, persistenceUnitName)).withCallbacks(callbacks) + .withTenantId(tenantId) + .createFlyway(dataSource); + return new FlywayContainer(flyway, matchingRuntimeConfig.baselineAtStart, matchingRuntimeConfig.cleanAtStart, + matchingRuntimeConfig.migrateAtStart, + matchingRuntimeConfig.repairAtStart, matchingRuntimeConfig.validateAtStart, + persistenceUnitName, hasMigrations, + createPossible); + } + + private List matchingConfigCustomizers( + List> configCustomizerInstances, String persistenceUnitName) { + if ((configCustomizerInstances == null) || configCustomizerInstances.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (InstanceHandle instance : configCustomizerInstances) { + Set qualifiers = instance.getBean().getQualifiers(); + boolean qualifierMatchesPS = false; + boolean hasFlywayPersistenceUnitQualifier = false; + for (Annotation qualifier : qualifiers) { + if (qualifier.annotationType().equals(FlywayPersistenceUnit.class)) { + hasFlywayPersistenceUnitQualifier = true; + if (persistenceUnitName.equals(((FlywayPersistenceUnit) qualifier).value())) { + qualifierMatchesPS = true; + break; + } + } + } + if (qualifierMatchesPS) { + result.add(instance.get()); + } else if (PersistenceUnitUtil.isDefaultPersistenceUnit(persistenceUnitName) + && !hasFlywayPersistenceUnitQualifier) { + // this is the case where a FlywayConfigurationCustomizer does not have a qualifier at all, therefore is applies to the default datasource + result.add(instance.get()); + } + } + return result; + } +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerUtil.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerUtil.java new file mode 100644 index 0000000000000..4c3a6d7ca0987 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantContainerUtil.java @@ -0,0 +1,20 @@ +package io.quarkus.flyway.multitenant.runtime; + +import io.quarkus.flyway.multitenant.FlywayPersistenceUnit; + +import java.lang.annotation.Annotation; + +import static io.quarkus.flyway.runtime.FlywayCreator.TENANT_ID_DEFAULT; + +public final class FlywayMultiTenantContainerUtil { + private FlywayMultiTenantContainerUtil() { + } + + public static Annotation getFlywayContainerQualifier(String persistenceUnitName) { + return getFlywayContainerQualifier(persistenceUnitName, TENANT_ID_DEFAULT); + } + + public static Annotation getFlywayContainerQualifier(String persistenceUnitName, String tenantId) { + return FlywayPersistenceUnit.FlywayPersistenceUnitLiteral.of(persistenceUnitName, tenantId); + } +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantRuntimeConfig.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantRuntimeConfig.java new file mode 100644 index 0000000000000..fc6ade408e743 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayMultiTenantRuntimeConfig.java @@ -0,0 +1,40 @@ +package io.quarkus.flyway.multitenant.runtime; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.flyway.runtime.FlywayDataSourceRuntimeConfig; +import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "flyway.multitenant", phase = ConfigPhase.RUN_TIME) +public final class FlywayMultiTenantRuntimeConfig { + + /** + * Gets the {@link FlywayDataSourceRuntimeConfig} for the given datasource name. + */ + public FlywayDataSourceRuntimeConfig getConfigForPersistenceUnitName(String persistenceUnitName) { + if (PersistenceUnitUtil.isDefaultPersistenceUnit(persistenceUnitName)) { + return defaultPersistenceUnit; + } + return namedPersistenceUnits.getOrDefault(persistenceUnitName, FlywayDataSourceRuntimeConfig.defaultConfig()); + } + + /** + * Flyway configuration for the default datasource. + */ + @ConfigItem(name = ConfigItem.PARENT) + public FlywayDataSourceRuntimeConfig defaultPersistenceUnit = FlywayDataSourceRuntimeConfig.defaultConfig(); + + /** + * Named persistenceUnits. + */ + @ConfigItem(name = ConfigItem.PARENT) + @ConfigDocMapKey("datasource-name") + @ConfigDocSection + public Map namedPersistenceUnits = Collections.emptyMap(); +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayRecorder.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayRecorder.java new file mode 100644 index 0000000000000..74ebb1a3932b4 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/FlywayRecorder.java @@ -0,0 +1,184 @@ +package io.quarkus.flyway.multitenant.runtime; + +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import javax.sql.DataSource; + +import io.quarkus.flyway.multitenant.FlywayPersistenceUnit; +import io.quarkus.flyway.runtime.FlywayContainer; +import io.quarkus.flyway.runtime.FlywayDataSourceRuntimeConfig; +import io.quarkus.flyway.runtime.UnconfiguredDataSourceFlywayContainer; +import jakarta.enterprise.inject.spi.InjectionPoint; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.FlywayExecutor; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.output.BaselineResult; +import org.flywaydb.core.internal.callback.CallbackExecutor; +import org.flywaydb.core.internal.database.base.Database; +import org.flywaydb.core.internal.database.base.Schema; +import org.flywaydb.core.internal.jdbc.StatementInterceptor; +import org.flywaydb.core.internal.resolver.CompositeMigrationResolver; +import org.flywaydb.core.internal.schemahistory.SchemaHistory; +import org.jboss.logging.Logger; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.agroal.runtime.UnconfiguredDataSource; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.configuration.ConfigurationException; + +import static io.quarkus.flyway.runtime.FlywayCreator.TENANT_ID_DEFAULT; + +@Recorder +public class FlywayRecorder { + + private static final Logger log = Logger.getLogger(FlywayRecorder.class); + + private final RuntimeValue config; + + public FlywayRecorder(RuntimeValue config) { + this.config = config; + } + + public void setApplicationMigrationFiles(Collection migrationFiles) { + log.debugv("Setting the following application migration files: {0}", migrationFiles); + QuarkusPathLocationScanner.setApplicationMigrationFiles(migrationFiles); + } + + public void setApplicationMigrationClasses(Collection> migrationClasses) { + log.debugv("Setting the following application migration classes: {0}", migrationClasses); + QuarkusPathLocationScanner.setApplicationMigrationClasses(migrationClasses); + } + + public void setApplicationCallbackClasses(Map> callbackClasses) { + log.debugv("Setting application callbacks: {0} total", callbackClasses.values().size()); + QuarkusPathLocationScanner.setApplicationCallbackClasses(callbackClasses); + } + + public Function, FlywayContainer> flywayContainerFunction(String dataSourceName, + String persistenceUnitName, + boolean multiTenant, + boolean hasMigrations, + boolean createPossible) { + return new Function<>() { + @Override + public FlywayContainer apply(SyntheticCreationalContext context) { + DataSource dataSource; + try { + dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName); + if (dataSource instanceof UnconfiguredDataSource) { + throw DataSourceUtil.dataSourceNotConfigured(dataSourceName); + } + } catch (ConfigurationException e) { + // TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured/inactive datasource? + // Assigning ApplicationScoped to the FlywayContainer + // and throwing UnsatisfiedResolutionException on bean creation (first access) + // would probably make more sense. + return new UnconfiguredDataSourceFlywayContainer(dataSourceName, String.format(Locale.ROOT, + "Unable to find datasource '%s' for Flyway: %s", + dataSourceName, e.getMessage()), e); + } + + String tenantId = getTenantId(multiTenant, context); + FlywayMultiTenantContainerProducer flywayProducer = context + .getInjectedReference(FlywayMultiTenantContainerProducer.class); + return flywayProducer.createFlyway(dataSource, persistenceUnitName, tenantId, hasMigrations, createPossible); + } + }; + } + + public Function, Flyway> flywayFunction(String persistenceUnitName, + boolean multiTenant) { + return new Function<>() { + @Override + public Flyway apply(SyntheticCreationalContext context) { + + String tenantId = getTenantId(multiTenant, context); + FlywayContainer flywayContainer = context.getInjectedReference(FlywayContainer.class, + FlywayMultiTenantContainerUtil.getFlywayContainerQualifier(persistenceUnitName, tenantId)); + return flywayContainer.getFlyway(); + } + }; + } + + public void doStartActions(String persistenceUnitName) { + FlywayDataSourceRuntimeConfig flywayPersistenceUnitRuntimeConfig = config.getValue() + .getConfigForPersistenceUnitName(persistenceUnitName); + + if (!flywayPersistenceUnitRuntimeConfig.active + // If not specified explicitly, Flyway is active when the datasource itself is active. + .orElseGet(() -> Arc.container().instance(DataSources.class).get().getActiveDataSourceNames() + .contains(persistenceUnitName))) { + return; + } + + InstanceHandle flywayContainerInstanceHandle = Arc.container().instance(FlywayContainer.class, + FlywayMultiTenantContainerUtil.getFlywayContainerQualifier(persistenceUnitName)); + + if (!flywayContainerInstanceHandle.isAvailable()) { + return; + } + + FlywayContainer flywayContainer = flywayContainerInstanceHandle.get(); + + if (flywayContainer instanceof UnconfiguredDataSourceFlywayContainer) { + return; + } + + if (flywayContainer.isCleanAtStart()) { + flywayContainer.getFlyway().clean(); + } + if (flywayContainer.isValidateAtStart()) { + flywayContainer.getFlyway().validate(); + } + if (flywayContainer.isBaselineAtStart()) { + new FlywayExecutor(flywayContainer.getFlyway().getConfiguration()) + .execute(new BaselineCommand(flywayContainer.getFlyway()), true, null); + } + if (flywayContainer.isRepairAtStart()) { + flywayContainer.getFlyway().repair(); + } + if (flywayContainer.isMigrateAtStart()) { + flywayContainer.getFlyway().migrate(); + } + } + + private static String getTenantId(boolean multiTenant, SyntheticCreationalContext context) { + if (multiTenant) { + InjectionPoint injectionPoint = context.getInjectedReference(InjectionPoint.class); + FlywayPersistenceUnit annotation = (FlywayPersistenceUnit) injectionPoint.getQualifiers().stream() + .filter(x -> x instanceof FlywayPersistenceUnit) + .findFirst() + .orElseThrow( + () -> new IllegalStateException( + "flyway must be qualified with FlywayPersistenceUnit")); + return annotation.tenantId(); + } + return TENANT_ID_DEFAULT; + } + + static class BaselineCommand implements FlywayExecutor.Command { + BaselineCommand(Flyway flyway) { + this.flyway = flyway; + } + + final Flyway flyway; + + @Override + public BaselineResult execute(CompositeMigrationResolver cmr, SchemaHistory schemaHistory, Database d, + Schema defaultSchema, Schema[] s, CallbackExecutor ce, StatementInterceptor si) { + if (!schemaHistory.exists()) { + return flyway.baseline(); + } + return null; + } + } +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/QuarkusPathLocationScanner.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/QuarkusPathLocationScanner.java new file mode 100644 index 0000000000000..678da4d39e84b --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/QuarkusPathLocationScanner.java @@ -0,0 +1,120 @@ +package io.quarkus.flyway.multitenant.runtime; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.api.resource.LoadableResource; +import org.flywaydb.core.internal.resource.classpath.ClassPathResource; +import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner; +import org.flywaydb.core.internal.scanner.filesystem.FileSystemScanner; +import org.jboss.logging.Logger; + +/** + * This class is used in order to prevent Flyway from doing classpath scanning which is both slow + * and won't work in native mode + */ +@SuppressWarnings("rawtypes") +public final class QuarkusPathLocationScanner implements ResourceAndClassScanner { + private static final Logger LOGGER = Logger.getLogger(QuarkusPathLocationScanner.class); + private static final String LOCATION_SEPARATOR = "/"; + private static Collection applicationMigrationFiles = Collections.emptyList(); // the set default to aid unit tests + private static Collection> applicationMigrationClasses = Collections.emptyList(); // the set default to aid unit tests + private static Map> applicationCallbackClasses = Collections.emptyMap(); // the set default to aid unit tests + + private final Collection scannedResources; + private final Collection> scannedMigrationClasses; + + public QuarkusPathLocationScanner(Configuration configuration, Collection locations) { + LOGGER.debugv("Locations: {0}", locations); + + this.scannedResources = new ArrayList<>(); + this.scannedMigrationClasses = new ArrayList<>(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + FileSystemScanner fileSystemScanner = null; + for (String migrationFile : applicationMigrationFiles) { + if (isClassPathResource(locations, migrationFile)) { + LOGGER.debugf("Loading %s", migrationFile); + + scannedResources.add(new ClassPathResource(null, migrationFile, classLoader, StandardCharsets.UTF_8)); + } else if (migrationFile.startsWith(Location.FILESYSTEM_PREFIX)) { + if (fileSystemScanner == null) { + fileSystemScanner = new FileSystemScanner(false, configuration); + } + LOGGER.debugf("Checking %s for migration files", migrationFile); + Collection resources = fileSystemScanner.scanForResources(new Location(migrationFile)); + LOGGER.debugf("%s contains %d migration files", migrationFile, resources.size()); + scannedResources.addAll(resources); + } + } + + // Filter the provided migration classes to match the provided locations. + for (Class migrationClass : applicationMigrationClasses) { + if (isClassPathResource(locations, migrationClass.getCanonicalName().replace('.', '/'))) { + LOGGER.debugf("Loading migration class %s", migrationClass.getCanonicalName()); + scannedMigrationClasses.add(migrationClass); + } + } + } + + public static void setApplicationCallbackClasses(Map> callbackClasses) { + QuarkusPathLocationScanner.applicationCallbackClasses = callbackClasses; + } + + public static Collection callbacksForPersistenceUnit(String dsName) { + return applicationCallbackClasses.getOrDefault(dsName, Collections.emptyList()); + } + + /** + * + * @return The resources that were found. + */ + @Override + public Collection scanForResources() { + return scannedResources; + } + + private boolean isClassPathResource(Collection locations, String migrationFile) { + for (Location location : locations) { + String locationPath = location.getPath(); + if (!locationPath.endsWith(LOCATION_SEPARATOR)) { + locationPath += "/"; + } + + if (migrationFile.startsWith(locationPath)) { + return true; + } else { + LOGGER.debugf("Migration file '%s' will be ignored because it does not start with '%s'", migrationFile, + locationPath); + } + } + + return false; + } + + /** + * Scans the classpath for concrete classes under the specified package implementing this interface. + * Non-instantiable abstract classes are filtered out. + * + * @return The non-abstract classes that were found. + */ + @Override + public Collection> scanForClasses() { + return scannedMigrationClasses; + } + + public static void setApplicationMigrationFiles(Collection applicationMigrationFiles) { + QuarkusPathLocationScanner.applicationMigrationFiles = applicationMigrationFiles; + } + + public static void setApplicationMigrationClasses(Collection> applicationMigrationClasses) { + QuarkusPathLocationScanner.applicationMigrationClasses = applicationMigrationClasses; + } +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayDevUIRecorder.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayDevUIRecorder.java new file mode 100644 index 0000000000000..f9248883475b1 --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayDevUIRecorder.java @@ -0,0 +1,20 @@ +package io.quarkus.flyway.multitenant.runtime.devui; + +import java.util.Map; +import java.util.function.Supplier; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class FlywayDevUIRecorder { + + public RuntimeValue setInitialSqlSuppliers(Map> initialSqlSuppliers, String artifactId) { + FlywayJsonRpcService rpcService = Arc.container().instance(FlywayJsonRpcService.class).get(); + rpcService.setInitialSqlSuppliers(initialSqlSuppliers); + rpcService.setArtifactId(artifactId); + return new RuntimeValue<>(true); + } + +} diff --git a/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayJsonRpcService.java b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayJsonRpcService.java new file mode 100644 index 0000000000000..0830ba8b22b1e --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/java/io/quarkus/flyway/multitenant/runtime/devui/FlywayJsonRpcService.java @@ -0,0 +1,231 @@ +package io.quarkus.flyway.multitenant.runtime.devui; + +import static java.util.List.of; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import io.quarkus.flyway.runtime.FlywayContainer; +import io.quarkus.flyway.runtime.FlywayContainersSupplier; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.output.CleanResult; +import org.flywaydb.core.api.output.MigrateResult; + +import io.quarkus.dev.config.CurrentConfig; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.runtime.configuration.ConfigUtils; + +public class FlywayJsonRpcService { + + private Map> initialSqlSuppliers; + private String artifactId; + private Map datasources; + + @ConfigProperty(name = "quarkus.flyway.locations") + private List locations; + + public void setInitialSqlSuppliers(Map> initialSqlSuppliers) { + this.initialSqlSuppliers = initialSqlSuppliers; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public Collection getDatasources() { + if (datasources == null) { + datasources = new HashMap<>(); + Collection flywayContainers = new FlywayContainersSupplier().get(); + for (FlywayContainer fc : flywayContainers) { + datasources.put(fc.getDataSourceName(), + new FlywayDatasource(fc.getDataSourceName(), fc.isHasMigrations(), fc.isCreatePossible())); + } + } + return datasources.values(); + } + + public FlywayActionResponse clean(String ds) { + Flyway flyway = getFlyway(ds); + if (flyway != null) { + CleanResult cleanResult = flyway.clean(); + if (cleanResult.warnings != null && cleanResult.warnings.size() > 0) { + return new FlywayActionResponse("warning", + "Cleaning failed", + cleanResult.warnings.size(), + null, + cleanResult.database, cleanResult.warnings); + } else { + return new FlywayActionResponse("success", + "Cleaned", + cleanResult.schemasCleaned.size(), + null, + cleanResult.database); + } + + } + return errorNoDatasource(ds); + } + + public FlywayActionResponse migrate(String ds) { + Flyway flyway = getFlyway(ds); + if (flyway != null) { + MigrateResult migrateResult = flyway.migrate(); + if (migrateResult.success) { + return new FlywayActionResponse("success", + "Migration executed", + migrateResult.migrationsExecuted, + migrateResult.schemaName, + migrateResult.database); + } else { + return new FlywayActionResponse("warning", + "Migration failed", + migrateResult.warnings.size(), + migrateResult.schemaName, + migrateResult.database, + migrateResult.warnings); + } + } + return errorNoDatasource(ds); + } + + public FlywayActionResponse create(String ds) { + this.getDatasources(); // Make sure we populated the datasources + + Supplier found = initialSqlSuppliers.get(ds); + if (found == null) { + return new FlywayActionResponse("error", "Unable to find SQL generator"); + } + + String script = found.get(); + + Flyway flyway = getFlyway(ds); + if (flyway != null) { + if (script != null) { + Map params = Map.of("ds", ds, "script", script, "artifactId", artifactId); + try { + if (locations.isEmpty()) { + return new FlywayActionResponse("error", "Datasource has no locations configured"); + } + + List resourcesDir = DevConsoleManager.getHotReplacementContext().getResourcesDir(); + if (resourcesDir.isEmpty()) { + return new FlywayActionResponse("error", "No resource directory found"); + } + + // In the current project only + Path path = resourcesDir.get(0); + + Path migrationDir = path.resolve(locations.get(0)); + Files.createDirectories(migrationDir); + Path file = migrationDir.resolve( + "V1.0.0__" + artifactId + ".sql"); + + Files.writeString(file, script); + + FlywayDatasource flywayDatasource = datasources.get(ds); + flywayDatasource.hasMigrations = true; + flywayDatasource.createPossible = false; + Map newConfig = new HashMap<>(); + boolean isBaselineOnMigrateConfigured = ConfigUtils + .isPropertyPresent("quarkus.flyway.baseline-on-migrate"); + boolean isMigrateAtStartConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.migrate-at-start"); + boolean isCleanAtStartConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.clean-at-start"); + if (!isBaselineOnMigrateConfigured) { + newConfig.put("quarkus.flyway.baseline-on-migrate", "true"); + } + if (!isMigrateAtStartConfigured) { + newConfig.put("quarkus.flyway.migrate-at-start", "true"); + } + for (var profile : of("test", "dev")) { + if (!isCleanAtStartConfigured) { + newConfig.put("%" + profile + ".quarkus.flyway.clean-at-start", "true"); + } + } + CurrentConfig.EDITOR.accept(newConfig); + //force a scan, to make sure everything is up-to-date + DevConsoleManager.getHotReplacementContext().doScan(true); + return new FlywayActionResponse("success", + "Initial migration created, Flyway will now manage this datasource"); + } catch (Throwable t) { + return new FlywayActionResponse("error", t.getMessage()); + } + } + return errorNoScript(ds); + } + return errorNoDatasource(ds); + } + + public int getNumberOfDatasources() { + Collection flywayContainers = new FlywayContainersSupplier().get(); + return flywayContainers.size(); + } + + private FlywayActionResponse errorNoDatasource(String ds) { + return new FlywayActionResponse("error", "Flyway datasource not found [" + ds + "]"); + } + + private FlywayActionResponse errorNoScript(String ds) { + return new FlywayActionResponse("error", "Missing Flyway initial script for [" + ds + "]"); + } + + private Flyway getFlyway(String ds) { + Collection flywayContainers = new FlywayContainersSupplier().get(); + for (FlywayContainer flywayContainer : flywayContainers) { + if (flywayContainer.getDataSourceName().equals(ds)) { + return flywayContainer.getFlyway(); + } + } + return null; + } + + public static class FlywayDatasource { + public String name; + public boolean hasMigrations; + public boolean createPossible; + + public FlywayDatasource() { + } + + public FlywayDatasource(String name, boolean hasMigrations, boolean createPossible) { + this.name = name; + this.hasMigrations = hasMigrations; + this.createPossible = createPossible; + } + } + + public static class FlywayActionResponse { + public String type; + public String message; + public int number; + public String schema; + public String database; + public List warnings; + + public FlywayActionResponse() { + } + + public FlywayActionResponse(String type, String message) { + this(type, message, -1, null, null, List.of()); + } + + public FlywayActionResponse(String type, String message, int number, String schema, String database) { + this(type, message, number, schema, database, List.of()); + } + + public FlywayActionResponse(String type, String message, int number, String schema, String database, + List warnings) { + this.type = type; + this.message = message; + this.number = number; + this.schema = schema; + this.database = database; + this.warnings = warnings; + } + } +} diff --git a/extensions/flyway-multitenant/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/flyway-multitenant/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..cc2fe5ca69a8c --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,14 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Flyway" +metadata: + keywords: + - "flyway" + - "database" + - "data" + guide: "https://quarkus.io/guides/flyway" + categories: + - "data" + status: "stable" + config: + - "quarkus.flyway." diff --git a/extensions/flyway-multitenant/runtime/src/test/java/io/quarkus/flyway/multitenant/runtime/FlywayCreatorTest.java b/extensions/flyway-multitenant/runtime/src/test/java/io/quarkus/flyway/multitenant/runtime/FlywayCreatorTest.java new file mode 100644 index 0000000000000..eb2c4e9f599ff --- /dev/null +++ b/extensions/flyway-multitenant/runtime/src/test/java/io/quarkus/flyway/multitenant/runtime/FlywayCreatorTest.java @@ -0,0 +1,295 @@ +package io.quarkus.flyway.multitenant.runtime; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.quarkus.flyway.runtime.FlywayCreator; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.configuration.Configuration; +import org.flywaydb.core.api.pattern.ValidatePattern; +import org.flywaydb.core.internal.util.ValidatePatternUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class FlywayCreatorTest { + + private FlywayPersistenceUnitRuntimeConfig runtimeConfig = FlywayPersistenceUnitRuntimeConfig.defaultConfig(); + private FlywayPersistenceUnitBuildTimeConfig buildConfig = FlywayPersistenceUnitBuildTimeConfig.defaultConfig(); + private Configuration defaultConfig = Flyway.configure().load().getConfiguration(); + + /** + * class under test. + */ + private FlywayCreator creator; + + @Test + @DisplayName("locations default matches flyway default") + void testLocationsDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(pathList(defaultConfig.getLocations()), pathList(createdFlywayConfig().getLocations())); + } + + @Test + @DisplayName("locations carried over from configuration") + void testLocationsOverridden() { + buildConfig.locations = Arrays.asList("db/migrations", "db/something"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(buildConfig.locations, pathList(createdFlywayConfig().getLocations())); + } + + @Test + @DisplayName("not configured locations replaced by default") + void testNotPresentLocationsOverridden() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(pathList(defaultConfig.getLocations()), pathList(createdFlywayConfig().getLocations())); + } + + @Test + @DisplayName("baseline description default matches flyway default") + void testBaselineDescriptionDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getBaselineDescription(), createdFlywayConfig().getBaselineDescription()); + } + + @Test + @DisplayName("baseline description carried over from configuration") + void testBaselineDescriptionOverridden() { + runtimeConfig.baselineDescription = Optional.of("baselineDescription"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.baselineDescription.get(), createdFlywayConfig().getBaselineDescription()); + } + + @Test + @DisplayName("baseline version default matches flyway default") + void testBaselineVersionDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getBaselineVersion(), createdFlywayConfig().getBaselineVersion()); + } + + @Test + @DisplayName("baseline version carried over from configuration") + void testBaselineVersionOverridden() { + runtimeConfig.baselineVersion = Optional.of("0.1.2"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.baselineVersion.get(), createdFlywayConfig().getBaselineVersion().getVersion()); + } + + @Test + @DisplayName("connection retries default matches flyway default") + void testConnectionRetriesDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getConnectRetries(), createdFlywayConfig().getConnectRetries()); + } + + @Test + @DisplayName("connection retries carried over from configuration") + void testConnectionRetriesOverridden() { + runtimeConfig.connectRetries = OptionalInt.of(12); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.connectRetries.getAsInt(), createdFlywayConfig().getConnectRetries()); + } + + @Test + @DisplayName("repeatable SQL migration prefix default matches flyway default") + void testRepeatableSqlMigrationPrefixDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getRepeatableSqlMigrationPrefix(), createdFlywayConfig().getRepeatableSqlMigrationPrefix()); + } + + @Test + @DisplayName("repeatable SQL migration prefix carried over from configuration") + void testRepeatableSqlMigrationPrefixOverridden() { + runtimeConfig.repeatableSqlMigrationPrefix = Optional.of("A"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.repeatableSqlMigrationPrefix.get(), createdFlywayConfig().getRepeatableSqlMigrationPrefix()); + } + + @Test + @DisplayName("schemas default matches flyway default") + void testSchemasDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(asList(defaultConfig.getSchemas()), asList(createdFlywayConfig().getSchemas())); + } + + @Test + @DisplayName("schemas carried over from configuration") + void testSchemasOverridden() { + runtimeConfig.schemas = Optional.of(Arrays.asList("TEST_SCHEMA_1", "TEST_SCHEMA_2")); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.schemas.get(), asList(createdFlywayConfig().getSchemas())); + } + + @Test + @DisplayName("SQL migration prefix default matches flyway default") + void testSqlMigrationPrefixDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getSqlMigrationPrefix(), createdFlywayConfig().getSqlMigrationPrefix()); + } + + @Test + @DisplayName("SQL migration prefix carried over from configuration") + void testSqlMigrationPrefixOverridden() { + runtimeConfig.sqlMigrationPrefix = Optional.of("M"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.sqlMigrationPrefix.get(), createdFlywayConfig().getSqlMigrationPrefix()); + } + + @Test + @DisplayName("table default matches flyway default") + void testTableDefault() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(defaultConfig.getTable(), createdFlywayConfig().getTable()); + } + + @Test + @DisplayName("table carried over from configuration") + void testTableOverridden() { + runtimeConfig.table = Optional.of("flyway_history_test_table"); + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.table.get(), createdFlywayConfig().getTable()); + } + + @Test + @DisplayName("validate on migrate default matches to true") + void testValidateOnMigrate() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.validateOnMigrate, createdFlywayConfig().isValidateOnMigrate()); + assertTrue(runtimeConfig.validateOnMigrate); + } + + @Test + @DisplayName("clean disabled default matches to false") + void testCleanDisabled() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.cleanDisabled, createdFlywayConfig().isCleanDisabled()); + assertFalse(runtimeConfig.cleanDisabled); + + runtimeConfig.cleanDisabled = false; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertFalse(createdFlywayConfig().isCleanDisabled()); + + runtimeConfig.cleanDisabled = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(createdFlywayConfig().isCleanDisabled()); + } + + @Test + @DisplayName("outOfOrder is correctly set") + void testOutOfOrder() { + runtimeConfig.outOfOrder = false; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertFalse(createdFlywayConfig().isOutOfOrder()); + + runtimeConfig.outOfOrder = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(createdFlywayConfig().isOutOfOrder()); + } + + @Test + @DisplayName("ignoreMissingMigrations is correctly set") + void testIgnoreMissingMigrations() { + runtimeConfig.ignoreMissingMigrations = false; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertFalse(ValidatePatternUtils.isMissingIgnored(createdFlywayConfig().getIgnoreMigrationPatterns())); + + runtimeConfig.ignoreMissingMigrations = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(ValidatePatternUtils.isMissingIgnored(createdFlywayConfig().getIgnoreMigrationPatterns())); + } + + @Test + @DisplayName("ignoreFutureMigrations is correctly set") + void testIgnoreFutureMigrations() { + runtimeConfig.ignoreFutureMigrations = false; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertFalse(ValidatePatternUtils.isFutureIgnored(createdFlywayConfig().getIgnoreMigrationPatterns())); + + runtimeConfig.ignoreFutureMigrations = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(ValidatePatternUtils.isFutureIgnored(createdFlywayConfig().getIgnoreMigrationPatterns())); + } + + @Test + @DisplayName("cleanOnValidationError defaults to false and is correctly set") + void testCleanOnValidationError() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.cleanOnValidationError, createdFlywayConfig().isCleanOnValidationError()); + assertFalse(runtimeConfig.cleanOnValidationError); + + runtimeConfig.cleanOnValidationError = false; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertFalse(createdFlywayConfig().isCleanOnValidationError()); + + runtimeConfig.cleanOnValidationError = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(createdFlywayConfig().isCleanOnValidationError()); + } + + @ParameterizedTest + @MethodSource("validateOnMigrateOverwritten") + @DisplayName("validate on migrate overwritten in configuration") + void testValidateOnMigrateOverwritten(final boolean input, final boolean expected) { + runtimeConfig.validateOnMigrate = input; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(createdFlywayConfig().isValidateOnMigrate(), expected); + assertEquals(runtimeConfig.validateOnMigrate, expected); + } + + @Test + @DisplayName("validateMigrationNaming defaults to false and it is correctly set") + void testValidateMigrationNaming() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.validateMigrationNaming, createdFlywayConfig().isValidateMigrationNaming()); + assertFalse(runtimeConfig.validateMigrationNaming); + + runtimeConfig.validateMigrationNaming = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(createdFlywayConfig().isValidateMigrationNaming()); + } + + @Test + @DisplayName("validateIgnoreMigrationPatterns defaults to false and it is correctly set") + void testIgnoreMigrationPatterns() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(0, createdFlywayConfig().getIgnoreMigrationPatterns().length); + assertFalse(runtimeConfig.ignoreMigrationPatterns.isPresent()); + + runtimeConfig.ignoreMigrationPatterns = Optional.of(new String[] { "*:missing" }); + creator = new FlywayCreator(runtimeConfig, buildConfig); + final ValidatePattern[] existingIgnoreMigrationPatterns = createdFlywayConfig().getIgnoreMigrationPatterns(); + assertEquals(1, existingIgnoreMigrationPatterns.length); + final String[] ignoreMigrationPatterns = runtimeConfig.ignoreMigrationPatterns.get(); + final ValidatePattern[] validatePatterns = Arrays.stream(ignoreMigrationPatterns) + .map(ValidatePattern::fromPattern).toArray(ValidatePattern[]::new); + assertArrayEquals(validatePatterns, existingIgnoreMigrationPatterns); + } + + private static List pathList(Location[] locations) { + return Stream.of(locations).map(Location::getPath).collect(Collectors.toList()); + } + + private Configuration createdFlywayConfig() { + return creator.createFlyway(null).getConfiguration(); + } + + private static Stream validateOnMigrateOverwritten() { + return Stream. builder() + .add(Arguments.arguments(false, false)) + .add(Arguments.arguments(true, true)) + .build(); + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java index bcdbb76e0361d..a2048955dc810 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java @@ -18,15 +18,16 @@ import io.quarkus.flyway.FlywayConfigurationCustomizer; import io.quarkus.runtime.configuration.ConfigurationException; -class FlywayCreator { +public class FlywayCreator { private static final String[] EMPTY_ARRAY = new String[0]; public static final Duration DEFAULT_CONNECT_RETRIES_INTERVAL = Duration.ofSeconds(120L); - + public static final String TENANT_ID_DEFAULT = "io.quarkus.flyway.runtime.FlywayCreator.NO_TENANT"; private final FlywayDataSourceRuntimeConfig flywayRuntimeConfig; private final FlywayDataSourceBuildTimeConfig flywayBuildTimeConfig; private final List customizers; private Collection callbacks = Collections.emptyList(); + private String tenantId = TENANT_ID_DEFAULT; // only used for tests public FlywayCreator(FlywayDataSourceRuntimeConfig flywayRuntimeConfig, @@ -49,6 +50,11 @@ public FlywayCreator withCallbacks(Collection callbacks) { return this; } + public FlywayCreator withTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + public Flyway createFlyway(DataSource dataSource) { FluentConfiguration configure = Flyway.configure(); @@ -80,7 +86,10 @@ public Flyway createFlyway(DataSource dataSource) { } configure.connectRetriesInterval( (int) flywayRuntimeConfig.connectRetriesInterval.orElse(DEFAULT_CONNECT_RETRIES_INTERVAL).toSeconds()); - if (flywayRuntimeConfig.defaultSchema.isPresent()) { + + if (tenantId != null && !tenantId.equals(TENANT_ID_DEFAULT)) { + configure.defaultSchema(tenantId); + } else if (flywayRuntimeConfig.defaultSchema.isPresent()) { configure.defaultSchema(flywayRuntimeConfig.defaultSchema.get()); } if (flywayRuntimeConfig.schemas.isPresent()) { diff --git a/extensions/pom.xml b/extensions/pom.xml index c11bbaa9679a1..a982e962d2393 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -191,6 +191,7 @@ flyway + flyway-multitenant flyway-postgresql flyway-oracle flyway-mysql diff --git a/independent-projects/vertx-utils/src/main/java/io/quarkus/vertx/utils/VertxOutputStream.java b/independent-projects/vertx-utils/src/main/java/io/quarkus/vertx/utils/VertxOutputStream.java index a12f2a31575c9..48eaca322deb3 100644 --- a/independent-projects/vertx-utils/src/main/java/io/quarkus/vertx/utils/VertxOutputStream.java +++ b/independent-projects/vertx-utils/src/main/java/io/quarkus/vertx/utils/VertxOutputStream.java @@ -73,7 +73,6 @@ public void handle(AsyncResult event) { }); } - private Buffer createBuffer(ByteBuf data) { return new NoBoundChecksBuffer(data); }