Liquibase computes identity of ChangeSet using three field:

  • id
  • author
  • filename

Where filename is changelog path.

When spring-boot runs liquibase migrations at startup, it by default (and usually) reads changelog file from classpath (not from local filesystem).

This makes liquibase to fill it's DATABASECHANGELOG.FILENAME field with this value: classpath:/db/changelog/db.changelog-master.yaml (the default value of LiquibaseProperties.changeLog field).

Then, if you try to run liquibase as maven plugin, you can't pass liquibase.changeLogFile property with classpath: prefix. You must pass it without prefix and without a leading slash. This way liquibase uses right classpath location.

mvn process-resources liquibase:updateSQL \
    -Dliquibase.url=jdbc:mysql://localhost:3306/db \
    -Dliquibase.username=user -Dliquibase.password=password \
    -Dliquibase.changeLogFile=db/changelog/db.changelog-master.yaml \
    -Dliquibase.verbose=true

But, this doesn't work for the following reason:

Liquibase creates the identity of each ChangeSet based on id, author and filename. And when you run liquibase as maven plugin, the filename of each ChangeSet is db/changelog/db.changelog-master.yaml, while filename in already existent rows of DATABASECHANGELOG is classpath:/db/changelog/db.changelog-master.yaml. These strings are not equal.

This makes liquibase to consider all ChangeSets as new, not yet applied, and try to reapply them.

The good news: it's already handled by Liquibase (see this PR https://github.com/liquibase/liquibase/pull/187). To handle this, Liquibase removes classpath: prefix, when compares changelog filenames.

The bad news: it only strips classpath: prefix, but not the leading slash. See https://github.com/liquibase/liquibase/blob/bc49461eb63e45fcf6bda0a15c7b41ef44936457/liquibase-core/src/main/java/liquibase/changelog/filter/ShouldRunChangeSetFilter.java#L69

So, it actually compares db/changelog/db.changelog-master.yaml to /db/changelog/db.changelog-master.yaml. These strings are almost equal. :)

So, back to this PR:

This patch just removes the leading slash from default changeLog property, as it is redundant anyway (See https://github.com/spring-projects/spring-framework/blob/e66e41029ccb31df18e2cc56c9fc87119cc90bd2/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java#L81).

With this small change liquibase is happy and understands that these are the same changesets, when you run liquibase as maven plugin.

This is useful, for example, to rollback changesets or to inspect changesets generated SQL.

Comment From: xak2000

@wilkinsona Thanks!

I fixed test and removed leading slash from several other places. This is not necessary but I made it for consistency.

Now I wonder 2 things:

  1. Should we also update a user documentation to warn users about this "leading slash problem"? Say something about that we recommend to not use leading slash at classpath because this may cause a problems when you use liquibase without spring-boot, or something similar? I'm not a native speaker so I just can't formulate it clear enough.

  2. Should I add a test, that confirms somehow that when liquibase is executed without spring-boot, it doesn't consider changeSet, generated by spring-boot, as different changeSet? I have no idea how to write such test. And it can ensure this only with default value of liquibase.change-log anyway..

Comment From: xak2000

If a user configures a changeLog property that begins with classpath:/ we could automatically remove the / before passing it to Liquibase.

I think it is a good idea!

I haven't any case in mind where leading slash is mandatory for end-user. Do you?

I think it would be good to add a test. It might be possible to do it by running the auto-configured Liquibase support and then using the Liquibase API to simulate what would happen on the command line.

The problem here is that it's not obvious what functionality of liquibase-without-spring should be tested. Should only rollback be tested? What about changelogSyncSQL, clearCheckSums and all other liquibase commands? Should we test all of them? Isn't it eventually turn up that we test Liquibase itself? :-)

I tried to understand if this can be fixed on Liquibase side, and should say that Liquibase code is a total mess. They are not consistent with this "remove classpath: prefix" hack in their code. Some Liquibase commands work good (e.g. rollback), but others are not (e.g. recalculation of checksums of changesets after clearCheckSums is executed) because they don't take into account classpath: prefix when comparing changesets identities. See this issue where I tried to describe the problems (in the comment).

Maybe we should just test that leading / is auto-removed from spring.liquibase.change-log property?

Comment From: snicoll

@xak2000 thanks again.

Comment From: wilkinsona

With thanks to @Marax for letting us know, I think we'll need to revert this:

Hi, this change actually broke my application. I have to manually remove slash from all previous changes in DATABASECHANGELOG, otherwise it tries to install them all again. EDIT: Ok, can be easily fixed by declaring previous form in configuration, but anyway should be mentioned somewhere. liquibase: change-log: 'classpath:/db/changelog/db.changelog-master.yaml'

We could consider keeping the change in 2.3, but for 2.1 and 2.2 I don't think the benefit justifies the problem that it creates (even with a workaround).

Comment From: snicoll

This is now superseded by #20177