Liquibase Rollback support and tests 79/142279/3 master
authordanielhanrahan <daniel.hanrahan@est.tech>
Fri, 17 Oct 2025 08:42:02 +0000 (09:42 +0100)
committerDaniel Hanrahan <daniel.hanrahan@est.tech>
Tue, 21 Oct 2025 12:16:05 +0000 (12:16 +0000)
This commit introduces:
- Liquibase tags: one tag per release.
- Needed rollback steps for each changelog.
- Unit tests of of Liquibase update and rollback.
- Tests comparing schemas before and after rollback.

The schema difference tests prove that the DB schema is 100%
identical before upgrade and after rollback. This reduces the
need for further rollback testing later in the release process.

Issue-ID: POLICY-5451
Change-Id: Ib6930ae59e846af45fa033c4bad62fafa2163edd
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
pom.xml
runtime-acm/pom.xml
runtime-acm/src/main/resources/db/changelog/changelog-1400.yaml
runtime-acm/src/main/resources/db/changelog/changelog-1500.yaml
runtime-acm/src/main/resources/db/changelog/changelog-1600.yaml
runtime-acm/src/main/resources/db/changelog/changelog-1700.yaml
runtime-acm/src/main/resources/db/changelog/changelog-1701.yaml
runtime-acm/src/main/resources/db/changelog/changelog-1702.yaml
runtime-acm/src/main/resources/db/changelog/changelog-1800.yaml
runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseRollbackTest.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index a89254e..b5c6e5c 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -64,9 +64,9 @@
         <module>participant</module>
     </modules>
 
-    <!-- Fix transitive dependencies' vulnerabilities -->
     <dependencyManagement>
         <dependencies>
+            <!-- Fix transitive dependencies' vulnerabilities -->
             <dependency>
                 <groupId>io.netty</groupId>
                 <artifactId>netty-bom</artifactId>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+            <!-- Dependencies specific to clamp repo -->
+            <dependency>
+                <groupId>org.testcontainers</groupId>
+                <artifactId>testcontainers-bom</artifactId>
+                <version>1.21.3</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 </project>
index 7e696ef..8622df7 100644 (file)
             <artifactId>spring-boot-test-autoconfigure</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
     <build>
index d682e09..41e1e88 100644 (file)
@@ -382,3 +382,10 @@ databaseChangeLog:
             referencedColumnNames: participantId
             onUpdate: RESTRICT
             onDelete: RESTRICT
+
+  - changeSet:
+      id: 1400-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1400
index 9af9b1b..1907edc 100644 (file)
@@ -155,3 +155,10 @@ databaseChangeLog:
             referencedColumnNames: participantId
             onUpdate: RESTRICT
             onDelete: RESTRICT
+
+  - changeSet:
+      id: 1500-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1500
index b2ce6d8..7284da1 100644 (file)
@@ -69,3 +69,10 @@ databaseChangeLog:
               - column:
                   name: stage
                   type: SMALLINT
+
+  - changeSet:
+      id: 1600-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1600
index 527d7ea..e8b846b 100644 (file)
@@ -102,3 +102,10 @@ databaseChangeLog:
             columns:
               - column:
                   name: identificationId
+
+  - changeSet:
+      id: 1700-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1700
index 43fcc09..26ed1d4 100644 (file)
@@ -75,6 +75,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: name
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: name
+        - dropDefaultValue:
+            tableName: AutomationComposition
+            columnName: name
 
   - changeSet:
       id: 1701-4
@@ -94,6 +101,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: version
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: version
+        - dropDefaultValue:
+            tableName: AutomationComposition
+            columnName: version
 
   - changeSet:
       id: 1701-5
@@ -113,6 +127,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: deployState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: deployState
+        - dropDefaultValue:
+            tableName: AutomationComposition
+            columnName: deployState
 
   - changeSet:
       id: 1701-6
@@ -132,6 +153,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: lockState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: lockState
+        - dropDefaultValue:
+            tableName: AutomationComposition
+            columnName: lockState
 
   - changeSet:
       id: 1701-7
@@ -151,6 +179,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: subState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: subState
+        - dropDefaultValue:
+            tableName: AutomationComposition
+            columnName: subState
 
   - changeSet:
       id: 1701-8
@@ -166,6 +201,10 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: lastMsg
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: lastMsg
 
   - changeSet:
       id: 1701-9
@@ -185,6 +224,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: definition_name
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: definition_name
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: definition_name
 
   - changeSet:
       id: 1701-10
@@ -204,6 +250,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: definition_version
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: definition_version
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: definition_version
 
   - changeSet:
       id: 1701-11
@@ -223,6 +276,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: deployState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: deployState
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: deployState
 
   - changeSet:
       id: 1701-12
@@ -242,6 +302,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: lockState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: lockState
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: lockState
 
   - changeSet:
       id: 1701-13
@@ -261,6 +328,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: subState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: subState
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: subState
 
   - changeSet:
       id: 1701-14
@@ -280,6 +354,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: outProperties
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: outProperties
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: outProperties
 
   - changeSet:
       id: 1701-15
@@ -299,6 +380,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionElement
             columnName: properties
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionElement
+            columnName: properties
+        - dropDefaultValue:
+            tableName: AutomationCompositionElement
+            columnName: properties
 
   - changeSet:
       id: 1701-16
@@ -318,6 +406,10 @@ databaseChangeLog:
             referencedColumnNames: compositionId
             onUpdate: RESTRICT
             onDelete: RESTRICT
+      rollback:
+        - dropForeignKeyConstraint:
+            baseTableName: AutomationComposition
+            constraintName: ac_composition_fk
 
   - changeSet:
       id: 1701-17
@@ -337,6 +429,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionDefinition
             columnName: name
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionDefinition
+            columnName: name
+        - dropDefaultValue:
+            tableName: AutomationCompositionDefinition
+            columnName: name
 
   - changeSet:
       id: 1701-18
@@ -356,6 +455,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionDefinition
             columnName: version
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionDefinition
+            columnName: version
+        - dropDefaultValue:
+            tableName: AutomationCompositionDefinition
+            columnName: version
 
   - changeSet:
       id: 1701-19
@@ -375,6 +481,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionDefinition
             columnName: state
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionDefinition
+            columnName: state
+        - dropDefaultValue:
+            tableName: AutomationCompositionDefinition
+            columnName: state
 
   - changeSet:
       id: 1701-20
@@ -394,6 +507,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionDefinition
             columnName: serviceTemplate
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionDefinition
+            columnName: serviceTemplate
+        - dropDefaultValue:
+            tableName: AutomationCompositionDefinition
+            columnName: serviceTemplate
 
   - changeSet:
       id: 1701-21
@@ -409,6 +529,10 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionDefinition
             columnName: lastMsg
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionDefinition
+            columnName: lastMsg
 
   - changeSet:
       id: 1701-22
@@ -428,6 +552,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: NodeTemplateState
             columnName: nodeTemplate_name
+      rollback:
+        - dropNotNullConstraint:
+            tableName: NodeTemplateState
+            columnName: nodeTemplate_name
+        - dropDefaultValue:
+            tableName: NodeTemplateState
+            columnName: nodeTemplate_name
 
   - changeSet:
       id: 1701-23
@@ -447,6 +578,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: NodeTemplateState
             columnName: nodeTemplate_version
+      rollback:
+        - dropNotNullConstraint:
+            tableName: NodeTemplateState
+            columnName: nodeTemplate_version
+        - dropDefaultValue:
+            tableName: NodeTemplateState
+            columnName: nodeTemplate_version
 
   - changeSet:
       id: 1701-24
@@ -466,6 +604,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: NodeTemplateState
             columnName: outProperties
+      rollback:
+        - dropNotNullConstraint:
+            tableName: NodeTemplateState
+            columnName: outProperties
+        - dropDefaultValue:
+            tableName: NodeTemplateState
+            columnName: outProperties
 
   - changeSet:
       id: 1701-25
@@ -485,6 +630,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: NodeTemplateState
             columnName: state
+      rollback:
+        - dropNotNullConstraint:
+            tableName: NodeTemplateState
+            columnName: state
+        - dropDefaultValue:
+            tableName: NodeTemplateState
+            columnName: state
 
   - changeSet:
       id: 1701-26
@@ -501,6 +653,10 @@ databaseChangeLog:
             columns:
               - column:
                   name: identificationId
+      rollback:
+        - dropIndex:
+            indexName: mb_identificationId_index
+            tableName: Message
 
   - changeSet:
       id: 1701-27
@@ -516,6 +672,10 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: ParticipantReplica
             columnName: lastMsg
+      rollback:
+        - dropNotNullConstraint:
+            tableName: ParticipantReplica
+            columnName: lastMsg
 
   - changeSet:
       id: 1701-28
@@ -528,6 +688,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: ParticipantReplica
             columnName: participantId
+      rollback:
+        - dropNotNullConstraint:
+            tableName: ParticipantReplica
+            columnName: participantId
+        - dropDefaultValue:
+            tableName: ParticipantReplica
+            columnName: participantId
 
   - changeSet:
       id: 1701-29
@@ -547,6 +714,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: ParticipantReplica
             columnName: participantState
+      rollback:
+        - dropNotNullConstraint:
+            tableName: ParticipantReplica
+            columnName: participantState
+        - dropDefaultValue:
+            tableName: ParticipantReplica
+            columnName: participantState
 
   - changeSet:
       id: 1701-30
@@ -559,6 +733,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: ParticipantSupportedAcElements
             columnName: participantId
+      rollback:
+        - dropNotNullConstraint:
+            tableName: ParticipantSupportedAcElements
+            columnName: participantId
+        - dropDefaultValue:
+            tableName: ParticipantSupportedAcElements
+            columnName: participantId
 
   - changeSet:
       id: 1701-31
@@ -578,6 +759,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: ParticipantSupportedAcElements
             columnName: typeName
+      rollback:
+        - dropNotNullConstraint:
+            tableName: ParticipantSupportedAcElements
+            columnName: typeName
+        - dropDefaultValue:
+            tableName: ParticipantSupportedAcElements
+            columnName: typeName
 
   - changeSet:
       id: 1701-32
@@ -597,3 +785,17 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: ParticipantSupportedAcElements
             columnName: typeVersion
+      rollback:
+        - dropNotNullConstraint:
+            tableName: ParticipantSupportedAcElements
+            columnName: typeVersion
+        - dropDefaultValue:
+            tableName: ParticipantSupportedAcElements
+            columnName: typeVersion
+
+  - changeSet:
+      id: 1701-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1701
index dc9e08b..e5c5a41 100644 (file)
@@ -67,6 +67,11 @@ databaseChangeLog:
             tableName: AutomationComposition
             columnName: phase
             newDataType: INT
+      rollback:
+        - modifyDataType:
+            tableName: AutomationComposition
+            columnName: phase
+            newDataType: SMALLINT
 
   - changeSet:
       author: policy
@@ -76,6 +81,11 @@ databaseChangeLog:
             tableName: AutomationCompositionElement
             columnName: stage
             newDataType: INT
+      rollback:
+        - modifyDataType:
+            tableName: AutomationCompositionElement
+            columnName: stage
+            newDataType: SMALLINT
 
   - changeSet:
       author: policy
@@ -84,6 +94,11 @@ databaseChangeLog:
         - dropDefaultValue:
             tableName: ParticipantReplica
             columnName: participantId
+      rollback:
+        - addDefaultValue:
+            tableName: ParticipantReplica
+            columnName: participantId
+            defaultValue: ''
 
   - changeSet:
       author: policy
@@ -97,6 +112,13 @@ databaseChangeLog:
         - dropColumn:
             tableName: AutomationComposition
             columnName: restarting
+      rollback:
+        - addColumn:
+            tableName: AutomationComposition
+            columns:
+              - column:
+                  name: restarting
+                  type: BOOLEAN
 
   - changeSet:
       author: policy
@@ -110,6 +132,13 @@ databaseChangeLog:
         - dropColumn:
             tableName: AutomationCompositionDefinition
             columnName: restarting
+      rollback:
+        - addColumn:
+            tableName: AutomationCompositionDefinition
+            columns:
+              - column:
+                  name: restarting
+                  type: BOOLEAN
 
   - changeSet:
       author: policy
@@ -123,6 +152,13 @@ databaseChangeLog:
         - dropColumn:
             tableName: Participant
             columnName: participantState
+      rollback:
+        - addColumn:
+            tableName: Participant
+            columns:
+              - column:
+                  name: participantState
+                  type: SMALLINT
 
   - changeSet:
       author: policy
@@ -136,3 +172,18 @@ databaseChangeLog:
         - dropColumn:
             tableName: Participant
             columnName: lastMsg
+      rollback:
+        - addColumn:
+            tableName: Participant
+            columns:
+              - column:
+                  name: lastMsg
+                  type: TIMESTAMP
+                  defaultValueComputed: CURRENT_TIMESTAMP
+
+  - changeSet:
+      id: 1702-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1702
index 42faace..99d534e 100644 (file)
@@ -26,6 +26,13 @@ databaseChangeLog:
         - dropColumn:
             tableName: AutomationCompositionElement
             columnName: restarting
+      rollback:
+        - addColumn:
+            tableName: AutomationCompositionElement
+            columns:
+              - column:
+                  name: restarting
+                  type: BOOLEAN
 
   - changeSet:
       author: policy
@@ -34,6 +41,13 @@ databaseChangeLog:
         - dropColumn:
             tableName: NodeTemplateState
             columnName: restarting
+      rollback:
+        - addColumn:
+            tableName: NodeTemplateState
+            columns:
+              - column:
+                  name: restarting
+                  type: BOOLEAN
 
   - changeSet:
       author: policy
@@ -67,6 +81,13 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationCompositionDefinition
             columnName: stateChangeResult
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationCompositionDefinition
+            columnName: stateChangeResult
+        - dropDefaultValue:
+            tableName: AutomationCompositionDefinition
+            columnName: stateChangeResult
 
   - changeSet:
       id: 1800-5
@@ -86,3 +107,17 @@ databaseChangeLog:
         - addNotNullConstraint:
             tableName: AutomationComposition
             columnName: stateChangeResult
+      rollback:
+        - dropNotNullConstraint:
+            tableName: AutomationComposition
+            columnName: stateChangeResult
+        - dropDefaultValue:
+            tableName: AutomationComposition
+            columnName: stateChangeResult
+
+  - changeSet:
+      id: 1800-tag
+      author: policy
+      changes:
+        - tagDatabase:
+            tag: 1800
diff --git a/runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseRollbackTest.java b/runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseRollbackTest.java
new file mode 100644 (file)
index 0000000..0ecce32
--- /dev/null
@@ -0,0 +1,166 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.clamp.acm.runtime.liquibase;
+
+import java.io.PrintStream;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.stream.Stream;
+import liquibase.Liquibase;
+import liquibase.database.DatabaseFactory;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.diff.DiffResult;
+import liquibase.diff.compare.CompareControl;
+import liquibase.diff.output.report.DiffToReport;
+import liquibase.resource.ClassLoaderResourceAccessor;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+// This test class verifies that rollbacks for each Liquibase release tag works correctly.
+@Testcontainers
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class LiquibaseRollbackTest {
+
+    @Container
+    private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
+
+    private Liquibase liquibase;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        liquibase = initLiquibase();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        liquibase.dropAll();
+        liquibase.close();
+    }
+
+    /**
+     * This test will apply all changesets up to the latest change, and roll them all back.
+     * This simple test detects many issues such as missing rollback instructions in changesets.
+     */
+    @Test
+    void testUpdateAndRollback() {
+        Assertions.assertDoesNotThrow(() -> liquibase.updateTestingRollback(null));
+    }
+
+    /**
+     * This test will apply changesets up to a specific tag, roll back to a previous tag,
+     * and then re-apply the changesets to ensure that the rollback was compatible with forward changes.
+     */
+    @ParameterizedTest
+    @MethodSource("rollbackTagProvider")
+    void testUpdateAndRollbackForTags(final String previousTag, final String targetTag) {
+        // Run all changesets up to the target release tag
+        Assertions.assertDoesNotThrow(() -> liquibase.update(targetTag, ""));
+        // Roll back to the previous release
+        Assertions.assertDoesNotThrow(() -> liquibase.rollback(previousTag, ""));
+        // Apply forward changes again to ensure that the rollback was compatible with forwards changes
+        Assertions.assertDoesNotThrow(() -> liquibase.update(targetTag, ""));
+    }
+
+    /**
+     * This test compares the database schema before and after a rollback to ensure they are identical.
+     * It works by creating two separate schemas in the database:
+     * - one to apply changes up to a certain target tag (the original pre-upgraded schema)
+     * - another to apply changes up to a later tag, and roll back to the target tag (the post-rollback schema)
+     * The two schemas are then compared for equality. The schemas should be identical if the rollbacks are correct.
+     */
+    @ParameterizedTest
+    @MethodSource("rollbackTagProvider")
+    void testSchemaEqualityAfterRollback(final String rollbackToTag, final String rollbackFromTag) throws Exception {
+        // Disable column order checking to avoid false positives when columns are dropped and re-added.
+        System.setProperty("liquibase.diffColumnOrder", "false");
+        // Create two schemas
+        try (Connection conn = initConnection(); Statement stmt = conn.createStatement()) {
+            stmt.execute("CREATE SCHEMA pre_upgrade_schema");
+            stmt.execute("CREATE SCHEMA post_rollback_schema");
+        }
+        try (Liquibase liquibaseBefore = initLiquibaseForSchema("pre_upgrade_schema");
+             Liquibase liquibaseAfter = initLiquibaseForSchema("post_rollback_schema")) {
+            // Apply pre-upgrade schema to pre_upgrade_schema
+            liquibaseBefore.update(rollbackToTag, "");
+
+            // Apply upgrade and rollback to post_rollback_schema
+            liquibaseAfter.update(rollbackFromTag, "");
+            liquibaseAfter.rollback(rollbackToTag, "");
+
+            // Compare the schemas and report any differences
+            DiffResult diffResult = liquibaseBefore.diff(liquibaseBefore.getDatabase(), liquibaseAfter.getDatabase(),
+                    CompareControl.STANDARD);
+            if (!diffResult.areEqual()) {
+                DiffToReport diffReport = new DiffToReport(diffResult, new PrintStream(System.out));
+                diffReport.print();
+            }
+            // Fail test if schemas are different
+            Assertions.assertTrue(diffResult.areEqual());
+
+        } finally {
+            try (Connection conn = initConnection(); Statement stmt = conn.createStatement()) {
+                stmt.execute("DROP SCHEMA IF EXISTS pre_upgrade_schema CASCADE");
+                stmt.execute("DROP SCHEMA IF EXISTS post_rollback_schema CASCADE");
+            }
+        }
+    }
+
+    private static Stream<Arguments> rollbackTagProvider() {
+        return Stream.of(
+                Arguments.of("1400", "1500"),
+                Arguments.of("1500", "1600"),
+                Arguments.of("1600", "1700"),
+                Arguments.of("1700", "1701"),
+                Arguments.of("1701", "1702"),
+                Arguments.of("1702", "1800")
+        );
+    }
+
+    private Liquibase initLiquibase() throws Exception {
+        return initLiquibaseForSchema(null);
+    }
+
+    private Liquibase initLiquibaseForSchema(String schema) throws Exception {
+        var connection = initConnection();
+        var database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+        database.setDefaultSchemaName(schema);
+        return new Liquibase("db/changelog/db.changelog-master.yaml", new ClassLoaderResourceAccessor(), database);
+    }
+
+    private Connection initConnection() throws SQLException {
+        return DriverManager.getConnection(
+                postgres.getJdbcUrl(),
+                postgres.getUsername(),
+                postgres.getPassword());
+    }
+
+}