From bd03dc7cb36a7d140cdaec6e7596b9d638d0071c Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Tue, 13 Oct 2026 03:06:57 -0400 Subject: [PATCH] Refuse to migrate a database with an unrecognized schema version ServerMigrator treated a SCHEMA_INFO version it could not find in the Version enum the same as an empty table, and started the migration chain at V0. Startup then failed in the first legacy delta script (derby-0-1.sql or its equivalent) with an error that says nothing about the real problem. On a stock schema that script fails at its first statement, but nothing guarded the path: had a step succeeded, updateVersion() would also have seen no current version and inserted a second SCHEMA_INFO row. Stop with a MigrationException that names the version found, before any migrator runs. An empty SCHEMA_INFO table still starts at V0 as before. Fixes #496. Issue: https://github.com/OpenIntegrationEngine/engine/issues/496 Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Tony Germano --- .../server/migration/ServerMigrator.java | 10 +- .../server/migration/ServerMigratorTest.java | 106 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/com/mirth/connect/server/migration/ServerMigratorTest.java diff --git a/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java b/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java index e50cdb7550..efb77503f8 100644 --- a/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java +++ b/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java @@ -380,7 +380,15 @@ private Version getCurrentVersion() throws MigrationException { resultSet = statement.executeQuery("SELECT VERSION FROM SCHEMA_INFO"); if (resultSet.next()) { - return Version.fromString(resultSet.getString(1)); + String schemaVersion = resultSet.getString(1); + Version version = Version.fromString(schemaVersion); + + // Returning null would run every migrator from V0 against an unknown schema + if (version == null) { + throw new MigrationException("Database schema version \"" + schemaVersion + "\" is not recognized by this server (version " + Version.getLatest() + "). The database may have been created by a newer or different release. No migrations were run."); + } + + return version; } return null; diff --git a/server/src/test/java/com/mirth/connect/server/migration/ServerMigratorTest.java b/server/src/test/java/com/mirth/connect/server/migration/ServerMigratorTest.java new file mode 100644 index 0000000000..67183dd48f --- /dev/null +++ b/server/src/test/java/com/mirth/connect/server/migration/ServerMigratorTest.java @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Tony Germano + +package com.mirth.connect.server.migration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.mirth.connect.client.core.Version; +import com.mirth.connect.model.util.MigrationException; + +/** + * Runs {@link ServerMigrator#migrate()} against an embedded Derby database that already has a + * schema, so the version recorded in SCHEMA_INFO decides what happens. + */ +public class ServerMigratorTest { + + private static final String URL = "jdbc:derby:memory:servermigrator"; + + /** Looks like a version from a newer release, which is how an unknown version shows up. */ + private static final String UNRECOGNIZED_VERSION = "99.0.0"; + + private Connection connection; + + @Before + public void setUp() throws Exception { + Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); + connection = DriverManager.getConnection(URL + ";create=true"); + + // An existing CONFIGURATION table means the schema is installed and must be migrated. + execute("CREATE TABLE CONFIGURATION (CATEGORY VARCHAR(255) NOT NULL, NAME VARCHAR(255) NOT NULL, VALUE CLOB)"); + execute("CREATE TABLE SCHEMA_INFO (VERSION VARCHAR(40))"); + } + + @After + public void tearDown() throws Exception { + if (connection != null) { + connection.close(); + } + try { + DriverManager.getConnection(URL + ";drop=true"); + } catch (SQLException e) { + // Derby always reports dropping an in-memory database as an exception (SQLState 08006). + } + } + + @Test + public void testLatestVersionIsLeftUnchanged() throws Exception { + setSchemaVersion(Version.getLatest().getSchemaVersion()); + + newMigrator().migrate(); + + assertEquals(Version.getLatest().getSchemaVersion(), getOnlySchemaVersion()); + } + + @Test + public void testUnrecognizedVersionIsRejected() throws Exception { + assertNull("UNRECOGNIZED_VERSION is now a real version; pick another", Version.fromString(UNRECOGNIZED_VERSION)); + setSchemaVersion(UNRECOGNIZED_VERSION); + + MigrationException e = assertThrows(MigrationException.class, () -> newMigrator().migrate()); + + assertTrue(e.getMessage(), e.getMessage().contains("\"" + UNRECOGNIZED_VERSION + "\"")); + assertEquals(UNRECOGNIZED_VERSION, getOnlySchemaVersion()); + } + + private ServerMigrator newMigrator() { + ServerMigrator migrator = new ServerMigrator(); + migrator.setConnection(connection); + migrator.setDatabaseType("derby"); + return migrator; + } + + private void setSchemaVersion(String version) throws SQLException { + execute("INSERT INTO SCHEMA_INFO (VERSION) VALUES ('" + version + "')"); + } + + /** Fails if SCHEMA_INFO does not hold exactly one row. */ + private String getOnlySchemaVersion() throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT VERSION FROM SCHEMA_INFO")) { + assertTrue("SCHEMA_INFO is empty", resultSet.next()); + String version = resultSet.getString(1); + assertFalse("SCHEMA_INFO has more than one row", resultSet.next()); + return version; + } + } + + private void execute(String sql) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + } + } +}