Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix #1694: Add ability to have cascading liquibase.properties files#7717

Open
MichaelKern-IVV wants to merge 44 commits into
liquibase:mainfrom
MichaelKern-IVV:feature/cascading_properties
Open

fix #1694: Add ability to have cascading liquibase.properties files#7717
MichaelKern-IVV wants to merge 44 commits into
liquibase:mainfrom
MichaelKern-IVV:feature/cascading_properties

Conversation

@MichaelKern-IVV
Copy link
Copy Markdown
Contributor

Impact

  • Bug fix (non-breaking change which fixes expected existing functionality)
  • Enhancement/New feature (adds functionality without impacting existing logic)
  • Breaking change (fix or feature that would cause existing functionality to change)

Description

Currently, we support a single "defaultsFile" (aka liquibase.properties) which can set liquibase configuration values. However, often times people have some of those values that change from environment to environment and it's a pain to have to keep multiple versions of those file with only slight differences.

In addition, there may be sections of the defaultsFile that are maintained by different groups of people and that not everyone involved is authorized to view, such as login credentials.

Unlike requirement #1694 (which should be reopened), this is not achieved by using an additional file whose name includes the stage, but rather by using files with the same name located in different places in the classpath. It is up to the user to configure the classpath according to their own requirements.

Technically, this means that the defaultsFile is read not by an InputStream but by an InputStreamList. For each InputStream contained within it, a separate DefaultsFileValueProvider is created, whose precedence depends on its position in the classpath.

Things to be aware of

Things to worry about

Additional Context

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors defaults-file loading to enumerate all matching resources and register a DefaultsFileValueProvider per resource with explicit precedence; InputStreamList exposes URI+InputStream entries; CLI and Maven plugin points updated; tests and helpers adjusted.

Changes

Multiple Defaults Files with Precedence

Layer / File(s) Summary
InputStreamList API refactor to expose URI+InputStream pairs
liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java
Expose streams as Map.Entry<URI,InputStream>, add getInputStreams() and getFirst(), and update close/iteration behavior.
DefaultsFileValueProvider precedence and multi-source support
liquibase-standard/src/main/java/liquibase/configuration/core/DefaultsFileValueProvider.java
Introduce DEFAULT_PRECEDENCE and instance precedence, add precedence-offset constructors, centralize property loading from streams/files/maps, and trim properties.
LiquibaseCommandLine multi-resource defaults resolution
liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.java
When configured defaults-file path is not a disk file, enumerate all matching classpath resources and register a DefaultsFileValueProvider per resource; update explicit command imports, configureHelp fluent usageMessage, and add version-provider logging after MDC storage.
AbstractLiquibaseMojo multi-stream property file loading
liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java
Switch property-file handling to InputStreamList, parse properties from all streams, register a DefaultsFileValueProvider per stream/URI with increasing precedence, and remove single-stream helpers.
Multiple defaults files integration test and test resources
liquibase-cli/src/test/groovy/liquibase/integration/commandline/MultipleDefaultsFilesOverlayEachOtherTest.groovy, liquibase-cli/src/test/resources/test.properties, liquibase-cli/src/test/resources/subfolder/test.properties
Add Spock integration test validating overlay behavior and add test properties in root and subfolder to exercise multiple defaults files.
ConfigurationDefinition key alias extraction
liquibase-standard/src/main/java/liquibase/configuration/ConfigurationDefinition.java
Add getKeyAndAliases() helper returning primary key plus aliases and replace inline array construction.
ChangeSet null-safe checksum validation
liquibase-standard/src/main/java/liquibase/changelog/ChangeSet.java
Make checksum wildcard comparisons null-safe by converting entries to strings before comparison.
Test API updates for new InputStreamList contract
liquibase-standard/src/test/groovy/liquibase/resource/DirectoryResourceAccessorTest.groovy, liquibase-standard/src/test/groovy/liquibase/resource/FileSystemResourceAccessorTest.groovy, liquibase-standard/src/test/groovy/liquibase/changelog/ChangeLogParametersTest.groovy
Update tests to use streams.getFirst().getValue()/getFirst() and adjust imports to match updated exception usage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

DocNeeded

Suggested reviewers

  • wwillard7800
  • filipelautert
  • rberezen
  • tati-qalified
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix #1694: Add ability to have cascading liquibase.properties files' clearly and specifically summarizes the main enhancement: enabling multiple defaults files to cascade.
Description check ✅ Passed The PR description covers the required template sections: Impact (marked as Enhancement/New feature), Description (explains the problem, solution approach, and technical implementation), and provides sufficient context for review.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 13, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java (1)

815-825: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include propertyFile in the failure message and drop the unreachable null guards in callers.

handlePropertyFileInputStreams now throws on missing/empty resolution, but the message "Failed to resolve the properties file: Not found" does not name the file, which is a regression vs the prior FileUtil.getFileNotFoundMessage(this.propertyFile) UX. Additionally, since this helper either returns a non-null list or throws, the if (isl == null) { throw new MojoExecutionException(FileUtil.getFileNotFoundMessage(this.propertyFile)); } blocks at lines 771-773 and 781-783 are dead code — the friendly message is wrapped in UnexpectedLiquibaseException by the surrounding catch instead.

🛠️ Proposed fix
     private static InputStreamList handlePropertyFileInputStreams(String propertyFile) throws MojoFailureException {
         try {
             InputStreamList inputStreamList = Scope.getCurrentScope().getResourceAccessor().openStreams(null, propertyFile);
             if (inputStreamList == null || inputStreamList.isEmpty()) {
-                throw new MojoFailureException("Failed to resolve the properties file: Not found");
+                throw new MojoFailureException(FileUtil.getFileNotFoundMessage(propertyFile));
             }
             return inputStreamList;
         } catch (IOException e) {
-            throw new MojoFailureException("Failed to resolve the properties file.", e);
+            throw new MojoFailureException("Failed to resolve the properties file: " + propertyFile, e);
         }
     }

And in configureFieldsAndValues, remove the redundant null guards:

             try (InputStreamList isl = handlePropertyFileInputStreams(propertyFile)) {
-                if (isl == null) {
-                    throw new MojoExecutionException(FileUtil.getFileNotFoundMessage(this.propertyFile));
-                }
-
                 parsePropertiesFiles(isl);
                 getLog().info(MavenUtils.LOG_SEPARATOR);
             } catch (IOException | MojoFailureException e) {
                 throw new UnexpectedLiquibaseException(e);
             }
             try (InputStreamList isl = handlePropertyFileInputStreams(this.propertyFile)) {
-                if (isl == null) {
-                    throw new MojoExecutionException(FileUtil.getFileNotFoundMessage(this.propertyFile));
-                }
-
                 LiquibaseConfiguration liquibaseConfiguration = Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java`
around lines 815 - 825, Update handlePropertyFileInputStreams to include the
resolved propertyFile name in its failure message (e.g., use
FileUtil.getFileNotFoundMessage(propertyFile) or append the propertyFile
variable) so the thrown MojoFailureException identifies which file failed; then
remove the now-unreachable null checks that throw MojoExecutionException around
calls to handlePropertyFileInputStreams in configureFieldsAndValues (the blocks
that check isl == null and throw with
FileUtil.getFileNotFoundMessage(this.propertyFile)), since
handlePropertyFileInputStreams either returns a non-null, non-empty
InputStreamList or throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.java`:
- Around line 752-761: The classpath lookup is being applied unconditionally and
opens streams without closing them; guard the
ClassLoaderResourceAccessor.getAll(defaultsFileConfigValue) loop so it only runs
if no on-disk defaults provider was already added (e.g., if returnList.isEmpty()
or by checking existing providers), and change the creation of
DefaultsFileValueProvider to use a try-with-resources around
res.openInputStream() so the InputStream is closed after constructing the
provider; then call liquibaseConfiguration.registerProvider(fileProvider) and
returnList.add(fileProvider) inside that guarded block.

In
`@liquibase-cli/src/test/groovy/liquibase/integration/commandline/MultipleDefaultsFilesOverlayEachOtherTest.groovy`:
- Around line 18-22: The test's classpath setup is non-deterministic because it
assumes the first java.class.path entry (variable classPath) contains test
resources; replace that logic by resolving the test resource URL
deterministically (e.g., using
MultipleDefaultsFilesOverlayEachOtherTest.class.getResource("/subfolder/") or
this.getClass().getResource("/subfolder/") ) and build the URLClassLoader from
that URL list instead of constructing urls from classPath[0]; keep using
contextClassLoader and Thread.currentThread().setContextClassLoader(classLoader)
but remove the reliance on System.getProperty("java.class.path") and the
classPath variable so the test consistently points to the intended test
resources.

In
`@liquibase-standard/src/main/java/liquibase/configuration/core/DefaultsFileValueProvider.java`:
- Around line 42-48: The public constructors DefaultsFileValueProvider(Map<?,?>
properties, int precedenceOffset) and DefaultsFileValueProvider(Map<?,?>
properties) accept non-string keys but later cast keys to String in validate(),
so either tighten the API or validate at copy time; change the constructor
signatures to use Map<String,?> (and update any callers) or, if signatures must
remain generic, add runtime validation in propertiesFromMap: iterate the input
Map entries, if any key is not a String throw an IllegalArgumentException with a
clear message (or coerce via toString() if acceptable), then copy entries into
Properties so validate() no longer risks a ClassCastException.

In `@liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java`:
- Line 15: The change makes InputStreamList iterate and expose getFirst() as
Map.Entry<URI,InputStream>, breaking existing extensions that expect
Iterable<InputStream> and an InputStream getFirst(); restore the old contract by
adding back an iterator() that yields InputStream (or make the class implement
Iterable<InputStream> again) and a getFirst() that returns InputStream
(delegating to the entry-based implementation), while keeping the new
entry-based methods under distinct names (e.g., entryIterator(), entries(), or
getFirstEntry()) so both APIs coexist; update InputStreamList to provide both
InputStream-based and Map.Entry<URI,InputStream>-based accessors (preserve
AutoCloseable behavior and close semantics).

---

Outside diff comments:
In
`@liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java`:
- Around line 815-825: Update handlePropertyFileInputStreams to include the
resolved propertyFile name in its failure message (e.g., use
FileUtil.getFileNotFoundMessage(propertyFile) or append the propertyFile
variable) so the thrown MojoFailureException identifies which file failed; then
remove the now-unreachable null checks that throw MojoExecutionException around
calls to handlePropertyFileInputStreams in configureFieldsAndValues (the blocks
that check isl == null and throw with
FileUtil.getFileNotFoundMessage(this.propertyFile)), since
handlePropertyFileInputStreams either returns a non-null, non-empty
InputStreamList or throws.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16f31eb3-c87b-4d22-8e92-888ae68eaa99

📥 Commits

Reviewing files that changed from the base of the PR and between 1873538 and 9ab71db.

📒 Files selected for processing (12)
  • liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.java
  • liquibase-cli/src/test/groovy/liquibase/integration/commandline/MultipleDefaultsFilesOverlayEachOtherTest.groovy
  • liquibase-cli/src/test/resources/subfolder/test.properties
  • liquibase-cli/src/test/resources/test.properties
  • liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java
  • liquibase-standard/src/main/java/liquibase/changelog/ChangeSet.java
  • liquibase-standard/src/main/java/liquibase/configuration/ConfigurationDefinition.java
  • liquibase-standard/src/main/java/liquibase/configuration/core/DefaultsFileValueProvider.java
  • liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java
  • liquibase-standard/src/test/groovy/liquibase/changelog/ChangeLogParametersTest.groovy
  • liquibase-standard/src/test/groovy/liquibase/resource/DirectoryResourceAccessorTest.groovy
  • liquibase-standard/src/test/groovy/liquibase/resource/FileSystemResourceAccessorTest.groovy
💤 Files with no reviewable changes (1)
  • liquibase-standard/src/test/groovy/liquibase/changelog/ChangeLogParametersTest.groovy

Comment on lines +752 to +761
}

ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor();
List<Resource> resources = classLoaderResourceAccessor.getAll(defaultsFileConfigValue);
if (resources != null) {
for (Resource res : resources) {
if (res.exists()) {
final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(res.openInputStream(), "File in classpath " + res.getUri(), returnList.size());
liquibaseConfiguration.registerProvider(fileProvider);
returnList.add(fileProvider);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid unconditional classpath overlay and close classpath streams explicitly.

Line 754 currently runs even when Line 744 already resolved an on-disk defaults file, so explicit file config can be unintentionally overlaid by classpath matches. Also, Line 759 opens streams without an explicit close scope.

Proposed fix
-        ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor();
-        List<Resource> resources = classLoaderResourceAccessor.getAll(defaultsFileConfigValue);
-        if (resources != null) {
-            for (Resource res : resources) {
-                if (res.exists()) {
-                    final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(res.openInputStream(), "File in classpath " + res.getUri(), returnList.size());
-                    liquibaseConfiguration.registerProvider(fileProvider);
-                    returnList.add(fileProvider);
-                }
-            }
-        }
+        else {
+            ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor();
+            List<Resource> resources = classLoaderResourceAccessor.getAll(defaultsFileConfigValue);
+            if (resources != null) {
+                for (Resource res : resources) {
+                    if (res.exists()) {
+                        try (InputStream classpathStream = res.openInputStream()) {
+                            final DefaultsFileValueProvider fileProvider =
+                                    new DefaultsFileValueProvider(classpathStream, "File in classpath " + res.getUri(), returnList.size());
+                            liquibaseConfiguration.registerProvider(fileProvider);
+                            returnList.add(fileProvider);
+                        }
+                    }
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.java`
around lines 752 - 761, The classpath lookup is being applied unconditionally
and opens streams without closing them; guard the
ClassLoaderResourceAccessor.getAll(defaultsFileConfigValue) loop so it only runs
if no on-disk defaults provider was already added (e.g., if returnList.isEmpty()
or by checking existing providers), and change the creation of
DefaultsFileValueProvider to use a try-with-resources around
res.openInputStream() so the InputStream is closed after constructing the
provider; then call liquibaseConfiguration.registerProvider(fileProvider) and
returnList.add(fileProvider) inside that guarded block.

Comment thread liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java (1)

126-127: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Public getFirst() signature change is a breaking API change.

Line 126 now returns Map.Entry<URI, InputStream> from getFirst(). If previous versions exposed InputStream getFirst(), this breaks existing extensions at compile time. Please preserve the old stream-returning contract and introduce a distinct entry-based accessor (for example getFirstEntry()), then migrate internal callers incrementally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java`
around lines 126 - 127, getFirst() was changed to return Map.Entry<URI,
InputStream> which is a breaking API change; restore the original public
signature InputStream getFirst() and add a new method Map.Entry<URI,
InputStream> getFirstEntry() that returns the entry (use the current
implementation for getFirstEntry()); update internal callers in this class to
call getFirstEntry() if they need the URI, otherwise call getFirst() to preserve
compatibility, and keep the same empty/exception behavior (e.g., handle
streams.isEmpty() or iterator().next() semantics) to avoid changing runtime
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java`:
- Around line 126-127: getFirst() was changed to return Map.Entry<URI,
InputStream> which is a breaking API change; restore the original public
signature InputStream getFirst() and add a new method Map.Entry<URI,
InputStream> getFirstEntry() that returns the entry (use the current
implementation for getFirstEntry()); update internal callers in this class to
call getFirstEntry() if they need the URI, otherwise call getFirst() to preserve
compatibility, and keep the same empty/exception behavior (e.g., handle
streams.isEmpty() or iterator().next() semantics) to avoid changing runtime
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29f0eed9-e787-4c2c-b33f-29954acfb97d

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab71db and df7d36a.

📒 Files selected for processing (2)
  • liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java
  • liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java

@MichaelKern-IVV MichaelKern-IVV requested a deployment to external May 27, 2026 06:13 — with GitHub Actions Abandoned
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants