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.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.model.Profile;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -48,7 +50,7 @@ public class JdbcDataSourceFactory {
|
|||
log.debug("Database loaded for profile {} has schema version {}.", profileName, loadedSchemaVersion);
|
||||
if (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) {
|
||||
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.");
|
||||
|
@ -66,13 +68,7 @@ public class JdbcDataSourceFactory {
|
|||
) {
|
||||
if (in == null) throw new IOException("Could not load database schema SQL file.");
|
||||
String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
List<String> statements = Arrays.stream(schemaStr.split(";"))
|
||||
.map(String::strip).filter(s -> !s.isBlank()).toList();
|
||||
for (String statementText : statements) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
stmt.executeUpdate(statementText);
|
||||
}
|
||||
}
|
||||
executeSqlScript(schemaStr, conn);
|
||||
try {
|
||||
writeCurrentSchemaVersion(profileName);
|
||||
} 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) {
|
||||
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