diff --git a/li.strolch.persistence.postgresql/src/test/java/li/strolch/persistence/postgresql/dao/test/DbMigrationTest.java b/li.strolch.persistence.postgresql/src/test/java/li/strolch/persistence/postgresql/dao/test/DbMigrationTest.java index cfc184cfa..c5113cd1a 100644 --- a/li.strolch.persistence.postgresql/src/test/java/li/strolch/persistence/postgresql/dao/test/DbMigrationTest.java +++ b/li.strolch.persistence.postgresql/src/test/java/li/strolch/persistence/postgresql/dao/test/DbMigrationTest.java @@ -15,6 +15,8 @@ */ package li.strolch.persistence.postgresql.dao.test; +import static java.util.Comparator.comparing; +import static li.strolch.persistence.postgresql.PostgreSqlPersistenceHandler.SCRIPT_PREFIX; import static li.strolch.persistence.postgresql.dao.test.CachedDaoTest.*; import java.io.File; @@ -49,8 +51,8 @@ public class DbMigrationTest { @Test public void shouldCreate() throws Exception { - DbSchemaVersionCheck dbCheck = new DbSchemaVersionCheck(PostgreSqlPersistenceHandler.SCRIPT_PREFIX, - PostgreSqlPersistenceHandler.class, true, true, true); + DbSchemaVersionCheck dbCheck = new DbSchemaVersionCheck(SCRIPT_PREFIX, PostgreSqlPersistenceHandler.class, true, + true, true); try (Connection con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) { @@ -59,7 +61,7 @@ public class DbMigrationTest { File scriptsD = new File("src/main/resources"); File[] scriptFiles = scriptsD.listFiles(f -> f.getName().endsWith("_initial.sql")); - Arrays.sort(scriptFiles, (f1, f2) -> f1.getName().compareTo(f2.getName())); + Arrays.sort(scriptFiles, comparing(File::getName)); for (File scriptFile : scriptFiles) { String name = scriptFile.getName(); @@ -84,28 +86,20 @@ public class DbMigrationTest { @Test public void shouldMigrate() throws Exception { - DbSchemaVersionCheck dbCheck = new DbSchemaVersionCheck(PostgreSqlPersistenceHandler.SCRIPT_PREFIX, - PostgreSqlPersistenceHandler.class, true, true, true); + DbSchemaVersionCheck dbCheck = new DbSchemaVersionCheck(SCRIPT_PREFIX, PostgreSqlPersistenceHandler.class, true, + true, true); try (Connection con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) { // CREATE 0.1.0 - dbCheck.createSchema(con, StrolchConstants.DEFAULT_REALM, Version.valueOf("0.1.0")); + Version currentVersion = Version.valueOf("0.1.0"); + dbCheck.createSchema(con, StrolchConstants.DEFAULT_REALM, currentVersion); - File scriptsD = new File("src/main/resources"); - File[] scriptFiles = scriptsD.listFiles(f -> f.getName().endsWith("_migration.sql")); - Arrays.sort(scriptFiles, (f1, f2) -> f1.getName().compareTo(f2.getName())); - for (File scriptFile : scriptFiles) { + Version expectedDbVersion = DbSchemaVersionCheck + .getExpectedDbVersion(SCRIPT_PREFIX, PostgreSqlPersistenceHandler.class); - String name = scriptFile.getName(); - String versionS = name - .substring("strolch_db_schema_".length(), name.length() - "_migration.sql".length()); - Version version = Version.valueOf(versionS); - logger.info("Migrating Version " + version); - - // MIGRATE - dbCheck.migrateSchema(con, StrolchConstants.DEFAULT_REALM, version); - } + // MIGRATE + dbCheck.migrateSchema(con, StrolchConstants.DEFAULT_REALM, currentVersion, expectedDbVersion); } catch (SQLException e) { String msg = "Failed to open DB connection to URL {0} due to: {1}"; //$NON-NLS-1$ diff --git a/li.strolch.utils/src/main/java/li/strolch/db/DbSchemaVersionCheck.java b/li.strolch.utils/src/main/java/li/strolch/db/DbSchemaVersionCheck.java index 35c15c347..ae047f0d5 100644 --- a/li.strolch.utils/src/main/java/li/strolch/db/DbSchemaVersionCheck.java +++ b/li.strolch.utils/src/main/java/li/strolch/db/DbSchemaVersionCheck.java @@ -19,14 +19,18 @@ import static li.strolch.db.DbConstants.PROP_DB_VERSION; import static li.strolch.db.DbConstants.RESOURCE_DB_VERSION; import javax.sql.DataSource; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URL; +import java.nio.file.Paths; +import java.security.CodeSource; import java.sql.*; import java.text.MessageFormat; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Properties; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import li.strolch.utils.Version; import li.strolch.utils.dbc.DBC; @@ -127,7 +131,7 @@ public class DbSchemaVersionCheck { createSchema(con, realm, expectedDbVersion); break; case MIGRATED: - migrateSchema(con, realm, expectedDbVersion); + migrateSchema(con, realm, currentVersion, expectedDbVersion); break; case DROPPED_CREATED: throw new DbException("Migration type " + migrationType + " not handled!"); @@ -266,11 +270,14 @@ public class DbSchemaVersionCheck { public static String getSql(String scriptPrefix, Class ctxClass, Version version, String type) throws DbException { String schemaResourceS = MessageFormat.format("/{0}_db_schema_{1}_{2}.sql", scriptPrefix, version, type); - try (InputStream stream = ctxClass.getResourceAsStream(schemaResourceS);) { + try (InputStream stream = ctxClass.getResourceAsStream(schemaResourceS)) { + DBC.PRE.assertNotNull( MessageFormat.format("Schema Resource file with name {0} does not exist!", schemaResourceS), stream); + return FileHelper.readStreamToString(stream); + } catch (IOException e) { throw new DbException("Schema creation resource file is missing or could not be read: " + schemaResourceS, e); @@ -315,13 +322,14 @@ public class DbSchemaVersionCheck { * * @param realm * the realm to migrate (a {@link DataSource} must exist for it) - * @param version + * @param expectedVersion * the version to upgrade to * * @throws DbException * if something goes wrong */ - public void migrateSchema(Connection con, String realm, Version version) throws DbException { + public void migrateSchema(Connection con, String realm, Version currentVersion, Version expectedVersion) + throws DbException { if (!this.allowSchemaMigration) { String msg = "[{0}:{1}] Schema is not valid. Schema migration is disabled, thus can not continue!"; @@ -329,18 +337,100 @@ public class DbSchemaVersionCheck { throw new DbException(msg); } - logger.info(MessageFormat.format("[{0}:{1}] Migrating schema to {2}...", this.app, realm, version)); + if (expectedVersion.equals(currentVersion)) + throw new IllegalStateException("Expected version " + expectedVersion + " is same as " + currentVersion + + " and thus no migration is necessary!"); + if (expectedVersion.compareTo(currentVersion) < 0) + throw new IllegalStateException( + "Expected version " + expectedVersion + " is weirdly before current version" + currentVersion + + " for " + this.app); - String sql = getSql(this.app, this.ctxClass, version, "migration"); - try (Statement st = con.createStatement()) { - st.execute(sql); + logger.info(MessageFormat + .format("[{0}:{1}] Migrating schema from {2} to {3}...", this.app, realm, currentVersion, + expectedVersion)); + + // first get all possible migration scripts + List versions = parseMigrationVersions(); + if (versions.isEmpty()) + throw new IllegalStateException("No migration versions found for context " + this.app); + versions.sort(Version::compareTo); + + if (!versions.contains(expectedVersion)) + throw new IllegalStateException( + "Expected version " + expectedVersion + " is missing as a migration version for " + this.app); + + for (Version version : versions) { + if (version.compareTo(currentVersion) <= 0) + continue; + + logger.info("Migrating to version " + version + "..."); + + String sql = getSql(this.app, this.ctxClass, version, "migration"); + try (Statement st = con.createStatement()) { + st.execute(sql); + } catch (SQLException e) { + logger.error("Failed to execute schema migration SQL: \n" + sql); + throw new DbException("Failed to execute schema migration SQL: " + e.getMessage(), e); + } + } + + try { + Version version = getCurrentVersion(con, this.app); + if (version == null || !version.equals(expectedVersion)) + throw new IllegalStateException( + "Migration to version " + expectedVersion + " failed as version after migration is " + version); } catch (SQLException e) { - logger.error("Failed to execute schema migration SQL: \n" + sql); - throw new DbException("Failed to execute schema migration SQL: " + e.getMessage(), e); + throw new IllegalStateException("Failed to read current version", e); } logger.info(MessageFormat - .format("[{0}:{1}] Successfully migrated schema to version {2}", this.app, realm, version)); + .format("[{0}:{1}] Successfully migrated schema to version {2}", this.app, realm, expectedVersion)); + } + + public List parseMigrationVersions() { + + List versions = new ArrayList<>(); + + try { + CodeSource src = this.ctxClass.getProtectionDomain().getCodeSource(); + URL url = src.getLocation(); + + String scheme = url.toURI().getScheme(); + if (scheme.equals("jar") || scheme.equals("file") && url.toString().endsWith(".jar")) { + try (ZipInputStream zip = new ZipInputStream(url.openStream())) { + ZipEntry ze; + while ((ze = zip.getNextEntry()) != null) { + String entryName = ze.getName(); + if (entryName.endsWith(".sql") && entryName.contains("migration")) + versions.add(parseVersion(entryName)); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to read JAR: " + url, e); + } + + } else if (scheme.equals("file")) { + + File file = Paths.get(url.toURI()).toFile(); + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + if (f.getName().endsWith(".sql") && f.getName().contains("migration")) + versions.add(parseVersion(f.getName())); + } + } + } + + } catch (Exception e) { + throw new IllegalStateException("Failed to parse migration script versions", e); + } + + return versions; + } + + private Version parseVersion(String scriptName) { + int versionStart = (this.app + "_db_schema_").length(); + int versionEnd = scriptName.indexOf("_", versionStart); + return Version.valueOf(scriptName.substring(versionStart, versionEnd)); } /**