Added schema migration infrastructure.
This commit is contained in:
parent
a7654e49ca
commit
ed6e2fba4a
|
@ -2,6 +2,8 @@ package com.andrewlalis.perfin.data.impl;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.DataSource;
|
import com.andrewlalis.perfin.data.DataSource;
|
||||||
import com.andrewlalis.perfin.data.ProfileLoadException;
|
import com.andrewlalis.perfin.data.ProfileLoadException;
|
||||||
|
import com.andrewlalis.perfin.data.impl.migration.Migration;
|
||||||
|
import com.andrewlalis.perfin.data.impl.migration.Migrations;
|
||||||
import com.andrewlalis.perfin.data.util.FileUtil;
|
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -48,7 +50,7 @@ public class JdbcDataSourceFactory {
|
||||||
log.debug("Database loaded for profile {} has schema version {}.", profileName, loadedSchemaVersion);
|
log.debug("Database loaded for profile {} has schema version {}.", profileName, loadedSchemaVersion);
|
||||||
if (loadedSchemaVersion < SCHEMA_VERSION) {
|
if (loadedSchemaVersion < SCHEMA_VERSION) {
|
||||||
log.debug("Schema version {} is lower than the app's version {}. Performing migration.", loadedSchemaVersion, SCHEMA_VERSION);
|
log.debug("Schema version {} is lower than the app's version {}. Performing migration.", loadedSchemaVersion, SCHEMA_VERSION);
|
||||||
// TODO: Do migration
|
migrateToCurrentSchemaVersion(profileName, loadedSchemaVersion);
|
||||||
} else if (loadedSchemaVersion > SCHEMA_VERSION) {
|
} else if (loadedSchemaVersion > SCHEMA_VERSION) {
|
||||||
log.debug("Schema version {} is higher than the app's version {}. Cannot continue.", loadedSchemaVersion, SCHEMA_VERSION);
|
log.debug("Schema version {} is higher than the app's version {}. Cannot continue.", loadedSchemaVersion, SCHEMA_VERSION);
|
||||||
throw new ProfileLoadException("Profile " + profileName + " has a database with an unsupported schema version.");
|
throw new ProfileLoadException("Profile " + profileName + " has a database with an unsupported schema version.");
|
||||||
|
@ -66,13 +68,7 @@ public class JdbcDataSourceFactory {
|
||||||
) {
|
) {
|
||||||
if (in == null) throw new IOException("Could not load database schema SQL file.");
|
if (in == null) throw new IOException("Could not load database schema SQL file.");
|
||||||
String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
List<String> statements = Arrays.stream(schemaStr.split(";"))
|
executeSqlScript(schemaStr, conn);
|
||||||
.map(String::strip).filter(s -> !s.isBlank()).toList();
|
|
||||||
for (String statementText : statements) {
|
|
||||||
try (Statement stmt = conn.createStatement()) {
|
|
||||||
stmt.executeUpdate(statementText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
writeCurrentSchemaVersion(profileName);
|
writeCurrentSchemaVersion(profileName);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -102,6 +98,63 @@ public class JdbcDataSourceFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void migrateToCurrentSchemaVersion(String profileName, int currentVersion) throws ProfileLoadException {
|
||||||
|
// Before starting, copy the database file to a backup folder.
|
||||||
|
Path backupDatabaseFile = getDatabaseFile(profileName).resolveSibling("migration-backup-database.mv.db");
|
||||||
|
try {
|
||||||
|
Files.copy(getDatabaseFile(profileName), backupDatabaseFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ProfileLoadException("Failed to prepare database backup prior to schema migration.", e);
|
||||||
|
}
|
||||||
|
int version = currentVersion;
|
||||||
|
JdbcDataSource dataSource = new JdbcDataSource(getJdbcUrl(profileName), Profile.getContentDir(profileName));
|
||||||
|
while (version < SCHEMA_VERSION) {
|
||||||
|
log.info("Migrating profile {} from version {} to version {}.", profileName, version, version + 1);
|
||||||
|
try {
|
||||||
|
Migration m = Migrations.get(version);
|
||||||
|
m.migrate(dataSource);
|
||||||
|
version++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Migration from version " + version + " to " + (version+1) + " failed!", e);
|
||||||
|
log.debug("Restoring database from pre-migration backup.");
|
||||||
|
FileUtil.deleteIfPossible(getDatabaseFile(profileName));
|
||||||
|
try {
|
||||||
|
Files.copy(backupDatabaseFile, getDatabaseFile(profileName));
|
||||||
|
FileUtil.deleteIfPossible(backupDatabaseFile);
|
||||||
|
} catch (IOException e2) {
|
||||||
|
log.error("Failed to restore backup!", e2);
|
||||||
|
throw new ProfileLoadException("Failed to restore backup after a failed migration.", e2);
|
||||||
|
}
|
||||||
|
throw new ProfileLoadException("Migration failed and data restored to pre-migration state.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeCurrentSchemaVersion(profileName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to write current schema version after migration.");
|
||||||
|
FileUtil.deleteIfPossible(getDatabaseFile(profileName));
|
||||||
|
try {
|
||||||
|
Files.copy(backupDatabaseFile, getDatabaseFile(profileName));
|
||||||
|
FileUtil.deleteIfPossible(backupDatabaseFile);
|
||||||
|
} catch (IOException e2) {
|
||||||
|
throw new ProfileLoadException("Failed to restore backup after failing to set schema version.", e2);
|
||||||
|
}
|
||||||
|
throw new ProfileLoadException("Failed to update the schema version file after the migration.", e);
|
||||||
|
}
|
||||||
|
FileUtil.deleteIfPossible(backupDatabaseFile);
|
||||||
|
log.info("Profile successfully migrated to latest version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void executeSqlScript(String script, Connection conn) throws SQLException {
|
||||||
|
List<String> statements = Arrays.stream(script.split(";"))
|
||||||
|
.map(String::strip).filter(s -> !s.isBlank()).toList();
|
||||||
|
for (String statementText : statements) {
|
||||||
|
try (Statement stmt = conn.createStatement()) {
|
||||||
|
stmt.executeUpdate(statementText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Path getDatabaseFile(String profileName) {
|
private static Path getDatabaseFile(String profileName) {
|
||||||
return Profile.getDir(profileName).resolve("database.mv.db");
|
return Profile.getDir(profileName).resolve("database.mv.db");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.andrewlalis.perfin.data.impl.migration;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.DataSource;
|
||||||
|
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
|
||||||
|
|
||||||
|
public interface JdbcMigration extends Migration {
|
||||||
|
default void migrate(DataSource dataSource) throws Exception {
|
||||||
|
if (dataSource instanceof JdbcDataSource ds) {
|
||||||
|
migrateJdbc(ds);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("This migration only accepts JDBC data sources.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void migrateJdbc(JdbcDataSource dataSource) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.andrewlalis.perfin.data.impl.migration;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.DataSource;
|
||||||
|
|
||||||
|
public interface Migration {
|
||||||
|
void migrate(DataSource dataSource) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.andrewlalis.perfin.data.impl.migration;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Migrations {
|
||||||
|
public static Map<Integer, Migration> getMigrations() {
|
||||||
|
final Map<Integer, Migration> migrations = new HashMap<>();
|
||||||
|
migrations.put(1, new PlainSQLMigration("/sql/migration/M1_AddBalanceRecordDeleted.sql"));
|
||||||
|
return migrations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Migration> getAll() {
|
||||||
|
return getMigrations().entrySet().stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Migration get(int currentVersion) {
|
||||||
|
Migration selectedMigration = getMigrations().get(currentVersion);
|
||||||
|
if (selectedMigration == null) {
|
||||||
|
throw new IllegalArgumentException("No migration available from version " + currentVersion);
|
||||||
|
}
|
||||||
|
return selectedMigration;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.andrewlalis.perfin.data.impl.migration;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PlainSQLMigration implements JdbcMigration {
|
||||||
|
private final String resourceName;
|
||||||
|
|
||||||
|
public PlainSQLMigration(String resourceName) {
|
||||||
|
this.resourceName = resourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrateJdbc(JdbcDataSource dataSource) throws Exception {
|
||||||
|
try (
|
||||||
|
var in = PlainSQLMigration.class.getResourceAsStream(resourceName);
|
||||||
|
var conn = dataSource.getConnection();
|
||||||
|
var stmt = conn.createStatement()
|
||||||
|
) {
|
||||||
|
if (in == null) throw new IOException("Failed to load resource " + resourceName);
|
||||||
|
String sqlString = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
List<String> sqlStatements = Arrays.stream(sqlString.split(";"))
|
||||||
|
.map(String::strip).filter(s -> !s.isBlank()).toList();
|
||||||
|
System.out.println("Running SQL Migration with " + sqlStatements.size() + " statements:");
|
||||||
|
for (String sqlStatement : sqlStatements) {
|
||||||
|
System.out.println(" Executing SQL statement:\n" + sqlStatement + "\n-----\n");
|
||||||
|
stmt.executeUpdate(sqlStatement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE balance_record
|
||||||
|
ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE AFTER currency;
|
||||||
|
|
||||||
|
ALTER TABLE account_entry
|
||||||
|
ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE AFTER currency;
|
Loading…
Reference in New Issue