Using diff via Java API

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 ?

I think you don’t have the json snapshot file specified quite right. Liquibase needs an indicator of the db type after the colon instead of json. For example, if your database is MySQL the snapshot specifier should look like: offline:mysql?snapshot=/tmp/liquibase_test/snap1.json

Hi, thank you for your answer, but this didn’t help, i get the same exception.
I think command is right, because when you want to compare files from command line i use :

liquibase --snapshotFormat=JSON   --referenceUrl="offline:json?snapshot=snap1.json"   --url="offline:json?snapshot=snap2.json"   diff

Is there any way you could copy and paste the snapshot files here so I can try loading one locally?

File has around 20k lines, i uploaded it to some transfer page:

but it has expiration date to 14.02, let me know if i can help more.

Your snapshot file looks fine.

I spent some time today stepping through scenarios in the CLI and in my own simplified program and I believe I have something for you to try. I don’t think the offline database specification works with absolute paths. I saw the same errors that you described when I was using the CLI with absolute paths. When I switched to relative paths the diff command works.

➜  ~ liquibase diff --reference-url="offline:json?snapshot=/Users/ppickerill/tmp/snap1.json" --url="offline:json?snapshot=/Users/ppickerill/tmp/snap1.json"
####################################################
##   _     _             _ _                      ##
##  | |   (_)           (_) |                     ##
##  | |    _  __ _ _   _ _| |__   __ _ ___  ___   ##
##  | |   | |/ _` | | | | | '_ \ / _` / __|/ _ \  ##
##  | |___| | (_| | |_| | | |_) | (_| \__ \  __/  ##
##  \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___|  ##
##              | |                               ##
##              |_|                               ##
##                                                ##
##  Get documentation at docs.liquibase.com       ##
##  Get certified courses at learn.liquibase.com  ##
##                                                ##
####################################################
Starting Liquibase at 15:30:14 using Java 21.0.6 (version 4.31.0 #6261 built at 2025-01-14 14:24+0000)
Liquibase Version: 4.31.0
Liquibase Open Source 4.31.0 by Liquibase
ERROR: Exception Details
ERROR: Exception Primary Class:  UnexpectedLiquibaseException
ERROR: Exception Primary Reason:  Resource does not exist
ERROR: Exception Primary Source:  4.31.0

Unexpected error running Liquibase: Cannot parse snapshot offline:json?snapshot=/Users/ppickerill/tmp/snap1.json
  - Caused by: Resource does not exist

For more information, please use the --log-level flag
➜  ~ liquibase diff --reference-url="offline:json?snapshot=tmp/snap1.json" --url="offline:json?snapshot=tmp/snap1.json"
####################################################
##   _     _             _ _                      ##
##  | |   (_)           (_) |                     ##
##  | |    _  __ _ _   _ _| |__   __ _ ___  ___   ##
##  | |   | |/ _` | | | | | '_ \ / _` / __|/ _ \  ##
##  | |___| | (_| | |_| | | |_) | (_| \__ \  __/  ##
##  \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___|  ##
##              | |                               ##
##              |_|                               ##
##                                                ##
##  Get documentation at docs.liquibase.com       ##
##  Get certified courses at learn.liquibase.com  ##
##                                                ##
####################################################
Starting Liquibase at 15:30:33 using Java 21.0.6 (version 4.31.0 #6261 built at 2025-01-14 14:24+0000)
Liquibase Version: 4.31.0
Liquibase Open Source 4.31.0 by Liquibase

Diff Results:
Reference Database: null @ offline:json?snapshot=tmp/snap1.json (Default Schema: test)
Comparison Database: null @ offline:json?snapshot=tmp/snap1.json (Default Schema: test)
Compared Schemas: test
Product Name: EQUAL
Product Version: EQUAL
Missing Catalog(s): NONE
Unexpected Catalog(s): NONE
Changed Catalog(s): NONE
Missing Column(s): NONE
Unexpected Column(s): NONE
Changed Column(s): NONE
Missing Foreign Key(s): NONE
Unexpected Foreign Key(s): NONE
Changed Foreign Key(s): NONE
Missing Index(s): NONE
Unexpected Index(s): NONE
Changed Index(s): NONE
Missing Primary Key(s): NONE
Unexpected Primary Key(s): NONE
Changed Primary Key(s): NONE
Missing Table(s): NONE
Unexpected Table(s): NONE
Changed Table(s): NONE
Missing Unique Constraint(s): NONE
Unexpected Unique Constraint(s): NONE
Changed Unique Constraint(s): NONE
Missing View(s): NONE
Unexpected View(s): NONE
Changed View(s): NONE
Liquibase command 'diff' was executed successfully.

To use relative paths with my java program, I copied the snapshot files to the directory where my class files were compiled and just referenced them by name. I’ve zipped that project up here.

Good luck!

Hi Pete, yes you are right. Looks like diff doesn’t handle path at all, i put resources files into package directory (e.g resoucres/com/bla/blub) and it finally put files next to class file, but this doesn’t help. Resources files snap1 and snap2 must be placed in resources directory without package path, then it works. Thank you!

1 Like