diff --git a/extensions/agroal/spi/src/main/java/io/quarkus/agroal/spi/JdbcUpdateSQLGeneratorBuildItem.java b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/spi/JdbcUpdateSQLGeneratorBuildItem.java new file mode 100644 index 0000000000000..37d897c99300b --- /dev/null +++ b/extensions/agroal/spi/src/main/java/io/quarkus/agroal/spi/JdbcUpdateSQLGeneratorBuildItem.java @@ -0,0 +1,24 @@ +package io.quarkus.agroal.spi; + +import java.util.function.Supplier; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class JdbcUpdateSQLGeneratorBuildItem extends MultiBuildItem { + + final String databaseName; + final Supplier sqlSupplier; + + public JdbcUpdateSQLGeneratorBuildItem(String databaseName, Supplier sqlSupplier) { + this.databaseName = databaseName; + this.sqlSupplier = sqlSupplier; + } + + public String getDatabaseName() { + return databaseName; + } + + public Supplier getSqlSupplier() { + return sqlSupplier; + } +} diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java index 90dd8c6ad9e19..e25464262b309 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java @@ -8,6 +8,7 @@ import java.util.function.Supplier; import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem; +import io.quarkus.agroal.spi.JdbcUpdateSQLGeneratorBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -24,17 +25,23 @@ public class FlywayDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) @Record(value = RUNTIME_INIT, optional = true) CardPageBuildItem create(FlywayDevUIRecorder recorder, FlywayBuildTimeConfig buildTimeConfig, - List generatorBuildItem, + List createGeneratorBuildItem, + List updateGeneratorBuildItem, CurateOutcomeBuildItem curateOutcomeBuildItem) { Map> initialSqlSuppliers = new HashMap<>(); - for (JdbcInitialSQLGeneratorBuildItem buildItem : generatorBuildItem) { + for (JdbcInitialSQLGeneratorBuildItem buildItem : createGeneratorBuildItem) { initialSqlSuppliers.put(buildItem.getDatabaseName(), buildItem.getSqlSupplier()); } + Map> updateSqlSuppliers = new HashMap<>(); + for (JdbcUpdateSQLGeneratorBuildItem buildItem : updateGeneratorBuildItem) { + updateSqlSuppliers.put(buildItem.getDatabaseName(), buildItem.getSqlSupplier()); + } + String artifactId = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getArtifactId(); - recorder.setInitialSqlSuppliers(initialSqlSuppliers, artifactId); + recorder.setSqlSuppliers(initialSqlSuppliers, updateSqlSuppliers, artifactId); CardPageBuildItem card = new CardPageBuildItem(); diff --git a/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js b/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js index 0057a64087972..2eb3087ae537a 100644 --- a/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js +++ b/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js @@ -76,6 +76,7 @@ export class QwcFlywayDatasources extends QwcHotReloadElement { _actionRenderer(ds) { return html`${this._renderMigrationButtons(ds)} + ${this._renderUpdateButton(ds)} ${this._renderCreateButton(ds)}`; } @@ -90,7 +91,16 @@ export class QwcFlywayDatasources extends QwcHotReloadElement { `; } } - + + _renderUpdateButton(ds) { + if(ds.hasMigrations){ + return html` + this._showCreateDialog(ds)} class="button" title="Create update migration file. Always manually review the created file as it can cause data loss"> + Generate Migration File + `; + } + } + _renderCreateButton(ds) { if(ds.createPossible){ return html` diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeUpdateFromHibernateTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeUpdateFromHibernateTest.java new file mode 100644 index 0000000000000..9a337f364138d --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeUpdateFromHibernateTest.java @@ -0,0 +1,73 @@ +package io.quarkus.flyway.test; + +import java.io.File; +import java.nio.file.Files; +import java.util.Map; + +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.dev.console.DevConsoleManager; +import io.quarkus.devui.tests.DevUIJsonRPCTest; +import io.quarkus.test.QuarkusDevModeTest; + +public class FlywayDevModeUpdateFromHibernateTest extends DevUIJsonRPCTest { + + public FlywayDevModeUpdateFromHibernateTest() { + super("io.quarkus.quarkus-flyway"); + } + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(FlywayDevModeUpdateFromHibernateTest.class, Fruit.class) + .addAsResource(new StringAsset( + " create sequence Fruit_SEQ start with 1 increment by 50;\n" + + "\n" + + " create table Fruit (\n" + + " id integer not null,\n" + + " primary key (id)\n" + + " );"), + "db/update/V2.0.0__Quarkus.sql") + .addAsResource(new StringAsset( + "quarkus.flyway.migrate-at-start=true\nquarkus.flyway.locations=db/update"), + "application.properties")); + + @Test + public void testGenerateMigrationFromHibernate() throws Exception { + + Map params = Map.of("ds", ""); + JsonNode devuiresponse = super.executeJsonRPCMethod("update", params); + + Assertions.assertNotNull(devuiresponse); + String type = devuiresponse.get("type").asText(); + Assertions.assertNotNull(type); + Assertions.assertEquals("success", type); + + File migrationsDir = DevConsoleManager.getHotReplacementContext().getResourcesDir().get(0).resolve("db/update") + .toFile(); + File[] newMigrations = migrationsDir.listFiles((dir, name) -> !name.equals("V2.0.0__Quarkus.sql")); + Assertions.assertNotNull(newMigrations); + Assertions.assertEquals(1, newMigrations.length); + Assertions.assertTrue(newMigrations[0].getName().startsWith("V2.")); + + String content = Files.readString(newMigrations[0].toPath()); + + Assertions.assertEquals("\n" + + " alter table if exists Fruit \n" + + " add column name varchar(40);\n" + + "\n" + + " alter table if exists Fruit \n" + + " drop constraint if exists UKqn1mp5t3oovyl0h02glapi2iv;\n" + + "\n" + + " alter table if exists Fruit \n" + + " add constraint UKqn1mp5t3oovyl0h02glapi2iv unique (name);\n", content); + } + +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayDevUIRecorder.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayDevUIRecorder.java index eab797073aa3d..438fa155f05ee 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayDevUIRecorder.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayDevUIRecorder.java @@ -10,9 +10,11 @@ @Recorder public class FlywayDevUIRecorder { - public RuntimeValue setInitialSqlSuppliers(Map> initialSqlSuppliers, String artifactId) { + public RuntimeValue setSqlSuppliers(Map> initialSqlSuppliers, + Map> updateSuppliers, String artifactId) { FlywayJsonRpcService rpcService = Arc.container().instance(FlywayJsonRpcService.class).get(); rpcService.setInitialSqlSuppliers(initialSqlSuppliers); + rpcService.setUpdateSqlSuppliers(updateSuppliers); rpcService.setArtifactId(artifactId); return new RuntimeValue<>(true); } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayJsonRpcService.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayJsonRpcService.java index 11ca2f9d8e090..f28cacb39cea9 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayJsonRpcService.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayJsonRpcService.java @@ -2,8 +2,11 @@ import static java.util.List.of; +import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -24,6 +27,7 @@ public class FlywayJsonRpcService { private Map> initialSqlSuppliers; + private Map> updateSqlSuppliers; private String artifactId; private Map datasources; @@ -34,6 +38,10 @@ public void setInitialSqlSuppliers(Map> initialSqlSuppl this.initialSqlSuppliers = initialSqlSuppliers; } + public void setUpdateSqlSuppliers(Map> updateSqlSuppliers) { + this.updateSqlSuppliers = updateSqlSuppliers; + } + public void setArtifactId(String artifactId) { this.artifactId = artifactId; } @@ -161,6 +169,50 @@ public FlywayActionResponse create(String ds) { return errorNoDatasource(ds); } + public FlywayActionResponse update(String ds) { + try { + Supplier found = updateSqlSuppliers.get(ds); + if (found == null) { + return new FlywayActionResponse("error", "Unable to find SQL Update generator"); + } + String script = found.get(); + if (script == null) { + return new FlywayActionResponse("error", "Missing Flyway update script for [" + ds + "]"); + } + Flyway flyway = getFlyway(ds); + if (flyway == null) { + return errorNoDatasource(ds); + } + 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); + SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd.HHmmss"); + String timestamp = format.format(new Timestamp(System.currentTimeMillis())); + BigInteger major = flyway.info().current().isVersioned() ? flyway.info().current().getVersion().getMajor() + : BigInteger.ONE; + Path file = migrationDir.resolve( + "V" + major + "." + timestamp + "__" + artifactId + ".sql"); + + Files.writeString(file, script); + + return new FlywayActionResponse("success", + "migration created"); + + } catch (Throwable t) { + return new FlywayActionResponse("error", t.getMessage()); + } + } + public int getNumberOfDatasources() { Collection flywayContainers = new FlywayContainersSupplier().get(); return flywayContainers.size(); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevUIProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevUIProcessor.java index 8db0f4d3084a5..d702b9314a299 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevUIProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevUIProcessor.java @@ -3,6 +3,7 @@ import java.util.List; import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem; +import io.quarkus.agroal.spi.JdbcUpdateSQLGeneratorBuildItem; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.IsDevelopment; @@ -16,6 +17,7 @@ import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem; import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; import io.quarkus.hibernate.orm.runtime.dev.HibernateOrmDevInfoCreateDDLSupplier; +import io.quarkus.hibernate.orm.runtime.dev.HibernateOrmDevInfoUpdateDDLSupplier; import io.quarkus.hibernate.orm.runtime.dev.HibernateOrmDevJsonRpcService; @BuildSteps(onlyIf = { HibernateOrmEnabled.class, IsDevelopment.class }) @@ -69,4 +71,15 @@ void handleInitialSql(List persistenceUnitDe } } + @BuildStep + void handleUpdateSql(List persistenceUnitDescriptorBuildItems, + BuildProducer updateSQLGeneratorBuildItemBuildProducer) { + for (PersistenceUnitDescriptorBuildItem puDescriptor : persistenceUnitDescriptorBuildItems) { + String puName = puDescriptor.getPersistenceUnitName(); + String dsName = puDescriptor.getConfig().getDataSource().orElse(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME); + updateSQLGeneratorBuildItemBuildProducer + .produce(new JdbcUpdateSQLGeneratorBuildItem(dsName, new HibernateOrmDevInfoUpdateDDLSupplier(puName))); + } + } + } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/dev/HibernateOrmDevInfoUpdateDDLSupplier.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/dev/HibernateOrmDevInfoUpdateDDLSupplier.java new file mode 100644 index 0000000000000..478b7888514e1 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/dev/HibernateOrmDevInfoUpdateDDLSupplier.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.orm.runtime.dev; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +import io.quarkus.runtime.annotations.RecordableConstructor; + +public class HibernateOrmDevInfoUpdateDDLSupplier implements Supplier { + + private final String puName; + + @RecordableConstructor + public HibernateOrmDevInfoUpdateDDLSupplier(String puName) { + this.puName = puName; + } + + @Override + public String get() { + Collection persistenceUnits = HibernateOrmDevController.get() + .getInfo().getPersistenceUnits(); + for (var p : persistenceUnits) { + if (Objects.equals(puName, p.getName())) { + return p.getUpdateDDL(); + } + } + return null; + } + + public String getPuName() { + return puName; + } +} 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); }