Whilst Liquibase Linter has lots of good core rules, you may have a use case that's particular to your project and wouldn't make sense as a core rule.
Fortunately it's trivial to implement custom rules and apply them in your own project; you just need to write a Java class implementing one of our rule interfaces, and do a little configuration.
Example use case
Let's say for argument's sake that we have a Liquibase project for an app. We ship customised implementations of this app to different clients, so we have a
core context and then client-specific contexts like
In the database for our app, we have a table called
FORM_LAYOUT that holds configuration for how forms are laid out, which is always different for each client. One day, somebody makes a mistake and does a Liquibase change script to update
FORM_LAYOUT for Jane, but accidentally uses the
core context. It slips through code review and torpedoes all other client's form layout configs.
We want to stop this happening again at the source, so let's see if we can do it with a lint rule.
Writing the rule
There are three interfaces you could implement when writing a custom rule in Java; which you use depends on what level you want to work at.
- ChangeRule for linting each individual change, useful when you want to prevent issues with the content of the change itself
- ChangeSetRule for linting each changeSet, useful when you want to check things like comments and contexts, or the overall content of a changeSet e.g. when you want ensure certain changes happen together, or in isolation
- ChangeLogRule for linting each changeLog file, useful when you want to check the overall content at changeLog level - rarely used in practise
ChangeRule interface uses generics, so you can target a specific change type e.g.
implements ChangeRule<InsertDataChange> for inserts, or just do
implements ChangeRule<Change> to catch all changes. You can also use this to lint custom changes, if you have any of those in your project.
It's worth noting that the three levels are not isolated from one another - a
Change has access to the
ChangeSet it belongs to, which in turn can access the
ChangeLog it belongs to, and vice versa, so you can freely traverse to get the information you need to decide if your rule is failed.
FORM_LAYOUT use case, it makes the most sense to lint at changeSet level. Here's the code to implement it:
Some notes about how we've done this:
- We've extended the
AbstractLintRuleclass, which saves us fussing about a lot of boilerplate ourselves. We just need to pass our rule name (i.e. the key uses in the rules config) and the failure message
- The key method we need to implement ourselves is
invalid- this accepts a Liquibase
ChangeSetand should return true if the rule has failed - in this case it will do so if any of the changes are an update on the
FORM_LAYOUTtable and the
corecontext is used
All the core rules are implemented in this way as well, so if you're not sure how best to hook something up you might try looking in the source at some existing core rules that do something similar
Making the rule discoverable
Now, our class above should go into a new Maven project that depends on both
The class existing isn't quite enough on its own; we need to tell Liquibase Linter that it's there. For this we are using the Service Provider Interface pattern - this is natively supported in Java and for use cases like this is preferable to powerful-but-heavy classpath scanning approaches like the one used by Spring.
In our newly-created project, we'll create a new file at:
And in the file, we'll write:
Configuring the rule
In the project where our scripts live, we'll add a dependency on our rules project to
liquibase-maven-plugin, in much the same way that we added a dependency for
So for our example custom rules project
wcg-liquibase-linter we would have the following dependency.
Then all we need is to configure the rule as normal in