Hi,
Liquibase 4.31.0
i have a question about diff command. I would like to use it in junit test.
I will have one snapshot from previous version of database and i will connect to current take a snapshot and compare it via diff.
My test :
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.logging.Logger;
import org.junit.jupiter.api.Test;
import liquibase.command.CommandScope;
public class MysqlLiquibaseCompareTest {
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/test18";
private static final String USERNAME = "root";
private static final String PASSWORD = "test";
private static final Logger LOGGER = Logger.getLogger(MysqlLiquibaseCompareTest.class.getName());
@Test
void testLiquibaseCompare() throws Exception {
// Use a persistent directory instead of JUnit's @TempDir
Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "liquibase_test");
Files.createDirectories(tempDir);
// Copy snap1.json from test resources to the temp directory
File snap1File = tempDir.resolve("snap1.json").toFile();
copyResourceToFile("com/schema/compare/mysql/snap1.json", snap1File);
// Verify snap1.json is correctly copied
if (!snap1File.exists()) {
throw new RuntimeException("Failed to copy snap1.json to: " + snap1File.getAbsolutePath());
}
System.out.println("snap1.json copied to: " + snap1File.getAbsolutePath());
// Generate snap2.json from the database
File snap2File = tempDir.resolve("snap2.json").toFile();
generateDatabaseSnapshot(snap2File);
// Run Liquibase diff command
runLiquibaseDiff(snap1File.getAbsolutePath(), snap2File.getAbsolutePath());
}
private void copyResourceToFile(String resourcePath, File outputFile) throws IOException {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
if (inputStream == null) {
throw new RuntimeException("Resource not found: " + resourcePath);
}
Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
private void generateDatabaseSnapshot(File outputFile) throws Exception {
LOGGER.info("Generating database snapshot: " + outputFile.getAbsolutePath());
// Create a ByteArrayOutputStream to capture console output
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream originalOut = System.out;
PrintStream capturingOut = new PrintStream(outputStream);
try {
System.setOut(capturingOut); // Redirect System.out to capture Liquibase output
// Run Liquibase snapshot command
new CommandScope("snapshot")
.addArgumentValue("url", JDBC_URL)
.addArgumentValue("username", USERNAME)
.addArgumentValue("password", PASSWORD)
.addArgumentValue("snapshotFormat", "json")
.execute();
// Restore original System.out
System.setOut(originalOut);
// Capture Liquibase output and trim whitespace
String jsonContent = outputStream.toString().trim();
// Verify JSON starts correctly
if (!jsonContent.startsWith("{")) {
throw new RuntimeException("Liquibase output is not valid JSON! Received:\n" + jsonContent);
}
// Write JSON to file
Files.write(outputFile.toPath(), jsonContent.getBytes(StandardCharsets.UTF_8));
// Verify file exists and has content
if (!outputFile.exists() || outputFile.length() == 0) {
throw new RuntimeException("Liquibase did not generate a valid JSON file: " + outputFile.getAbsolutePath());
}
LOGGER.info("Database snapshot successfully saved to: " + outputFile.getAbsolutePath());
System.out.println("Generated JSON Snapshot:\n" + jsonContent);
} finally {
System.setOut(originalOut); // Restore System.out to prevent side effects
}
}
private void runLiquibaseDiff(String snap1Path, String snap2Path) throws Exception {
System.out.println("Running Liquibase diff between:");
System.out.println("Reference snapshot: " + snap1Path);
System.out.println("Target snapshot: " + snap2Path);
// Ensure paths are valid
File snap1File = new File(snap1Path);
File snap2File = new File(snap2Path);
if (!snap1File.exists() || !snap2File.exists()) {
throw new RuntimeException("One of the snapshot files does not exist!");
}
// Read and validate JSON content
String snap1Content = Files.readString(snap1File.toPath()).trim();
String snap2Content = Files.readString(snap2File.toPath()).trim();
if (!snap1Content.startsWith("{") || !snap2Content.startsWith("{")) {
throw new RuntimeException("Invalid JSON snapshot! One of the snapshots is not valid JSON.");
}
System.out.println("==== SNAP1.JSON CONTENT ====");
System.out.println(snap1Content);
System.out.println("==== SNAP2.JSON CONTENT ====");
System.out.println(snap2Content);
// Use Liquibase CommandScope to run the diff, ensuring correct arguments
new CommandScope("diff")
.addArgumentValue("snapshotFormat", "JSON") // ✅ Force JSON format exactly like CLI
.addArgumentValue("referenceUrl", "offline:json?snapshot=" + snap1File.getAbsolutePath()) // ✅ Matches CLI
.addArgumentValue("url", "offline:json?snapshot=" + snap2File.getAbsolutePath()) // ✅ Matches CLI
.addArgumentValue("logLevel", "DEBUG") // Optional: debug mode
.execute();
System.out.println("Liquibase diff completed successfully.");
}
}
After run i get following exception (Files are in path and they are not empty and they are valid):
Interesting here is that i want to use JSON parser but in stack trace i see YAML parser.
liquibase.exception.CommandExecutionException: liquibase.exception.DatabaseException: liquibase.exception.UnexpectedLiquibaseException: Cannot parse snapshot offline:json?snapshot=/tmp/liquibase_test/snap1.json
at liquibase.command.CommandScope.lambda$execute$6(CommandScope.java:300)
at liquibase.Scope.child(Scope.java:210)
at liquibase.Scope.child(Scope.java:186)
at liquibase.command.CommandScope.execute(CommandScope.java:241)
at com.consol.cmrf.schema.compare.MysqlLiquibaseCompareTest.runLiquibaseDiff(MysqlLiquibaseCompareTest.java:155)
at com.consol.cmrf.schema.compare.MysqlLiquibaseCompareTest.testLiquibaseCompare(MysqlLiquibaseCompareTest.java:56)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: liquibase.exception.DatabaseException: liquibase.exception.UnexpectedLiquibaseException: Cannot parse snapshot offline:json?snapshot=/tmp/liquibase_test/snap1.json
at liquibase.command.core.helpers.AbstractDatabaseConnectionCommandStep.createDatabaseObject(AbstractDatabaseConnectionCommandStep.java:106)
at liquibase.command.core.helpers.ReferenceDbUrlConnectionCommandStep.obtainDatabase(ReferenceDbUrlConnectionCommandStep.java:89)
at liquibase.command.core.helpers.ReferenceDbUrlConnectionCommandStep.run(ReferenceDbUrlConnectionCommandStep.java:70)
at liquibase.command.CommandScope.lambda$execute$6(CommandScope.java:253)
... 74 more
Caused by: liquibase.exception.UnexpectedLiquibaseException: Cannot parse snapshot offline:json?snapshot=/tmp/liquibase_test/snap1.json
at liquibase.database.OfflineConnection.<init>(OfflineConnection.java:115)
at liquibase.database.DatabaseFactory.openConnection(DatabaseFactory.java:200)
at liquibase.database.DatabaseFactory.openConnection(DatabaseFactory.java:188)
at liquibase.database.DatabaseFactory.openDatabase(DatabaseFactory.java:153)
at liquibase.command.core.helpers.AbstractDatabaseConnectionCommandStep.createDatabaseObject(AbstractDatabaseConnectionCommandStep.java:74)
... 77 more
Caused by: liquibase.exception.LiquibaseParseException: liquibase.exception.UnexpectedLiquibaseException: Resource does not exist
at liquibase.parser.core.yaml.YamlSnapshotParser.parse(YamlSnapshotParser.java:72)
at liquibase.database.OfflineConnection.<init>(OfflineConnection.java:105)
... 81 more
Caused by: liquibase.exception.UnexpectedLiquibaseException: Resource does not exist
at liquibase.resource.ResourceAccessor$NotFoundResource.openInputStream(ResourceAccessor.java:311)
at liquibase.parser.core.yaml.YamlSnapshotParser.parse(YamlSnapshotParser.java:42)
... 82 more
Am i doing something wrong ?