diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java new file mode 100644 index 0000000000..1839cf4917 --- /dev/null +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java @@ -0,0 +1,56 @@ +package io.ebeaninternal.dbmigration; + +import java.io.IOException; + +import io.ebean.plugin.Plugin; +import io.ebean.plugin.SpiServer; + +/** + * Plugin to generate db-migration scripts automatically. + * @author Roland Praml, FOCONIS AG + */ +public class DbMigrationPlugin implements Plugin { + + private DefaultDbMigration dbMigration; + + private static String lastMigration; + private static String lastInit; + + @Override + public void configure(SpiServer server) { + dbMigration = new DefaultDbMigration(); + dbMigration.setServer(server); + } + + @Override + public void online(boolean online) { + try { + lastInit = null; + lastMigration = null; + if (dbMigration.generate) { + String tmp = lastMigration = dbMigration.generateMigration(); + if (tmp == null) { + return; + } + } + if (dbMigration.generateInit) { + lastInit = dbMigration.generateInitMigration(); + } + } catch (IOException e) { + throw new RuntimeException("Error while generating migration", e); + } + } + + @Override + public void shutdown() { + dbMigration = null; + } + + public static String getLastInit() { + return lastInit; + } + + public static String getLastMigration() { + return lastMigration; + } +} diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java index c44c6da5f4..0338c5c36f 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java @@ -1,6 +1,7 @@ package io.ebeaninternal.dbmigration; import io.avaje.applog.AppLog; +import io.avaje.classpath.scanner.core.Location; import io.ebean.DB; import io.ebean.Database; import io.ebean.DatabaseBuilder; @@ -10,6 +11,7 @@ import io.ebean.config.dbplatform.DatabasePlatformProvider; import io.ebean.dbmigration.DbMigration; import io.ebean.util.IOUtils; +import io.ebean.util.StringHelper; import io.ebeaninternal.api.DbOffline; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions; @@ -24,10 +26,7 @@ import java.io.File; import java.io.IOException; import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.ServiceLoader; +import java.util.*; import static io.ebeaninternal.api.PlatformMatch.matchPlatform; import static java.lang.System.Logger.Level.*; @@ -59,8 +58,8 @@ public class DefaultDbMigration implements DbMigration { private static final String initialVersion = "1.0"; private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY"; - private final List platformProviders = new ArrayList<>(); - protected final boolean online; + private List platformProviders = new ArrayList<>(); + protected boolean online; private boolean logToSystemOut = true; protected SpiEbeanServer server; protected String pathToResources = "src/main/resources"; @@ -75,8 +74,10 @@ public class DefaultDbMigration implements DbMigration { protected List platforms = new ArrayList<>(); protected DatabaseBuilder.Settings databaseBuilder; protected DbConstraintNaming constraintNaming; + @Deprecated protected Boolean strictMode; - protected Boolean includeGeneratedFileComment; + protected boolean includeGeneratedFileComment; + @Deprecated protected String header; protected String applyPrefix = ""; protected String version; @@ -86,6 +87,9 @@ public class DefaultDbMigration implements DbMigration { private int lockTimeoutSeconds; protected boolean includeBuiltInPartitioning = true; protected boolean includeIndex; + protected boolean generate = false; + protected boolean generateInit = false; + private boolean keepLastInit = true; /** * Create for offline migration generation. @@ -122,12 +126,66 @@ public void setServerConfig(DatabaseBuilder builder) { if (constraintNaming == null) { this.constraintNaming = databaseBuilder.getConstraintNaming(); } + if (databasePlatform == null) { + this.databasePlatform = databaseBuilder.getDatabasePlatform(); + } Properties properties = config.getProperties(); if (properties != null) { - PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, null); + PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, config.getClassLoadConfig()); migrationPath = props.get("migration.migrationPath", migrationPath); migrationInitPath = props.get("migration.migrationInitPath", migrationInitPath); pathToResources = props.get("migration.pathToResources", pathToResources); + addForeignKeySkipCheck = props.getBoolean("migration.addForeignKeySkipCheck", addForeignKeySkipCheck); + applyPrefix = props.get("migration.applyPrefix", applyPrefix); + databasePlatform = props.createInstance(DatabasePlatform.class, "migration.databasePlatform", databasePlatform); + generatePendingDrop = props.get("migration.generatePendingDrop", generatePendingDrop); + includeBuiltInPartitioning = props.getBoolean("migration.includeBuiltInPartitioning", includeBuiltInPartitioning); + includeGeneratedFileComment = props.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment); + includeIndex = props.getBoolean("migration.includeIndex", includeIndex); + lockTimeoutSeconds = props.getInt("migration.lockTimeoutSeconds", lockTimeoutSeconds); + logToSystemOut = props.getBoolean("migration.logToSystemOut", logToSystemOut); + modelPath = props.get("migration.modelPath", modelPath); + modelSuffix = props.get("migration.modelSuffix", modelSuffix); + name = props.get("migration.name", name); + online = props.getBoolean("migration.online", online); + vanillaPlatform = props.getBoolean("migration.vanillaPlatform", vanillaPlatform); + version = props.get("migration.version", version); + generate = props.getBoolean("migration.generate", generate); + generateInit = props.getBoolean("migration.generateInit", generateInit); + // header & strictMode must be configured at DatabaseConfig level + parsePlatforms(props, config); + } + } + + protected void parsePlatforms(PropertiesWrapper props, DatabaseBuilder.Settings config) { + String platforms = props.get("migration.platforms"); + if (platforms == null || platforms.isEmpty()) { + return; + } + String[] tmp = StringHelper.splitNames(platforms); + for (String plat : tmp) { + DatabasePlatform dbPlatform; + String platformName = plat; + String platformPrefix = null; + int pos = plat.indexOf('='); + if (pos != -1) { + platformName = plat.substring(0, pos); + platformPrefix = plat.substring(pos + 1); + } + + if (platformName.indexOf('.') == -1) { + // parse platform as enum value + Platform platform = Enum.valueOf(Platform.class, platformName.toUpperCase()); + dbPlatform = platform(platform); + } else { + // parse platform as class + dbPlatform = (DatabasePlatform) config.getClassLoadConfig().newInstance(platformName); + } + if (platformPrefix == null) { + platformPrefix = dbPlatform.platform().name().toLowerCase(); + } + + addDatabasePlatform(dbPlatform, platformPrefix); } } @@ -318,7 +376,18 @@ private String generateMigrationFor(boolean initMigration) throws IOException { } String pendingVersion = generatePendingDrop(); - if (pendingVersion != null) { + if ("auto".equals(pendingVersion)) { + StringJoiner sj = new StringJoiner(","); + String diff = generateDiff(request); + if (diff != null) { + sj.add(diff); + request = createRequest(initMigration); + } + for (String pendingDrop : request.getPendingDrops()) { + sj.add(generatePendingDrop(request, pendingDrop)); + } + return sj.length() == 0 ? null : sj.toString(); + } else if (pendingVersion != null) { return generatePendingDrop(request, pendingVersion); } else { return generateDiff(request); @@ -553,7 +622,7 @@ private String generateMigration(Request request, Migration dbMigration, String return null; } else { if (!platforms.isEmpty()) { - writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir); + writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir, request.initMigration && keepLastInit); } else if (databasePlatform != null) { // writer needs the current model to provide table/column details for @@ -633,12 +702,17 @@ private String toUnderScore(String name) { /** * Write any extra platform ddl. */ - private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { + private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath, boolean clear) throws IOException { DdlOptions options = new DdlOptions(addForeignKeySkipCheck); for (Pair pair : platforms) { DdlWrite writer = new DdlWrite(new MConfiguration(), currentModel.read(), options); PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); File subPath = platformWriter.subPath(writePath, pair.prefix); + if (clear) { + for (File existing : subPath.listFiles()) { + existing.delete(); + } + } platformWriter.processMigration(dbMigration, writer, subPath, fullVersion); } } @@ -656,7 +730,7 @@ private boolean writeMigrationXml(Migration dbMigration, File resourcePath, Stri if (file.exists()) { return false; } - String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null; + String comment = includeGeneratedFileComment ? GENERATED_COMMENT : null; MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); xmlWriter.write(dbMigration, file); return true; @@ -674,11 +748,14 @@ private void setDefaults() { databasePlatform = server.databasePlatform(); } if (databaseBuilder != null) { + // FIXME: StrictMode and header may be defined HERE and in DatabaseConfig. + // We shoild change either DefaultDbMigration or databaseConfig, so that it is only + // defined on one place if (strictMode != null) { - databaseBuilder.setDdlStrictMode(strictMode); + databaseBuilder.ddlStrictMode(strictMode); } if (header != null) { - databaseBuilder.setDdlHeader(header); + databaseBuilder.ddlHeader(header); } } } @@ -748,15 +825,20 @@ public File migrationDirectory() { * Return the file path to write the xml and sql to. */ File migrationDirectory(boolean initMigration) { - // path to src/main/resources in typical maven project - File resourceRootDir = new File(pathToResources); - if (!resourceRootDir.exists()) { - String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); - throw new UnknownResourcePathException(msg); - } - String resourcePath = migrationPath(initMigration); + Location resourcePath = migrationPath(initMigration); // expect to be a path to something like - src/main/resources/dbmigration - File path = new File(resourceRootDir, resourcePath); + File path; + if (resourcePath.isClassPath()) { + // path to src/main/resources in typical maven project + File resourceRootDir = new File(pathToResources); + if (!resourceRootDir.exists()) { + String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); + throw new UnknownResourcePathException(msg); + } + path = new File(resourceRootDir, resourcePath.path()); + } else { + path = new File(resourcePath.path()); + } if (!path.exists()) { if (!path.mkdirs()) { logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath()); @@ -765,8 +847,9 @@ File migrationDirectory(boolean initMigration) { return path; } - private String migrationPath(boolean initMigration) { - return initMigration ? migrationInitPath : migrationPath; + private Location migrationPath(boolean initMigration) { + // remove classpath: or filesystem: prefix + return new Location(initMigration ? migrationInitPath : migrationPath); } /** diff --git a/ebean-ddl-generator/src/main/java/module-info.java b/ebean-ddl-generator/src/main/java/module-info.java index 50a8c62311..47e9d4aa78 100644 --- a/ebean-ddl-generator/src/main/java/module-info.java +++ b/ebean-ddl-generator/src/main/java/module-info.java @@ -1,5 +1,6 @@ module io.ebean.ddl.generator { + uses io.ebean.plugin.Plugin; exports io.ebean.dbmigration; provides io.ebean.dbmigration.DbMigration with io.ebeaninternal.dbmigration.DefaultDbMigration; diff --git a/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin new file mode 100644 index 0000000000..83ec94df48 --- /dev/null +++ b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin @@ -0,0 +1 @@ +io.ebeaninternal.dbmigration.DbMigrationPlugin diff --git a/ebean-ddl-generator/src/test/resources/application-test.properties b/ebean-ddl-generator/src/test/resources/application-test.properties index dfcb052ab0..64e7f78869 100644 --- a/ebean-ddl-generator/src/test/resources/application-test.properties +++ b/ebean-ddl-generator/src/test/resources/application-test.properties @@ -10,27 +10,3 @@ datasource.h2.url=jdbc:h2:mem:h2AutoTune datasource.pg.username=sa datasource.pg.password= datasource.pg.url=jdbc:h2:mem:h2AutoTune - -# parameters for migration test -datasource.migrationtest.username=SA -datasource.migrationtest.password=SA -datasource.migrationtest.url=jdbc:h2:mem:migration -ebean.migrationtest.applyPrefix=V -ebean.migrationtest.ddl.generate=false -ebean.migrationtest.ddl.run=false -ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest -ebean.migrationtest.migration.appName=migrationtest -ebean.migrationtest.migration.migrationPath=dbmigration/migrationtest -ebean.migrationtest.migration.strict=true - -# parameters for migration test -datasource.migrationtest-history.username=SA -datasource.migrationtest-history.password=SA -datasource.migrationtest-history.url=jdbc:h2:mem:migration -ebean.migrationtest-history.applyPrefix=V -ebean.migrationtest-history.ddl.generate=false -ebean.migrationtest-history.ddl.run=false -ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest -ebean.migrationtest-history.migration.appName=migrationtest-history -ebean.migrationtest-history.migration.migrationPath=dbmigration/migrationtest-history -ebean.migrationtest-history.migration.strict=true diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java index f1657ca272..b6cb57149d 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java @@ -80,13 +80,11 @@ public static void main(String[] args) throws IOException { List pendingDrops = migration.getPendingDrops(); assertThat(pendingDrops).contains("1.1"); - //System.setProperty("ddl.migration.pendingDropsFor", "1.1"); migration.setGeneratePendingDrop("1.1"); assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); assertThatThrownBy(()->migration.generateMigration()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("No 'pendingDrops'"); // subsequent call - System.clearProperty("ddl.migration.pendingDropsFor"); server.shutdown(); logger.info("end"); diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java index 6d19a976b6..3328cfb417 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java @@ -5,6 +5,8 @@ import io.ebean.annotation.Platform; import io.ebean.DatabaseBuilder; import io.ebean.config.DatabaseConfig; +import io.ebeaninternal.api.DbOffline; + import io.ebean.dbmigration.DbMigration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -18,7 +20,6 @@ import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** @@ -97,7 +98,7 @@ public static void run(String pathToResources) throws IOException { config.getProperties().put("ebean.hana.generateUniqueDdl", "true"); // need to generate unique statements to prevent them from being filtered out as duplicates by the DdlRunner config.setPackages(Arrays.asList("misc.migration.v1_0")); - Database server = DatabaseFactory.create(config); + Database server = createServer(config); migration.setServer(server); // then we generate migration scripts for v1_0 @@ -108,43 +109,28 @@ public static void run(String pathToResources) throws IOException { // and now for v1_1 config.setPackages(Arrays.asList("misc.migration.v1_1")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.1"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - - System.setProperty("ddl.migration.pendingDropsFor", "1.1"); - assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); - - assertThatThrownBy(()->migration.generateMigration()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.1,1.2__dropsFor_1.1"); assertThat(migration.generateMigration()).isNull(); // subsequent call // and now for v1_2 with config.setPackages(Arrays.asList("misc.migration.v1_2")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.3"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - System.setProperty("ddl.migration.pendingDropsFor", "1.3"); - assertThat(migration.generateMigration()).isEqualTo("1.4__dropsFor_1.3"); - assertThatThrownBy(migration::generateMigration) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.3,1.4__dropsFor_1.3"); assertThat(migration.generateMigration()).isNull(); // subsequent call server.shutdown(); logger.info("end"); } + private static Database createServer(DatabaseConfig config) { + DbOffline.setGenerateMigration(); + Database server = DatabaseFactory.create(config); + DbOffline.reset(); + return server; + } + } diff --git a/ebean-test/src/test/resources/ebean.properties b/ebean-test/src/test/resources/ebean.properties index 6e3485c75b..444b2ad0a2 100644 --- a/ebean-test/src/test/resources/ebean.properties +++ b/ebean-test/src/test/resources/ebean.properties @@ -199,6 +199,7 @@ datasource.hana.username=EBEAN_TEST datasource.hana.password=Eb3an_test datasource.hana.url=jdbc:sap://hxehost:39013/?databaseName=HXE #datasource.hana.driver=com.sap.db.jdbc.Driver +# # parameters for migration test datasource.migrationtest.username=SA @@ -210,6 +211,21 @@ ebean.migrationtest.ddl.run=false ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest ebean.migrationtest.migration.appName=migrationtest ebean.migrationtest.migration.migrationPath=migrationtest/dbmigration +ebean.migrationtest.migration.migrationInitPath=migrationtest/dbinit +ebean.migrationtest.migration.strict=true +ebean.migrationtest.migration.generate=true +ebean.migrationtest.migration.run=false +ebean.migrationtest.migration.includeIndex=true +ebean.migrationtest.migration.generateInit=true +ebean.migrationtest.migration.generatePendingDrop=auto +ebean.migrationtest.migration.platforms=db2luw,h2,hsqldb,mysql,mysql55,mariadb,postgres,oracle,sqlite,sqlserver17,hana,yugabyte +#migration.migrationtest.db2luw.prefix=db2 +#migration.migrationtest.sqlserver17.prefix=sqlserver +dbmigration.platform.mariadb.useMigrationStoredProcedures=true +dbmigration.platform.mysql.useMigrationStoredProcedures=true + + + ebean.migrationtest.migration.strict=true # enable stored procedures f dbmigration.platform.mariadb.useMigrationStoredProcedures=true @@ -225,4 +241,5 @@ ebean.migrationtest-history.ddl.run=false ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest ebean.migrationtest-history.migration.appName=migrationtest-history ebean.migrationtest-history.migration.migrationPath=migrationtest-history/dbmigration +ebean.migrationtest-history.migration.migrationInitPath=migrationtest-history/dbinit ebean.migrationtest-history.migration.strict=true