diff --git a/pom.xml b/pom.xml index fa66ecb43..7a8bc375f 100644 --- a/pom.xml +++ b/pom.xml @@ -135,7 +135,7 @@ io.ebean ebean-migration - 11.11.1 + 11.12.1 diff --git a/src/main/java/io/ebean/config/DbMigrationConfig.java b/src/main/java/io/ebean/config/DbMigrationConfig.java index cdbb3a416..8946d4000 100644 --- a/src/main/java/io/ebean/config/DbMigrationConfig.java +++ b/src/main/java/io/ebean/config/DbMigrationConfig.java @@ -5,7 +5,6 @@ import io.ebean.annotation.Platform; import io.ebean.migration.MigrationConfig; import io.ebean.migration.MigrationRunner; import io.ebean.util.StringHelper; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +32,8 @@ public class DbMigrationConfig { */ protected String migrationPath = "dbmigration"; + protected String migrationInitPath = "dbinit"; + /** * Subdirectory the model xml files go into. */ @@ -118,6 +119,15 @@ public class DbMigrationConfig { this.platform = platform; } + /** + * Return the path for normal migrations or dbinit migrations. + * + * @param dbinitMigration When true return the path for dbinit migrations. + */ + public String getMigrationPath(boolean dbinitMigration) { + return dbinitMigration ? migrationInitPath : migrationPath; + } + /** * Return the resource path for db migrations. */ @@ -419,6 +429,7 @@ public class DbMigrationConfig { public void loadSettings(PropertiesWrapper properties, String serverName) { migrationPath = properties.get("migration.migrationPath", migrationPath); + migrationInitPath = properties.get("migration.migrationInitPath", migrationInitPath); modelPath = properties.get("migration.modelPath", modelPath); applyPrefix = properties.get("migration.applyPrefix", applyPrefix); applySuffix = properties.get("migration.applySuffix", applySuffix); @@ -472,6 +483,7 @@ public class DbMigrationConfig { runnerConfig.setMetaTable(metaTable); runnerConfig.setApplySuffix(applySuffix); runnerConfig.setMigrationPath(migrationPath); + runnerConfig.setMigrationInitPath(migrationInitPath); runnerConfig.setRunPlaceholderMap(runPlaceholderMap); runnerConfig.setRunPlaceholders(runPlaceholders); runnerConfig.setDbUsername(getDbUsername()); diff --git a/src/main/java/io/ebean/dbmigration/DbMigration.java b/src/main/java/io/ebean/dbmigration/DbMigration.java index 8a9f3f427..2d9018806 100644 --- a/src/main/java/io/ebean/dbmigration/DbMigration.java +++ b/src/main/java/io/ebean/dbmigration/DbMigration.java @@ -154,7 +154,7 @@ public interface DbMigration { List getPendingDrops(); /** - * Generate the next migration xml file and associated apply and rollback sql scripts. + * Generate the next migration sql script and associated model xml. *

* This does not run the migration or ddl scripts but just generates them. *

@@ -186,4 +186,15 @@ public interface DbMigration { * @return the version of the generated migration or null */ String generateMigration() throws IOException; + + /** + * Generate an "init" migration which has all changes. + *

+ * An "init" migration can only be executed and used on a database that has had no + * prior migrations run on it. + *

+ * @return the version of the generated migration + */ + String generateInitMigration() throws IOException; + } diff --git a/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java b/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java index a85e3f651..bf92237dc 100644 --- a/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java +++ b/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java @@ -271,6 +271,15 @@ public class DefaultDbMigration implements DbMigration { */ @Override public String generateMigration() throws IOException { + return generateMigrationFor(false); + } + + @Override + public String generateInitMigration() throws IOException { + return generateMigrationFor(true); + } + + private String generateMigrationFor(boolean dbinitMigration) throws IOException { // use this flag to stop other plugins like full DDL generation if (!online) { @@ -286,14 +295,17 @@ public class DefaultDbMigration implements DbMigration { configurePlatforms(); } try { - Request request = createRequest(); - if (platforms.isEmpty()) { - generateExtraDdl(request.migrationDir, databasePlatform, request.isTablePartitioning()); - } else { - for (Pair pair : platforms) { - PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); - File subPath = platformWriter.subPath(request.migrationDir, pair.prefix); - generateExtraDdl(subPath, pair.platform, request.isTablePartitioning()); + Request request = createRequest(dbinitMigration); + if (!dbinitMigration) { + // repeatable migrations + if (platforms.isEmpty()) { + generateExtraDdl(request.migrationDir, databasePlatform, request.isTablePartitioning()); + } else { + for (Pair pair : platforms) { + PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); + File subPath = platformWriter.subPath(request.migrationDir, pair.prefix); + generateExtraDdl(subPath, pair.platform, request.isTablePartitioning()); + } } } @@ -321,7 +333,7 @@ public class DefaultDbMigration implements DbMigration { } setDefaults(); try { - return createRequest().getPendingDrops(); + return createRequest(false).getPendingDrops(); } finally { if (!online) { DbOffline.reset(); @@ -433,32 +445,48 @@ public class DefaultDbMigration implements DbMigration { return version; } - private Request createRequest() { - return new Request(); + private Request createRequest(boolean dbinitMigration) { + return new Request(dbinitMigration); } private class Request { + final boolean dbinitMigration; final File migrationDir; final File modelDir; - final MigrationModel migrationModel; final CurrentModel currentModel; final ModelContainer migrated; final ModelContainer current; - private Request() { - this.migrationDir = getMigrationDirectory(); - this.modelDir = getModelDirectory(migrationDir); - this.migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix()); - this.migrated = migrationModel.read(); + private Request(boolean dbinitMigration) { + this.dbinitMigration = dbinitMigration; this.currentModel = new CurrentModel(server, constraintNaming); this.current = currentModel.read(); + this.migrationDir = getMigrationDirectory(dbinitMigration); + if (dbinitMigration) { + this.modelDir = null; + this.migrated = new ModelContainer(); + } else { + this.modelDir = getModelDirectory(migrationDir); + MigrationModel migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix()); + this.migrated = migrationModel.read(dbinitMigration); + } } boolean isTablePartitioning() { return current.isTablePartitioning(); } + /** + * Return the next migration version (based on existing migration versions). + */ + String nextVersion() { + // always read the next version using the main migration directory (not dbinit) + File migDirectory = getMigrationDirectory(false); + File modelDir = getModelDirectory(migDirectory); + return LastMigration.nextVersion(migDirectory, modelDir, dbinitMigration); + } + /** * Return the migration for the pending drops for a given version. */ @@ -490,10 +518,10 @@ public class DefaultDbMigration implements DbMigration { private String generateMigration(Request request, Migration dbMigration, String dropsFor) throws IOException { - String fullVersion = getFullVersion(request.migrationModel, dropsFor); + String fullVersion = getFullVersion(request.nextVersion(), dropsFor); logger.info("generating migration:{}", fullVersion); - if (!writeMigrationXml(dbMigration, request.modelDir, fullVersion)) { + if (!request.dbinitMigration && !writeMigrationXml(dbMigration, request.modelDir, fullVersion)) { logger.warn("migration already exists, not generating DDL"); return null; } else { @@ -528,11 +556,11 @@ public class DefaultDbMigration implements DbMigration { *

* The full version can contain a comment suffix after a "__" double underscore. */ - private String getFullVersion(MigrationModel migrationModel, String dropsFor) { + private String getFullVersion(String nextVersion, String dropsFor) { String version = getVersion(); if (version == null) { - version = migrationModel.getNextVersion(initialVersion); + version = (nextVersion != null) ? nextVersion : initialVersion; } String fullVersion = migrationConfig.getApplyPrefix() + version; @@ -672,13 +700,20 @@ public class DefaultDbMigration implements DbMigration { } /** - * Return the file path to write the xml and sql to. + * Return the main migration directory. */ File getMigrationDirectory() { + return getMigrationDirectory(false); + } + + /** + * Return the file path to write the xml and sql to. + */ + File getMigrationDirectory(boolean dbinitMigration) { // path to src/main/resources in typical maven project File resourceRootDir = new File(pathToResources); - String resourcePath = migrationConfig.getMigrationPath(); + String resourcePath = migrationConfig.getMigrationPath(dbinitMigration); // expect to be a path to something like - src/main/resources/dbmigration/model File path = new File(resourceRootDir, resourcePath); diff --git a/src/main/java/io/ebeaninternal/dbmigration/LastMigration.java b/src/main/java/io/ebeaninternal/dbmigration/LastMigration.java new file mode 100644 index 000000000..102eb87a9 --- /dev/null +++ b/src/main/java/io/ebeaninternal/dbmigration/LastMigration.java @@ -0,0 +1,91 @@ +package io.ebeaninternal.dbmigration; + +import io.ebeaninternal.dbmigration.model.MigrationVersion; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility to determine the last sql migration version and next version. + */ +class LastMigration { + + private static final String SQL = ".sql"; + + private static final String MODEL_XML = ".model.xml"; + + /** + * Return the next migation version given the migration directory. + */ + static String nextVersion(File migDir, File modelDir, boolean dbinitMigration) { + + String last = lastVersion(migDir, modelDir); + if (last == null) { + return null; + } + return (dbinitMigration) ? last : MigrationVersion.parse(last).nextVersion(); + } + + /** + * Return the last migation version given the migration directory. + */ + static String lastVersion(File migDirectory, File modelDir) { + + List fileNames = new ArrayList<>(); + + File[] sqlFiles = migDirectory.listFiles(pathname -> includeSqlFile(pathname.getName().toLowerCase())); + if (sqlFiles != null) { + for (File file : sqlFiles) { + fileNames.add(trim(file.getName())); + } + } + + if (modelDir != null) { + File[] xmlFiles = modelDir.listFiles(pathname -> includeModelFile(pathname.getName().toLowerCase())); + if (xmlFiles != null) { + for (File file : xmlFiles) { + fileNames.add(trim(file.getName())); + } + } + } + + Collections.sort(fileNames); + if (!fileNames.isEmpty()) { + return fileNames.get(fileNames.size() - 1); + } + return null; + } + + private static boolean includeSqlFile(String lowerFileName) { + if (lowerFileName.startsWith("r") || lowerFileName.startsWith("i") || !lowerFileName.endsWith(SQL)) { + return false; + } + return true; + } + + private static boolean includeModelFile(String lowerFileName) { + return lowerFileName.endsWith(MODEL_XML); + } + + private static String trim(String name) { + name = name.toLowerCase(); + char c = name.charAt(0); + if (c == 'v') { + name = name.substring(1); + } + int p = name.indexOf("__"); + if (p > -1) { + name = name.substring(0, p); + } + if (name.endsWith(SQL)) { + name = name.substring(0, name.length() - 4); + } + if (name.endsWith(MODEL_XML)) { + name = name.substring(0, name.length() - 10); + } + return name; + } + +} diff --git a/src/main/java/io/ebeaninternal/dbmigration/model/MigrationModel.java b/src/main/java/io/ebeaninternal/dbmigration/model/MigrationModel.java index a27feb783..cc1776c13 100644 --- a/src/main/java/io/ebeaninternal/dbmigration/model/MigrationModel.java +++ b/src/main/java/io/ebeaninternal/dbmigration/model/MigrationModel.java @@ -31,14 +31,16 @@ public class MigrationModel { /** * Read all the migrations returning the model with all * the migrations applied in version order. + * + * @param dbinitMigration If true we don't apply model changes, migration is from scratch. */ - public ModelContainer read() { + public ModelContainer read(boolean dbinitMigration) { - readMigrations(); + readMigrations(dbinitMigration); return model; } - private void readMigrations() { + private void readMigrations(boolean dbinitMigration) { // find all the migration xml files File[] xmlFiles = modelDirectory.listFiles(pathname -> pathname.getName().toLowerCase().endsWith(modelSuffix)); @@ -53,9 +55,11 @@ public class MigrationModel { // sort into version order before applying Collections.sort(resources); - for (MigrationResource migrationResource : resources) { - logger.debug("read {}", migrationResource); - model.apply(migrationResource.read(), migrationResource.getVersion()); + if (!dbinitMigration) { + for (MigrationResource migrationResource : resources) { + logger.debug("read {}", migrationResource); + model.apply(migrationResource.read(), migrationResource.getVersion()); + } } // remember the last version diff --git a/src/test/java/io/ebeaninternal/dbmigration/LastMigrationTest.java b/src/test/java/io/ebeaninternal/dbmigration/LastMigrationTest.java new file mode 100644 index 000000000..e0adf54b2 --- /dev/null +++ b/src/test/java/io/ebeaninternal/dbmigration/LastMigrationTest.java @@ -0,0 +1,66 @@ +package io.ebeaninternal.dbmigration; + +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LastMigrationTest { + + @Test + public void lastVersion() { + File d = new File("src/test/resources/dbmigration/migrationtest/h2"); + assertThat(LastMigration.lastVersion(d, null)).isEqualTo("1.4"); + assertThat(LastMigration.nextVersion(d, null, false)).isEqualTo("1.5"); + assertThat(LastMigration.nextVersion(d, null, true)).isEqualTo("1.4"); + } + + @Test + public void lastVersion_no_v_Prefix() { + File d = new File("src/test/resources/dbmigration/migrationtest-history"); + assertThat(LastMigration.lastVersion(d, null)).isEqualTo("1.2"); + } + + @Test + public void lastVersion_test() { + File d = new File("src/test/resources/test-dbmigration"); + assertThat(LastMigration.lastVersion(d, null)).isEqualTo("2.1"); + assertThat(LastMigration.nextVersion(d, null, false)).isEqualTo("2.2"); + + } + + @Test + public void lastVersion_app2() { + File d = new File("src/test/resources/dbmigration/app2"); + assertThat(LastMigration.lastVersion(d, null)).isEqualTo("3.1"); + assertThat(LastMigration.nextVersion(d, null, false)).isEqualTo("3.2"); + } + + @Test + public void lastVersion_app3() { + File d = new File("src/test/resources/dbmigration/app3"); + assertThat(LastMigration.lastVersion(d, null)).isEqualTo("3.1.2"); + assertThat(LastMigration.nextVersion(d, null, false)).isEqualTo("3.1.3"); + assertThat(LastMigration.nextVersion(d, null, true)).isEqualTo("3.1.2"); + } + + @Test + public void lastVersion_app3_with_model_larger() { + File d = new File("src/test/resources/dbmigration/app3"); + File m = new File("src/test/resources/dbmigration/app3/model_larger"); + assertThat(LastMigration.lastVersion(d, m)).isEqualTo("4.1"); + assertThat(LastMigration.nextVersion(d, m, false)).isEqualTo("4.2"); + assertThat(LastMigration.nextVersion(d, m, true)).isEqualTo("4.1"); + } + + @Test + public void lastVersion_app3_with_model_smaller() { + + File d = new File("src/test/resources/dbmigration/app3"); + File m = new File("src/test/resources/dbmigration/app3/model_smaller"); + assertThat(LastMigration.lastVersion(d, m)).isEqualTo("3.1.2"); + assertThat(LastMigration.nextVersion(d, m, false)).isEqualTo("3.1.3"); + assertThat(LastMigration.nextVersion(d, m, true)).isEqualTo("3.1.2"); + } +} diff --git a/src/test/resources/dbmigration/app2/v1.0__one.sql b/src/test/resources/dbmigration/app2/v1.0__one.sql new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app2/v1.1__one.sql b/src/test/resources/dbmigration/app2/v1.1__one.sql new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app2/v3.1.sql b/src/test/resources/dbmigration/app2/v3.1.sql new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app3/model_larger/v4.1__foo.model.xml b/src/test/resources/dbmigration/app3/model_larger/v4.1__foo.model.xml new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app3/model_smaller/v2.0.model.xml b/src/test/resources/dbmigration/app3/model_smaller/v2.0.model.xml new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app3/model_smaller/v2.1__foo.model.xml b/src/test/resources/dbmigration/app3/model_smaller/v2.1__foo.model.xml new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app3/v1.0__one.sql b/src/test/resources/dbmigration/app3/v1.0__one.sql new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app3/v1.1__one.sql b/src/test/resources/dbmigration/app3/v1.1__one.sql new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/dbmigration/app3/v3.1.2__foo.sql b/src/test/resources/dbmigration/app3/v3.1.2__foo.sql new file mode 100644 index 000000000..e69de29bb